"use client"; import { useState, useMemo, useEffect } from "react"; import { Search, ArrowUpDown, AlertTriangle, Pencil, Check, Power, Trash2, Loader2, X } from "lucide-react"; import { cn } from "@/lib/utils"; // The register view only carries what it renders. `supplier`, // `monthly_excl`, `monthly_incl`, and `purchase_value` still live on the // PocketBase `assets` collection and are used by other pages — they are just // not columns on this table, so they aren't sent here. type Asset = { id: string; code: string; name: string; category: string; type: string; is_provisional: boolean; is_active: boolean; /** Sites id this machine is parked at, or "" when unassigned. */ site: string; }; type Site = { id: string; name: string; code: string; is_active: boolean; }; type SortKey = "type" | "code" | "name" | "site"; type StatusFilter = "all" | "active" | "inactive"; type EditableField = "code" | "name" | "type"; type Impact = { asset: { id: string; code: string; name: string }; counts: Record; total: number; }; // Cells the user can edit inline, and how wide each should render. const EDITABLE: { field: EditableField; label: string; placeholder: string; mono?: boolean }[] = [ { field: "code", label: "Code", placeholder: "e.g. TLB08", mono: true }, { field: "name", label: "Name", placeholder: "Machine name" }, { field: "type", label: "Type", placeholder: "e.g. Backhoe Loader" }, ]; export function PlantRegisterTable({ assets: initialAssets, sites = [], }: { assets: Asset[]; sites?: Site[]; }) { const [assets, setAssets] = useState(initialAssets); const [query, setQuery] = useState(""); const [typeFilter, setTypeFilter] = useState("All"); const [siteFilter, setSiteFilter] = useState("All"); // Defaults to Active: the register is a working list of machines currently in // the fleet, and retired rows are noise until someone asks for them. Rows with // an unsaved toggle staged stay visible so an edit is never hidden mid-change. const [statusFilter, setStatusFilter] = useState("active"); const [sortKey, setSortKey] = useState("type"); const [sortDir, setSortDir] = useState<"asc" | "desc">("asc"); const [editing, setEditing] = useState(false); // Inline cell edits. `drafts` holds unsaved text keyed by `${assetId}:${field}` // so several cells can be mid-edit at once. `dirty` is the set of keys whose // draft differs from the stored value — those are what Save commits and what // Discard reverts. // // `activeDrafts` does the same job for the Active/Inactive toggle. It used to // PATCH on click, which meant toggling a machine left Save disabled and the // change looked like it hadn't stuck. Now every edit on this screen — text // and status alike — is a draft committed by one Save. const [drafts, setDrafts] = useState>({}); const [dirty, setDirty] = useState>(new Set()); const [activeDrafts, setActiveDrafts] = useState>({}); // Site assignment, staged the same way: `{ assetId: siteId }` for rows whose // selection differs from what's stored. Site is a closed set of 10 named // yards, so a setQuery(e.target.value)} placeholder="Filter code, name, type…" className="h-11 min-h-[44px] pl-9 pr-3 w-full sm:w-56 bg-gray-50 dark:bg-white/5 border border-transparent rounded-lg text-sm placeholder:text-gray-400 focus:bg-white dark:focus:bg-white/10 focus:border-[#30638E] focus:outline-none" /> {!editing ? ( ) : (
{/* Edit mode always offers a way out. Previously Discard was only rendered when something had changed and Save was disabled until then, so entering Edit and changing nothing left the table stuck with no control to leave. */} {confirmDiscard ? ( <> Discard {changeCount} change{changeCount === 1 ? "" : "s"}? ) : ( )}
)} {/* Edit-mode banner / unsaved changes indicator */} {editing && (
{saveError ? saveError : hasUnsaved ? `${changeCount} unsaved ${changeCount === 1 ? "change" : "changes"} — edit Code, Name, Type, or the active toggle, then press Save.` : "Edit mode: Code, Name, Type, and the active toggle are editable. Enter commits a cell, Escape reverts a row."} {hasUnsaved && Unsaved}
)} {/* Status filter — segmented toggle group at every breakpoint. Only 3 options, so unlike the 13-item type filter it never needs to collapse into a dropdown below lg. */}
{([ { id: "all", label: "All", count: statusCounts.all }, { id: "active", label: "Active", count: statusCounts.active }, { id: "inactive", label: "Inactive", count: statusCounts.inactive }, ] as const).map((opt) => { const on = statusFilter === opt.id; return ( ); })}
{/* Type filter — Round 4 Part B.3: pills on lg+, dropdown below lg */}
{types.map((t) => { const count = t === "All" ? assets.length : assets.filter((a) => a.type === t).length; return ( ); })}
{/* Site filter — 10 named yards plus "unassigned", so a select at every width rather than another pill row. */} {sites.length > 0 && (
)} {/* Table */}
{headerCell("code", "Code")} {headerCell("name", "Name")} {headerCell("type", "Type")} {headerCell("site", "Site")} {editing && } {filtered.map((a) => { const saving = savingId === a.id; const errored = errorId === a.id; const shownActive = effectiveActive(a); const rowDirty = EDITABLE.some((f) => dirty.has(draftKey(a.id, f.field))) || activeIsDirty(a) || siteIsDirty(a); return ( {/* Code — inline editable */} {/* Name — inline editable */} {/* Type — inline editable */} {/* Site — a closed set of named yards, so a select, not a text box */} {editing && ( )} ); })}
StatusDelete
{editing ? (
{a.is_provisional && ( )} setDraft(a.id, "code", e.target.value)} onKeyDown={(e) => onCellKeyDown(e, a)} aria-label={`Code for ${a.name}`} placeholder="—" maxLength={32} className={cn( "w-24 min-h-[44px] px-2 rounded-md text-xs font-mono font-semibold border bg-gray-50 dark:bg-white/5 focus:bg-white dark:focus:bg-white/10 focus:outline-none", dirty.has(draftKey(a.id, "code")) ? "border-[#EDAE49] text-[#EDAE49]" : "border-transparent focus:border-[#30638E]" )} />
) : (
{a.is_provisional && ( )} {a.code || "—"}
)}
{editing ? (
setDraft(a.id, "name", e.target.value)} onKeyDown={(e) => onCellKeyDown(e, a)} aria-label={`Name for ${a.code || a.name}`} placeholder="Machine name" maxLength={120} className={cn( "w-full sm:w-44 min-h-[44px] px-2 rounded-md text-sm font-medium border bg-gray-50 dark:bg-white/5 focus:bg-white dark:focus:bg-white/10 focus:outline-none", dirty.has(draftKey(a.id, "name")) ? "border-[#EDAE49] text-[#EDAE49]" : "border-transparent text-[#003D5B] dark:text-white focus:border-[#30638E]" )} /> {!shownActive && ( inactive )}
) : ( {a.name} {a.is_provisional && ( provisional )} {!shownActive && ( inactive )} )}
{editing ? ( setDraft(a.id, "type", e.target.value)} onKeyDown={(e) => onCellKeyDown(e, a)} aria-label={`Type for ${a.name}`} placeholder="e.g. Backhoe Loader" maxLength={80} list="plant-type-options" className={cn( "w-full sm:w-40 min-h-[44px] px-2 rounded-md text-sm border bg-gray-50 dark:bg-white/5 focus:bg-white dark:focus:bg-white/10 focus:outline-none", dirty.has(draftKey(a.id, "type")) ? "border-[#EDAE49] text-[#EDAE49]" : "border-transparent text-gray-600 dark:text-gray-400 focus:border-[#30638E]" )} /> ) : ( {a.type || "—"} )} {editing ? ( <> ) : effectiveSite(a) ? ( {siteName(effectiveSite(a)) || "Unknown site"} ) : ( — )} {editing ? ( ) : ( {shownActive ? "Active" : "Inactive"} )}
{filtered.length} {filtered.length === 1 ? "item" : "items"} shown {filtered.filter((a) => effectiveSite(a)).length} assigned ·{" "} {filtered.filter((a) => !effectiveSite(a)).length} unassigned {filtered.filter((a) => effectiveActive(a)).length} active ·{" "} {filtered.filter((a) => !effectiveActive(a)).length} inactive {editing && }
{/* Datalist for the Type cell — offers existing types without locking the field to a closed set, so a genuinely new type can be typed in. */} {types.filter((t) => t !== "All").map((t) => ( {filtered.length === 0 && (
No items match "{query}" in {typeFilter} ({statusFilter}).
)} {/* Delete confirmation. Shows the exact cascade row counts from /api/assets/:id/impact rather than a generic warning, and requires typing DELETE to arm the destructive button. */} {deleteTarget && (
e.stopPropagation()} className="w-full sm:max-w-md bg-white dark:bg-surface rounded-2xl border border-gray-200 dark:border-white/10 shadow-2xl p-5 max-h-[90vh] overflow-y-auto" >

Delete {deleteTarget.code ? `${deleteTarget.code} · ` : ""}{deleteTarget.name}?

This cannot be undone from the dashboard.

{/* Cascade impact — real counts from the API */}
Also permanently deletes
{impactLoading ? (
Counting linked rows…
) : impact ? ( impact.total === 0 ? (

No linked financial rows — only the register entry itself.

) : (
    {Object.entries(impact.counts).map(([label, n]) => (
  • {label} 0 ? "text-[#D1495B]" : "text-gray-400 dark:text-text-soft" )}> {n}
  • ))}
  • Total rows lost {impact.total}
) ) : (

{deleteError || "Could not determine impact."}

)}
{impact && impact.total > 0 && (

To keep the P&L history instead, close this and mark the machine{" "} Inactive — it disappears from the default view but its income and cost rows stay intact.

)} {/* Type-to-confirm */} setConfirmText(e.target.value)} placeholder="Type DELETE to confirm" autoComplete="off" className="w-full min-h-[44px] px-3 mb-3 text-sm rounded-lg border border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5 focus:bg-white dark:focus:bg-white/10 focus:border-[#30638E] focus:outline-none" /> {deleteError && impact && (

{deleteError}

)}
)} ); }