diff --git a/src/app/(app)/customers/[id]/page.tsx b/src/app/(app)/customers/[id]/page.tsx
index 30e2b90..3b17134 100644
--- a/src/app/(app)/customers/[id]/page.tsx
+++ b/src/app/(app)/customers/[id]/page.tsx
@@ -29,6 +29,19 @@ function hasRealCoordinates(c: Customer["coordinates"]): c is { lon: number; lat
return !!c && (c.lon !== 0 || c.lat !== 0);
}
+/**
+ * `customers.status` (added 2026-09) is a PB bool — but PB returns
+ * `undefined` for legacy rows written before the field existed.
+ * Treat those as active: we deliberately don't backfill on the
+ * server (the migration leaves existing rows untouched), so the
+ * first generation of read sites has to coerce. Returning `true`
+ * for `undefined` matches the form's `?? true` default — same
+ * rationale in both places.
+ */
+function isActiveCustomer(status: boolean | undefined): boolean {
+ return status ?? true;
+}
+
/**
* Strip the visual formatting from a phone number for use in a `tel:`
* href. PB sometimes deserialises a numeric `tel` column as `number`
@@ -125,7 +138,23 @@ export default function CustomerDetailPage({ params }: { params: Promise<{ id: s
-
{customer.customer}
+
+
{customer.customer}
+ {/*
+ Use the helper for default-on semantics — see
+ `isActiveCustomer` JSDoc. The form treats undefined as
+ active to match.
+ */}
+ {isActiveCustomer(customer.status) ? (
+
+ Active
+
+ ) : (
+
+ Inactive
+
+ )}
+
@@ -207,6 +243,17 @@ export default function CustomerDetailPage({ params }: { params: Promise<{ id: s
{customer.expand.company.company}
)}
+ {/* `site_location` (added 2026-09) — the shaft/site label
+ keyed off the customer's company. Distinct from the
+ older `site` free-text mine tag owned by the pin
+ tool. Render only when set so an empty cell doesn't
+ add visual noise. */}
+ {customer.site_location && (
+
+
+ {customer.site_location}
+
+ )}
{customer.expand?.rep && (
diff --git a/src/app/(app)/customers/page.tsx b/src/app/(app)/customers/page.tsx
index a051468..38ca5c9 100644
--- a/src/app/(app)/customers/page.tsx
+++ b/src/app/(app)/customers/page.tsx
@@ -7,7 +7,8 @@ import { CustomerForm } from "@/components/customer-form";
import { DeleteDialog } from "@/components/delete-dialog";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
-import { Plus, Download, Pencil, Trash2, Mail, Phone, Building2, UserRound } from "lucide-react";
+import { Badge } from "@/components/ui/badge";
+import { Plus, Download, Pencil, Trash2, Mail, Phone, Building2, UserRound, MapPin } from "lucide-react";
import { toast } from "sonner";
import getPocketBase from "@/lib/pocketbase";
import { formatDate, exportToCSV } from "@/lib/pb-helpers";
@@ -101,7 +102,17 @@ export default function CustomersPage() {
className: "max-w-[200px] whitespace-normal",
render: (item: Customer) => (
-
{item.customer}
+
+
{item.customer}
+ {/* Inactive pill (added 2026-09). Active rows just show
+ the name — the default needs no ornament. Same
+ `?? true` semantics as the detail page. */}
+ {item.status === false ? (
+
+ Inactive
+
+ ) : null}
+
{item.expand?.company && (
@@ -110,6 +121,15 @@ export default function CustomersPage() {
diff --git a/src/app/api/v1/customers/sites/route.ts b/src/app/api/v1/customers/sites/route.ts
new file mode 100644
index 0000000..d2ebcea
--- /dev/null
+++ b/src/app/api/v1/customers/sites/route.ts
@@ -0,0 +1,37 @@
+import { NextRequest, NextResponse } from "next/server";
+import { assertBrowserSession } from "@/lib/server/browser-session";
+import { listDistinctSiteLocations } from "@/lib/server/customers-service";
+
+/**
+ * GET /api/v1/customers/sites
+ *
+ * Returns the distinct, non-blank `site_location` values currently in
+ * use across the customers collection, each with how many customers
+ * carry it. Powers the customer form's `
+
+
+ {/*
+ `site_location` (added 2026-09) is the shaft/site label
+ specific to this customer. PB storage is plain text, so
+ we wrap a regular with a that
+ pre-fills suggestions from other customers already
+ typed — see `listDistinctSiteLocations`. The operator
+ can still type anything; the datalist just nudges them
+ toward consistency.
+
+ Stable ID on the lets the bind to
+ it via `list=`. cmdk-style filtering not needed —
+ this is a free-text field, not a constrained picker.
+ */}
+ setForm({ ...form, site_location: e.target.value })}
+ placeholder="e.g. Kusasalethu shaft"
+ className="h-9"
+ aria-label="Site or location"
+ />
+
+ {siteLocationSuggestions.map((s) => (
+
+ ))}
+
+
+ The shaft or site within the company (each company
+ usually has more than one). Leave blank if the
+ customer is office-only or you don't know yet.
@@ -400,9 +531,53 @@ export function CustomerForm({ open, onOpenChange, customer, onSaved }: Customer
setForm({ ...form, rep: id })}
+ // Prop the rep's resolved name/email into the trigger so
+ // it renders immediately on edit, before the user-search
+ // popover has been opened and the user roster fetched
+ // (see UserSearch JSDoc — `selectedLabel` fills the gap).
+ selectedLabel={
+ customer?.expand?.rep?.name || customer?.expand?.rep?.email
+ }
placeholder="Select sales rep..."
/>
+ {/*
+ Active-customer toggle (added 2026-09). PB `status` is a
+ bool: true = active, false = inactive (e.g. the mine
+ stopped trading, customer moved to a different mine).
+ Existing customers the operator creates without touching
+ this row will land as `true` — see `?? true` in
+ `buildCustomerFormState`. The icon flips with state so
+ the meaning is clear even without the label colour.
+ */}
+
+
+
+
+ Turn off when this customer no longer works for
+ the mine. Historical quotes and orders are kept;
+ the customer stops appearing on active pickers.
+