"use client"; import { useEffect, useState } from "react"; import { UsersIcon } from "lucide-react"; import { toast } from "sonner"; import { fireStaff, setLaborHeadcount } from "@/app/(frontend)/logistics/base/actions"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Separator } from "@/components/ui/separator"; import { currencyLabel, formatAmount, type CurrencyConfig } from "@/lib/banking/format"; import type { LaborClassificationOption, StructureStaffingOption } from "@/lib/base"; import { HireStaffDialog } from "@/components/frontend/baseManagement/HireStaffDialog"; interface StaffMember { readonly id: number; readonly npcName: string | null; readonly positionType: string; readonly positionTitle: string; readonly salary: number; readonly headcount: number; readonly laborClassificationId: number | null; readonly hiredAt: string; } interface StaffRosterProps { readonly structure: StructureStaffingOption; readonly staff: readonly StaffMember[]; readonly classifications: readonly LaborClassificationOption[]; readonly npcs: readonly { id: number; name: string }[]; readonly canManage: boolean; readonly hiringEnabled: boolean; readonly hiringDisabledMessage?: string; readonly currencyConfig: CurrencyConfig; readonly onChanged?: () => void; } interface LaborCrew { readonly classificationId: number; readonly classificationName: string; headcount: number; unitSalary: number; readonly requiredHeadcount: number; readonly maxHeadcount: number; readonly enforcedSalary: number | null; } interface LaborEdit { readonly headcount: string; readonly unitSalary: string; } export function StaffRoster({ structure, staff, classifications, npcs, canManage, hiringEnabled, hiringDisabledMessage, currencyConfig, onChanged, }: StaffRosterProps) { const [staffMembers, setStaffMembers] = useState(staff); const [fireTarget, setFireTarget] = useState(null); const [loading, setLoading] = useState(false); const [laborLoading, setLaborLoading] = useState(null); const [laborEdits, setLaborEdits] = useState>({}); useEffect(() => { setStaffMembers(staff); setLaborEdits({}); }, [staff]); async function handleFire() { if (!fireTarget) return; setLoading(true); try { const result = await fireStaff(fireTarget.id); if (result.success) { toast.success(`${fireTarget.npcName} fired`); setStaffMembers((current) => current.filter((member) => member.id !== fireTarget.id)); setFireTarget(null); onChanged?.(); } else { toast.error(result.error ?? "Fire failed"); } } catch { toast.error("Fire failed"); } finally { setLoading(false); } } function getLaborEdit(crew: LaborCrew): LaborEdit { return ( laborEdits[crew.classificationId] ?? { headcount: String(crew.headcount), unitSalary: String(crew.unitSalary), } ); } function updateLaborEdit(crew: LaborCrew, field: keyof LaborEdit, value: string) { setLaborEdits((current) => { const edit = current[crew.classificationId] ?? ({ headcount: String(crew.headcount), unitSalary: String(crew.unitSalary), } satisfies LaborEdit); return { ...current, [crew.classificationId]: { ...edit, [field]: value, }, }; }); } async function handleLaborSave(crew: LaborCrew) { const edit = getLaborEdit(crew); const headcount = Number(edit.headcount); const unitSalary = Number(edit.unitSalary) || 0; if (!Number.isInteger(headcount) || headcount < 0) { toast.error("Headcount must be a whole number of 0 or more."); return; } if (!Number.isFinite(unitSalary) || unitSalary < 0) { toast.error("Wage cannot be negative."); return; } setLaborLoading(crew.classificationId); try { const result = await setLaborHeadcount( structure.id, crew.classificationId, headcount, unitSalary, ); if (!result.success) { toast.error(result.error ?? "Labor update failed"); return; } const isCrew = (member: StaffMember) => member.positionType === "labor" && member.npcName === null && member.laborClassificationId === crew.classificationId; const requirement = structure.laborRequirements.find( (entry) => entry.classificationId === crew.classificationId, ); const effectiveUnitSalary = requirement?.enforcedSalary ?? unitSalary; if (headcount === 0) { setStaffMembers((current) => current.filter((member) => !isCrew(member))); toast.success(`${crew.classificationName} crew released`); } else { setStaffMembers((current) => { let keptCrew = false; return current.flatMap((member) => { if (!isCrew(member)) return [member]; if (keptCrew) return []; keptCrew = true; return [{ ...member, headcount, salary: effectiveUnitSalary }]; }); }); toast.success(`${crew.classificationName} crew updated`); } setLaborEdits((current) => { const next = { ...current }; delete next[crew.classificationId]; return next; }); onChanged?.(); } catch { toast.error("Labor update failed"); } finally { setLaborLoading(null); } } const namedStaff = staffMembers.filter( (member) => !(member.positionType === "labor" && member.npcName === null), ); const laborMembers = staffMembers.filter( (member) => member.positionType === "labor" && member.npcName === null, ); const laborCrews = structure.laborRequirements.flatMap((requirement) => { const classification = classifications.find( (entry) => entry.id === requirement.classificationId, ); if (!classification) return []; const members = laborMembers.filter( (member) => member.laborClassificationId === classification.id, ); return [ { classificationId: classification.id, classificationName: classification.name, headcount: members.reduce((sum, member) => sum + member.headcount, 0), unitSalary: requirement.enforcedSalary ?? members[0]?.salary ?? classification.defaultUnitSalary ?? 0, requiredHeadcount: requirement.requiredHeadcount, maxHeadcount: requirement.maxHeadcount, enforcedSalary: requirement.enforcedSalary, }, ]; }); const showLaborSection = laborMembers.length > 0 || structure.laborRequirements.length > 0 || (canManage && structure.staffPositions.length > 0); return ( <> Staff {staffMembers.length} active assignment{staffMembers.length === 1 ? "" : "s"}. {structure.maintenanceCost > 0 && (

Maintenance: {formatAmount(structure.maintenanceCost, currencyConfig)}/tick

)}
{namedStaff.length === 0 && !showLaborSection ? (

No staff assigned to this structure.

) : (
{namedStaff.length > 0 && (
{namedStaff.map((member, index) => (
{index > 0 && }

{member.npcName}

{member.positionTitle} · {member.positionType}

{formatAmount(member.salary, currencyConfig)}/tick {canManage && ( )}
))}
)} {showLaborSection && ( <> {namedStaff.length > 0 && }

Labor crew

Classification-based workers assigned to this structure.

{canManage && hiringEnabled && ( )}
{!hiringEnabled && canManage && (

{hiringDisabledMessage || "Staff hiring is currently disabled."}

)} {laborCrews.length === 0 ? (

No labor crew — add workers via Hire Staff.

) : (
{laborCrews.map((crew, index) => { const edit = getLaborEdit(crew); const crewIsLoading = laborLoading === crew.classificationId; return (
{index > 0 && }

{crew.classificationName} — {crew.headcount} hired ·{" "} {crew.requiredHeadcount} required · max {crew.maxHeadcount}

{formatAmount(crew.unitSalary, currencyConfig)}/tick each ·{" "} {formatAmount( crew.unitSalary * crew.headcount, currencyConfig, )} /tick total

{canManage && (
updateLaborEdit(crew, "headcount", event.target.value) } disabled={crewIsLoading || !hiringEnabled} />
updateLaborEdit(crew, "unitSalary", event.target.value) } disabled={ crewIsLoading || !hiringEnabled || crew.enforcedSalary !== null } />
)}
); })}
)}
)}
)}
{ if (!open && !loading) setFireTarget(null); }} > Fire {fireTarget?.npcName}? This ends their assignment. ); }