Skip to content

Commit 58bce59

Browse files
committed
fix(formacion): distinguir homonimos en calendario y seguimiento del catalogo
Bug reportado por Julio en produccion: dos fichas reales distintas de "Pamela Guzman Velasquez" (1be0b366-... en Sala, 147289a6-... en Cocina) parecian una sola tarjeta con 3 pendientes en "Que vence". Verificado antes de tocar nada: tanto TrainingCalendarView.tsx (Map keyed por g.employeeId) como training_gaps (RPC C2, ya aplicada -- select e.id as employee_id desde employees, sin dedup ni join por nombre) y resolveTrackingRows (filtra por e.id/at.employeeId) YA agrupaban por employeeId real, nunca por nombre -- no encontre el punto exacto donde se fusionaran dos ids distintos. Lo que si faltaba en los tres sitios: ninguna tarjeta/fila mostraba nada aparte del nombre, asi que dos fichas homonimas (reales, en distinta cuenta/local) eran indistinguibles a la vista aunque estuvieran correctamente separadas por debajo. Arreglo, en los dos sitios que senalo Julio: - TrainingCalendarView.tsx: cada tarjeta/fila de "Que vence" y "Que caduca" (interno y externo) anade puesto + local bajo el nombre. - coursesService.ts (TrackingRow) + CoursesPage.tsx (SeguimientoTab): cada fila de seguimiento anade puesto + area bajo el nombre. Semaforo de ficha (StaffPage.tsx) revisado: no aplica -- esta scoped a UN employee.id ya elegido al abrir esa ficha concreta, no hay lista donde mezclar homonimos. Semaforo del cuadrante (CalendarioPage.tsx) tambien revisado: cada chip es ya una persona asignada individualmente (assignedIds por celda), no hay agrupacion por nombre que pudiera fusionar dos fichas. Si el problema persiste con estos datos visibles, seria indicio de algo mas profundo (p.ej. duplicados reales de employee_id) y merece RECON aparte -- avisar si se repite tras este cambio.
1 parent b36a206 commit 58bce59

3 files changed

Lines changed: 45 additions & 8 deletions

File tree

src/components/personal/TrainingCalendarView.tsx

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export default function TrainingCalendarView() {
5959
}, [activeAccountId])
6060

6161
const empById = useMemo(() => new Map(staff.map(e => [e.id, e])), [staff])
62+
const locationById = useMemo(() => new Map(locations.map(l => [l.id, l])), [locations])
6263
const positions = useMemo(() => [...new Set(staff.map(e => e.position).filter(Boolean))].sort(), [staff])
6364

6465
function passesFilter(employeeId: string): boolean {
@@ -69,11 +70,25 @@ export default function TrainingCalendarView() {
6970
return true
7071
}
7172

73+
// Homónimos son reales (fichas distintas con el mismo nombre, en distinta
74+
// cuenta/local) — visto en producción con dos "Pamela Guzmán Velásquez"
75+
// (ids distintos, una en Sala y otra en Cocina). El Map de abajo agrupa por
76+
// employeeId (nunca por nombre: dos ids nunca comparten entrada aquí), pero
77+
// dos tarjetas con el mismo nombre y SIN nada más que las distinga eran
78+
// indistinguibles a la vista — de ahí el aviso. Cada tarjeta lleva ahora
79+
// puesto + local para que un vistazo baste.
80+
function employeeDetail(employeeId: string): string {
81+
const emp = empById.get(employeeId)
82+
if (!emp) return ''
83+
const loc = emp.locationId ? locationById.get(emp.locationId)?.name : null
84+
return [emp.position, loc].filter(Boolean).join(' · ')
85+
}
86+
7287
const venceByEmployee = useMemo(() => {
73-
const map = new Map<string, { name: string; items: TrainingGap[] }>()
88+
const map = new Map<string, { name: string; detail: string; items: TrainingGap[] }>()
7489
for (const g of gaps) {
7590
if (!VENCE_KINDS.includes(g.gapKind) || !passesFilter(g.employeeId)) continue
76-
const entry = map.get(g.employeeId) ?? { name: g.employeeName, items: [] }
91+
const entry = map.get(g.employeeId) ?? { name: g.employeeName, detail: employeeDetail(g.employeeId), items: [] }
7792
entry.items.push(g)
7893
map.set(g.employeeId, entry)
7994
}
@@ -86,7 +101,7 @@ export default function TrainingCalendarView() {
86101
if (aBlocking !== bBlocking) return aBlocking ? -1 : 1
87102
return b[1].items.length - a[1].items.length
88103
})
89-
}, [gaps, empById, locationFilter, positionFilter, blockingIds])
104+
}, [gaps, empById, locationById, locationFilter, positionFilter, blockingIds])
90105

