diff --git a/src/app/(app)/repairs/dashboard/page.tsx b/src/app/(app)/repairs/dashboard/page.tsx new file mode 100644 index 0000000..3648ed5 --- /dev/null +++ b/src/app/(app)/repairs/dashboard/page.tsx @@ -0,0 +1,210 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { ChevronRight, Wrench } from "lucide-react"; +import { Badge } from "@/components/ui/badge"; +import { + REPAIR_STATUS, + REPAIR_STATUS_LABEL, + REPAIR_STATUS_VARIANT, + type RepairStatus, +} from "@/lib/repair-status"; +import getPocketBase from "@/lib/pocketbase"; +import type { Customer, Repair } from "@/lib/types"; + +type Counts = { total: number; byStatus: Record }; +type Recent = Repair & { expand?: { customer?: Customer } }; + +// The working pipeline, in order. Done / Cancelled are shown apart. +const PIPELINE = REPAIR_STATUS.filter( + (s) => s !== "completed" && s !== "cancelled", +); + +// One tone per stage, drawn from the theme so it follows light/dark. +const STAGE_TONE: Record = { + new: "bg-primary/30", + assessing: "bg-primary/45", + quoted: "bg-primary/60", + approved: "bg-primary/75", + in_progress: "bg-primary", +}; + +function authHeaders(): Record { + const token = getPocketBase().authStore.token; + return token ? { Authorization: `Bearer ${token}` } : {}; +} + +export default function RepairsDashboardPage() { + const [counts, setCounts] = useState(null); + const [recent, setRecent] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + const headers = authHeaders(); + fetch("/api/v1/repairs/status-counts", { headers }) + .then(async (res) => { + if (!res.ok) throw new Error(`Could not load repairs (HTTP ${res.status}).`); + setCounts((await res.json()) as Counts); + }) + .catch((e: unknown) => + setError(e instanceof Error ? e.message : "Could not load repairs."), + ); + fetch("/api/v1/repairs?page=1&perPage=6", { headers }) + .then(async (res) => { + if (res.ok) setRecent(((await res.json()) as { items: Recent[] }).items ?? []); + }) + .catch(() => {}); + }, []); + + const by = (s: RepairStatus) => counts?.byStatus[s] ?? 0; + const active = PIPELINE.reduce((n, s) => n + by(s), 0); + + return ( +
+
+
+

+ Repairs +

+

+ Where every machine is right now. +

+
+ + Open the list + +
+ + {error &&

{error}

} + {!counts && !error && ( +

Loading…

+ )} + + {counts && ( + <> + {/* Hero: the number that matters, plus the pipeline as one bar */} +
+
+
+
+ In progress +
+
+ {active} +
+
+
+
+
+ {by("completed")} +
+
Done
+
+
+
+ {by("cancelled")} +
+
Cancelled
+
+
+
+ {counts.total} +
+
All time
+
+
+
+ +
+ {PIPELINE.map( + (s) => + by(s) > 0 && ( +
+ ), + )} +
+
+ + {/* The pipeline, stage by stage */} +
+

+ The journey +

+
    + {PIPELINE.map((s, i) => ( +
  1. +
    + + {i + 1} + + {REPAIR_STATUS_LABEL[s]} +
    +
    + {by(s)} +
    +
  2. + ))} +
+
+ + )} + + {/* Latest */} + {recent.length > 0 && ( +
+

+ Latest repairs +

+
    + {recent.map((r) => ( +
  • + + + + +
    +
    + {r.item_description || "Machine"}{" "} + + · {r.repair_number} + +
    +
    + {r.expand?.customer?.customer ?? "No customer"} +
    +
    + + {REPAIR_STATUS_LABEL[r.status] || r.status} + + + +
  • + ))} +
+
+ )} +
+ ); +} diff --git a/src/components/sidebar.tsx b/src/components/sidebar.tsx index dd86e34..e9cd986 100644 --- a/src/components/sidebar.tsx +++ b/src/components/sidebar.tsx @@ -137,12 +137,14 @@ const stockNavItems: NavEntry[] = [ { href: "/pricelist", label: "Pricelist", icon: Tag }, ]; -// Repairs lives here for now — it doesn't fit Logistics (it's not a -// driver / collection workflow) and it doesn't fit Operations (which -// is reserved for the order-close-out loop). Move it once it has a -// clearer home. +// Repairs is its own section: more repair-related pages are coming, so +// the list sits under a section heading next to a small overview. +const repairsNavItems: NavEntry[] = [ + { href: "/repairs/dashboard", label: "Repairs Dashboard", icon: LayoutDashboard }, + { href: "/repairs", label: "Repairs List", icon: Wrench }, +]; + const otherItems: NavEntry[] = [ - { href: "/repairs", label: "Repairs", icon: Wrench }, { href: "/giellie", label: "Giellie Agent", icon: Bot }, // Standard Operating Procedures for the operator roles. Markdown // source of truth lives in docs/sops/ — the /sops route renders @@ -402,7 +404,7 @@ function NavContent({ onLinkClick }: { onLinkClick?: () => void }) { {/* Nav */} - +