Просмотр исходного кода

Add sortable columns to the customers and vehicles lists (#223)

* Add sortable columns to the customers and vehicles lists

Clickable column headers following the work-orders pattern: sort state
lives in the URL (sortBy/sortOrder), the server re-fetches with a
Prisma orderBy switch, and sorting resets to page 1. Customers sort by
name, company, phone, email or vehicle count; vehicles by plate,
vehicle (make, model, year), customer, mileage or service count.
Default order stays updatedAt desc.

* Stabilize list column widths while sorting

table-fixed layout with explicit widths on both lists, so columns no
longer resize as page data changes; long values truncate with ellipsis
instead of stretching their column.

* Add vehicle and customer sorting to the work orders list

The two columns had no sort. Vehicle sorts by make, model, year;
customer by name through the vehicle relation. Same table-fixed layout
and truncation as the customers and vehicles lists so columns stay
still while sorting.

* Add sortable columns to the quotes and inspections lists

Quotes sort by number, title, customer, vehicle (make/model/year),
status, date or total; inspections by vehicle, template, status or
date (progress is computed client-side and stays unsortable). Same
URL-driven server-side pattern and table-fixed stable layout as the
other lists.

* Add sortable columns to the billing list

Invoice, title, vehicle, customer, date and total are sortable in both
query paths: the raw SQL path orders by the computed effective total
and coalesced invoice date, the Prisma path mirrors it. Paid and
balance stay unsortable (computed from payments at render). Same
fixed-layout stable columns as the other lists.

* Fix billing date sorting and clipped status badge

Billing now always uses the raw SQL query, so date sorting is an exact
COALESCE(invoiceDate, startDateTime, serviceDate) matching the
displayed date, and total sorts by the computed effective total in all
tabs; the approximate Prisma path is gone. Shared date-sort helpers in
src/lib/date-sort.ts are used by billing (SQL) and the work orders
list (Prisma). The balance column is wider with nowrap so the payment
status badge no longer clips.
Bernt Christian Egeland 3 месяцев назад
Родитель
Сommit
d97135802a

+ 64 - 14
src/app/(authenticated)/billing/billing-client.tsx

@@ -26,6 +26,9 @@ import {
   DollarSign,
   DollarSign,
   TrendingUp,
   TrendingUp,
   AlertCircle,
   AlertCircle,
+  ArrowDown,
+  ArrowUp,
+  ArrowUpDown,
 } from "lucide-react";
 } from "lucide-react";
 import { cn } from "@/lib/utils";
 import { cn } from "@/lib/utils";
 import { useFormatCurrency } from '@/components/currency-settings-context'
 import { useFormatCurrency } from '@/components/currency-settings-context'
