From d471dc2d886b3734241c234422f05585e698f3aa Mon Sep 17 00:00:00 2001 From: David Date: Fri, 2 Oct 2026 18:37:54 +0200 Subject: [PATCH] Invoices: make the machine picker searchable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Booking a line meant scrolling a native book(e.target.value)} - className={`min-h-[44px] w-full rounded-lg border px-2 text-sm transition focus:outline-none focus:border-[#30638E] disabled:opacity-60 ${ - current - ? "border-[#00798C]/30 bg-[#00798C]/5 text-[#003D5B] dark:text-white" - : "border-[#D1495B]/40 bg-[#D1495B]/5 text-[#D1495B]" - }`} - > - - {groups.map((g) => ( - - {g.items.map((a) => ( - - ))} - - ))} - - {saving && } - - {error && ( -

- - {error} -

- )} - - ); -} \ No newline at end of file diff --git a/components/InvoiceTable.tsx b/components/InvoiceTable.tsx index 1fc0686..8ddd8f2 100644 --- a/components/InvoiceTable.tsx +++ b/components/InvoiceTable.tsx @@ -1,7 +1,7 @@ import type { Asset, BookedLine, Invoice } from "@/lib/data"; import { ZAR2 } from "@/lib/utils"; import { AlertTriangle, CheckCircle2 } from "lucide-react"; -import { BookingSelect } from "@/components/BookingSelect"; +import { MachineCombobox } from "@/components/MachineCombobox"; import { ScrollFade } from "@/components/ScrollFade"; import { SiteBookingSelect, SiteBookingLegend } from "@/components/SiteBookingSelect"; import { InvoiceUploader, AttachedDocument } from "@/components/InvoiceUploader"; @@ -223,9 +223,9 @@ function TotalRow({ label, value, strong }: { label: string; value: number; stro } export function InvoiceTable({ lines, assets }: { lines: BookedLine[]; assets: Asset[] }) { - const options = assets + const machines = assets .filter((a) => a.is_active !== false) - .map((a) => ({ id: a.id, code: a.code || "", name: a.name, category: a.category })); + .map((a) => ({ id: a.id, code: a.code || "", name: a.name, category: a.category || "Other" })); return ( // Rule 1 + 4: the table scrolls inside its own container, so the page itself never @@ -284,7 +284,7 @@ export function InvoiceTable({ lines, assets }: { lines: BookedLine[]; assets: A
- +
{(!l.isBooked || l.booked_site) && ( diff --git a/components/MachineCombobox.tsx b/components/MachineCombobox.tsx new file mode 100644 index 0000000..7c1af7b --- /dev/null +++ b/components/MachineCombobox.tsx @@ -0,0 +1,256 @@ +"use client"; + +import { useState, useRef, useEffect, useMemo } from "react"; +import { useRouter } from "next/navigation"; +import { Loader2, AlertCircle, ChevronDown, Check, Search, X } from "lucide-react"; + +type Machine = { id: string; code: string; name: string; category: string }; + +// A searchable replacement for the native setOpen(true)} + onChange={(e) => { + setQuery(e.target.value); + setOpen(true); + setHighlight(0); + }} + onKeyDown={onKeyDown} + className={`min-h-[44px] w-full pl-8 pr-8 rounded-lg border text-sm transition focus:outline-none focus:border-[#30638E] ${ + selected + ? "border-[#00798C]/30 bg-[#00798C]/5 text-[#003D5B] dark:text-white font-medium" + : "border-[#D1495B]/40 bg-[#D1495B]/5 text-[#D1495B]" + }`} + /> + {busy ? ( + + ) : ( + + )} +
+ {selected && !busy && ( + + )} + + + {open && ( +
+
+ {groups.flat.length} of {machines.length} machines + {!selected && " · type a code or name to narrow it"} +
+ + {value && ( +
    +
  • + +
  • +
+ )} + + {groups.flat.length === 0 ? ( +

+ No machine matches “{query}”. +

+ ) : ( +
    + {groups.list.map((g) => ( +
  • +
    + {g.category} +
    +
      + {g.items.map((m) => { + const idx = groups.flat.indexOf(m); + const isSel = m.id === value; + return ( +
    • + +
    • + ); + })} +
    +
  • + ))} +
+ )} +
+ )} + + {error && ( +

+ + {error} +

+ )} + + ); +}