"use client"; import { useState } from "react"; import { UsersIcon } from "lucide-react"; import { toast } from "sonner"; import { hireStaff, setLaborHeadcount } from "@/app/(frontend)/logistics/base/actions"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { useRouter } from "next/navigation"; import type { LaborClassificationOption, StructureStaffingOption } from "@/lib/base"; import { currencyLabel, type CurrencyConfig } from "@/lib/banking/format"; interface HireStaffDialogProps { readonly structures: readonly StructureStaffingOption[]; readonly npcs: readonly { id: number; name: string }[]; readonly fixedNpcId?: number | null; readonly defaultStructureId?: number | null; readonly canManage: boolean; readonly triggerLabel?: string; readonly classifications?: readonly LaborClassificationOption[]; readonly hiringEnabled: boolean; readonly hiringDisabledMessage?: string; readonly currencyConfig: CurrencyConfig; } const POSITION_TYPES = [ { value: "administrative", label: "Administrative" }, { value: "technical", label: "Technical" }, { value: "other", label: "Other" }, ] as const; export function HireStaffDialog({ structures, npcs, fixedNpcId, defaultStructureId, canManage, triggerLabel = "Hire staff", classifications = [], hiringEnabled, hiringDisabledMessage, currencyConfig, }: HireStaffDialogProps) { const defaultStructure = structures.find((structure) => structure.id === defaultStructureId); const initialRequirement = defaultStructure?.laborRequirements[0]; const initialClassification = classifications.find( (classification) => classification.id === initialRequirement?.classificationId, ); const initialPositionType = fixedNpcId != null || !initialRequirement ? (defaultStructure?.staffPositions[0]?.positionType ?? "administrative") : "labor"; const initialPositionRule = defaultStructure?.staffPositions.find( (rule) => rule.positionType === initialPositionType, ); const initialSalary = initialPositionType === "labor" ? (initialRequirement?.enforcedSalary ?? initialClassification?.defaultUnitSalary ?? null) : (initialPositionRule?.enforcedSalary ?? null); const router = useRouter(); const [open, setOpen] = useState(false); const [npcId, setNpcId] = useState(fixedNpcId != null ? String(fixedNpcId) : ""); const [structureId, setStructureId] = useState( defaultStructureId != null ? String(defaultStructureId) : "", ); const [positionType, setPositionType] = useState(initialPositionType); const [positionTitle, setPositionTitle] = useState(""); const [laborClassification, setLaborClassification] = useState(() => initialRequirement ? String(initialRequirement.classificationId) : "", ); const [headcount, setHeadcount] = useState("1"); const [salary, setSalary] = useState(initialSalary !== null ? String(initialSalary) : ""); const [salaryDirty, setSalaryDirty] = useState(false); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); if (!canManage) return null; if (!hiringEnabled) { return ( ); } const fixedNpc = fixedNpcId != null ? npcs.find((npc) => npc.id === fixedNpcId) : null; const selectedStructure = structures.find((structure) => String(structure.id) === structureId); const approvedClassifications = classifications.filter((classification) => selectedStructure?.laborRequirements.some( (requirement) => requirement.classificationId === classification.id, ), ); const approvedPositionTypes = POSITION_TYPES.filter((position) => selectedStructure?.staffPositions.some((rule) => rule.positionType === position.value), ); const selectedClassification = classifications.find( (classification) => String(classification.id) === laborClassification, ); const selectedRequirement = selectedStructure?.laborRequirements.find( (requirement) => String(requirement.classificationId) === laborClassification, ); const selectedPositionRule = selectedStructure?.staffPositions.find( (rule) => rule.positionType === positionType, ); const salaryValue = Number(salary) || 0; const marketRate = selectedClassification?.defaultUnitSalary ?? 0; const effectiveAvailable = selectedClassification ? Math.floor( selectedClassification.availableHeadcount * (marketRate > 0 ? Math.min(1, salaryValue / marketRate) : 1), ) : 0; const laborHeadcountMax = selectedRequirement ? selectedRequirement.currentHeadcount + Math.min( Math.max(0, selectedRequirement.maxHeadcount - selectedRequirement.currentHeadcount), effectiveAvailable, ) : 0; function reset() { setNpcId(fixedNpcId != null ? String(fixedNpcId) : ""); setStructureId(defaultStructureId != null ? String(defaultStructureId) : ""); setPositionType(initialPositionType); setPositionTitle(""); setLaborClassification(initialRequirement ? String(initialRequirement.classificationId) : ""); setHeadcount("1"); setSalary(initialSalary !== null ? String(initialSalary) : ""); setSalaryDirty(false); setError(null); } function handleClassificationChange(value: string) { setLaborClassification(value); const requirement = selectedStructure?.laborRequirements.find( (entry) => String(entry.classificationId) === value, ); if (requirement?.enforcedSalary !== null && requirement?.enforcedSalary !== undefined) { setSalary(String(requirement.enforcedSalary)); setSalaryDirty(false); return; } if (salaryDirty) return; const classification = classifications.find((c) => String(c.id) === value); if (classification && classification.defaultUnitSalary != null) { setSalary(String(classification.defaultUnitSalary)); } else { setSalary(""); } } function handleStructureChange(value: string) { setStructureId(value); const structure = structures.find((entry) => String(entry.id) === value); const firstRequirement = structure?.laborRequirements[0]; const firstPosition = structure?.staffPositions[0]; if (fixedNpcId != null) { setPositionType(firstPosition?.positionType ?? "administrative"); setSalary( firstPosition?.enforcedSalary !== null && firstPosition?.enforcedSalary !== undefined ? String(firstPosition.enforcedSalary) : "", ); } else { setPositionType(firstRequirement ? "labor" : (firstPosition?.positionType ?? "labor")); setLaborClassification(firstRequirement ? String(firstRequirement.classificationId) : ""); const classification = classifications.find( (entry) => entry.id === firstRequirement?.classificationId, ); setSalary( firstRequirement?.enforcedSalary !== null && firstRequirement?.enforcedSalary !== undefined ? String(firstRequirement.enforcedSalary) : classification?.defaultUnitSalary != null ? String(classification.defaultUnitSalary) : "", ); } setSalaryDirty(false); } function handlePositionTypeChange(value: string) { setPositionType(value); if (value === "labor") { handleClassificationChange(laborClassification); return; } const rule = selectedStructure?.staffPositions.find((entry) => entry.positionType === value); setSalary( rule?.enforcedSalary !== null && rule?.enforcedSalary !== undefined ? String(rule.enforcedSalary) : "", ); setSalaryDirty(false); } async function handleSubmit() { const selectedStructureId = Number(structureId); if (positionType === "labor") { if (!Number.isInteger(selectedStructureId) || selectedStructureId <= 0) { setError("Select a structure."); return; } if (!laborClassification) { setError("Select a classification."); return; } const headcountValue = Number(headcount); if (!Number.isInteger(headcountValue) || headcountValue < 1) { setError("Headcount must be a whole number of at least 1."); return; } if (!Number.isFinite(salaryValue) || salaryValue < 0) { setError("Wage cannot be negative."); return; } setLoading(true); setError(null); try { const result = await setLaborHeadcount( selectedStructureId, Number(laborClassification), headcountValue, salaryValue, ); if (result.success) { const label = classifications.find((c) => String(c.id) === laborClassification)?.name ?? "Labor"; toast.success(`${label} crew set to ${headcountValue}`); setOpen(false); reset(); router.refresh(); } else { const message = result.error ?? "Labor update failed"; setError(message); toast.error(message); } } catch { toast.error("Labor update failed"); } finally { setLoading(false); } return; } const selectedNpcId = fixedNpcId ?? Number(npcId); const title = positionTitle.trim(); if (!Number.isInteger(selectedNpcId) || selectedNpcId <= 0) { setError("Select an NPC."); return; } if (!Number.isInteger(selectedStructureId) || selectedStructureId <= 0) { setError("Select a structure."); return; } if (!title) { setError("Enter a position title."); return; } if (!Number.isFinite(salaryValue) || salaryValue < 0) { setError("Salary cannot be negative."); return; } setLoading(true); setError(null); try { const result = await hireStaff({ npcId: selectedNpcId, structureId: selectedStructureId, positionType, positionTitle: title, salary: salaryValue, }); if (result.success) { const name = fixedNpc?.name ?? npcs.find((npc) => npc.id === selectedNpcId)?.name; toast.success(`${name ?? `NPC #${selectedNpcId}`} hired as ${title}`); setOpen(false); reset(); router.refresh(); } else { const message = result.error ?? "Hire failed"; setError(message); toast.error(message); } } catch { toast.error("Hire failed"); } finally { setLoading(false); } } return ( { setOpen(nextOpen); if (!nextOpen) reset(); }} > Hire staff {positionType === "labor" ? "Set a classification-based labor crew for a structure." : "Assign an NPC to a structure and define their role."}
{positionType !== "labor" && (
{fixedNpcId != null ? (

{fixedNpc?.name ?? `NPC #${fixedNpcId}`}

) : ( )}
)}
{selectedStructure && approvedClassifications.length === 0 && approvedPositionTypes.length === 0 && (

This structure has no approved staff positions.

)}
{positionType === "labor" ? ( <>
{selectedStructure && approvedClassifications.length === 0 && (

This structure has no approved labor positions.

)}
setHeadcount(event.target.value)} placeholder="Number of workers" disabled={!selectedRequirement || laborHeadcountMax < 1} /> {selectedRequirement && (

{selectedRequirement.currentHeadcount} hired ·{" "} {selectedRequirement.requiredHeadcount} required · max{" "} {selectedRequirement.maxHeadcount}

)}
{ setSalaryDirty(true); setSalary(event.target.value); }} placeholder="Per worker per base tick" disabled={selectedRequirement?.enforcedSalary != null} /> {selectedRequirement?.enforcedSalary != null && (

Wage enforced by this structure.

)} {selectedClassification && effectiveAvailable < selectedClassification.availableHeadcount && (

Only {effectiveAvailable} of {selectedClassification.availableHeadcount}{" "} {selectedClassification.name} workers are willing to work at this wage.

)}
) : ( <>
setPositionTitle(event.target.value)} placeholder="e.g. Head Surgeon, Site Director" />
{ setSalaryDirty(true); setSalary(event.target.value); }} placeholder="Per base tick" disabled={selectedPositionRule?.enforcedSalary != null} /> {selectedPositionRule?.enforcedSalary != null && (

Salary enforced by this structure.

)} {selectedPositionRule?.max != null && (

{selectedPositionRule.currentHeadcount} of {selectedPositionRule.max} positions filled.

)}
)} {error &&

{error}

}
); }