Ir al contenido

Usuarios

Generada 2026-08-13. Fuente: exploración de código de controlgmg-v2 + observación manual del navegador (QA).

  • Controlador principal: App\Http\Controllers\UserController (app/Http/Controllers/UserController.php, sin subcarpeta Users/ — a diferencia de los controladores de acción única en app/Http/Controllers/Users/* como GetExecutiveUsersByModuleController). Usa App\Models\User (no un modelo de dominio propio) — es el modelo base de autenticación, extendido con Spark Teams (teams(), currentTeam(), roles(), getTeamPermissions($teamId), getTeamsWithRolesAndPermissions()). No se localizó el archivo app/Models/User.php en esta pasada (fillable/casts/relaciones completas quedan por verificar).
  • Query común (commonUserQuery, reutilizada por getUsers y getPaginatedUsers): búsqueda (scopedSearch + teléfono sin espacios), eager-load de teams (agrupado por user_id,team_id) y last_authentication, filtros opcionales team_id, role, module_id (vía whereIsInModule), permission_id (vía roles.permissions), only_trashed, with_trailers, with_roles.
  • delete($user_id) → soft delete ($user->delete()), gateado por DeleteUserRequest::authorize() → $this->user()->hasPermission(PermissionConst::deleteUser) (delete-user). Es el único FormRequest con authorize() no trivial encontrado en este controlador — GetUserRequest, UpdatePasswordRequest, UpdatePhoneNumberRequest no se inspeccionaron a fondo pero el patrón de la ruta de borrado confirma que sí existe control de permiso a nivel de FormRequest (igual que en Liquidaciones/Remolques), aunque no aparezca como middleware de ruta.
  • restore($user_id) también pasa por DeleteUserRequest (mismo permiso delete-user gobierna borrar y restaurar).
  • Invitaciones (getAllInvitations) usa Laravel\Spark\Invitation (paquete laravel/spark-aurelius), no una tabla propia del dominio.
  • getAllPermissions() devuelve App\Core\Security\Permission::all() (catálogo completo, sin scoping — usado para poblar el filtro “Filtrar por permiso” en el listado).

(Ya confirmadas previamente, no se repite la investigación)

routes/users/users.php, montado bajo prefix users dentro de auth:api+password_expired+hasTeam:

GET /users UserController@getUsers
GET /users/by-module/{module} Users\GetExecutiveUsersByModuleController
GET /users/assigned-to-units Users\GetExecutiveUsersAssignedToUnitsController
GET /users/assigned-to-trailers Users\GetExecutiveUsersAssignedToTrailersController
GET /users/paginated UserController@getPaginatedUsers
POST /users/update-password UserController@updatePassword
POST /users/update-phone-number UserController@updatePhoneNumber
GET /users/get-roles/{user}/{team} UserController@getUserRoles
GET /users/invitations UserController@getAllInvitations
GET /users/me/teams-with-roles UserController@getUserTeamsWithRoles
GET /users/get-all-permissions UserController@getAllPermissions
GET /users/get-all-without-paginate UserController@getUsersWithoutPaginate
DELETE /users/{id} UserController@delete
PATCH /users/{id} UserController@restore

Además, routes/web.php líneas 368-372 (mismo grupo): GET /users/role/{user}/{team}, GET /users/roles, GET /users/{user}/teams, GET /users/{user}/team/{teamId}/modules, PUT /team/{team}/members/{user} → TeamController@updateRoles. Team-management vía paquete laravel/spark-aurelius bajo /settings/teams/... (invitaciones, miembros, switch de equipo) registrado en vendor/laravel/spark-aurelius/src/Http/routes.php.

Sin middleware permission:/role: en ninguna de estas rutas de usuarios (solo protegidas por auth:api+password_expired+hasTeam) — la autorización real vive en el frontend (beforeEnter: can(...) en routes.js) y, para borrar/restaurar, en el FormRequest DeleteUserRequest.

  • Ruta raíz /usuarios → resources/js/components/Members/MembersPage.vue (resources/js/routes.js línea ~222), con beforeEnter: can('view-panel-user') — este es el guard real que decide si el módulo es visible, no las rutas de API. Nota de naming: el dominio visible en UI es “Usuarios” pero el árbol de componentes se llama “Members” (components/Members/*), igual que “Liquidaciones” internamente es “Operations” — no buscar una carpeta Users/ para el listado principal (esa carpeta sí existe pero solo contiene UserSelector.vue/UserPicture.vue, componentes reutilizables, no la página).
  • MembersPage.vue — página de listado: barra de filtros (filter-row) con búsqueda (search-everything), selects de “Filtrar por oficina” (team_id), “Filtrar por rol” (role), “Filtrar por permiso” (permission_id); botón “Enviar Invitación” (visible solo si getTeamsCanInvite().length>0, es decir si el usuario tiene el permiso invite-user en alguno de sus equipos); botón “Invitaciones” (abre diálogo modal con lista de invitaciones pendientes, botón “Eliminar” por invitación visible solo con hasRole('owner')); toggle de papelera (toggle-delete-button) visible con can('delete-operator') — mismo permiso “prestado” de Operadores que en Operations.vue, probable copy-paste, no delete-user (a validar/reportar como posible bug, igual que en Liquidaciones).
  • Listado: v-for sobre pagination.elements renderiza MembersTableRow.vue por cada usuario (fila) + MemberForm.vue en expansión inline (el-collapse-transition) al hacer click en la fila — mismo patrón de expansión que Remolques/Liquidaciones.
  • MembersTableRow.vue (resources/js/components/Members/MembersTableRow.vue): fila con clases CSS .user-name, .user-photo, .user-email, .roles (lista de equipos/roles separados por coma) — no usa tabla HTML, son divs (igual patrón que Liquidaciones/Remolques).
  • MemberForm.vue no se inspeccionó a fondo en esta pasada (formulario de alta/edición de usuario, permisos, teléfono, contraseña) — estructura interna de campos por verificar.
  • Servicio: UserService (resources/js/services/users/user-service.js, ruta inferida por el import @s/users/user-service en MembersPage.vue; no se abrió el archivo) — expone al menos getUsers, getMyTeamsWithRoles, getAllPermissions usados por MembersPage.vue. TeamService (@s/teams/team-service) expone getRoles, sendInvitation, getInvitations, deleteInvitation.
  • Menú lateral: entrada path: '/usuarios', canView: 'view-panel-user' en resources/js/components/Nav/menuOptions.js (línea ~615; hay una entrada anterior comentada en línea ~577, ignorar).
  • Selector de equipo (header/topbar, no específico del listado de usuarios): resources/js/components/Modules/TeamSelector.vue. Tabla de miembros de equipo (posiblemente usada en contexto de settings/teams, del paquete Spark): resources/js/components/Teams/TeamMembersTable.vue — relación exacta con /usuarios por verificar (podría ser un componente legado de Spark no usado en el flujo principal de /usuarios).

Fuente: config/laratrust_seeder.php, resource user: r,invite,set-roles,set-password,d,view-panel → generadas en App\Core\Security\PermissionConst:

  • view-panel-user (gobierna la ruta /usuarios completa vía beforeEnter en routes.js — es el permiso principal a validar en cualquier test de acceso al módulo).
  • read-user, invite-user (gobierna si aparece el botón “Enviar Invitación” por equipo), set-roles-user, set-password-user, delete-user (gobierna borrar/restaurar vía DeleteUserRequest).
  • También existen update-executive-user-trailer-reports / update-executive-user-unit-reports (relacionados con asignación ejecutiva a unidades/remolques, no con el CRUD de usuarios en sí).

Nota de posible inconsistencia: el toggle de papelera en MembersPage.vue usa can('delete-operator') en vez de can('delete-user') — si un test verifica visibilidad del toggle de papelera con un usuario que solo tiene delete-user pero no delete-operator (o viceversa), el resultado puede no ser el esperado. Confirmar contra QA real antes de asumir el comportamiento.

No hay middleware permission:/role: a nivel de rutas de API para usuarios — todo el control de acceso relevante para el tester vive en el frontend (beforeEnter) y, para mutaciones destructivas, en DeleteUserRequest::authorize().

Selectores usados por el tester (riesgo de fragilidad)

Sección titulada «Selectores usados por el tester (riesgo de fragilidad)»

No hay specs de Playwright para este módulo todavía — a diferencia de Remolques (que sí tiene tests/remolques/expand-trailer-detail.spec.ts), Usuarios no ha sido explorado con Playwright/codegen contra QA real. Todo lo que sigue es inferencia de código, no verificación en vivo:

  • No hay data-testid en la app (mismo patrón que el resto de controlgmg-v2, no se modifica el repo para agregarlos).
  • La fila de la lista (MembersTableRow.vue) no usa tabla HTML sino divs con clases .user-name/.user-email/.roles — probablemente aplique una estrategia similar a Liquidaciones/Remolques: getByText(email) + heurística de ancestro para ubicar la fila completa.
  • El formulario de alta/edición (MemberForm.vue) se expande inline igual que en Remolques/Liquidaciones (mismo patrón el-collapse-transition), pero su estructura interna de campos no fue inspeccionada en esta pasada — no asumir que los labels estén asociados correctamente a inputs sin verificar (en Remolques el login ya demostró que getByLabel puede fallar).
  • El flujo de “Enviar Invitación” abre una sección inline (no modal) con select de oficina, input de correo y select de rol — sin label for=/id visibles en el HTML leído, apoyarse en el texto de la etiqueta (text=Oficina, text=Correo electrónico, text=Rol) + selector de ancestro, a confirmar en vivo.
  • El diálogo de “Invitaciones” sí es un el-dialog real (modal), a diferencia del formulario de alta que es inline — un test que necesite interactuar con invitaciones pendientes debe esperar el modal (.invitations__dialog), no tratarlo como sección inline.
  • El botón “Eliminar” de una invitación solo es visible con hasRole('owner') — un test con un usuario sin ese rol no lo encontrará, esto no es un bug de la app sino comportamiento esperado.

Antes de escribir cualquier spec para este módulo, correr npm run codegen:gmg contra QA real para verificar selectores, igual que se hizo para Remolques.

La página carga en https://qa.controlgmg.com/usuarios bajo el título “Usuarios”. No se inspeccionó el contenido a mayor detalle visual.