Comentarios de usuarios
Los administradores pueden revisar, priorizar y responder a los comentarios enviados por los usuarios (reportes de errores, solicitudes de funciones, quejas, sugerencias y comentarios generales) desde /feedback. Los usuarios ya contaban con una superficie completa de envío y votación (submitFeedback, publicFeedback, upvoteFeedback/downvoteFeedback — ver Comentarios de usuarios), pero no existía previamente ninguna forma, desde el lado de administración, de ver, priorizar o responder a ninguno de ellos.
Esto se construyó casi por completo sobre la infraestructura existente: database/models/UserFeedback.js ya tenía todas las columnas necesarias para la priorización (status, priority, adminResponse, reviewedBy, reviewedAt), y managers/user-managers/user-feedback.manager.js ya tenía la mayor parte de la lógica de negocio (getAllFeedback, updateStatus, setPriority, getFeedbackStats). Lo que faltaba era la superficie de GraphQL de administración en sí, una acción respondToFeedback que establece adminResponse/reviewedBy/reviewedAt juntos, y soporte de filtrado en getAll.
Checklist de implementación
Priorización de administrador (/feedback, requiere MANAGE_FEEDBACK)
- Listar comentarios con filtros opcionales de
status/priority/feedbackType, del más reciente al más antiguo (adminGetFeedback) - Conteos agregados por tipo/estado/prioridad, además del total de votos a favor/en contra (
adminGetFeedbackStats) - Actualizar el estado de un elemento —
pending | in_review | in_progress | resolved | rejected | duplicate(adminUpdateFeedbackStatus) - Establecer la prioridad de un elemento —
low | medium | high | critical(adminSetFeedbackPriority) - Escribir una respuesta de administrador, que también registra quién revisó el elemento y cuándo, en una sola acción (
adminRespondToFeedback)
Dos brechas reales cerradas en el camino
data-access-services/user/user-feedback.access-service.js#getAll(options)solo admitía{limit, offset}— sin filtrado. Ahora aceptastatus/priority/feedbackTypeopcionales y los combina en elwherede Sequelize. Los métodos anteriores del managergetByStatus/getByPriorityllamaban a métodos del access-service (getByStatus/getByPriority) que nunca existieron — un error de tipo "is not a function" si alguna vez se invocaban en la práctica. Se dejan como código muerto sin referencias (nada los llama) en lugar de agregar tres métodos de listado casi duplicados;getAllFeedback/getAllcon filtros cumple el propósito que tenían.UserFeedback.reviewedByestá pensado para contener el id de un administrador, pero la migración original (20220122175043-create-user-feedback.js) apuntaba su llave foránea auser(copiado de las otras columnas de esa tabla, que sí referencian correctamente a quien envía el comentario). La migración20260802140000-fix-user-feedback-reviewed-by-references-admin-user.jscorrige la FK para que referencieadmin_user(la misma distinción queappeal.reviewed_byya tenía correcta), poniendo enNULLprimero las pocas filas existentes cuyoreviewed_bycontenía un id deuser(datos de prueba preexistentes — nunca fue una acción real de un administrador, ya que no existía ninguna superficie de revisión de administrador hasta ahora). La asociación del modelo (UserFeedback.js) se corrigió para que coincida (belongsTo(models.AdminUser, ...)).
Una compensación deliberada en el schema
status/priority/adminResponse/reviewedBy/reviewedAt ya estaban declarados en el tipo compartido UserFeedback de GraphQL (el schema completo se ensambla una sola vez antes de dividirse en los ámbitos de cliente/administración, así que un tipo declarado una vez se resuelve en ambos). Eso significa que las consultas myFeedback/feedbackById de un usuario regular también pueden leer esos campos sobre sus propios comentarios — no es una fuga de datos (ambas consultas ya están verificadas por propiedad), y hasta podría considerarse útil (un usuario puede ver la respuesta del administrador a su propio reporte). Ver el comentario en la parte superior de graphql/types/user-feedback.type.js.
Dónde vive esto
Backend
apps/backend/graphql/types/user-feedback.type.js— tipo compartidoUserFeedback(cliente + administración)apps/backend/graphql/types/admin/user-feedback-admin.type.js— schema de administración (FeedbackStats,adminGetFeedback,adminGetFeedbackStats,adminUpdateFeedbackStatus,adminSetFeedbackPriority,adminRespondToFeedback)apps/backend/graphql/resolvers/user-feedback.resolver.js/graphql/resolvers/admin/user-feedback-admin.resolver.jsapps/backend/managers/user-managers/user-feedback.manager.js— un solo manager respalda tanto el flujo de envío/votación de usuarios como la superficie de priorización de administraciónapps/backend/data-access-services/user/user-feedback.access-service.js—getAllahora admite filtros de status/priority/feedbackTypeapps/backend/database/migrations/20260802140000-fix-user-feedback-reviewed-by-references-admin-user.js— corrige la llave foránea dereviewed_by, deuseraadmin_user
Frontend — apps/frontend-admin/src/app/feedback/page.tsx + FeedbackContent.tsx. Entrada de navegación protegida por el permiso MANAGE_FEEDBACK (o super_admin), el mismo patrón que la sección de Control de versiones de la app protegida por MANAGE_APP_VERSIONS.
Permiso — MANAGE_FEEDBACK, agregado al catálogo real de permisos en admin-user.manager.js#getAdminPermissions (categoría content_moderation, junto a MODERATE_CONTENT/REMOVE_CONTENT).
Referencia técnica de GraphQL
# Administrador (requiere MANAGE_FEEDBACK)
query AdminGetFeedback($status: String, $priority: String, $feedbackType: String, $limit: Int, $offset: Int) {
adminGetFeedback(status: $status, priority: $priority, feedbackType: $feedbackType, limit: $limit, offset: $offset) {
id title description feedbackType status priority adminResponse
reviewedBy reviewedAt upvotes downvotes createdAt
user { id username profilePicture }
}
}
query AdminGetFeedbackStats {
adminGetFeedbackStats { total byType byStatus byPriority totalUpvotes totalDownvotes }
}
mutation AdminUpdateFeedbackStatus($id: ID!, $status: String!) {
adminUpdateFeedbackStatus(id: $id, status: $status) { id status }
}
mutation AdminSetFeedbackPriority($id: ID!, $priority: String!) {
adminSetFeedbackPriority(id: $id, priority: $priority) { id priority }
}
mutation AdminRespondToFeedback($id: ID!, $response: String!) {
adminRespondToFeedback(id: $id, response: $response) { id adminResponse reviewedBy reviewedAt }
}
# Cliente (autenticado - sin cambios por este trabajo)
query MyFeedback { myFeedback { id title status priority adminResponse } }
mutation SubmitFeedback($input: SubmitFeedbackInput!) { submitFeedback(input: $input) { id } }