Usuarios
Generada 2026-08-13. Fuente: exploración de código de controlgmg-v2 + observación manual del navegador (QA).
Backend
Sección titulada «Backend»- Controlador principal:
App\Http\Controllers\UserController(app/Http/Controllers/UserController.php, sin subcarpetaUsers/— a diferencia de los controladores de acción única enapp/Http/Controllers/Users/*comoGetExecutiveUsersByModuleController). UsaApp\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 archivoapp/Models/User.phpen esta pasada (fillable/casts/relaciones completas quedan por verificar). - Query común (
commonUserQuery, reutilizada porgetUsersygetPaginatedUsers): búsqueda (scopedSearch+ teléfono sin espacios), eager-load deteams(agrupado poruser_id,team_id) ylast_authentication, filtros opcionalesteam_id,role,module_id(víawhereIsInModule),permission_id(víaroles.permissions),only_trashed,with_trailers,with_roles. delete($user_id)→ soft delete ($user->delete()), gateado porDeleteUserRequest::authorize()→$this->user()->hasPermission(PermissionConst::deleteUser)(delete-user). Es el único FormRequest conauthorize()no trivial encontrado en este controlador —GetUserRequest,UpdatePasswordRequest,UpdatePhoneNumberRequestno 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 porDeleteUserRequest(mismo permisodelete-usergobierna borrar y restaurar).- Invitaciones (
getAllInvitations) usaLaravel\Spark\Invitation(paquetelaravel/spark-aurelius), no una tabla propia del dominio. getAllPermissions()devuelveApp\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@getUsersGET /users/by-module/{module} Users\GetExecutiveUsersByModuleControllerGET /users/assigned-to-units Users\GetExecutiveUsersAssignedToUnitsControllerGET /users/assigned-to-trailers Users\GetExecutiveUsersAssignedToTrailersControllerGET /users/paginated UserController@getPaginatedUsersPOST /users/update-password UserController@updatePasswordPOST /users/update-phone-number UserController@updatePhoneNumberGET /users/get-roles/{user}/{team} UserController@getUserRolesGET /users/invitations UserController@getAllInvitationsGET /users/me/teams-with-roles UserController@getUserTeamsWithRolesGET /users/get-all-permissions UserController@getAllPermissionsGET /users/get-all-without-paginate UserController@getUsersWithoutPaginateDELETE /users/{id} UserController@deletePATCH /users/{id} UserController@restoreAdemá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.
Frontend (Vue 2)
Sección titulada «Frontend (Vue 2)»- Ruta raíz
/usuarios→resources/js/components/Members/MembersPage.vue(resources/js/routes.jslínea ~222), conbeforeEnter: 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 carpetaUsers/para el listado principal (esa carpeta sí existe pero solo contieneUserSelector.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 sigetTeamsCanInvite().length>0, es decir si el usuario tiene el permisoinvite-useren alguno de sus equipos); botón “Invitaciones” (abre diálogo modal con lista de invitaciones pendientes, botón “Eliminar” por invitación visible solo conhasRole('owner')); toggle de papelera (toggle-delete-button) visible concan('delete-operator')— mismo permiso “prestado” de Operadores que enOperations.vue, probable copy-paste, nodelete-user(a validar/reportar como posible bug, igual que en Liquidaciones).- Listado:
v-forsobrepagination.elementsrenderizaMembersTableRow.vuepor cada usuario (fila) +MemberForm.vueen 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.vueno 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-serviceenMembersPage.vue; no se abrió el archivo) — expone al menosgetUsers,getMyTeamsWithRoles,getAllPermissionsusados porMembersPage.vue.TeamService(@s/teams/team-service) exponegetRoles,sendInvitation,getInvitations,deleteInvitation. - Menú lateral: entrada
path: '/usuarios',canView: 'view-panel-user'enresources/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 desettings/teams, del paquete Spark):resources/js/components/Teams/TeamMembersTable.vue— relación exacta con/usuariospor verificar (podría ser un componente legado de Spark no usado en el flujo principal de/usuarios).
Permisos (Laratrust)
Sección titulada «Permisos (Laratrust)»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/usuarioscompleta víabeforeEnterenroutes.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íaDeleteUserRequest).- 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-testiden la app (mismo patrón que el resto decontrolgmg-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ónel-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ó quegetByLabelpuede 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=/idvisibles 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-dialogreal (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.
Datos observados en QA (2026-08-12)
Sección titulada «Datos observados en QA (2026-08-12)»La página carga en https://qa.controlgmg.com/usuarios bajo el título “Usuarios”. No se inspeccionó el contenido a mayor detalle visual.