@@ -73,6 +76,8 @@ interface BillingClientProps {
   currencyCode?: string;
   currencyCode?: string;
   search: string;
   search: string;
   statusFilter: string;
   statusFilter: string;
+  sortBy?: string;
+  sortOrder?: "asc" | "desc";
 }
 }
 
 
 const STATUS_TABS = [
 const STATUS_TABS = [
@@ -87,6 +92,8 @@ export default function BillingClient({
   currencyCode = "USD",
   currencyCode = "USD",
   search,
   search,
   statusFilter,
   statusFilter,
+  sortBy = "",
+  sortOrder = "desc",
 }: BillingClientProps) {
 }: BillingClientProps) {
   const formatCurrency = useFormatCurrency();
   const formatCurrency = useFormatCurrency();
   const router = useRouter();
   const router = useRouter();
@@ -111,6 +118,25 @@ export default function BillingClient({
     [searchParams]
     [searchParams]
   );
   );
 
 
+  const handleSort = useCallback(
+    (column: string) => {
+      const newOrder = sortBy === column && sortOrder === "asc" ? "desc" : "asc";
+      startTransition(() => {
+        router.push(
+          `${pathname}?${createQueryString({ sortBy: column, sortOrder: newOrder, page: "1" })}`
+        );
+      });
+    },
+    [createQueryString, pathname, router, sortBy, sortOrder]
+  );
+
+  const SortIcon = ({ column }: { column: string }) => {
+    if (sortBy !== column) return <ArrowUpDown className="ml-1 h-3 w-3 opacity-50" />;
+    return sortOrder === "asc"
+      ? <ArrowUp className="ml-1 h-3 w-3" />
+      : <ArrowDown className="ml-1 h-3 w-3" />;
+  };
+
   const handleStatusChange = (status: string) => {
   const handleStatusChange = (status: string) => {
     startTransition(() => {
     startTransition(() => {
       router.push(
       router.push(
@@ -292,17 +318,41 @@ export default function BillingClient({
 
 
       {/* Billing Table */}
       {/* Billing Table */}
       <div className="rounded-md border">
       <div className="rounded-md border">
-        <Table>
+        <Table className="table-fixed">
           <TableHeader>
           <TableHeader>
             <TableRow>
             <TableRow>
-              <TableHead>{t("history.columnInvoice")}</TableHead>
-              <TableHead>{t("history.columnTitle")}</TableHead>
-              <TableHead>{t("history.columnVehicle")}</TableHead>
-              <TableHead>{t("history.columnCustomer")}</TableHead>
-              <TableHead>{t("history.columnDate")}</TableHead>
-              <TableHead className="text-right">{t("history.columnTotal")}</TableHead>
-              <TableHead className="text-right">{t("history.columnPaid")}</TableHead>
-              <TableHead className="text-right">{t("history.columnBalance")}</TableHead>
+              <TableHead className="w-[110px]">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("invoiceNumber")}>
+                  {t("history.columnInvoice")}<SortIcon column="invoiceNumber" />
+                </button>
+              </TableHead>
+              <TableHead>
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("title")}>
+                  {t("history.columnTitle")}<SortIcon column="title" />
+                </button>
+              </TableHead>
+              <TableHead className="w-[16%]">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("vehicle")}>
+                  {t("history.columnVehicle")}<SortIcon column="vehicle" />
+                </button>
+              </TableHead>
+              <TableHead className="w-[14%]">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("customer")}>
+                  {t("history.columnCustomer")}<SortIcon column="customer" />
+                </button>
+              </TableHead>
+              <TableHead className="w-[110px]">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("date")}>
+                  {t("history.columnDate")}<SortIcon column="date" />
+                </button>
+              </TableHead>
+              <TableHead className="w-[100px]">
+                <button type="button" className="ml-auto flex items-center hover:text-foreground" onClick={() => handleSort("total")}>
+                  {t("history.columnTotal")}<SortIcon column="total" />
+                </button>
+              </TableHead>
+              <TableHead className="w-[100px] text-right">{t("history.columnPaid")}</TableHead>
+              <TableHead className="w-[180px] text-right">{t("history.columnBalance")}</TableHead>
             </TableRow>
             </TableRow>
           </TableHeader>
           </TableHeader>
           <TableBody>
           <TableBody>
@@ -321,15 +371,15 @@ export default function BillingClient({
                     className="cursor-pointer hover:bg-muted/50"
                     className="cursor-pointer hover:bg-muted/50"
                     onClick={() => handleRowClick(record)}
                     onClick={() => handleRowClick(record)}
                   >
                   >
-                    <TableCell className="font-medium">
+                    <TableCell className="truncate font-medium">
                       {record.invoiceNumber || "\u2014"}
                       {record.invoiceNumber || "\u2014"}
                     </TableCell>
                     </TableCell>
-                    <TableCell>{record.title}</TableCell>
-                    <TableCell>
+                    <TableCell className="truncate">{record.title}</TableCell>
+                    <TableCell className="truncate">
                       {record.vehicle.year} {record.vehicle.make}{" "}
                       {record.vehicle.year} {record.vehicle.make}{" "}
                       {record.vehicle.model}
                       {record.vehicle.model}
                     </TableCell>
                     </TableCell>
-                    <TableCell>
+                    <TableCell className="truncate">
                       {record.vehicle.customer?.name || "\u2014"}
                       {record.vehicle.customer?.name || "\u2014"}
                     </TableCell>
                     </TableCell>
                     <TableCell>
                     <TableCell>
@@ -343,7 +393,7 @@ export default function BillingClient({
                     </TableCell>
                     </TableCell>
                     <TableCell
                     <TableCell
                       className={cn(
                       className={cn(
-                        "text-right font-medium",
+                        "whitespace-nowrap text-right font-medium",
                         getBalanceColor(record.status)
                         getBalanceColor(record.status)
                       )}
                       )}
                     >
                     >

+ 7 - 1
src/app/(authenticated)/billing/page.tsx

@@ -12,6 +12,8 @@ export default async function BillingPage({
     pageSize?: string;
     pageSize?: string;
     search?: string;
     search?: string;
     status?: string;
     status?: string;
+    sortBy?: string;
+    sortOrder?: string;
   }>;
   }>;
 }) {
 }) {
   const params = await searchParams;
   const params = await searchParams;
@@ -19,9 +21,11 @@ export default async function BillingPage({
   const pageSize = Number(params.pageSize) || 20;
   const pageSize = Number(params.pageSize) || 20;
   const search = params.search || "";
   const search = params.search || "";
   const statusFilter = params.status || "all";
   const statusFilter = params.status || "all";
+  const sortBy = params.sortBy || "";
+  const sortOrder = (params.sortOrder as "asc" | "desc") || "desc";
 
 
   const [result, settingsResult] = await Promise.all([
   const [result, settingsResult] = await Promise.all([
-    getBillingHistory({ page, pageSize, search, status: statusFilter }),
+    getBillingHistory({ page, pageSize, search, status: statusFilter, sortBy, sortOrder }),
     getSettings([SETTING_KEYS.CURRENCY_CODE]),
     getSettings([SETTING_KEYS.CURRENCY_CODE]),
   ]);
   ]);
 
 
@@ -48,6 +52,8 @@ export default async function BillingPage({
           currencyCode={currencyCode}
           currencyCode={currencyCode}
           search={search}
           search={search}
           statusFilter={statusFilter}
           statusFilter={statusFilter}
+          sortBy={sortBy}
+          sortOrder={sortOrder}
         />
         />
       </div>
       </div>
     </>
     </>

+ 53 - 11
src/app/(authenticated)/customers/customers-client.tsx

@@ -30,6 +30,9 @@ import { Checkbox } from "@/components/ui/checkbox";
 import { deleteCustomer, deleteCustomers } from "@/features/customers/Actions/customerActions";
 import { deleteCustomer, deleteCustomers } from "@/features/customers/Actions/customerActions";
 import { toast } from "sonner";
 import { toast } from "sonner";
 import {
 import {
+  ArrowDown,
+  ArrowUp,
+  ArrowUpDown,
   Loader2,
   Loader2,
   MoreVertical,
   MoreVertical,
   Pencil,
   Pencil,
@@ -64,9 +67,13 @@ interface PaginatedData {
 export function CustomersClient({
 export function CustomersClient({
   data,
   data,
   search,
   search,
+  sortBy,
+  sortOrder,
 }: {
 }: {
   data: PaginatedData;
   data: PaginatedData;
   search: string;
   search: string;
+  sortBy: string;
+  sortOrder: "asc" | "desc";
 }) {
 }) {
   const t = useTranslations("customers.list");
   const t = useTranslations("customers.list");
   const tc = useTranslations("common");
   const tc = useTranslations("common");
@@ -102,7 +109,7 @@ export function CustomersClient({
           newParams.set(key, String(value));
           newParams.set(key, String(value));
         }
         }
       }
       }
-      if (!("page" in params) && "search" in params) {
+      if (!("page" in params) && ("search" in params || "sortBy" in params)) {
         newParams.delete("page");
         newParams.delete("page");
       }
       }
       startTransition(() => {
       startTransition(() => {
@@ -112,6 +119,21 @@ export function CustomersClient({
     [router, pathname, searchParams]
     [router, pathname, searchParams]
   );
   );
 
 
+  const handleSort = useCallback(
+    (column: string) => {
+      const newOrder = sortBy === column && sortOrder === "asc" ? "desc" : "asc";
+      navigate({ sortBy: column, sortOrder: newOrder });
+    },
+    [navigate, sortBy, sortOrder]
+  );
+
+  const SortIcon = ({ column }: { column: string }) => {
+    if (sortBy !== column) return <ArrowUpDown className="ml-1 h-3 w-3 opacity-50" />;
+    return sortOrder === "asc"
+      ? <ArrowUp className="ml-1 h-3 w-3" />
+      : <ArrowDown className="ml-1 h-3 w-3" />;
+  };
+
   // Live search: filters as you type, no Enter required. Submitting the
   // Live search: filters as you type, no Enter required. Submitting the
   // form (Enter) commits immediately, bypassing the debounce.
   // form (Enter) commits immediately, bypassing the debounce.
   const {
   const {
@@ -227,7 +249,7 @@ export function CustomersClient({
 
 
       {/* Table - only this scrolls */}
       {/* Table - only this scrolls */}
       <div className="overflow-auto rounded-lg border max-h-[calc(100vh-220px)]">
       <div className="overflow-auto rounded-lg border max-h-[calc(100vh-220px)]">
-        <Table>
+        <Table className="table-fixed">
           <TableHeader className="sticky top-0 z-10 bg-background">
           <TableHeader className="sticky top-0 z-10 bg-background">
             <TableRow>
             <TableRow>
               <TableHead className="w-[40px]">
               <TableHead className="w-[40px]">
@@ -243,11 +265,31 @@ export function CustomersClient({
                   onClick={(e) => e.stopPropagation()}
                   onClick={(e) => e.stopPropagation()}
                 />
                 />
               </TableHead>
               </TableHead>
-              <TableHead>{t("table.name")}</TableHead>
-              <TableHead className="hidden sm:table-cell">{t("table.company")}</TableHead>
-              <TableHead className="hidden md:table-cell">{t("table.phone")}</TableHead>
-              <TableHead className="hidden lg:table-cell">{t("table.email")}</TableHead>
-              <TableHead className="w-[80px] text-center">{t("table.vehicles")}</TableHead>
+              <TableHead>
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("name")}>
+                  {t("table.name")}<SortIcon column="name" />
+                </button>
+              </TableHead>
+              <TableHead className="hidden w-[20%] sm:table-cell">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("company")}>
+                  {t("table.company")}<SortIcon column="company" />
+                </button>
+              </TableHead>
+              <TableHead className="hidden w-[16%] md:table-cell">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("phone")}>
+                  {t("table.phone")}<SortIcon column="phone" />
+                </button>
+              </TableHead>
+              <TableHead className="hidden w-[22%] lg:table-cell">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("email")}>
+                  {t("table.email")}<SortIcon column="email" />
+                </button>
+              </TableHead>
+              <TableHead className="w-[80px]">
+                <button type="button" className="mx-auto flex items-center hover:text-foreground" onClick={() => handleSort("vehicles")}>
+                  {t("table.vehicles")}<SortIcon column="vehicles" />
+                </button>
+              </TableHead>
               <TableHead className="w-[50px]" />
               <TableHead className="w-[50px]" />
             </TableRow>
             </TableRow>
           </TableHeader>
           </TableHeader>
@@ -275,16 +317,16 @@ export function CustomersClient({
                   <TableCell>
                   <TableCell>
                     <div className="flex items-center gap-2">
                     <div className="flex items-center gap-2">
                       <Users className="h-4 w-4 text-muted-foreground shrink-0" />
                       <Users className="h-4 w-4 text-muted-foreground shrink-0" />
-                      <span className="font-medium">{c.name}</span>
+                      <span className="truncate font-medium">{c.name}</span>
                     </div>
                     </div>
                   </TableCell>
                   </TableCell>
-                  <TableCell className="hidden sm:table-cell text-muted-foreground">
+                  <TableCell className="hidden truncate sm:table-cell text-muted-foreground">
                     {c.company || "-"}
                     {c.company || "-"}
                   </TableCell>
                   </TableCell>
-                  <TableCell className="hidden md:table-cell text-muted-foreground">
+                  <TableCell className="hidden truncate md:table-cell text-muted-foreground">
                     {c.phone || "-"}
                     {c.phone || "-"}
                   </TableCell>
                   </TableCell>
-                  <TableCell className="hidden lg:table-cell text-muted-foreground">
+                  <TableCell className="hidden truncate lg:table-cell text-muted-foreground">
                     {c.email || "-"}
                     {c.email || "-"}
                   </TableCell>
                   </TableCell>
                   <TableCell className="text-center">{c._count.vehicles}</TableCell>
                   <TableCell className="text-center">{c._count.vehicles}</TableCell>

+ 11 - 1
src/app/(authenticated)/customers/page.tsx

@@ -6,13 +6,21 @@ import { PageHeader } from "@/components/page-header";
 export default async function CustomersPage({
 export default async function CustomersPage({
   searchParams,
   searchParams,
 }: {
 }: {
-  searchParams: Promise<{ page?: string; pageSize?: string; search?: string }>;
+  searchParams: Promise<{
+    page?: string;
+    pageSize?: string;
+    search?: string;
+    sortBy?: string;
+    sortOrder?: string;
+  }>;
 }) {
 }) {
   const params = await searchParams;
   const params = await searchParams;
   const result = await getCustomersPaginated({
   const result = await getCustomersPaginated({
     page: params.page ? parseInt(params.page) : 1,
     page: params.page ? parseInt(params.page) : 1,
     pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
     pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
     search: params.search,
     search: params.search,
+    sortBy: params.sortBy,
+    sortOrder: params.sortOrder as "asc" | "desc" | undefined,
   });
   });
 
 
   if (!result.success || !result.data) {
   if (!result.success || !result.data) {
@@ -36,6 +44,8 @@ export default async function CustomersPage({
         <CustomersClient
         <CustomersClient
           data={result.data}
           data={result.data}
           search={params.search || ""}
           search={params.search || ""}
+          sortBy={params.sortBy || ""}
+          sortOrder={(params.sortOrder as "asc" | "desc") || "desc"}
         />
         />
       </div>
       </div>
     </>
     </>

+ 44 - 9
src/app/(authenticated)/inspections/inspections-client.tsx

@@ -12,7 +12,7 @@ import {
   Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
   Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
 } from "@/components/ui/table";
 } from "@/components/ui/table";
 import { DataTablePagination } from "@/components/data-table-pagination";
 import { DataTablePagination } from "@/components/data-table-pagination";
-import { Loader2, Plus, Search } from "lucide-react";
+import { ArrowDown, ArrowUp, ArrowUpDown, Loader2, Plus, Search } from "lucide-react";
 import { NewInspectionDialog } from "@/features/inspections/Components/NewInspectionDialog";
 import { NewInspectionDialog } from "@/features/inspections/Components/NewInspectionDialog";
 
 
 interface InspectionRecord {
 interface InspectionRecord {
@@ -93,11 +93,15 @@ export function InspectionsClient({
   templates,
   templates,
   search,
   search,
   statusFilter,
   statusFilter,
+  sortBy = "",
+  sortOrder = "desc",
 }: {
 }: {
   data: PaginatedData;
   data: PaginatedData;
   templates: TemplateOption[];
   templates: TemplateOption[];
   search: string;
   search: string;
   statusFilter: string;
   statusFilter: string;
+  sortBy?: string;
+  sortOrder?: "asc" | "desc";
 }) {
 }) {
   const router = useRouter();
   const router = useRouter();
   const { formatDate } = useFormatDate();
   const { formatDate } = useFormatDate();
@@ -132,6 +136,21 @@ export function InspectionsClient({
     commitNow: handleSearch,
     commitNow: handleSearch,
   } = useDebouncedSearch(search, (term) => navigate({ search: term }));
   } = useDebouncedSearch(search, (term) => navigate({ search: term }));
 
 
+  const handleSort = useCallback(
+    (column: string) => {
+      const newOrder = sortBy === column && sortOrder === "asc" ? "desc" : "asc";
+      navigate({ sortBy: column, sortOrder: newOrder });
+    },
+    [navigate, sortBy, sortOrder]
+  );
+
+  const SortIcon = ({ column }: { column: string }) => {
+    if (sortBy !== column) return <ArrowUpDown className="ml-1 h-3 w-3 opacity-50" />;
+    return sortOrder === "asc"
+      ? <ArrowUp className="ml-1 h-3 w-3" />
+      : <ArrowDown className="ml-1 h-3 w-3" />;
+  };
+
   return (
   return (
     <div className="space-y-4">
     <div className="space-y-4">
       <div className="flex flex-wrap gap-2">
       <div className="flex flex-wrap gap-2">
@@ -176,14 +195,30 @@ export function InspectionsClient({
       </div>
       </div>
 
 
       <div className="rounded-lg border">
       <div className="rounded-lg border">
-        <Table>
+        <Table className="table-fixed">
           <TableHeader>
           <TableHeader>
             <TableRow>
             <TableRow>
-              <TableHead>Vehicle</TableHead>
-              <TableHead className="hidden md:table-cell">Template</TableHead>
+              <TableHead>
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("vehicle")}>
+                  Vehicle<SortIcon column="vehicle" />
+                </button>
+              </TableHead>
+              <TableHead className="hidden w-[24%] md:table-cell">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("template")}>
+                  Template<SortIcon column="template" />
+                </button>
+              </TableHead>
               <TableHead className="w-32">Progress</TableHead>
               <TableHead className="w-32">Progress</TableHead>
-              <TableHead className="w-28">Status</TableHead>
-              <TableHead className="w-24">Date</TableHead>
+              <TableHead className="w-28">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("status")}>
+                  Status<SortIcon column="status" />
+                </button>
+              </TableHead>
+              <TableHead className="w-24">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("createdAt")}>
+                  Date<SortIcon column="createdAt" />
+                </button>
+              </TableHead>
             </TableRow>
             </TableRow>
           </TableHeader>
           </TableHeader>
           <TableBody>
           <TableBody>
@@ -201,8 +236,8 @@ export function InspectionsClient({
                   onClick={() => router.push(`/inspections/${insp.id}`)}
                   onClick={() => router.push(`/inspections/${insp.id}`)}
                 >
                 >
                   <TableCell>
                   <TableCell>
-                    <div>
-                      <p className="font-medium">
+                    <div className="min-w-0">
+                      <p className="truncate font-medium">
                         {insp.vehicle.year} {insp.vehicle.make} {insp.vehicle.model}
                         {insp.vehicle.year} {insp.vehicle.make} {insp.vehicle.model}
                       </p>
                       </p>
                       {insp.vehicle.licensePlate && (
                       {insp.vehicle.licensePlate && (
@@ -212,7 +247,7 @@ export function InspectionsClient({
                       )}
                       )}
                     </div>
                     </div>
                   </TableCell>
                   </TableCell>
-                  <TableCell className="hidden md:table-cell text-muted-foreground">
+                  <TableCell className="hidden truncate md:table-cell text-muted-foreground">
                     {insp.template.name}
                     {insp.template.name}
                   </TableCell>
                   </TableCell>
                   <TableCell>
                   <TableCell>

+ 6 - 0
src/app/(authenticated)/inspections/page.tsx

@@ -11,6 +11,8 @@ export default async function InspectionsPage({
     pageSize?: string;
     pageSize?: string;
     search?: string;
     search?: string;
     status?: string;
     status?: string;
+    sortBy?: string;
+    sortOrder?: string;
   }>;
   }>;
 }) {
 }) {
   const params = await searchParams;
   const params = await searchParams;
@@ -20,6 +22,8 @@ export default async function InspectionsPage({
       pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
       pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
       search: params.search,
       search: params.search,
       status: params.status || "all",
       status: params.status || "all",
+      sortBy: params.sortBy,
+      sortOrder: params.sortOrder as "asc" | "desc" | undefined,
     }),
     }),
     getTemplates(),
     getTemplates(),
   ]);
   ]);
@@ -48,6 +52,8 @@ export default async function InspectionsPage({
           templates={templates}
           templates={templates}
           search={params.search || ""}
           search={params.search || ""}
           statusFilter={params.status || "all"}
           statusFilter={params.status || "all"}
+          sortBy={params.sortBy || ""}
+          sortOrder={(params.sortOrder as "asc" | "desc") || "desc"}
         />
         />
       </div>
       </div>
     </>
     </>

+ 6 - 0
src/app/(authenticated)/quotes/page.tsx

@@ -12,6 +12,8 @@ export default async function QuotesPage({
     pageSize?: string
     pageSize?: string
     search?: string
     search?: string
     status?: string
     status?: string
+    sortBy?: string
+    sortOrder?: string
   }>
   }>
 }) {
 }) {
   const params = await searchParams
   const params = await searchParams
@@ -21,6 +23,8 @@ export default async function QuotesPage({
       pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
       pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
       search: params.search,
       search: params.search,
       status: params.status || 'all',
       status: params.status || 'all',
+      sortBy: params.sortBy,
+      sortOrder: params.sortOrder as 'asc' | 'desc' | undefined,
     }),
     }),
     getSettings([SETTING_KEYS.CURRENCY_CODE]),
     getSettings([SETTING_KEYS.CURRENCY_CODE]),
   ])
   ])
@@ -48,6 +52,8 @@ export default async function QuotesPage({
           currencyCode={currencyCode}
           currencyCode={currencyCode}
           search={params.search || ''}
           search={params.search || ''}
           statusFilter={params.status || 'all'}
           statusFilter={params.status || 'all'}
+          sortBy={params.sortBy || ''}
+          sortOrder={(params.sortOrder as 'asc' | 'desc') || 'desc'}
         />
         />
       </div>
       </div>
     </>
     </>

+ 59 - 12
src/app/(authenticated)/quotes/quotes-client.tsx

@@ -18,7 +18,7 @@ import {
   TableRow,
   TableRow,
 } from '@/components/ui/table'
 } from '@/components/ui/table'
 import { DataTablePagination } from '@/components/data-table-pagination'
 import { DataTablePagination } from '@/components/data-table-pagination'
-import { Loader2, Plus, Search } from 'lucide-react'
+import { ArrowDown, ArrowUp, ArrowUpDown, Loader2, Plus, Search } from 'lucide-react'
 import { useFormatCurrency } from '@/components/currency-settings-context'
 import { useFormatCurrency } from '@/components/currency-settings-context'
 import { NewQuoteDialog } from '@/features/quotes/Components/NewQuoteDialog'
 import { NewQuoteDialog } from '@/features/quotes/Components/NewQuoteDialog'
 
 
@@ -72,11 +72,15 @@ export function QuotesClient({
   currencyCode = 'USD',
   currencyCode = 'USD',
   search,
   search,
   statusFilter,
   statusFilter,
+  sortBy = '',
+  sortOrder = 'desc',
 }: {
 }: {
   data: PaginatedData
   data: PaginatedData
   currencyCode?: string
   currencyCode?: string
   search: string
   search: string
   statusFilter: string
   statusFilter: string
+  sortBy?: string
+  sortOrder?: 'asc' | 'desc'
 }) {
 }) {
   const formatCurrency = useFormatCurrency()
   const formatCurrency = useFormatCurrency()
   const router = useRouter()
   const router = useRouter()
@@ -127,6 +131,21 @@ export function QuotesClient({
 
 
   const openNewDialog = () => setShowNewDialog(true)
   const openNewDialog = () => setShowNewDialog(true)
 
 
+  const handleSort = useCallback(
+    (column: string) => {
+      const newOrder = sortBy === column && sortOrder === 'asc' ? 'desc' : 'asc'
+      navigate({ sortBy: column, sortOrder: newOrder })
+    },
+    [navigate, sortBy, sortOrder]
+  )
+
+  const SortIcon = ({ column }: { column: string }) => {
+    if (sortBy !== column) return <ArrowUpDown className="ml-1 h-3 w-3 opacity-50" />
+    return sortOrder === 'asc'
+      ? <ArrowUp className="ml-1 h-3 w-3" />
+      : <ArrowDown className="ml-1 h-3 w-3" />
+  }
+
   return (
   return (
     <div className="space-y-4">
     <div className="space-y-4">
       <div className="flex flex-wrap gap-2">
       <div className="flex flex-wrap gap-2">
@@ -171,16 +190,44 @@ export function QuotesClient({
       </div>
       </div>
 
 
       <div className="rounded-lg border">
       <div className="rounded-lg border">
-        <Table>
+        <Table className="table-fixed">
           <TableHeader>
           <TableHeader>
             <TableRow>
             <TableRow>
-              <TableHead className="w-25">{t('list.columnQuoteNumber')}</TableHead>
-              <TableHead>{t('list.columnTitle')}</TableHead>
-              <TableHead className="hidden md:table-cell">{t('list.columnCustomer')}</TableHead>
-              <TableHead className="hidden lg:table-cell">{t('list.columnVehicle')}</TableHead>
-              <TableHead className="w-27.5">{t('list.columnStatus')}</TableHead>
-              <TableHead className="w-22.5">{t('list.columnDate')}</TableHead>
-              <TableHead className="w-22.5 text-right">{t('list.columnTotal')}</TableHead>
+              <TableHead className="w-25">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort('quoteNumber')}>
+                  {t('list.columnQuoteNumber')}<SortIcon column="quoteNumber" />
+                </button>
+              </TableHead>
+              <TableHead>
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort('title')}>
+                  {t('list.columnTitle')}<SortIcon column="title" />
+                </button>
+              </TableHead>
+              <TableHead className="hidden w-[18%] md:table-cell">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort('customer')}>
+                  {t('list.columnCustomer')}<SortIcon column="customer" />
+                </button>
+              </TableHead>
+              <TableHead className="hidden w-[18%] lg:table-cell">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort('vehicle')}>
+                  {t('list.columnVehicle')}<SortIcon column="vehicle" />
+                </button>
+              </TableHead>
+              <TableHead className="w-27.5">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort('status')}>
+                  {t('list.columnStatus')}<SortIcon column="status" />
+                </button>
+              </TableHead>
+              <TableHead className="w-22.5">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort('createdAt')}>
+                  {t('list.columnDate')}<SortIcon column="createdAt" />
+                </button>
+              </TableHead>
+              <TableHead className="w-22.5">
+                <button type="button" className="ml-auto flex items-center hover:text-foreground" onClick={() => handleSort('totalAmount')}>
+                  {t('list.columnTotal')}<SortIcon column="totalAmount" />
+                </button>
+              </TableHead>
             </TableRow>
             </TableRow>
           </TableHeader>
           </TableHeader>
           <TableBody>
           <TableBody>
@@ -200,11 +247,11 @@ export function QuotesClient({
                   <TableCell className="font-mono text-xs text-muted-foreground">
                   <TableCell className="font-mono text-xs text-muted-foreground">
                     {q.quoteNumber || '-'}
                     {q.quoteNumber || '-'}
                   </TableCell>
                   </TableCell>
-                  <TableCell className="font-medium">{q.title}</TableCell>
-                  <TableCell className="hidden md:table-cell text-muted-foreground">
+                  <TableCell className="truncate font-medium">{q.title}</TableCell>
+                  <TableCell className="hidden truncate md:table-cell text-muted-foreground">
                     {q.customer?.name || '-'}
                     {q.customer?.name || '-'}
                   </TableCell>
                   </TableCell>
-                  <TableCell className="hidden lg:table-cell text-muted-foreground">
+                  <TableCell className="hidden truncate lg:table-cell text-muted-foreground">
                     {q.vehicle ? `${q.vehicle.year} ${q.vehicle.make} ${q.vehicle.model}` : '-'}
                     {q.vehicle ? `${q.vehicle.year} ${q.vehicle.make} ${q.vehicle.model}` : '-'}
                   </TableCell>
                   </TableCell>
                   <TableCell>
                   <TableCell>

+ 12 - 1
src/app/(authenticated)/vehicles/page.tsx

@@ -8,7 +8,14 @@ import { PageHeader } from "@/components/page-header";
 export default async function VehiclesPage({
 export default async function VehiclesPage({
   searchParams,
   searchParams,
 }: {
 }: {
-  searchParams: Promise<{ page?: string; pageSize?: string; search?: string; archived?: string }>;
+  searchParams: Promise<{
+    page?: string;
+    pageSize?: string;
+    search?: string;
+    archived?: string;
+    sortBy?: string;
+    sortOrder?: string;
+  }>;
 }) {
 }) {
   const params = await searchParams;
   const params = await searchParams;
   const isArchived = params.archived === "true";
   const isArchived = params.archived === "true";
@@ -21,6 +28,8 @@ export default async function VehiclesPage({
       pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
       pageSize: params.pageSize ? parseInt(params.pageSize) : 20,
       search: params.search,
       search: params.search,
       archived: isArchived,
       archived: isArchived,
+      sortBy: params.sortBy,
+      sortOrder: params.sortOrder as "asc" | "desc" | undefined,
     }),
     }),
     getCustomersList(),
     getCustomersList(),
   ]);
   ]);
@@ -46,6 +55,8 @@ export default async function VehiclesPage({
           data={result.data}
           data={result.data}
           customers={customersResult.data ?? []}
           customers={customersResult.data ?? []}
           search={params.search || ""}
           search={params.search || ""}
+          sortBy={params.sortBy || ""}
+          sortOrder={(params.sortOrder as "asc" | "desc") || "desc"}
           initialView={initialView}
           initialView={initialView}
           isArchived={isArchived}
           isArchived={isArchived}
           archivedCount={result.data.archivedCount}
           archivedCount={result.data.archivedCount}

+ 51 - 9
src/app/(authenticated)/vehicles/vehicles-client.tsx

@@ -33,6 +33,9 @@ import { toast } from 'sonner'
 import {
 import {
   Archive,
   Archive,
   ArchiveRestore,
   ArchiveRestore,
+  ArrowDown,
+  ArrowUp,
+  ArrowUpDown,
   Gauge,
   Gauge,
   Grid3X3,
   Grid3X3,
   LayoutGrid,
   LayoutGrid,
@@ -88,6 +91,8 @@ export function VehiclesClient({
   data,
   data,
   customers,
   customers,
   search,
   search,
+  sortBy = '',
+  sortOrder = 'desc',
   initialView = 'table',
   initialView = 'table',
   isArchived = false,
   isArchived = false,
   archivedCount = 0,
   archivedCount = 0,
@@ -95,6 +100,8 @@ export function VehiclesClient({
   data: PaginatedData
   data: PaginatedData
   customers: CustomerOption[]
   customers: CustomerOption[]
   search: string
   search: string
+  sortBy?: string
+  sortOrder?: 'asc' | 'desc'
   initialView?: 'table' | 'grid' | 'grid6'
   initialView?: 'table' | 'grid' | 'grid6'
   isArchived?: boolean
   isArchived?: boolean
   archivedCount?: number
   archivedCount?: number
@@ -141,7 +148,7 @@ export function VehiclesClient({
           newParams.set(key, String(value))
           newParams.set(key, String(value))
         }
         }
       }
       }
-      if (!('page' in params) && 'search' in params) {
+      if (!('page' in params) && ('search' in params || 'sortBy' in params)) {
         newParams.delete('page')
         newParams.delete('page')
       }
       }
       startTransition(() => {
       startTransition(() => {
@@ -151,6 +158,21 @@ export function VehiclesClient({
     [router, pathname, searchParams]
     [router, pathname, searchParams]
   )
   )
 
 
+  const handleSort = useCallback(
+    (column: string) => {
+      const newOrder = sortBy === column && sortOrder === 'asc' ? 'desc' : 'asc'
+      navigate({ sortBy: column, sortOrder: newOrder })
+    },
+    [navigate, sortBy, sortOrder]
+  )
+
+  const SortIcon = ({ column }: { column: string }) => {
+    if (sortBy !== column) return <ArrowUpDown className="ml-1 h-3 w-3 opacity-50" />
+    return sortOrder === 'asc'
+      ? <ArrowUp className="ml-1 h-3 w-3" />
+      : <ArrowDown className="ml-1 h-3 w-3" />
+  }
+
   const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
   const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
 
 
   const handleSearchChange = useCallback(
   const handleSearchChange = useCallback(
@@ -285,14 +307,34 @@ export function VehiclesClient({
       ) : view === 'table' ? (
       ) : view === 'table' ? (
         /* Table view */
         /* Table view */
         <div className="rounded-lg border">
         <div className="rounded-lg border">
-          <Table>
+          <Table className="table-fixed">
             <TableHeader>
             <TableHeader>
               <TableRow>
               <TableRow>
-                <TableHead className="w-[120px]">{t('table.plate')}</TableHead>
-                <TableHead>{t('table.vehicle')}</TableHead>
-                <TableHead className="hidden sm:table-cell">{t('table.customer')}</TableHead>
-                <TableHead className="hidden md:table-cell w-[100px] text-right">{serviceType === 'marine' ? t('table.mileageMarine') : t('table.mileage')}</TableHead>
-                <TableHead className="w-[80px] text-center">{t('table.services')}</TableHead>
+                <TableHead className="w-[120px]">
+                  <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort('plate')}>
+                    {t('table.plate')}<SortIcon column="plate" />
+                  </button>
+                </TableHead>
+                <TableHead>
+                  <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort('vehicle')}>
+                    {t('table.vehicle')}<SortIcon column="vehicle" />
+                  </button>
+                </TableHead>
+                <TableHead className="hidden w-[24%] sm:table-cell">
+                  <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort('customer')}>
+                    {t('table.customer')}<SortIcon column="customer" />
+                  </button>
+                </TableHead>
+                <TableHead className="hidden md:table-cell w-[100px]">
+                  <button type="button" className="ml-auto flex items-center hover:text-foreground" onClick={() => handleSort('mileage')}>
+                    {serviceType === 'marine' ? t('table.mileageMarine') : t('table.mileage')}<SortIcon column="mileage" />
+                  </button>
+                </TableHead>
+                <TableHead className="w-[80px]">
+                  <button type="button" className="mx-auto flex items-center hover:text-foreground" onClick={() => handleSort('services')}>
+                    {t('table.services')}<SortIcon column="services" />
+                  </button>
+                </TableHead>
                 <TableHead className="w-[50px]" />
                 <TableHead className="w-[50px]" />
               </TableRow>
               </TableRow>
             </TableHeader>
             </TableHeader>
@@ -304,12 +346,12 @@ export function VehiclesClient({
                   onClick={() => router.push(`/vehicles/${v.id}`)}
                   onClick={() => router.push(`/vehicles/${v.id}`)}
                 >
                 >
                   <TableCell className="font-mono text-sm">{v.licensePlate || '-'}</TableCell>
                   <TableCell className="font-mono text-sm">{v.licensePlate || '-'}</TableCell>
-                  <TableCell>
+                  <TableCell className="truncate">
                     <span className="font-medium">
                     <span className="font-medium">
                       {v.year} {v.make} {v.model}
                       {v.year} {v.make} {v.model}
                     </span>
                     </span>
                   </TableCell>
                   </TableCell>
-                  <TableCell className="hidden sm:table-cell text-muted-foreground">
+                  <TableCell className="hidden truncate sm:table-cell text-muted-foreground">
                     {v.customer?.name || '-'}
                     {v.customer?.name || '-'}
                   </TableCell>
                   </TableCell>
                   <TableCell className="hidden md:table-cell text-right font-mono text-sm">
                   <TableCell className="hidden md:table-cell text-right font-mono text-sm">

+ 16 - 8
src/app/(authenticated)/work-orders/work-orders-client.tsx

@@ -275,7 +275,7 @@ export function WorkOrdersClient({
 
 
       {/* Table */}
       {/* Table */}
       <div className="rounded-lg border">
       <div className="rounded-lg border">
-        <Table>
+        <Table className="table-fixed">
           <TableHeader>
           <TableHeader>
             <TableRow>
             <TableRow>
               <TableHead className="hidden sm:table-cell w-[100px]">
               <TableHead className="hidden sm:table-cell w-[100px]">
@@ -283,8 +283,16 @@ export function WorkOrdersClient({
                   {t("table.invoice")}<SortIcon column="invoiceNumber" />
                   {t("table.invoice")}<SortIcon column="invoiceNumber" />
                 </button>
                 </button>
               </TableHead>
               </TableHead>
-              <TableHead>{t("table.vehicle")}</TableHead>
-              <TableHead className="hidden md:table-cell">{t("table.customer")}</TableHead>
+              <TableHead className="w-[18%]">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("vehicle")}>
+                  {t("table.vehicle")}<SortIcon column="vehicle" />
+                </button>
+              </TableHead>
+              <TableHead className="hidden w-[14%] md:table-cell">
+                <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("customer")}>
+                  {t("table.customer")}<SortIcon column="customer" />
+                </button>
+              </TableHead>
               <TableHead>
               <TableHead>
                 <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("title")}>
                 <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("title")}>
                   {t("table.title")}<SortIcon column="title" />
                   {t("table.title")}<SortIcon column="title" />
@@ -295,7 +303,7 @@ export function WorkOrdersClient({
                   {t("table.status")}<SortIcon column="status" />
                   {t("table.status")}<SortIcon column="status" />
                 </button>
                 </button>
               </TableHead>
               </TableHead>
-              <TableHead className="hidden lg:table-cell">
+              <TableHead className="hidden w-[12%] lg:table-cell">
                 <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("techName")}>
                 <button type="button" className="flex items-center hover:text-foreground" onClick={() => handleSort("techName")}>
                   {t("table.tech")}<SortIcon column="techName" />
                   {t("table.tech")}<SortIcon column="techName" />
                 </button>
                 </button>
@@ -337,19 +345,19 @@ export function WorkOrdersClient({
                       {r.invoiceNumber || "-"}
                       {r.invoiceNumber || "-"}
                     </TableCell>
                     </TableCell>
                     <TableCell>
                     <TableCell>
-                      <div>
+                      <div className="min-w-0">
                         {r.vehicle.licensePlate && (
                         {r.vehicle.licensePlate && (
                           <span className="font-mono text-sm font-medium">{r.vehicle.licensePlate}</span>
                           <span className="font-mono text-sm font-medium">{r.vehicle.licensePlate}</span>
                         )}
                         )}
-                        <p className="text-xs text-muted-foreground">
+                        <p className="truncate text-xs text-muted-foreground">
                           {r.vehicle.year} {r.vehicle.make} {r.vehicle.model}
                           {r.vehicle.year} {r.vehicle.make} {r.vehicle.model}
                         </p>
                         </p>
                       </div>
                       </div>
                     </TableCell>
                     </TableCell>
-                    <TableCell className="hidden md:table-cell text-muted-foreground">
+                    <TableCell className="hidden truncate md:table-cell text-muted-foreground">
                       {r.vehicle.customer?.name || "-"}
                       {r.vehicle.customer?.name || "-"}
                     </TableCell>
                     </TableCell>
-                    <TableCell>
+                    <TableCell className="truncate">
                       <span className="font-medium">{r.title}</span>
                       <span className="font-medium">{r.title}</span>
                     </TableCell>
                     </TableCell>
                     <TableCell>
                     <TableCell>

+ 22 - 74
src/features/billing/Actions/billingActions.ts

@@ -4,6 +4,7 @@ import { db } from "@/lib/db";
 import { withAuth } from "@/lib/with-auth";
 import { withAuth } from "@/lib/with-auth";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { Prisma } from "@/generated/prisma/client";
 import { Prisma } from "@/generated/prisma/client";
+import { effectiveDateSql } from "@/lib/date-sort";
 
 
 interface BillingSummary {
 interface BillingSummary {
   totalRevenue: number;
   totalRevenue: number;
@@ -64,6 +65,8 @@ export async function getBillingHistory(params: {
   pageSize?: number;
   pageSize?: number;
   search?: string;
   search?: string;
   status?: string;
   status?: string;
+  sortBy?: string;
+  sortOrder?: "asc" | "desc";
 }) {
 }) {
   return withAuth(async ({ organizationId }) => {
   return withAuth(async ({ organizationId }) => {
     const page = params.page || 1;
     const page = params.page || 1;
@@ -72,21 +75,10 @@ export async function getBillingHistory(params: {
 
 
     const summary = await getBillingSummary(organizationId);
     const summary = await getBillingSummary(organizationId);
 
 
-    // Build where clause
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const where: any = { vehicle: { organizationId } };
-
-    if (params.search) {
-      where.OR = [
-        { title: { contains: params.search, mode: "insensitive" } },
-        { invoiceNumber: { contains: params.search, mode: "insensitive" } },
-        { vehicle: { make: { contains: params.search, mode: "insensitive" } } },
-        { vehicle: { model: { contains: params.search, mode: "insensitive" } } },
-      ];
-    }
-
-    // When filtering by payment status, use raw SQL for efficiency
-    if (params.status === "paid" || params.status === "partial" || params.status === "unpaid") {
+    // Raw SQL for every path: payment status and effective total are computed
+    // values, and the displayed date is COALESCE(invoiceDate, startDateTime,
+    // serviceDate) — only SQL can filter and sort by them exactly.
+    {
       const searchCondition = params.search
       const searchCondition = params.search
         ? Prisma.sql`AND (
         ? Prisma.sql`AND (
             sr.title ILIKE ${`%${params.search}%`}
             sr.title ILIKE ${`%${params.search}%`}
@@ -101,8 +93,10 @@ export async function getBillingHistory(params: {
         statusCondition = Prisma.sql`AND (manually_paid = true OR (paid_amount >= effective_total AND effective_total > 0))`;
         statusCondition = Prisma.sql`AND (manually_paid = true OR (paid_amount >= effective_total AND effective_total > 0))`;
       } else if (params.status === "partial") {
       } else if (params.status === "partial") {
         statusCondition = Prisma.sql`AND manually_paid = false AND paid_amount > 0 AND paid_amount < effective_total`;
         statusCondition = Prisma.sql`AND manually_paid = false AND paid_amount > 0 AND paid_amount < effective_total`;
-      } else {
+      } else if (params.status === "unpaid") {
         statusCondition = Prisma.sql`AND manually_paid = false AND paid_amount = 0`;
         statusCondition = Prisma.sql`AND manually_paid = false AND paid_amount = 0`;
+      } else {
+        statusCondition = Prisma.empty;
       }
       }
 
 
       const countRows = await db.$queryRaw<{ cnt: bigint }[]>(Prisma.sql`
       const countRows = await db.$queryRaw<{ cnt: bigint }[]>(Prisma.sql`
@@ -190,7 +184,18 @@ export async function getBillingHistory(params: {
           ${searchCondition}
           ${searchCondition}
         ) sub
         ) sub
         WHERE 1=1 ${statusCondition}
         WHERE 1=1 ${statusCondition}
-        ORDER BY COALESCE(sub."invoiceDate", sub."startDateTime", sub."serviceDate") DESC
+        ORDER BY ${(() => {
+          const dir = params.sortOrder === "asc" ? Prisma.sql`ASC` : Prisma.sql`DESC`;
+          switch (params.sortBy) {
+            case "invoiceNumber": return Prisma.sql`sub."invoiceNumber" ${dir} NULLS LAST`;
+            case "title": return Prisma.sql`sub.title ${dir}`;
+            case "vehicle": return Prisma.sql`sub.vehicle_make ${dir}, sub.vehicle_model ${dir}, sub.vehicle_year ${dir}`;
+            case "customer": return Prisma.sql`sub.customer_name ${dir} NULLS LAST`;
+            case "total": return Prisma.sql`sub.effective_total ${dir}`;
+            case "date": return Prisma.sql`${effectiveDateSql("sub")} ${dir}`;
+            default: return Prisma.sql`${effectiveDateSql("sub")} DESC`;
+          }
+        })()}
         LIMIT ${pageSize} OFFSET ${skip}
         LIMIT ${pageSize} OFFSET ${skip}
       `);
       `);
 
 
@@ -230,62 +235,5 @@ export async function getBillingHistory(params: {
         summary,
         summary,
       };
       };
     }
     }
-
-    // No status filter — use Prisma for clean pagination
-    const [records, total] = await Promise.all([
-      db.serviceRecord.findMany({
-        where,
-        include: {
-          payments: { select: { amount: true } },
-          vehicle: {
-            select: {
-              id: true,
-              make: true,
-              model: true,
-              year: true,
-              licensePlate: true,
-              customer: { select: { id: true, name: true } },
-            },
-          },
-        },
-        orderBy: [
-          { invoiceDate: { sort: "desc", nulls: "last" } },
-          { startDateTime: { sort: "desc", nulls: "last" } },
-          { serviceDate: "desc" },
-        ],
-        skip,
-        take: pageSize,
-      }),
-      db.serviceRecord.count({ where }),
-    ]);
-
-    return {
-      records: records.map((r) => {
-        const effectiveTotal = r.totalAmount > 0 ? r.totalAmount : r.cost;
-        const paidFromPayments = r.payments.reduce((s, p) => s + p.amount, 0);
-        const totalPaid = r.manuallyPaid ? effectiveTotal : paidFromPayments;
-        const paymentStatus = r.manuallyPaid || (totalPaid >= effectiveTotal && effectiveTotal > 0)
-          ? "paid"
-          : totalPaid > 0
-            ? "partial"
-            : "unpaid";
-        return {
-          id: r.id,
-          title: r.title,
-          invoiceNumber: r.invoiceNumber,
-          startDateTime: r.startDateTime,
-          serviceDate: r.invoiceDate ?? r.startDateTime ?? r.serviceDate,
-          totalAmount: effectiveTotal,
-          totalPaid,
-          status: paymentStatus,
-          vehicle: r.vehicle,
-        };
-      }),
-      total,
-      page,
-      pageSize,
-      totalPages: Math.ceil(total / pageSize),
-      summary,
-    };
   }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.BILLING }] });
   }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.BILLING }] });
 }
 }

+ 14 - 1
src/features/customers/Actions/customerActions.ts

@@ -142,6 +142,8 @@ export async function getCustomersPaginated(params: {
   page?: number;
   page?: number;
   pageSize?: number;
   pageSize?: number;
   search?: string;
   search?: string;
+  sortBy?: string;
+  sortOrder?: "asc" | "desc";
 }) {
 }) {
   return withAuth(async ({ userId, organizationId }) => {
   return withAuth(async ({ userId, organizationId }) => {
     const page = params.page || 1;
     const page = params.page || 1;
@@ -178,7 +180,18 @@ export async function getCustomersPaginated(params: {
         include: {
         include: {
           _count: { select: { vehicles: true } },
           _count: { select: { vehicles: true } },
         },
         },
-        orderBy: { updatedAt: "desc" },
+        orderBy: (() => {
+          const dir = params.sortOrder || "desc";
+          const nullable = { sort: dir, nulls: "last" } as const;
+          switch (params.sortBy) {
+            case "name": return { name: dir };
+            case "company": return { company: nullable };
+            case "phone": return { phone: nullable };
+            case "email": return { email: nullable };
+            case "vehicles": return { vehicles: { _count: dir } };
+            default: return { updatedAt: "desc" as const };
+          }
+        })(),
         skip,
         skip,
         take: pageSize,
         take: pageSize,
       }),
       }),

+ 18 - 1
src/features/inspections/Actions/inspectionActions.ts

@@ -12,6 +12,8 @@ export async function getInspectionsPaginated(params: {
   pageSize?: number;
   pageSize?: number;
   search?: string;
   search?: string;
   status?: string;
   status?: string;
+  sortBy?: string;
+  sortOrder?: "asc" | "desc";
 }) {
 }) {
   return withAuth(async ({ organizationId }) => {
   return withAuth(async ({ organizationId }) => {
     const page = params.page || 1;
     const page = params.page || 1;
@@ -42,7 +44,22 @@ export async function getInspectionsPaginated(params: {
           template: { select: { id: true, name: true } },
           template: { select: { id: true, name: true } },
           items: { select: { id: true, condition: true } },
           items: { select: { id: true, condition: true } },
         },
         },
-        orderBy: { createdAt: "desc" },
+        orderBy: (() => {
+          const dir = params.sortOrder || "desc";
+          switch (params.sortBy) {
+            // Column shows "year make model"; sort make, model, year so
+            // identical models group together
+            case "vehicle": return [
+              { vehicle: { make: dir } },
+              { vehicle: { model: dir } },
+              { vehicle: { year: dir } },
+            ];
+            case "template": return { template: { name: dir } };
+            case "status": return { status: dir };
+            case "createdAt": return { createdAt: dir };
+            default: return { createdAt: "desc" as const };
+          }
+        })(),
         skip,
         skip,
         take: pageSize,
         take: pageSize,
       }),
       }),

+ 21 - 1
src/features/quotes/Actions/quoteActions.ts

@@ -28,6 +28,8 @@ export async function getQuotesPaginated(params: {
   pageSize?: number;
   pageSize?: number;
   search?: string;
   search?: string;
   status?: string;
   status?: string;
+  sortBy?: string;
+  sortOrder?: "asc" | "desc";
 }) {
 }) {
   return withAuth(async ({ userId, organizationId }) => {
   return withAuth(async ({ userId, organizationId }) => {
     const page = params.page || 1;
     const page = params.page || 1;
@@ -56,7 +58,25 @@ export async function getQuotesPaginated(params: {
           customer: { select: { id: true, name: true } },
           customer: { select: { id: true, name: true } },
           vehicle: { select: { id: true, make: true, model: true, year: true, licensePlate: true } },
           vehicle: { select: { id: true, make: true, model: true, year: true, licensePlate: true } },
         },
         },
-        orderBy: { createdAt: "desc" },
+        orderBy: (() => {
+          const dir = params.sortOrder || "desc";
+          switch (params.sortBy) {
+            case "quoteNumber": return { quoteNumber: { sort: dir, nulls: "last" as const } };
+            case "title": return { title: dir };
+            case "customer": return { customer: { name: dir } };
+            // Column shows "year make model"; sort make, model, year so
+            // identical models group together
+            case "vehicle": return [
+              { vehicle: { make: dir } },
+              { vehicle: { model: dir } },
+              { vehicle: { year: dir } },
+            ];
+            case "status": return { status: dir };
+            case "totalAmount": return { totalAmount: dir };
+            case "createdAt": return { createdAt: dir };
+            default: return { createdAt: "desc" as const };
+          }
+        })(),
         skip,
         skip,
         take: pageSize,
         take: pageSize,
       }),
       }),

+ 10 - 1
src/features/vehicles/Actions/serviceActions.ts

@@ -9,6 +9,7 @@ import { unlink } from "fs/promises";
 import { randomUUID } from "crypto";
 import { randomUUID } from "crypto";
 import { resolveUploadPath } from "@/lib/resolve-upload-path";
 import { resolveUploadPath } from "@/lib/resolve-upload-path";
 import { resolveInvoicePrefix, toSafeDate } from "@/lib/invoice-utils";
 import { resolveInvoicePrefix, toSafeDate } from "@/lib/invoice-utils";
+import { serviceDateOrderBy } from "@/lib/date-sort";
 import { notificationBus } from "@/lib/notification-bus";
 import { notificationBus } from "@/lib/notification-bus";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { PermissionAction, PermissionSubject } from "@/lib/permissions";
 import { reconcileInventoryForParts } from "@/features/inventory/Lib/reconcileStock";
 import { reconcileInventoryForParts } from "@/features/inventory/Lib/reconcileStock";
@@ -707,8 +708,16 @@ export async function getWorkOrders(params: {
             case "status": return { status: dir };
             case "status": return { status: dir };
             case "techName": return { techName: dir };
             case "techName": return { techName: dir };
             case "totalAmount": return { totalAmount: dir };
             case "totalAmount": return { totalAmount: dir };
+            // Column shows "year make model"; sort make, model, year so
+            // identical models group together
+            case "vehicle": return [
+              { vehicle: { make: dir } },
+              { vehicle: { model: dir } },
+              { vehicle: { year: dir } },
+            ];
+            case "customer": return { vehicle: { customer: { name: dir } } };
             case "serviceDate":
             case "serviceDate":
-            default: return [{ startDateTime: { sort: dir, nulls: dir === "desc" ? "last" : "first" } }, { serviceDate: dir }];
+            default: return serviceDateOrderBy(dir);
           }
           }
         })(),
         })(),
         skip,
         skip,

+ 15 - 1
src/features/vehicles/Actions/vehicleActions.ts

@@ -76,6 +76,8 @@ export async function getVehiclesPaginated(params: {
   pageSize?: number;
   pageSize?: number;
   search?: string;
   search?: string;
   archived?: boolean;
   archived?: boolean;
+  sortBy?: string;
+  sortOrder?: "asc" | "desc";
 }) {
 }) {
   return withAuth(async ({ organizationId }) => {
   return withAuth(async ({ organizationId }) => {
     const page = params.page || 1;
     const page = params.page || 1;
@@ -115,7 +117,19 @@ export async function getVehiclesPaginated(params: {
           customer: { select: { id: true, name: true, company: true } },
           customer: { select: { id: true, name: true, company: true } },
           _count: { select: { serviceRecords: true } },
           _count: { select: { serviceRecords: true } },
         },
         },
-        orderBy: { updatedAt: "desc" },
+        orderBy: (() => {
+          const dir = params.sortOrder || "desc";
+          switch (params.sortBy) {
+            case "plate": return { licensePlate: { sort: dir, nulls: "last" as const } };
+            // The column shows "year make model" as one value; sort by make,
+            // then model, then year so identical models group together
+            case "vehicle": return [{ make: dir }, { model: dir }, { year: dir }];
+            case "customer": return { customer: { name: dir } };
+            case "mileage": return { mileage: dir };
+            case "services": return { serviceRecords: { _count: dir } };
+            default: return { updatedAt: "desc" as const };
+          }
+        })(),
         skip,
         skip,
         take: pageSize,
         take: pageSize,
       }),
       }),

+ 30 - 0
src/lib/date-sort.ts

@@ -0,0 +1,30 @@
+import { Prisma } from "@/generated/prisma/client";
+
+/**
+ * SQL ORDER BY expression for the date a work order/invoice is presented
+ * with: the explicitly set invoice date, else the scheduled start, else the
+ * service date. Matches what the invoice PDF and list views display.
+ * `alias` must be a code-supplied table alias, never user input.
+ */
+export function effectiveDateSql(alias: string): Prisma.Sql {
+  const a = Prisma.raw(`"${alias}"`);
+  return Prisma.sql`COALESCE(${a}."invoiceDate", ${a}."startDateTime", ${a}."serviceDate")`;
+}
+
+/**
+ * Prisma orderBy approximating COALESCE(startDateTime, serviceDate) for
+ * lists that display the scheduled start when present. Records without a
+ * scheduled start (imported/legacy data) sort by serviceDate and group at
+ * the old end: nulls first ascending, nulls last descending.
+ */
+export function serviceDateOrderBy(dir: "asc" | "desc") {
+  return [
+    {
+      startDateTime: {
+        sort: dir,
+        nulls: dir === "asc" ? ("first" as const) : ("last" as const),
+      },
+    },
+    { serviceDate: dir },
+  ];
+}