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.
Backend
Sección titulada «Backend»- Modelo raíz:
App\Core\Staff\Staff(app/Core/Staff/Staff.php), tablastaff. ExtiendeApp\BaseModel, usaSoftDeletes,ApprovableTrait,SearchableTrait,MessageCenterNotifiableTrait,BusinessRuleLockableTrait/BusinessRuleAppendsTrait,LoggableTrait,UserNotifiableTrait. ImplementaHasModule→getModule()devuelveModule::personal.- Columnas fillable:
operator_id,admin_record_id,euc(código QR/identificador único de acceso),last_access_reading,team_id,schedule_cut_type(defaultweekly),schedule_cut_day(default4= 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 unoperatoro unadmin_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 roladmin/owner,jefe-personal,recursos-humanos), cálculo de horas trabajadas (calculateWorkedHours/processWorkedHoursFromLogs, procesastaff_access_registersagrupados por día interpretandoreading_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).
- Columnas fillable:
- Horarios (schedules):
App\Models\StaffSchedule(tabla implícitastaff_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 scopesglobal()/getGlobalForDay()) o específico de un empleado (specific()).App\Models\StaffTemporarySchedule(tablastaff_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(tablastaff_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 constaff,companies,users(comoaccess_control), y conoperators/admin_records(comoemployee, 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, incluyeaccessReading,accessApproval,accessNotApproval),StaffBusinessRuleTypes.php. Controladores enapp/Http/Controllers/Staff/(StaffScheduleController,StaffTemporaryScheduleController,AccessControl/StaffAccessControlController,AccessControl/StaffApprovalController).App\Observers\StaffObserverregistrado 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 destaff/*localizadas (ver abajo) cubren horarios y control de acceso/aprobaciones, pero el CRUD base de Staff probablemente vive fuera del gruporoutes/staff/(posiblemente junto a Operators/AdminRecords o enroutes/web.phpdirectamente, 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}/approveStaff\AccessControl\StaffApprovalController@approvePOST /staff/{staff}/disapproveStaff\AccessControl\StaffApprovalController@disapprovePOST /staff/{staff}/notifyStaff\AccessControl\StaffApprovalController@notifyroutes/staff/schedules.php:Método Ruta Acción GET /staff/{staff}/schedulesStaff\StaffScheduleController@getByStaffPOST /staff/{staff}/schedulesStaff\StaffScheduleController@saveAllDELETE /staff/schedules/{schedule}Staff\StaffScheduleController@deletePATCH /staff/{staff}/schedule-configurationStaff\StaffScheduleController@updateConfigurationGET /staff/schedules/globalStaff\StaffScheduleController@getGlobalPOST /staff/schedules/globalStaff\StaffScheduleController@saveAllGlobalGET /staff/{staff}/temporary-schedulesStaff\StaffTemporaryScheduleController@getByStaffPOST /staff/{staff}/temporary-schedulesStaff\StaffTemporaryScheduleController@saveAllDELETE /staff/temporary-schedules/{schedule}Staff\StaffTemporaryScheduleController@deleteGET /staff/{staff}/temporary-schedules/active/{date}/{dayOfWeek}Staff\StaffTemporaryScheduleController@getActiveScheduleroutes/staff/access-control/staff_access_control.php(bajo/staff/access-control/...):Método Ruta Acción GET /staff/access-control/get-paginatedStaff\AccessControl\StaffAccessControlController@getPaginatedGET /staff/access-control/get-by-euc/{euc}Staff\AccessControl\StaffAccessControlController@getByEuc(usado por el lector QR)POST /staff/access-control/update-qrStaff\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.
Frontend (Vue 2)
Sección titulada «Frontend (Vue 2)»- Ruta raíz
/personal→resources/js/components/Staff/StaffPage.vue(resources/js/routes.jslínea ~344), guardbeforeEnter: can('view-staff'). Props:{user: Spark.state.user}.StaffPage.vue: listado principal con filtros por rol — sección “Escuela” (candidato/operador, visible sihasRole('recursos-humanos'|'owner'|'admin')), sección “Operador” (maestros/graduados/prospectos/alta/activo), sección “Admin” (prospecto/alta/candidato, visible sihasRole('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, conFormSections/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, guardcan('read-staff'). IncluyeContinuousQrReader.vue(lector QR continuo, consumeget-by-euc/{euc})./personal/lista-de-asistencia→components/ControlData/Staff/StaffControlData.vue, guardcan(['view-attendance-list', 'view-admins-attendance-list']), con hijos que usanredirectByRole(ya documentado en ficha de Control Data).- Menú lateral (
resources/js/components/Nav/menuOptions.js, líneas ~539-566): entradaname: '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ú usaapprove-staff, noview-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'].
- “Personal” →
Permisos (Laratrust)
Sección titulada «Permisos (Laratrust)»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'(sinr/read explícito en esa línea puntual, revisar si aplica a todos los seeders del archivo — hay múltiples bloques de seed, algunos conv,c,r,u,d,ay otros conv,c,u,d,a).controlAcceso:staff => 'r,v'(solo lectura/visualización — coincide con el uso enAccessControlPage/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-testiden la app (patrón consistente en todocontrolgmg-v2). - El listado (
StaffPage.vue) usa filtros conel-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.vueestá 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, enroutes.js) y el guard del ítem de submenú “Personal” (approve-staff, enmenuOptions.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 deAccessControlPage) 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.
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/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.