2026-10-22 09:39:18 +00:00
'use client' ;
import { useMemo , useState } from 'react' ;
import Link from 'next/link' ;
import { useRouter } from 'next/navigation' ;
2026-10-22 10:54:59 +00:00
import { ChevronRight , Download , Truck , UserPlus , X } from 'lucide-react' ;
2026-10-22 09:39:18 +00:00
import type { WorkforceData } from '@/lib/workforce-store' ;
import { attendanceSummary , employeeSlug } from '@/lib/workforce' ;
2026-10-22 10:54:59 +00:00
import { buildBadgesPdf , downloadPdf } from '@/lib/qr-pdf' ;
2026-10-22 09:39:18 +00:00
import { Kpi , StatusChip , rateText } from './WorkforceBits' ;
/ * *
* The people register . Every operator and mechanic , with the machine an operator runs
* and their rate — the information that used to live only on / running / operators .
* /
export function EmployeesView ( { data } : { data :WorkforceData } ) {
const router = useRouter ( ) ;
const [ query , setQuery ] = useState ( '' ) ;
const [ role , setRole ] = useState ( 'all' ) ;
const [ adding , setAdding ] = useState ( false ) ;
const [ kind , setKind ] = useState < 'operators' | 'mechanics' > ( 'operators' ) ;
const [ busy , setBusy ] = useState ( false ) ;
const [ error , setError ] = useState ( '' ) ;
2026-10-22 10:54:59 +00:00
const [ building , setBuilding ] = useState ( false ) ;
const mechanics = data . employees . filter ( e = > e . kind === 'mechanics' && e . employeeNo . trim ( ) ) ;
async function downloadBadges() {
setBuilding ( true ) ;
try { downloadPdf ( await buildBadgesPdf ( mechanics . map ( m = > ( { name :m.name , employee_code :m.employeeNo , role :m.role } ) ) ) , 'mega-plant-mechanic-badges.pdf' ) ; }
catch { alert ( 'Could not build the PDF. Please try again.' ) ; }
finally { setBuilding ( false ) ; }
}
2026-10-22 09:39:18 +00:00
const status = useMemo ( ( ) = > {
const byEmployee = new Map < string , typeof data.events > ( ) ;
for ( const e of data . events ) { const l = byEmployee . get ( e . employee ) ? ? [ ] ; l . push ( e ) ; byEmployee . set ( e . employee , l ) ; }
return new Map ( data . employees . map ( e = > [ e . id , attendanceSummary ( byEmployee . get ( e . id ) ? ? [ ] , data . asOf ) . status ] ) ) ;
} , [ data ] ) ;
const staffed = new Set ( data . employees . map ( e = > e . machine ? . id ) . filter ( Boolean ) ) ;
// Same scope as the old operator register: machines on the operator sheet.
const emptyMachines = data . machines . filter ( m = > m . onSheet && ! staffed . has ( m . id ) ) ;
const operators = data . employees . filter ( e = > e . kind === 'operators' ) ;
const filtered = data . employees . filter ( e = > ` ${ e . name } ${ e . employeeNo } ${ e . role } ${ e . machine ? . code ? ? '' } ${ e . machine ? . name ? ? '' } ` . toLowerCase ( ) . includes ( query . toLowerCase ( ) )
&& ( role === 'all' || ( role === 'operators' ? e . kind === 'operators' : role === 'mechanics' ? e . kind === 'mechanics' : e . kind === 'operators' && ! e . machine ) ) ) ;
async function add ( ev :React.FormEvent < HTMLFormElement > ) {
ev . preventDefault ( ) ; const fd = new FormData ( ev . currentTarget ) ; setBusy ( true ) ; setError ( '' ) ;
try {
const body = kind === 'operators'
? { name :fd.get ( 'name' ) , employeeCode :fd.get ( 'employeeCode' ) , rate :fd.get ( 'rate' ) , assetId :fd.get ( 'assetId' ) || '' }
: { name :fd.get ( 'name' ) , employeeCode :fd.get ( 'employeeCode' ) , role :fd.get ( 'role' ) || 'Mechanic' } ;
const r = await fetch ( ` /api/ ${ kind } ` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' } , body :JSON.stringify ( body ) } ) ;
const out = await r . json ( ) ;
if ( ! out . ok ) throw new Error ( out . error || 'Could not add the employee.' ) ;
setAdding ( false ) ; router . refresh ( ) ;
} catch ( e ) { setError ( e instanceof Error ? e . message : 'Could not save — check your connection.' ) ; }
finally { setBusy ( false ) ; }
}
return < section className = "plant-clock-page p-4 sm:p-6 lg:p-8 max-w-7xl mx-auto" >
< div className = "flex flex-wrap justify-between items-start gap-4 mb-6" >
< div > < p className = "plant-clock-eyebrow" > MEGA PLANT / WORKFORCE < / p > < h1 className = "text-3xl font-semibold tracking-tight mt-2" > Employees < / h1 >
< p className = "text-sm text-text-muted mt-2" > Everyone on the Plant roster , the machine they run and their rate . Open someone for their dashboard . < / p > < / div >
2026-10-22 10:54:59 +00:00
< div className = "flex flex-wrap gap-2" >
{ mechanics . length > 0 && < button className = "plant-clock-button plant-clock-secondary" disabled = { building } onClick = { downloadBadges } > < Download size = { 18 } / > { building ? 'Building PDF…' : 'Download mechanic badges' } < / button > }
2026-10-22 09:39:18 +00:00
{ ! adding && < button className = "plant-clock-button plant-clock-primary" onClick = { ( ) = > { setError ( '' ) ; setAdding ( true ) ; } } > < UserPlus size = { 18 } / > Add employee < / button > }
2026-10-22 10:54:59 +00:00
< / div >
2026-10-22 09:39:18 +00:00
< / div >
< div className = "grid grid-cols-2 lg:grid-cols-4 gap-2 sm:gap-4 mb-5" >
< Kpi label = "Employees" value = { data . employees . length } / >
< Kpi label = "At work now" value = { [ . . . status . values ( ) ] . filter ( s = > s === 'in' ) . length } / >
< Kpi label = "Operators on a machine" value = { ` ${ operators . filter ( o = > o . machine ) . length } / ${ operators . length } ` } / >
< Kpi label = "Machines with no operator" value = { emptyMachines . length } / >
< / div >
{ adding && < form onSubmit = { add } className = "plant-clock-panel p-4 sm:p-5 mb-5" >
< div className = "flex justify-between items-center gap-3 mb-4" > < h2 className = "text-lg font-semibold" > Add an employee < / h2 >
< button type = "button" aria-label = "Close" className = "plant-clock-button plant-clock-secondary" onClick = { ( ) = > setAdding ( false ) } > < X size = { 16 } / > < / button > < / div >
< div className = "grid gap-3 sm:grid-cols-2 lg:grid-cols-4 items-end" >
< label className = "text-sm" > Type < select className = "plant-clock-input mt-2" value = { kind } onChange = { e = > setKind ( e . target . value as typeof kind ) } > < option value = "operators" > Operator < / option > < option value = "mechanics" > Mechanic < / option > < / select > < / label >
< label className = "text-sm" > Name < input name = "name" required className = "plant-clock-input mt-2" placeholder = "e.g. SIPHO NKOSI" / > < / label >
< label className = "text-sm" > Employee number < input name = "employeeCode" required className = "plant-clock-input mt-2" placeholder = "e.g. MCW201" autoCapitalize = "characters" / > < / label >
{ kind === 'operators' ? < >
< label className = "text-sm" > Rate per hour ( R ) < input name = "rate" required type = "number" min = "0" step = "0.01" inputMode = "decimal" className = "plant-clock-input mt-2" / > < / label >
< label className = "text-sm sm:col-span-2" > Machine < select name = "assetId" className = "plant-clock-input mt-2" defaultValue = "" > < option value = "" > No machine yet < / option >
{ data . machines . filter ( m = > m . onSheet ) . map ( m = > < option key = { m . id } value = { m . id } > { m . code } — { m . name } < / option > ) } < / select > < / label >
< / > : < label className = "text-sm" > Role < input name = "role" className = "plant-clock-input mt-2" placeholder = "Mechanic" / > < / label > }
< button disabled = { busy } className = "plant-clock-button plant-clock-primary" > { busy ? 'Saving…' : 'Add employee' } < / button >
< / div >
< p className = "text-xs text-text-muted mt-3" > The employee number is what their QR badge encodes , so it must be unique . < / p >
{ error && < p role = "alert" className = "plant-clock-warning mt-3" > { error } < / p > }
< / form > }
< div className = "plant-clock-panel p-4 flex flex-wrap items-end gap-3 mb-5" >
< label className = "flex-1 min-w-0 basis-48 text-sm" > Find an employee < input className = "plant-clock-input mt-2" placeholder = "Name, number or machine" value = { query } onChange = { e = > setQuery ( e . target . value ) } / > < / label >
< label className = "text-sm flex-1 min-w-0 basis-40" > Show < select className = "plant-clock-input mt-2" value = { role } onChange = { e = > setRole ( e . target . value ) } >
< option value = "all" > Everyone < / option > < option value = "operators" > Operators < / option > < option value = "mechanics" > Mechanics < / option > < option value = "nomachine" > Operators with no machine < / option > < / select > < / label >
< / div >
< div className = "grid gap-3 md:grid-cols-2 xl:grid-cols-3" >
{ filtered . map ( e = > < Link key = { e . id } href = { ` /employees/ ${ employeeSlug ( e . id ) } ` } className = "plant-clock-panel p-4 sm:p-5 min-w-0 block hover:border-[#30638E] transition-colors" >
< div className = "flex justify-between items-start gap-3" > < span className = "text-xs font-medium text-text-muted break-all" > { e . employeeNo || 'No employee number' } < / span > < StatusChip status = { status . get ( e . id ) ? ? 'away' } / > < / div >
< h2 className = "text-lg font-semibold mt-3 break-words" > { e . name } < / h2 >
< p className = "text-sm text-text-muted mt-0.5" > { e . role } { e . kind === 'operators' ? ` · ${ rateText ( e . rate ) } ` : '' } < / p >
< div className = "border-t border-border mt-3 pt-3 flex items-center gap-2 text-sm" >
< Truck size = { 16 } className = "shrink-0 text-text-muted" / >
< span className = "min-w-0 flex-1 truncate" > { e . machine ? < > < strong > { e . machine . code } < / strong > · { e . machine . name } < / > : e . kind === 'operators' ? < span className = "rounded px-1.5 py-0.5 bg-[#EDAE49]/20 text-[#003D5B] dark:text-white" > No machine allocated < / span > : < span className = "text-text-muted" > Not on a machine < / span > } < / span >
< ChevronRight size = { 18 } className = "shrink-0 text-text-muted" / >
< / div >
< / Link > ) }
< / div >
{ ! filtered . length && < div className = "plant-clock-panel p-8 text-center text-sm text-text-muted" > No employees match . < / div > }
{ emptyMachines . length > 0 && < div className = "plant-clock-panel mt-6 p-4 sm:p-5" >
< h2 className = "text-base font-semibold" > Machines with no operator < / h2 >
< p className = "text-xs text-text-muted mt-1" > On the operator sheet but nobody allocated . Open an operator to put them on one . < / p >
< ul className = "flex flex-wrap gap-2 mt-3" > { emptyMachines . map ( m = > < li key = { m . id } className = "plant-clock-status" > < strong className = "mr-1" > { m . code } < / strong > { m . name } < / li > ) } < / ul >
< / div > }
< / section > ;
}