From d01ab85cbbbca7025cd7f60a1fad24229665a2eb Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 30 May 2026 12:23:32 +0200 Subject: [PATCH] feat(admin): add search, sorting and pagination to admin tables --- .../admin/billing/new-invoice-form.tsx | 162 ++++++++++++++++-- 1 file changed, 143 insertions(+), 19 deletions(-) diff --git a/src/components/admin/billing/new-invoice-form.tsx b/src/components/admin/billing/new-invoice-form.tsx index 296a1b1..f4c8809 100644 --- a/src/components/admin/billing/new-invoice-form.tsx +++ b/src/components/admin/billing/new-invoice-form.tsx @@ -1,6 +1,6 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; import { useTranslations } from "next-intl"; import { Card } from "@/components/ui/card"; @@ -104,25 +104,14 @@ export function NewInvoiceForm({ orgs }: Props) { - + onChange={onOrgChange} + placeholder={t("newInvoiceOrgPlaceholder")} + noBillingLabel={t("newInvoiceOrgNoBilling")} + noMatchesLabel={t("newInvoiceOrgNoMatches")} + /> {selected && !selected.hasBillingAddress && (

{t("newInvoiceOrgBillingMissing")} @@ -164,3 +153,138 @@ export function NewInvoiceForm({ orgs }: Props) { ); } + +/** + * Searchable single-select for the billing org. Replaces a plain + * { + setQuery(e.target.value); + setOpen(true); + setHi(0); + }} + onFocus={() => { + setOpen(true); + setQuery(""); + }} + onKeyDown={(e) => { + if (e.key === "ArrowDown") { + e.preventDefault(); + setOpen(true); + setHi((h) => Math.min(h + 1, filtered.length - 1)); + } else if (e.key === "ArrowUp") { + e.preventDefault(); + setHi((h) => Math.max(h - 1, 0)); + } else if (e.key === "Enter") { + e.preventDefault(); + if (open && filtered[hi]) choose(filtered[hi]); + } else if (e.key === "Escape") { + setOpen(false); + } + }} + placeholder={placeholder} + role="combobox" + aria-expanded={open} + aria-autocomplete="list" + className="w-full px-3 py-2 rounded-md border border-border bg-surface-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-1 focus:ring-accent focus:border-accent transition-colors" + /> + {open && ( +

+ )} + + ); +}