@@ -14,7 +14,8 @@ import {
1414 Infinity ,
1515 X ,
1616} from 'lucide-react'
17- import { Card , Button , Input , Select , Label , Textarea } from '../ui'
17+ import { Card , Button , Badge , Input , Select , Label , Textarea } from '../ui'
18+ import { useApp } from '../../context/AppContext'
1819import type { Employee } from '../../types'
1920import type { Formation , FormationType } from '../../types/personal'
2021import { FORMATION_CATALOG } from '../../types/personal'
@@ -25,6 +26,8 @@ import {
2526 deleteFormation ,
2627 getFormationStatus ,
2728} from '../../services/formationsService'
29+ import * as trainingComplianceService from '../../services/trainingComplianceService'
30+ import type { TrainingCellState } from '../../services/trainingComplianceService'
2831
2932interface Props {
3033 employee : Employee
@@ -89,9 +92,13 @@ export default function FormacionesTab({ employee }: Props) {
8992 </ div >
9093 </ Card >
9194
95+ { /* Formación interna (C1/C2): cursos impartidos por Folvy con test y firma.
96+ NO toca employee_formations ni la lista de abajo — es una sección aparte. */ }
97+ < InternalTrainingSection employee = { employee } />
98+
9299 < div className = "flex items-center justify-between" >
93100 < p className = "text-xs text-text-secondary" >
94- { formations . length } formación{ formations . length !== 1 ? 'es' : '' }
101+ { formations . length } formación{ formations . length !== 1 ? 'es' : '' } externa { formations . length !== 1 ? 's' : '' }
95102 </ p >
96103 < Button size = "sm" onClick = { ( ) => { setEditing ( null ) ; setShowModal ( true ) } } >
97104 + Añadir formación
@@ -197,6 +204,81 @@ export default function FormacionesTab({ employee }: Props) {
197204 )
198205}
199206
207+ /* =====================================================
208+ FORMACIÓN INTERNA (C1/C2) — cursos impartidos por Folvy
209+ ===================================================== */
210+
211+ const INTERNAL_STATE_LABEL : Record < TrainingCellState , string > = {
212+ vigente : 'Vigente' , caducado : 'Caducado' , pendiente : 'Pendiente' , en_curso : 'En curso (sin firmar)' , no_aplica : 'No aplica' ,
213+ }
214+ const INTERNAL_STATE_COLOR : Record < TrainingCellState , string > = {
215+ vigente : 'green' , caducado : 'red' , pendiente : 'gray' , en_curso : 'yellow' , no_aplica : 'gray' ,
216+ }
217+
218+ function InternalTrainingSection ( { employee } : { employee : Employee } ) {
219+ const { activeAccountId } = useApp ( )
220+ const [ courses , setCourses ] = useState < { code : string ; title : string ; state : TrainingCellState ; scorePct : number | null ; expiresAt : string | null } [ ] > ( [ ] )
221+ const [ loading , setLoading ] = useState ( true )
222+ const [ error , setError ] = useState ( false )
223+
224+ useEffect ( ( ) => {
225+ if ( ! activeAccountId ) { setLoading ( false ) ; return }
226+ let cancel = false
227+ setLoading ( true )
228+ setError ( false )
229+ Promise . all ( [
230+ trainingComplianceService . getTrainingComplianceMatrix ( activeAccountId , employee . locationId , false ) ,
231+ trainingComplianceService . getTrainingCourseSummary ( activeAccountId ) ,
232+ ] )
233+ . then ( ( [ matrix , summary ] ) => {
234+ if ( cancel ) return
235+ const row = matrix . find ( ( r ) => r . employeeId === employee . id )
236+ const titleByCode = new Map ( summary . map ( ( c ) => [ c . courseCode , c . courseTitle ] ) )
237+ const list = Object . entries ( row ?. courses ?? { } )
238+ . filter ( ( [ , cell ] ) => cell . state !== 'no_aplica' )
239+ . map ( ( [ code , cell ] ) => ( {
240+ code,
241+ title : titleByCode . get ( code ) ?? code ,
242+ state : cell . state ,
243+ scorePct : cell . scorePct ,
244+ expiresAt : cell . expiresAt ,
245+ } ) )
246+ setCourses ( list )
247+ } )
248+ . catch ( ( ) => { if ( ! cancel ) setError ( true ) } )
249+ . finally ( ( ) => { if ( ! cancel ) setLoading ( false ) } )
250+ return ( ) => { cancel = true }
251+ } , [ activeAccountId , employee . id , employee . locationId ] )
252+
253+ return (
254+ < Card className = "p-3" >
255+ < p className = "text-sm font-semibold text-text-primary inline-flex items-center gap-1.5 mb-2" >
256+ < GraduationCap size = { 14 } /> Formación interna (Folvy)
257+ </ p >
258+ { loading ? (
259+ < p className = "text-xs text-text-secondary" > Cargando…</ p >
260+ ) : error ? (
261+ < p className = "text-xs text-danger" > No se pudo cargar la formación interna.</ p >
262+ ) : courses . length === 0 ? (
263+ < p className = "text-xs text-text-secondary" > Sin cursos internos asignados todavía.</ p >
264+ ) : (
265+ < div className = "space-y-1.5" >
266+ { courses . map ( ( c ) => (
267+ < div key = { c . code } className = "flex items-center justify-between gap-2 text-xs" >
268+ < span className = "text-text-primary" > { c . title } </ span >
269+ < div className = "flex items-center gap-2 shrink-0" >
270+ { c . scorePct != null && < span className = "text-text-secondary" > { c . scorePct } %</ span > }
271+ { c . expiresAt && < span className = "text-text-secondary" > hasta { new Date ( c . expiresAt ) . toLocaleDateString ( 'es-ES' ) } </ span > }
272+ < Badge color = { INTERNAL_STATE_COLOR [ c . state ] } > { INTERNAL_STATE_LABEL [ c . state ] } </ Badge >
273+ </ div >
274+ </ div >
275+ ) ) }
276+ </ div >
277+ ) }
278+ </ Card >
279+ )
280+ }
281+
200282/* =====================================================
201283 MODAL DE EDICIÓN / ALTA
202284 ===================================================== */
0 commit comments