import { useState, useEffect, useRef, useCallback } from "react"; import { useParams, useNavigate } from "react-router-dom"; import { useAuth } from "../auth/AuthContext"; import api from "../api/client"; const BOARD_TYPE_LABELS = { vesper: "Vesper", vesper_plus: "Vesper+", vesper_pro: "Vesper Pro", chronos: "Chronos", chronos_pro: "Chronos Pro", agnus_mini: "Agnus Mini", agnus: "Agnus", }; const LIFECYCLE = [ { key: "manufactured", label: "Manufactured", icon: "πŸ”©", color: "#94a3b8", bg: "#1e2a3a", accent: "#64748b" }, { key: "flashed", label: "Flashed", icon: "⚑", color: "#60a5fa", bg: "#1e3a5f", accent: "#3b82f6" }, { key: "provisioned", label: "Provisioned", icon: "πŸ“‘", color: "#34d399", bg: "#064e3b", accent: "#10b981" }, { key: "sold", label: "Sold", icon: "πŸ“¦", color: "#c084fc", bg: "#2e1065", accent: "#a855f7" }, { key: "claimed", label: "Claimed", icon: "βœ…", color: "#fb923c", bg: "#431407", accent: "#f97316" }, { key: "decommissioned", label: "Decommissioned", icon: "πŸ—‘", color: "#f87171", bg: "#450a0a", accent: "#ef4444" }, ]; const STEP_INDEX = Object.fromEntries(LIFECYCLE.map((s, i) => [s.key, i])); const PROTECTED_STATUSES = ["sold", "claimed"]; function formatHwVersion(v) { if (!v) return "β€”"; if (/^\d+\.\d+/.test(v)) return `Rev ${v}`; const n = parseInt(v, 10); if (!isNaN(n)) return `Rev ${n}.0`; return `Rev ${v}`; } function formatDate(iso) { if (!iso) return "β€”"; try { return new Date(iso).toLocaleString("en-US", { year: "numeric", month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } catch { return iso; } } function toDatetimeLocal(iso) { if (!iso) return ""; try { const d = new Date(iso); const pad = (n) => String(n).padStart(2, "0"); return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`; } catch { return ""; } } function Field({ label, value, mono = false }) { return (

{label}

{value || "β€”"}

); } // ─── User Search Modal (for Claimed) ────────────────────────────────────────── function UserSearchModal({ onSelect, onCancel, existingUsers = [] }) { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [searching, setSearching] = useState(false); const inputRef = useRef(null); useEffect(() => { inputRef.current?.focus(); }, []); const search = useCallback(async (q) => { setSearching(true); try { const data = await api.get(`/users?search=${encodeURIComponent(q)}&limit=20`); setResults(data.users || data || []); } catch { setResults([]); } finally { setSearching(false); } }, []); useEffect(() => { const t = setTimeout(() => search(query), 300); return () => clearTimeout(t); }, [query, search]); return (
βœ…

Set as Claimed

Assign a User

A device is "Claimed" when a registered user has been assigned to it. Search and select the user to assign.

{/* Keep existing users option */} {existingUsers.length > 0 && (

Already assigned

{existingUsers.map((u) => ( ))}
or assign a different user
)}
setQuery(e.target.value)} placeholder="Search by name or email…" className="w-full px-3 py-2 rounded-md text-sm border" style={{ backgroundColor: "var(--bg-input)", borderColor: "var(--border-input)", color: "var(--text-primary)" }} /> {searching && …}
{results.length === 0 ? (

{searching ? "Searching…" : query ? "No users found." : "Type to search users…"}

) : results.map((u) => ( ))}
); } // ─── Customer Search Modal ───────────────────────────────────────────────── function CustomerSearchModal({ onSelect, onCancel }) { const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [searching, setSearching] = useState(false); const inputRef = useRef(null); useEffect(() => { inputRef.current?.focus(); }, []); const search = useCallback(async (q) => { setSearching(true); try { const data = await api.get(`/manufacturing/customers/search?q=${encodeURIComponent(q)}`); setResults(data.results || []); } catch { setResults([]); } finally { setSearching(false); } }, []); useEffect(() => { const t = setTimeout(() => search(query), 250); return () => clearTimeout(t); }, [query, search]); return (

Assign to Customer

setQuery(e.target.value)} placeholder="Search by name, email, phone, org, tags…" className="w-full px-3 py-2 rounded-md text-sm border" style={{ backgroundColor: "var(--bg-input)", borderColor: "var(--border-input)", color: "var(--text-primary)" }} /> {searching && …}
{results.length === 0 ? (

{searching ? "Searching…" : query ? "No customers found." : "Type to search customers…"}

) : results.map((c) => { const fullName = [c.name, c.surname].filter(Boolean).join(" ") || c.email || c.id; return ( ); })}
); } // ─── Delete Device Modal ────────────────────────────────────────────────────── function DeleteModal({ device, onConfirm, onCancel, deleting }) { const isProtected = PROTECTED_STATUSES.includes(device?.mfg_status); const [typed, setTyped] = useState(""); const confirmed = !isProtected || typed === device?.serial_number; return (

{isProtected ? "⚠ Delete Protected Device" : "Delete Device"}

{isProtected ? ( <>

This device has status {device.mfg_status} and is linked to a customer. Deleting it is irreversible.

To confirm, type the serial number exactly:

{device.serial_number}

setTyped(e.target.value)} onPaste={(e) => e.preventDefault()} placeholder="Type serial number here…" autoComplete="off" className="w-full px-3 py-2 rounded-md text-sm border mb-4 font-mono" style={{ backgroundColor: "var(--bg-input)", borderColor: typed === device.serial_number ? "var(--accent)" : "var(--border-input)", color: "var(--text-primary)" }} /> ) : (

Are you sure you want to delete {device?.serial_number}? This cannot be undone.

)}
); } // ─── Lifecycle Entry Edit Modal ──────────────────────────────────────────────── function LifecycleEditModal({ entry, stepMeta, isCurrent, onSave, onDelete, onCancel }) { const isNew = !entry; const [dateVal, setDateVal] = useState(() => isNew ? toDatetimeLocal(new Date().toISOString()) : toDatetimeLocal(entry?.date)); const [note, setNote] = useState(entry?.note || ""); const [saving, setSaving] = useState(false); const [deleting, setDeleting] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); const handleSave = async () => { setSaving(true); try { const isoDate = dateVal ? new Date(dateVal).toISOString() : new Date().toISOString(); await onSave(isoDate, note); } finally { setSaving(false); } }; const handleDelete = async () => { setDeleting(true); try { await onDelete(); } finally { setDeleting(false); setConfirmDelete(false); } }; return (
{/* Header */}
{stepMeta.icon}

{isNew ? "Create Step" : "Edit Step"}

{stepMeta.label}

{/* Date field */}
setDateVal(e.target.value)} className="w-full px-3 py-2 rounded-lg text-sm border" style={{ backgroundColor: "var(--bg-input)", borderColor: "var(--border-input)", color: "var(--text-primary)" }} />
{/* Note field */}