Ir al contenido

Personal

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

Aclaración de nombres: “Personal” es un dominio propio, distinto de “Expediente Admin” (AdminRecord). El modelo App\Core\Staff\Staff es una capa que une un Operator (operador de flotilla) o un AdminRecord (expediente administrativo) bajo un mismo registro de control de horario/acceso — staff.operator_id XOR staff.admin_record_id. No son lo mismo: “Personal” = control de horarios/asistencia/acceso físico para cualquier tipo de empleado (operador o admin); “Expediente Admin” = el expediente administrativo en sí (datos personales, documentos, etc. de un empleado no-operador). El módulo Personal consume/relaciona AdminRecord y Operator, no los reemplaza.

  • Modelo raíz: App\Core\Staff\Staff (app/Core/Staff/Staff.php), tabla staff. Extiende App\BaseModel, usa SoftDeletes, ApprovableTrait, SearchableTrait, MessageCenterNotifiableTrait, BusinessRuleLockableTrait/BusinessRuleAppendsTrait, LoggableTrait, UserNotifiableTrait. Implementa HasModule → getModule() devuelve Module::personal.
    • Columnas fillable: operator_id, admin_record_id, euc (código QR/identificador único de acceso), last_access_reading, team_id, schedule_cut_type (default weekly), schedule_cut_day (default 4 = jueves), schedule_target_hours.
    • Relaciones: operator() → belongsTo(Operator), admin_record() → belongsTo(AdminRecord), staff_access_registers() → hasMany(StaffAccessRegister).
    • Accessors útiles: staff_name/staff_code/staff_role/staff_photo/name_with_code — todos resuelven condicionalmente si el registro es de un operator o un admin_record. getModelTitleAttribute() devuelve literalmente "Personal". getActionUrlAttribute() devuelve /personal?id={id}.
    • Lógica de negocio embebida en el modelo (poco común, no es solo un Eloquent model plano): filtros de listado por rol (applyAdminOrOwnerRole, applyJefePersonalRole, applyDefaultRole — el query cambia según si el usuario tiene rol admin/owner, jefe-personal, recursos-humanos), cálculo de horas trabajadas (calculateWorkedHours/processWorkedHoursFromLogs, procesa staff_access_registers agrupados por día interpretando reading_type: 1=Entrada, 0=Salida, 2=Break In, 3=Break Out), y filtros de anomalías (applyStaffWithExcessiveDailyRecordsFilter — más de 4 registros/día, applyStaffWithLateRecordsOnlyFilter, applyStaffWithToleranceRecordsOnlyFilter, applyStaffWithEarlyDepartureFilter).
  • Horarios (schedules):
    • App\Models\StaffSchedule (tabla implícita staff_schedules, no confirmada por migración directa) — columnas: staff_id, day_of_week, entry_time, entry_tolerance, break_in, break_out, break_out_tolerance, exit_time, is_global. SoftDeletes. Un horario puede ser “global” (is_global=true, staff_id=null, vía scopes global()/getGlobalForDay()) o específico de un empleado (specific()).
    • App\Models\StaffTemporarySchedule (tabla staff_temporary_schedules, no confirmada por migración) — columnas: staff_id, day_of_week, start_date, end_date, entry_tolerance, break_out_tolerance, created_by. Horarios temporales que sobreescriben el horario normal en un rango de fechas (scopeActiveForDate, isActive(), overlapsWithDates()).
  • Registro de accesos: App\Core\Staff\StaffAccessRegister (tabla staff_access_registers) — columnas usadas en queries: staff_id, company_id, user_id (quién hizo la lectura/control de acceso), reading_type (0-3, ver arriba), is_late, is_within_tolerance, is_early_departure, approval_expiration_date, approval_not_approved_comment, created_at. Se relaciona con staff, companies, users (como access_control), y con operators/admin_records (como employee, según el tipo de staff).
  • Otros archivos de dominio en app/Core/Staff/: StaffService.php, StaffLatenessDetectionService.php (detección de retardos), StaffApprovalTypes.php, StaffReadingTypes.php (constantes de tipo de lectura/log, incluye accessReading, accessApproval, accessNotApproval), StaffBusinessRuleTypes.php. Controladores en app/Http/Controllers/Staff/ (StaffScheduleController, StaffTemporaryScheduleController, AccessControl/StaffAccessControlController, AccessControl/StaffApprovalController). App\Observers\StaffObserver registrado para el modelo.
  • Nota: no se encontró el controlador CRUD principal de Staff (alta/edición/listado del propio /personal) en esta pasada — las rutas de staff/* localizadas (ver abajo) cubren horarios y control de acceso/aprobaciones, pero el CRUD base de Staff probablemente vive fuera del grupo routes/staff/ (posiblemente junto a Operators/AdminRecords o en routes/web.php directamente, como pasa con Operations). Por verificar.

routes/web.php (~línea 524-530) monta el grupo prefix('staff'):

Route::group(['prefix' => 'staff'], function () {
require base_path('routes/staff/access-control/approvals.php');
require base_path('routes/staff/schedules.php');
Route::group(['prefix' => 'access-control'], function() {
require base_path('routes/staff/access-control/staff_access_control.php');
});
});
  • routes/staff/access-control/approvals.php (bajo /staff/{staff}/...):
    Método Ruta Acción
    POST /staff/{staff}/approve Staff\AccessControl\StaffApprovalController@approve
    POST /staff/{staff}/disapprove Staff\AccessControl\StaffApprovalController@disapprove
    POST /staff/{staff}/notify Staff\AccessControl\StaffApprovalController@notify
  • routes/staff/schedules.php:
    Método Ruta Acción
    GET /staff/{staff}/schedules Staff\StaffScheduleController@getByStaff
    POST /staff/{staff}/schedules Staff\StaffScheduleController@saveAll
    DELETE /staff/schedules/{schedule} Staff\StaffScheduleController@delete
    PATCH /staff/{staff}/schedule-configuration Staff\StaffScheduleController@updateConfiguration
    GET /staff/schedules/global Staff\StaffScheduleController@getGlobal
    POST /staff/schedules/global Staff\StaffScheduleController@saveAllGlobal
    GET /staff/{staff}/temporary-schedules Staff\StaffTemporaryScheduleController@getByStaff
    POST /staff/{staff}/temporary-schedules Staff\StaffTemporaryScheduleController@saveAll
    DELETE /staff/temporary-schedules/{schedule} Staff\StaffTemporaryScheduleController@delete
    GET /staff/{staff}/temporary-schedules/active/{date}/{dayOfWeek} Staff\StaffTemporaryScheduleController@getActiveSchedule
  • routes/staff/access-control/staff_access_control.php (bajo /staff/access-control/...):
    Método Ruta Acción
    GET /staff/access-control/get-paginated Staff\AccessControl\StaffAccessControlController@getPaginated
    GET /staff/access-control/get-by-euc/{euc} Staff\AccessControl\StaffAccessControlController@getByEuc (usado por el lector QR)
    POST /staff/access-control/update-qr Staff\AccessControl\StaffAccessControlController@updateQr

Las rutas de listado/control-data de asistencia (view-attendance-list) viven aparte, en routes/control-data/staff/staff-control-data.php bajo prefix('control-data') — ya documentadas en la ficha de Control Data. No se encontraron rutas explícitas de CRUD del propio recurso Staff (crear/editar/eliminar un registro staff) en routes/staff/* ni se buscó exhaustivamente en routes/web.php fuera del bloque citado — pendiente de confirmar dónde vive el alta/edición que usa StaffForm.vue.

  • Ruta raíz /personal → resources/js/components/Staff/StaffPage.vue (resources/js/routes.js línea ~344), guard beforeEnter: can('view-staff'). Props: {user: Spark.state.user}.
    • StaffPage.vue: listado principal con filtros por rol — sección “Escuela” (candidato/operador, visible si hasRole('recursos-humanos'|'owner'|'admin')), sección “Operador” (maestros/graduados/prospectos/alta/activo), sección “Admin” (prospecto/alta/candidato, visible si hasRole('jefe-personal'|'owner'|'admin')), buscador (search-everything), indicador de paginación, toggle de papelera (only_trashed).
    • Subcomponentes en resources/js/components/Staff/: StaffRow.vue (fila de listado), StaffForm.vue (formulario, con FormSections/StaffGeneralForm.vue, FormSections/StaffQrForm.vue — gestión de código QR/euc, FormSections/StaffSemaphore.vue — semáforo de estado, FormSections/StaffScheduleForm.vue — configuración de horario), StaffManualApproval.vue (aprobación manual de acceso).
  • /personal/control-de-acceso → @c/Staff/AccessControl/AccessControlPage.vue, guard can('read-staff'). Incluye ContinuousQrReader.vue (lector QR continuo, consume get-by-euc/{euc}).
  • /personal/lista-de-asistencia → components/ControlData/Staff/StaffControlData.vue, guard can(['view-attendance-list', 'view-admins-attendance-list']), con hijos que usan redirectByRole (ya documentado en ficha de Control Data).
  • Menú lateral (resources/js/components/Nav/menuOptions.js, líneas ~539-566): entrada name: 'Personal', module: 'staff', path: '/personal', canView: 'view-staff', ícono /img/exp-cliente.svg (mismo ícono que Expediente Admin — posible copy-paste, no confirma que sean el mismo módulo). Submódulos:
    • “Personal” → /personal, canView: 'approve-staff' (ojo: el submenú usa approve-staff, no view-staff — inconsistencia entre el guard de ruta y el guard de menú, a validar en QA real).
    • “Control Acceso” → /personal/control-de-acceso, canView: 'read-staff'.
    • “Lista de Asistencia” → /personal/lista-de-asistencia, canView: ['view-attendance-list', 'view-admins-attendance-list'].

Fuente: config/laratrust_seeder.php, resource staff (usualmente v,c,r,u,d,a para admin/owner) → generadas como constantes en App\Core\Security\PermissionConst:

  • view-staff, create-staff, read-staff, update-staff, delete-staff, approve-staff
  • Ya conocidos de Control Data: view-attendance-list, view-admins-attendance-list

Asignación por rol (config/laratrust_seeder.php):

  • admin/owner: staff => 'v,c,r,u,d,a' (control total).
  • recursosHumanos: staff => 'v,c,u,d,a' (sin r/read explícito en esa línea puntual, revisar si aplica a todos los seeders del archivo — hay múltiples bloques de seed, algunos con v,c,r,u,d,a y otros con v,c,u,d,a).
  • controlAcceso: staff => 'r,v' (solo lectura/visualización — coincide con el uso en AccessControlPage/lector QR).

Roles relevantes para este dominio (por nombre usado en el propio modelo Staff.php): admin, owner, jefe-personal, recursos-humanos (controlan qué query de listado se aplica vía applyAdminOrOwnerRole/applyJefePersonalRole/applyDefaultRole).

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 — todo lo que sigue es inferencia de código, no verificación en vivo:

  • No hay data-testid en la app (patrón consistente en todo controlgmg-v2).
  • El listado (StaffPage.vue) usa filtros con el-checkbox-group/el-checkbox (Element UI) agrupados por rol visible (Escuela/Operador/Admin) — la visibilidad de cada bloque de filtro depende del rol del usuario logueado (hasRole(...)), por lo que un test debe confirmar primero con qué rol de QA se está autenticando antes de asumir qué filtros son visibles.
  • StaffForm.vue está compuesto de sub-secciones (StaffGeneralForm, StaffQrForm, StaffSemaphore, StaffScheduleForm) — no se inspeccionó su estructura interna de campos/labels; no asumir asociación correcta label↔input sin verificar (mismo riesgo ya visto en Remolques/Liquidaciones).
  • Hay una inconsistencia de permisos entre el guard de ruta de /personal (view-staff, en routes.js) y el guard del ítem de submenú “Personal” (approve-staff, en menuOptions.js) — si un usuario de prueba tiene uno pero no el otro, el link del menú podría no mostrarse aunque la ruta sea accesible directamente por URL, o viceversa. Confirmar en QA real con el rol específico que se vaya a usar en los specs.
  • El componente de lectura QR (ContinuousQrReader.vue, dentro de AccessControlPage) probablemente dependa de acceso a cámara del navegador — riesgo alto de fragilidad/no-automatizable en Playwright sin mocks; validar si el flujo de prueba necesita este componente o puede evitarlo.

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

La página carga en https://qa.controlgmg.com/personal bajo el título “Personal”. No se inspeccionó el contenido a mayor detalle visual. Falta una pasada manual más profunda (listado con datos reales, formulario de alta/edición, sub-secciones de horario/QR/semáforo, y el flujo del lector QR en /personal/control-de-acceso) para documentar selectores y datos de prueba concretos, análogo a lo hecho para Remolques/Liquidaciones.