Skip to content

Commit ac40f8b

Browse files
authored
Merge pull request #30 from Llorente29/fix/formacion-homonimos-agrupacion
fix(formacion): distinguir homonimos en calendario y seguimiento del …
2 parents b36a206 + 58bce59 commit ac40f8b

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)