91106
const caducaInternal = useMemo(
92107
() => gaps.filter(g => CADUCA_KINDS.includes(g.gapKind) && passesFilter(g.employeeId)),
@@ -159,12 +174,15 @@ export default function TrainingCalendarView() {
159174
<Card className="p-6 text-center text-sm text-text-secondary">Nadie tiene formación pendiente con este filtro.</Card>
160175
) : (
161176
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
162-
{venceByEmployee.map(([employeeId, { name, items }]) => {
177+
{venceByEmployee.map(([employeeId, { name, detail, items }]) => {
163178
const hasBlocking = items.some(i => blockingIds.has(i.courseId))
164179
return (
165180
<Card key={employeeId} className={`p-4 ${hasBlocking ? 'border-danger/40' : ''}`}>
166181
<div className="flex items-center justify-between gap-2">
167-
<p className="font-semibold text-text-primary truncate">{name}</p>
182+
<div className="min-w-0">
183+
<p className="font-semibold text-text-primary truncate">{name}</p>
184+
{detail && <p className="text-xs text-text-secondary truncate">{detail}</p>}
185+
</div>
168186
<Badge color={hasBlocking ? 'red' : 'gray'}>{items.length}</Badge>
169187
</div>
170188
<ul className="mt-2 space-y-1">
@@ -197,7 +215,9 @@ export default function TrainingCalendarView() {
197215
<Card key={`int-${i}`} className="p-3 flex items-center justify-between gap-3">
198216
<div className="min-w-0">
199217
<p className="text-sm text-text-primary truncate">{g.employeeName} · {g.courseTitle}</p>
200-
<p className="text-xs text-text-secondary">Formación interna</p>
218+
<p className="text-xs text-text-secondary truncate">
219+
Formación interna{employeeDetail(g.employeeId) ? ` · ${employeeDetail(g.employeeId)}` : ''}
220+
</p>
201221
</div>
202222
<Badge color={g.gapKind === 'caducado' ? 'red' : 'yellow'}>
203223
{g.gapKind === 'caducado' ? 'Caducado' : g.daysLeft != null ? `${g.daysLeft} días` : 'Caduca pronto'}
@@ -208,7 +228,9 @@ export default function TrainingCalendarView() {
208228
<Card key={`ext-${i}`} className="p-3 flex items-center justify-between gap-3">
209229
<div className="min-w-0">
210230
<p className="text-sm text-text-primary truncate">{empById.get(formation.employeeId)?.name ?? '—'} · {formation.name}</p>
211-
<p className="text-xs text-text-secondary">Certificado externo</p>
231+
<p className="text-xs text-text-secondary truncate">
232+
Certificado externo{employeeDetail(formation.employeeId) ? ` · ${employeeDetail(formation.employeeId)}` : ''}
233+
</p>
212234
</div>
213235
<Badge color={status.color === 'red' ? 'red' : status.color === 'orange' ? 'yellow' : 'yellow'}>{status.label}</Badge>
214236
</Card>

src/pages/CoursesPage.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1521,7 +1521,14 @@ function SeguimientoTab({ course, staff }: { course: Course; staff: Employee[] }
15211521
<tbody>
15221522
{rows.map((r, i) => (
15231523
<tr key={`${r.assignmentId}-${r.employeeId}-${i}`} className="border-t border-border-default">
1524-
<td className="px-3 py-2 text-text-primary">{r.employeeName}</td>
1524+
<td className="px-3 py-2 text-text-primary">
1525+
{r.employeeName}
1526+
{(r.employeePosition || r.employeeDepartment) && (
1527+
<span className="block text-xs text-text-secondary font-normal">
1528+
{[r.employeePosition, r.employeeDepartment].filter(Boolean).join(' · ')}
1529+
</span>
1530+
)}
1531+
</td>
15251532
<td className="px-3 py-2"><Badge color={TRACKING_BADGE[r.status].color}>{TRACKING_BADGE[r.status].label}</Badge></td>
15261533
<td className="px-3 py-2 text-text-secondary">{r.scorePct != null ? `${r.scorePct}%` : '—'}</td>
15271534
<td className="px-3 py-2 text-text-secondary">{r.signedAt ? new Date(r.signedAt).toLocaleString('es-ES') : '—'}</td>

src/services/coursesService.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,12 @@ export interface TrackingRow {
750750
attemptId: string | null
751751
employeeId: string
752752
employeeName: string
753+
/** Puesto/área del empleado — homónimos son reales (fichas distintas con el
754+
* mismo nombre, en distinta cuenta/local): sin esto, dos filas de personas
755+
* diferentes son indistinguibles a la vista aunque estén correctamente
756+
* separadas por employeeId. */
757+
employeePosition: string | null
758+
employeeDepartment: string | null
753759
dueAt: string | null
754760
overdue: boolean
755761
status: TrackingStatus
@@ -828,6 +834,8 @@ export function resolveTrackingRows(
828834
attemptId: lastAttempt?.id ?? null,
829835
employeeId: emp.id,
830836
employeeName: emp.name,
837+
employeePosition: emp.position || null,
838+
employeeDepartment: emp.department || null,
831839
dueAt: a.dueAt,
832840
overdue: !!a.dueAt && status !== 'firmado' && new Date(a.dueAt).getTime() < now,
833841
status,

0 commit comments

Comments
 (0)