"use client"; import { useState, useRef, useEffect, useTransition } from "react"; import { useRouter } from "next/navigation"; import { UserRound, UserPlus, X, Check, TriangleAlert, Loader2 } from "lucide-react"; import { ScrollFade } from "@/components/ScrollFade"; import type { OperatorRow } from "@/lib/rows"; import { ZAR2 } from "@/lib/utils"; const ACCENT = "#7A9E06"; // the profit-line green — operators are people, not machines export function OperatorsView({ rows }: { rows: OperatorRow[] }) { const router = useRouter(); const [pending, startTransition] = useTransition(); const [savedId, setSavedId] = useState(null); const [assigningId, setAssigningId] = useState(null); const [adding, setAdding] = useState(false); const [addingBusy, setAddingBusy] = useState(false); const scroller = useRef(null); const [overflows, setOverflows] = useState(false); useEffect(() => { const el = scroller.current; if (!el) return; const check = () => setOverflows(el.scrollWidth > el.clientWidth + 1); check(); const ro = new ResizeObserver(check); ro.observe(el); return () => ro.disconnect(); }, [rows.length]); async function allocate(operatorId: string, machineId: string | null) { startTransition(async () => { try { const res = await fetch(`/api/operators/${operatorId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ assetId: machineId }), }); const body = await res.json(); if (body.ok) { setSavedId(operatorId); setTimeout(() => setSavedId((s) => (s === operatorId ? null : s)), 1600); if (body.sharedWith) { alert( `Heads up — ${body.operator.name} now shares ${body.operator.asset.code} with ${body.sharedWith.name}.`, ); } router.refresh(); } else { alert(body.error || "Could not save"); } } catch { alert("Could not save — check your connection"); } }); } async function addOperator(e: React.FormEvent) { e.preventDefault(); const fd = new FormData(e.currentTarget); setAddingBusy(true); try { const res = await fetch("/api/operators", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: fd.get("name"), employeeCode: fd.get("employeeCode"), rate: fd.get("rate"), assetId: fd.get("assetId") || "", }), }); const body = await res.json(); if (body.ok) { setAdding(false); router.refresh(); } else { alert(body.error || "Could not add the operator"); } } catch { alert("Could not save — check your connection"); } finally { setAddingBusy(false); } } const machineRows = rows.filter((r) => !r.isUnassignedOperator); const unassignedRows = rows.filter((r) => r.isUnassignedOperator); const assigningRow = assigningId ? rows.find((r) => r.operatorId === assigningId) ?? null : null; // EVERY machine is offered, not just the staffed ones. Filtering to staffed rows // here meant the picker could not offer an EMPTY machine — so you could move an // operator around but never staff one, which is the opposite of the point. const options = machineRows; return (
{machineRows.length} machines {machineRows.filter((r) => !r.operatorId).length} with no operator {unassignedRows.length > 0 && ( {unassignedRows.length} operator{unassignedRows.length !== 1 && "s"} unallocated )}
{!adding && ( )} {adding && (
Add an operator

Added here, this person is not on the operator sheet — they will be kept when the seed runs again.

)} {/* Outside the table on purpose: inside a colSpan row it inherits the table's full width and puts the controls ~600px off-screen on a 375px phone. */} {assigningRow && (
{assigningRow.operatorName} {assigningRow.employeeCode} · {ZAR2(assigningRow.rate ?? 0)} / hour
{ allocate(assigningRow.operatorId!, machineId); setAssigningId(null); }} />
)}
{machineRows.map((r) => ( ))} {unassignedRows.length > 0 && ( )} {unassignedRows.map((r) => ( ))}
Code Machine Operator Employee code Rate per hour Allocate an operatorAllocate
{r.machineCode} {r.machineName} {r.operatorId ? ( {r.operatorName} ) : ( )} {r.employeeCode || } {r.rate !== null ? ZAR2(r.rate) : } {r.operatorId ? ( setAssigningId(assigningId === r.operatorId ? null : r.operatorId)} /> ) : ( /* No operator to move from this row. You staff an empty machine by allocating one of the unallocated operators at the bottom of the table — saying "pick someone" here would point at the machine column, which holds no names. */ — )}
Operators with no machine
{r.operatorName} {r.employeeCode} {r.rate !== null ? ZAR2(r.rate) : } setAssigningId(assigningId === r.operatorId ? null : r.operatorId)} />
{overflows && }
); } /** Blank means "nobody here yet" — never a zero and never a dash that reads as zero. */ function Blank() { return none; } function RowButton({ label, onClick, busy }: { label: string; onClick: () => void; busy: boolean }) { return ( ); } function AssignPicker({ row, options, pending, onAllocate, }: { row: OperatorRow; options: OperatorRow[]; pending: boolean; onAllocate: (machineId: string | null) => void; }) { const [value, setValue] = useState(row.machineId ?? ""); return (
Two people on one machine is allowed — you will be told if you create one.
); }