feat(customers): lift active/inactive toggle to top of customer form

The active/inactive customer switch was buried under the
'Details' section after the sales rep and description — easy
to miss when the only thing the operator wants to do is mark
a customer as inactive.

Moved it to the second row of the form, immediately after
the customer name field, where it's the first decision the
operator makes. Visual treatment bumped to match the new
prominence: a green-tinted container when active, muted
grey when inactive, so the state is readable from across
the room instead of requiring the operator to read the
trigger label.

  state semantics (unchanged from bbfa888):
  - 'status' is a PB bool, true = active, false = inactive
  - new customers default to true via the form's
    'status: customer?.status ?? true' initializer
  - legacy rows with status undefined render as Active on
    both the detail and list pages (same default-active
    contract on read) — no migration needed
  - the only way to render an inactive customer is to flip
    the switch and save it; PB then stores false
This commit is contained in:
Dave 2026-09-16 11:04:39 +02:00
parent da90706ce0
commit 1bc0da40e7

View file

@ -24,6 +24,7 @@ import {
CheckCircle2,
XCircle,
} from "lucide-react";
import { cn } from "@/lib/utils";
import getPocketBase from "@/lib/pocketbase";
import { getAllCompanies, findCustomerDuplicates } from "@/lib/pb-helpers";
import { CompanySearch } from "@/components/ui/company-search";
@ -384,6 +385,68 @@ export function CustomerForm({ open, onOpenChange, customer, onSaved }: Customer
/>
<DuplicateWarning matches={duplicateMatches} entityLabel="Customer" />
</div>
{/*
Active/Inactive customer toggle — lifted to the top of the
form (2026-09) so the operator can flip the flag without
scrolling past company / sales rep / description. State
semantics: `status` is a PB bool, true = active, false =
inactive; legacy rows that pre-date the field come back
as `undefined` and collapse to `true` via `?? true` in
`buildCustomerFormState`, so untouched existing customers
read as Active on first edit.
Visual: the switch has a coloured background that flips
with state — green when active, muted when inactive — so
the answer is one glance from across the room, not "did
I read the trigger label right?". The icon (Check /
X-circle) reinforces the same answer for screen-reader
users.
*/}
<div
className={cn(
"flex items-center justify-between gap-4 rounded-md border px-3 py-3 transition-colors",
form.status
? "border-success/40 bg-success/10"
: "border-muted-foreground/30 bg-muted/40"
)}
>
<div className="min-w-0 flex items-start gap-2.5">
{form.status ? (
<CheckCircle2
className="h-4 w-4 text-success shrink-0 mt-0.5"
aria-hidden
/>
) : (
<XCircle
className="h-4 w-4 text-muted-foreground shrink-0 mt-0.5"
aria-hidden
/>
)}
<div className="min-w-0">
<Label
htmlFor="customer-status"
className="text-sm font-medium leading-none"
>
{form.status ? "Active customer" : "Inactive customer"}
</Label>
<p className="mt-1 text-[11px] text-muted-foreground">
New customers are active by default. Flip this
off when the customer stops working for the
mine — historical quotes and orders stay
attached, but the customer drops off the
active pickers.
</p>
</div>
</div>
<Switch
id="customer-status"
checked={!!form.status}
onCheckedChange={(checked) =>
setForm({ ...form, status: checked === true })
}
aria-label={form.status ? "Active customer" : "Inactive customer"}
/>
</div>
<div>
<FieldLabel icon={Building2} label="Company" />
<CompanySearch
@ -542,42 +605,13 @@ export function CustomerForm({ open, onOpenChange, customer, onSaved }: Customer
/>
</div>
{/*
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.
Active/Inactive toggle used to live here, but it was
buried past company / rep / description and easy to
miss. It's now the second row of the form (right
under the customer name) so flipping the flag is one
tap. Keeping the Save layer in `performSave` unchanged
— `status: !!form.status` already shipped 2026-09.
*/}
<div className="flex items-start justify-between gap-4 rounded-md border border-input/40 px-3 py-2.5">
<div className="min-w-0">
<Label
htmlFor="customer-status"
className="flex items-center gap-1.5 text-sm font-medium"
>
{form.status ? (
<CheckCircle2 className="h-3.5 w-3.5 text-success" aria-hidden />
) : (
<XCircle className="h-3.5 w-3.5 text-muted-foreground" aria-hidden />
)}
{form.status ? "Active customer" : "Inactive customer"}
</Label>
<p className="mt-1 text-[11px] text-muted-foreground">
Turn off when this customer no longer works for
the mine. Historical quotes and orders are kept;
the customer stops appearing on active pickers.
</p>
</div>
<Switch
id="customer-status"
checked={!!form.status}
onCheckedChange={(checked) =>
setForm({ ...form, status: checked === true })
}
aria-label="Active customer"
/>
</div>
<div>
<FieldLabel icon={FileText} label="Description" />
<Textarea