Forráskód Böngészése

Improve dropdown performance with lazy loading and search (#64)

* moved customer dropdown into seperate component with inifite scroll

* css

* improved seach performance customer dropdown
Bernt Christian Egeland 6 hónapja
szülő
commit
fb377fb635

+ 10 - 58
src/app/(authenticated)/quotes/[id]/page.tsx

@@ -1,8 +1,6 @@
 import { getQuote } from "@/features/quotes/Actions/quoteActions";
 import { getQuote } from "@/features/quotes/Actions/quoteActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
-import { getCustomers } from "@/features/customers/Actions/customerActions";
-import { getVehicles } from "@/features/vehicles/Actions/vehicleActions";
 import { getAuthContext } from "@/lib/get-auth-context";
 import { getAuthContext } from "@/lib/get-auth-context";
 import { getFeatures } from "@/lib/features";
 import { getFeatures } from "@/lib/features";
 import { PageHeader } from "@/components/page-header";
 import { PageHeader } from "@/components/page-header";
@@ -14,19 +12,16 @@ export default async function QuoteDetailPage({
   params: Promise<{ id: string }>;
   params: Promise<{ id: string }>;
 }) {
 }) {
   const { id } = await params;
   const { id } = await params;
-  const [result, settingsResult, customersResult, vehiclesResult, authContext] =
-    await Promise.all([
-      getQuote(id),
-      getSettings([
-        SETTING_KEYS.CURRENCY_CODE,
-        SETTING_KEYS.DEFAULT_TAX_RATE,
-        SETTING_KEYS.TAX_ENABLED,
-        SETTING_KEYS.DEFAULT_LABOR_RATE,
-      ]),
-      getCustomers(),
-      getVehicles(),
-      getAuthContext(),
-    ]);
+  const [result, settingsResult, authContext] = await Promise.all([
+    getQuote(id),
+    getSettings([
+      SETTING_KEYS.CURRENCY_CODE,
+      SETTING_KEYS.DEFAULT_TAX_RATE,
+      SETTING_KEYS.TAX_ENABLED,
+      SETTING_KEYS.DEFAULT_LABOR_RATE,
+    ]),
+    getAuthContext(),
+  ]);
 
 
   const features = authContext?.organizationId
   const features = authContext?.organizationId
     ? await getFeatures(authContext.organizationId)
     ? await getFeatures(authContext.organizationId)
@@ -54,14 +49,6 @@ export default async function QuoteDetailPage({
     : 0;
     : 0;
   const defaultLaborRate =
   const defaultLaborRate =
     Number(settings[SETTING_KEYS.DEFAULT_LABOR_RATE]) || 0;
     Number(settings[SETTING_KEYS.DEFAULT_LABOR_RATE]) || 0;
-  const customers =
-    customersResult.success && customersResult.data
-      ? customersResult.data
-      : [];
-  const vehicles =
-    vehiclesResult.success && vehiclesResult.data
-      ? vehiclesResult.data
-      : [];
   const organizationId = authContext?.organizationId || "";
   const organizationId = authContext?.organizationId || "";
 
 
   // Separate attachments by category
   // Separate attachments by category
@@ -89,41 +76,6 @@ export default async function QuoteDetailPage({
         defaultLaborRate={defaultLaborRate}
         defaultLaborRate={defaultLaborRate}
         smsEnabled={features?.sms ?? false}
         smsEnabled={features?.sms ?? false}
         emailEnabled={features?.smtp ?? false}
         emailEnabled={features?.smtp ?? false}
-        customers={customers.map(
-          (c: {
-            id: string;
-            name: string;
-            email: string | null;
-            company: string | null;
-          }) => ({
-            id: c.id,
-            name: c.name,
-            email: c.email,
-            company: c.company,
-          })
-        )}
-        vehicles={vehicles.map(
-          (v: {
-            id: string;
-            make: string;
-            model: string;
-            year: number;
-            licensePlate: string | null;
-            customer: {
-              id: string;
-              name: string;
-              company: string | null;
-            } | null;
-          }) => ({
-            id: v.id,
-            make: v.make,
-            model: v.model,
-            year: v.year,
-            licensePlate: v.licensePlate,
-            customerId: v.customer?.id || null,
-            customerName: v.customer?.name || null,
-          })
-        )}
       />
       />
     </div>
     </div>
   );
   );

+ 1 - 25
src/app/(authenticated)/quotes/page.tsx

@@ -1,8 +1,6 @@
 import { getQuotesPaginated } from "@/features/quotes/Actions/quoteActions";
 import { getQuotesPaginated } from "@/features/quotes/Actions/quoteActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
-import { getVehicles } from "@/features/vehicles/Actions/vehicleActions";
-import { getCustomersList } from "@/features/customers/Actions/customerActions";
 import { QuotesClient } from "./quotes-client";
 import { QuotesClient } from "./quotes-client";
 import { PageHeader } from "@/components/page-header";
 import { PageHeader } from "@/components/page-header";
 
 
@@ -17,7 +15,7 @@ export default async function QuotesPage({
   }>;
   }>;
 }) {
 }) {
   const params = await searchParams;
   const params = await searchParams;
-  const [result, settingsResult, vehiclesResult, customersResult] = await Promise.all([
+  const [result, settingsResult] = await Promise.all([
     getQuotesPaginated({
     getQuotesPaginated({
       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,
@@ -25,8 +23,6 @@ export default async function QuotesPage({
       status: params.status || "all",
       status: params.status || "all",
     }),
     }),
     getSettings([SETTING_KEYS.CURRENCY_CODE]),
     getSettings([SETTING_KEYS.CURRENCY_CODE]),
-    getVehicles(),
-    getCustomersList(),
   ]);
   ]);
 
 
   if (!result.success || !result.data) {
   if (!result.success || !result.data) {
@@ -44,24 +40,6 @@ export default async function QuotesPage({
 
 
   const settings = settingsResult.success && settingsResult.data ? settingsResult.data : {};
   const settings = settingsResult.success && settingsResult.data ? settingsResult.data : {};
   const currencyCode = settings[SETTING_KEYS.CURRENCY_CODE] || "USD";
   const currencyCode = settings[SETTING_KEYS.CURRENCY_CODE] || "USD";
-  const vehicles = vehiclesResult.success && vehiclesResult.data
-    ? vehiclesResult.data.map((v) => ({
-        id: v.id,
-        make: v.make,
-        model: v.model,
-        year: v.year,
-        licensePlate: v.licensePlate,
-        customerId: v.customer?.id ?? null,
-        customerName: v.customer?.name ?? null,
-      }))
-    : [];
-  const customers = customersResult.success && customersResult.data
-    ? customersResult.data.map((c) => ({
-        id: c.id,
-        name: c.name,
-        company: c.company,
-      }))
-    : [];
 
 
   return (
   return (
     <>
     <>
@@ -72,8 +50,6 @@ export default async function QuotesPage({
           currencyCode={currencyCode}
           currencyCode={currencyCode}
           search={params.search || ""}
           search={params.search || ""}
           statusFilter={params.status || "all"}
           statusFilter={params.status || "all"}
-          vehicles={vehicles}
-          customers={customers}
         />
         />
       </div>
       </div>
     </>
     </>

+ 20 - 133
src/app/(authenticated)/quotes/quotes-client.tsx

@@ -14,16 +14,13 @@ import {
 import {
 import {
   Dialog, DialogContent, DialogHeader, DialogTitle,
   Dialog, DialogContent, DialogHeader, DialogTitle,
 } from "@/components/ui/dialog";
 } from "@/components/ui/dialog";
-import {
-  Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList,
-} from "@/components/ui/command";
-import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
 import { DataTablePagination } from "@/components/data-table-pagination";
 import { DataTablePagination } from "@/components/data-table-pagination";
-import { Check, ChevronsUpDown, Loader2, Plus, Search } from "lucide-react";
-import { cn } from "@/lib/utils";
+import { Loader2, Plus, Search } from "lucide-react";
 import { formatCurrency } from "@/lib/format";
 import { formatCurrency } from "@/lib/format";
 import { toast } from "sonner";
 import { toast } from "sonner";
 import { createQuote } from "@/features/quotes/Actions/quoteActions";
 import { createQuote } from "@/features/quotes/Actions/quoteActions";
+import { VehicleCombobox } from "@/features/quotes/Components/VehicleCombobox";
+import { CustomerCombobox } from "@/features/quotes/Components/CustomerCombobox";
 
 
 interface QuoteRecord {
 interface QuoteRecord {
   id: string;
   id: string;
@@ -46,22 +43,6 @@ interface PaginatedData {
   statusCounts: Record<string, number>;
   statusCounts: Record<string, number>;
 }
 }
 
 
-interface VehicleOption {
-  id: string;
-  make: string;
-  model: string;
-  year: number;
-  licensePlate: string | null;
-  customerId: string | null;
-  customerName: string | null;
-}
-
-interface CustomerOption {
-  id: string;
-  name: string;
-  company: string | null;
-}
-
 const statusTabs = [
 const statusTabs = [
   { key: "all", titleKey: "list.statusAll" },
   { key: "all", titleKey: "list.statusAll" },
   { key: "draft", titleKey: "list.statusDraft" },
   { key: "draft", titleKey: "list.statusDraft" },
@@ -85,15 +66,11 @@ export function QuotesClient({
   currencyCode = "USD",
   currencyCode = "USD",
   search,
   search,
   statusFilter,
   statusFilter,
-  vehicles = [],
-  customers = [],
 }: {
 }: {
   data: PaginatedData;
   data: PaginatedData;
   currencyCode?: string;
   currencyCode?: string;
   search: string;
   search: string;
   statusFilter: string;
   statusFilter: string;
-  vehicles?: VehicleOption[];
-  customers?: CustomerOption[];
 }) {
 }) {
   const router = useRouter();
   const router = useRouter();
   const { formatDate } = useFormatDate();
   const { formatDate } = useFormatDate();
@@ -108,8 +85,6 @@ export function QuotesClient({
   const [newTitle, setNewTitle] = useState("");
   const [newTitle, setNewTitle] = useState("");
   const [newVehicleId, setNewVehicleId] = useState("");
   const [newVehicleId, setNewVehicleId] = useState("");
   const [newCustomerId, setNewCustomerId] = useState("");
   const [newCustomerId, setNewCustomerId] = useState("");
-  const [vehicleOpen, setVehicleOpen] = useState(false);
-  const [customerOpen, setCustomerOpen] = useState(false);
   const [creating, setCreating] = useState(false);
   const [creating, setCreating] = useState(false);
 
 
   const navigate = useCallback(
   const navigate = useCallback(
@@ -145,15 +120,6 @@ export function QuotesClient({
     setShowNewDialog(true);
     setShowNewDialog(true);
   };
   };
 
 
-  const handleVehicleSelect = (vehicleId: string) => {
-    setNewVehicleId(vehicleId);
-    setVehicleOpen(false);
-    const vehicle = vehicles.find((v) => v.id === vehicleId);
-    if (vehicle?.customerId) {
-      setNewCustomerId(vehicle.customerId);
-    }
-  };
-
   const handleCreateQuote = async (e: React.FormEvent) => {
   const handleCreateQuote = async (e: React.FormEvent) => {
     e.preventDefault();
     e.preventDefault();
     if (!newTitle.trim()) return;
     if (!newTitle.trim()) return;
@@ -182,26 +148,6 @@ export function QuotesClient({
     setCreating(false);
     setCreating(false);
   };
   };
 
 
-  const filteredVehicles = newCustomerId
-    ? vehicles.filter((v) => v.customerId === newCustomerId)
-    : vehicles;
-  const selectedVehicle = vehicles.find((v) => v.id === newVehicleId);
-  const selectedCustomer = customers.find((c) => c.id === newCustomerId);
-
-  const handleCustomerSelect = (customerId: string) => {
-    setNewCustomerId(customerId);
-    setCustomerOpen(false);
-    const customerVehicles = vehicles.filter((v) => v.customerId === customerId);
-    if (customerVehicles.length > 0) {
-      setNewVehicleId(customerVehicles[0].id);
-    } else if (newVehicleId) {
-      const vehicle = vehicles.find((v) => v.id === newVehicleId);
-      if (vehicle && vehicle.customerId !== customerId) {
-        setNewVehicleId("");
-      }
-    }
-  };
-
   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">
@@ -329,86 +275,27 @@ export function QuotesClient({
 
 
             <div className="space-y-2">
             <div className="space-y-2">
               <Label>{t("details.vehicle")}</Label>
               <Label>{t("details.vehicle")}</Label>
-              <Popover open={vehicleOpen} onOpenChange={setVehicleOpen}>
-                <PopoverTrigger asChild>
-                  <Button
-                    variant="outline"
-                    role="combobox"
-                    className="w-full justify-between font-normal"
-                  >
-                    {selectedVehicle
-                      ? `${selectedVehicle.year} ${selectedVehicle.make} ${selectedVehicle.model}${selectedVehicle.licensePlate ? ` · ${selectedVehicle.licensePlate}` : ""}`
-                      : t("details.selectVehicle")}
-                    <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
-                  </Button>
-                </PopoverTrigger>
-                <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
-                  <Command>
-                    <CommandInput placeholder={t("details.selectVehicle")} />
-                    <CommandList>
-                      <CommandEmpty>{t("details.none")}</CommandEmpty>
-                      <CommandGroup>
-                        {filteredVehicles.map((v) => (
-                          <CommandItem
-                            key={v.id}
-                            value={`${v.year} ${v.make} ${v.model} ${v.licensePlate || ""} ${v.customerName || ""}`}
-                            onSelect={() => handleVehicleSelect(v.id)}
-                          >
-                            <Check className={cn("mr-2 h-4 w-4", newVehicleId === v.id ? "opacity-100" : "opacity-0")} />
-                            <div>
-                              <p className="text-sm">
-                                {v.year} {v.make} {v.model}
-                                {v.licensePlate && <span className="ml-1.5 text-muted-foreground">· {v.licensePlate}</span>}
-                              </p>
-                              {v.customerName && (
-                                <p className="text-xs text-muted-foreground">{v.customerName}</p>
-                              )}
-                            </div>
-                          </CommandItem>
-                        ))}
-                      </CommandGroup>
-                    </CommandList>
-                  </Command>
-                </PopoverContent>
-              </Popover>
+              <VehicleCombobox
+                value={newVehicleId}
+                placeholder={t("details.selectVehicle")}
+                noneLabel={t("details.none")}
+                onChange={(id, vehicle) => {
+                  setNewVehicleId(id);
+                  if (vehicle?.customerId) {
+                    setNewCustomerId(vehicle.customerId);
+                  }
+                }}
+              />
             </div>
             </div>
 
 
             <div className="space-y-2">
             <div className="space-y-2">
               <Label>{t("details.customer")}</Label>
               <Label>{t("details.customer")}</Label>
-              <Popover open={customerOpen} onOpenChange={setCustomerOpen}>
-                <PopoverTrigger asChild>
-                  <Button
-                    variant="outline"
-                    role="combobox"
-                    className="w-full justify-between font-normal"
-                  >
-                    {selectedCustomer
-                      ? `${selectedCustomer.name}${selectedCustomer.company ? ` (${selectedCustomer.company})` : ""}`
-                      : t("details.selectCustomer")}
-                    <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
-                  </Button>
-                </PopoverTrigger>
-                <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
-                  <Command>
-                    <CommandInput placeholder={t("details.selectCustomer")} />
-                    <CommandList>
-                      <CommandEmpty>{t("details.none")}</CommandEmpty>
-                      <CommandGroup>
-                        {customers.map((c) => (
-                          <CommandItem
-                            key={c.id}
-                            value={`${c.name} ${c.company || ""}`}
-                            onSelect={() => handleCustomerSelect(c.id)}
-                          >
-                            <Check className={cn("mr-2 h-4 w-4", newCustomerId === c.id ? "opacity-100" : "opacity-0")} />
-                            <span>{c.name}{c.company ? ` (${c.company})` : ""}</span>
-                          </CommandItem>
-                        ))}
-                      </CommandGroup>
-                    </CommandList>
-                  </Command>
-                </PopoverContent>
-              </Popover>
+              <CustomerCombobox
+                value={newCustomerId}
+                placeholder={t("details.selectCustomer")}
+                noneLabel={t("details.none")}
+                onChange={(id) => setNewCustomerId(id)}
+              />
             </div>
             </div>
 
 
             <div className="flex justify-end gap-3 pt-2">
             <div className="flex justify-end gap-3 pt-2">

+ 10 - 10
src/app/(authenticated)/vehicles/[id]/service/[serviceId]/page.tsx

@@ -2,7 +2,7 @@ import { getServiceRecord } from "@/features/vehicles/Actions/serviceActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { getSettings } from "@/features/settings/Actions/settingsActions";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { SETTING_KEYS } from "@/features/settings/Schema/settingsSchema";
 import { getInventoryPartsList } from "@/features/inventory/Actions/inventoryActions";
 import { getInventoryPartsList } from "@/features/inventory/Actions/inventoryActions";
-import { getVehicles } from "@/features/vehicles/Actions/vehicleActions";
+
 import { getTechnicians } from "@/features/workboard/Actions/technicianActions";
 import { getTechnicians } from "@/features/workboard/Actions/technicianActions";
 import { getAuthContext } from "@/lib/get-auth-context";
 import { getAuthContext } from "@/lib/get-auth-context";
 import { getFeatures } from "@/lib/features";
 import { getFeatures } from "@/lib/features";
@@ -19,7 +19,7 @@ export default async function ServiceDetailPage({
 }) {
 }) {
   const { id, serviceId } = await params;
   const { id, serviceId } = await params;
 
 
-  const [result, settingsResult, inventoryResult, vehiclesResult, techniciansResult, authContext, session] =
+  const [result, settingsResult, inventoryResult, techniciansResult, authContext, session] =
     await Promise.all([
     await Promise.all([
       getServiceRecord(serviceId),
       getServiceRecord(serviceId),
       getSettings([
       getSettings([
@@ -30,7 +30,6 @@ export default async function ServiceDetailPage({
         SETTING_KEYS.DEFAULT_LABOR_RATE,
         SETTING_KEYS.DEFAULT_LABOR_RATE,
       ]),
       ]),
       getInventoryPartsList(),
       getInventoryPartsList(),
-      getVehicles(),
       getTechnicians(),
       getTechnicians(),
       getAuthContext(),
       getAuthContext(),
       getCachedSession(),
       getCachedSession(),
@@ -64,12 +63,13 @@ export default async function ServiceDetailPage({
     Number(settings[SETTING_KEYS.DEFAULT_LABOR_RATE]) || 0;
     Number(settings[SETTING_KEYS.DEFAULT_LABOR_RATE]) || 0;
   const inventoryParts =
   const inventoryParts =
     inventoryResult.success && inventoryResult.data ? inventoryResult.data : [];
     inventoryResult.success && inventoryResult.data ? inventoryResult.data : [];
-  const vehicles = (
-    vehiclesResult.success && vehiclesResult.data ? vehiclesResult.data : []
-  ).map((v) => ({
-    id: v.id,
-    label: `${v.year} ${v.make} ${v.model}${v.licensePlate ? ` (${v.licensePlate})` : ""}`,
-  }));
+  const initialVehicle = {
+    id: record.vehicle.id,
+    make: record.vehicle.make,
+    model: record.vehicle.model,
+    year: record.vehicle.year,
+    licensePlate: record.vehicle.licensePlate,
+  };
   const boardTechnicians = (
   const boardTechnicians = (
     techniciansResult.success && techniciansResult.data ? techniciansResult.data : []
     techniciansResult.success && techniciansResult.data ? techniciansResult.data : []
   ).map((t) => ({ id: t.id, name: t.name }));
   ).map((t) => ({ id: t.id, name: t.name }));
@@ -166,7 +166,7 @@ export default async function ServiceDetailPage({
         defaultLaborRate={defaultLaborRate}
         defaultLaborRate={defaultLaborRate}
         initialData={initialData}
         initialData={initialData}
         inventoryParts={inventoryParts}
         inventoryParts={inventoryParts}
-        vehicles={vehicles}
+        initialVehicle={initialVehicle}
         boardTechnicians={boardTechnicians}
         boardTechnicians={boardTechnicians}
         currentUserName={currentUserName}
         currentUserName={currentUserName}
         imageAttachmentsForManager={imageAttachmentsForManager}
         imageAttachmentsForManager={imageAttachmentsForManager}

+ 53 - 6
src/features/customers/Actions/customerActions.ts

@@ -150,12 +150,24 @@ export async function getCustomersPaginated(params: {
     const where: any = { organizationId };
     const where: any = { organizationId };
 
 
     if (params.search) {
     if (params.search) {
-      where.OR = [
-        { name: { contains: params.search, mode: "insensitive" } },
-        { email: { contains: params.search, mode: "insensitive" } },
-        { phone: { contains: params.search, mode: "insensitive" } },
-        { company: { contains: params.search, mode: "insensitive" } },
-      ];
+      const words = params.search.trim().split(/\s+/).filter(Boolean);
+      if (words.length > 1) {
+        where.AND = words.map((word: string) => ({
+          OR: [
+            { name: { contains: word, mode: "insensitive" } },
+            { email: { contains: word, mode: "insensitive" } },
+            { phone: { contains: word, mode: "insensitive" } },
+            { company: { contains: word, mode: "insensitive" } },
+          ],
+        }));
+      } else {
+        where.OR = [
+          { name: { contains: params.search, mode: "insensitive" } },
+          { email: { contains: params.search, mode: "insensitive" } },
+          { phone: { contains: params.search, mode: "insensitive" } },
+          { company: { contains: params.search, mode: "insensitive" } },
+        ];
+      }
     }
     }
 
 
     const [customers, total] = await Promise.all([
     const [customers, total] = await Promise.all([
@@ -372,3 +384,38 @@ export async function getCustomersList() {
     });
     });
   }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.CUSTOMERS }] });
   }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.CUSTOMERS }] });
 }
 }
+
+export async function searchCustomers(search?: string, limit = 20, offset = 0) {
+  return withAuth(async ({ organizationId }) => {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const where: any = { organizationId };
+    if (search) {
+      const words = search.trim().split(/\s+/).filter(Boolean);
+      if (words.length > 1) {
+        // Every word must match in at least one field
+        where.AND = words.map((word) => ({
+          OR: [
+            { name: { contains: word, mode: "insensitive" } },
+            { email: { contains: word, mode: "insensitive" } },
+            { phone: { contains: word, mode: "insensitive" } },
+            { company: { contains: word, mode: "insensitive" } },
+          ],
+        }));
+      } else {
+        where.OR = [
+          { name: { contains: search, mode: "insensitive" } },
+          { email: { contains: search, mode: "insensitive" } },
+          { phone: { contains: search, mode: "insensitive" } },
+          { company: { contains: search, mode: "insensitive" } },
+        ];
+      }
+    }
+    return db.customer.findMany({
+      where,
+      select: { id: true, name: true, company: true },
+      orderBy: { name: "asc" },
+      skip: offset,
+      take: limit,
+    });
+  }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.CUSTOMERS }] });
+}

+ 171 - 0
src/features/quotes/Components/CustomerCombobox.tsx

@@ -0,0 +1,171 @@
+'use client'
+
+import { useState, useEffect, useRef, useCallback } from 'react'
+import { Button } from '@/components/ui/button'
+import {
+  Command,
+  CommandEmpty,
+  CommandGroup,
+  CommandInput,
+  CommandItem,
+  CommandList,
+} from '@/components/ui/command'
+import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
+import { Check, ChevronsUpDown, Loader2 } from 'lucide-react'
+import { cn } from '@/lib/utils'
+import { searchCustomers } from '@/features/customers/Actions/customerActions'
+
+interface CustomerOption {
+  id: string
+  name: string
+  company: string | null
+}
+
+const PAGE_SIZE = 50
+
+interface CustomerComboboxProps {
+  value: string
+  onChange: (id: string, customer: CustomerOption | null) => void
+  initialCustomer?: CustomerOption | null
+  placeholder?: string
+  noneLabel?: string
+}
+
+export function CustomerCombobox({
+  value,
+  onChange,
+  initialCustomer,
+  placeholder = 'Select customer...',
+  noneLabel = 'None',
+}: CustomerComboboxProps) {
+  const [open, setOpen] = useState(false)
+  const [search, setSearch] = useState('')
+  const [options, setOptions] = useState<CustomerOption[]>([])
+  const [searching, setSearching] = useState(false)
+  const [loadingMore, setLoadingMore] = useState(false)
+  const [hasMore, setHasMore] = useState(true)
+  const [selected, setSelected] = useState<CustomerOption | null>(initialCustomer ?? null)
+  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null)
+  const listRef = useRef<HTMLDivElement | null>(null)
+  const loadingMoreRef = useRef(false)
+
+  const loadOptions = useCallback(async (query?: string, offset = 0, append = false) => {
+    if (offset === 0) setSearching(true)
+    else {
+      setLoadingMore(true)
+      loadingMoreRef.current = true
+    }
+
+    const result = await searchCustomers(query || undefined, PAGE_SIZE, offset)
+
+    if (result.success && result.data) {
+      if (append) {
+        setOptions((prev) => [...prev, ...result.data!])
+      } else {
+        setOptions(result.data)
+      }
+      setHasMore(result.data.length === PAGE_SIZE)
+    }
+
+    setSearching(false)
+    setLoadingMore(false)
+    loadingMoreRef.current = false
+  }, [])
+
+  // Prefetch on mount
+  useEffect(() => {
+    loadOptions()
+  }, [loadOptions])
+
+  // Debounced search — resets to page 0
+  useEffect(() => {
+    if (!open) return
+    if (debounceRef.current) clearTimeout(debounceRef.current)
+    if (!search) {
+      loadOptions()
+      return
+    }
+    debounceRef.current = setTimeout(() => {
+      loadOptions(search)
+    }, 300)
+    return () => {
+      if (debounceRef.current) clearTimeout(debounceRef.current)
+    }
+  }, [search, open, loadOptions])
+
+  // Keep selected label in sync
+  useEffect(() => {
+    if (initialCustomer) setSelected(initialCustomer)
+  }, [initialCustomer])
+
+  // Infinite scroll
+  const handleScroll = useCallback(() => {
+    const el = listRef.current
+    if (!el || !hasMore || loadingMoreRef.current) return
+    const { scrollTop, scrollHeight, clientHeight } = el
+    if (scrollHeight - scrollTop - clientHeight < 50) {
+      loadOptions(search || undefined, options.length, true)
+    }
+  }, [hasMore, search, options.length, loadOptions])
+
+  return (
+    <Popover open={open} onOpenChange={setOpen}>
+      <PopoverTrigger asChild>
+        <Button variant="outline" role="combobox" className="w-full justify-between font-normal">
+          <span className="truncate">
+            {selected
+              ? `${selected.name}${selected.company ? ` (${selected.company})` : ''}`
+              : placeholder}
+          </span>
+          <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+        </Button>
+      </PopoverTrigger>
+      <PopoverContent className="w-(--radix-popover-trigger-width) p-0">
+        <Command shouldFilter={false}>
+          <CommandInput placeholder={placeholder} value={search} onValueChange={setSearch} />
+          <CommandList ref={listRef} onScroll={handleScroll}>
+            {searching && options.length === 0 && (
+              <div className="flex items-center justify-center py-6">
+                <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
+              </div>
+            )}
+            <CommandEmpty>No customers found.</CommandEmpty>
+            <CommandGroup>
+              <CommandItem
+                onSelect={() => {
+                  onChange('', null)
+                  setSelected(null)
+                  setOpen(false)
+                }}
+              >
+                <Check className={cn('mr-2 h-4 w-4', !value ? 'opacity-100' : 'opacity-0')} />
+                {noneLabel}
+              </CommandItem>
+              {options.map((c) => (
+                <CommandItem
+                  key={c.id}
+                  onSelect={() => {
+                    onChange(c.id, c)
+                    setSelected(c)
+                    setOpen(false)
+                  }}
+                >
+                  <Check
+                    className={cn('mr-2 h-4 w-4', value === c.id ? 'opacity-100' : 'opacity-0')}
+                  />
+                  {c.name}
+                  {c.company ? ` (${c.company})` : ''}
+                </CommandItem>
+              ))}
+            </CommandGroup>
+            {loadingMore && (
+              <div className="flex items-center justify-center py-2">
+                <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
+              </div>
+            )}
+          </CommandList>
+        </Command>
+      </PopoverContent>
+    </Popover>
+  )
+}

+ 44 - 40
src/features/quotes/Components/QuotePageClient.tsx

@@ -62,6 +62,8 @@ import {
 } from "lucide-react";
 } from "lucide-react";
 import { SharedLinkCard } from "@/components/shared-link-card";
 import { SharedLinkCard } from "@/components/shared-link-card";
 import { formatCurrency, getCurrencySymbol } from "@/lib/format";
 import { formatCurrency, getCurrencySymbol } from "@/lib/format";
+import { VehicleCombobox } from "./VehicleCombobox";
+import { CustomerCombobox } from "./CustomerCombobox";
 
 
 const statusColors: Record<string, string> = {
 const statusColors: Record<string, string> = {
   draft: "bg-gray-500/10 text-gray-500 border-gray-500/20",
   draft: "bg-gray-500/10 text-gray-500 border-gray-500/20",
@@ -171,8 +173,8 @@ export function QuotePageClient({
   defaultTaxRate = 0,
   defaultTaxRate = 0,
   taxEnabled = true,
   taxEnabled = true,
   defaultLaborRate = 0,
   defaultLaborRate = 0,
-  customers = [],
-  vehicles = [],
+  customers: _customers,
+  vehicles: _vehicles,
   smsEnabled = false,
   smsEnabled = false,
   emailEnabled = false,
   emailEnabled = false,
   imageAttachments = [],
   imageAttachments = [],
@@ -280,18 +282,13 @@ export function QuotePageClient({
   const taxAmount = (subtotal - discountAmount) * (taxRate / 100);
   const taxAmount = (subtotal - discountAmount) * (taxRate / 100);
   const totalAmount = subtotal - discountAmount + taxAmount;
   const totalAmount = subtotal - discountAmount + taxAmount;
 
 
-  const selectedVehicle = vehicles.find((v) => v.id === vehicleId);
-  const selectedCustomer = customers.find((c) => c.id === customerId);
-
-  const handleVehicleChange = (v: string) => {
-    const vid = v === "none" ? "" : v;
-    setVehicleId(vid);
-    if (vid) {
-      const vehicle = vehicles.find((veh) => veh.id === vid);
-      if (vehicle?.customerId) setCustomerId(vehicle.customerId);
-    }
-    markDirty();
-  };
+  // Track selected vehicle/customer for display (initial from quote data)
+  const [selectedVehicle, setSelectedVehicle] = useState<{ id: string; make: string; model: string; year: number; licensePlate: string | null; customerId: string | null; customer: { id: string; name: string } | null } | null>(
+    quote.vehicle ? { id: quote.vehicle.id, make: quote.vehicle.make, model: quote.vehicle.model, year: quote.vehicle.year, licensePlate: quote.vehicle.licensePlate, customerId: quote.customer?.id || null, customer: quote.customer ? { id: quote.customer.id, name: quote.customer.name } : null } : null
+  );
+  const [selectedCustomer, setSelectedCustomer] = useState<{ id: string; name: string; company: string | null } | null>(
+    quote.customer ? { id: quote.customer.id, name: quote.customer.name, company: quote.customer.company } : null
+  );
 
 
   const updatePart = useCallback((index: number, field: keyof QuotePartInput, value: string | number | boolean) => {
   const updatePart = useCallback((index: number, field: keyof QuotePartInput, value: string | number | boolean) => {
     setPartItems((prev) => {
     setPartItems((prev) => {
@@ -563,15 +560,21 @@ export function QuotePageClient({
       <div className="rounded-lg border p-3 space-y-3">
       <div className="rounded-lg border p-3 space-y-3">
         <div className="space-y-1">
         <div className="space-y-1">
           <Label className="text-xs">{t("details.vehicle")}</Label>
           <Label className="text-xs">{t("details.vehicle")}</Label>
-          <Select value={vehicleId || "none"} onValueChange={handleVehicleChange}>
-            <SelectTrigger><SelectValue placeholder={t("details.selectVehicle")} /></SelectTrigger>
-            <SelectContent>
-              <SelectItem value="none">{t("details.none")}</SelectItem>
-              {vehicles.map((v) => (
-                <SelectItem key={v.id} value={v.id}>{v.year} {v.make} {v.model}{v.licensePlate ? ` (${v.licensePlate})` : ""}</SelectItem>
-              ))}
-            </SelectContent>
-          </Select>
+          <VehicleCombobox
+            value={vehicleId}
+            initialVehicle={selectedVehicle}
+            placeholder={t("details.selectVehicle")}
+            noneLabel={t("details.none")}
+            onChange={(id, vehicle) => {
+              setVehicleId(id);
+              setSelectedVehicle(vehicle);
+              if (vehicle?.customerId) {
+                setCustomerId(vehicle.customerId);
+                if (vehicle.customer) setSelectedCustomer({ id: vehicle.customer.id, name: vehicle.customer.name, company: null });
+              }
+              markDirty();
+            }}
+          />
         </div>
         </div>
         {selectedVehicle && (
         {selectedVehicle && (
           <div className="flex items-center gap-2 rounded-md bg-muted/50 px-3 py-2">
           <div className="flex items-center gap-2 rounded-md bg-muted/50 px-3 py-2">
@@ -585,15 +588,17 @@ export function QuotePageClient({
         )}
         )}
         <div className="space-y-1">
         <div className="space-y-1">
           <Label className="text-xs">{t("details.customer")}</Label>
           <Label className="text-xs">{t("details.customer")}</Label>
-          <Select value={customerId || "none"} onValueChange={(v) => { setCustomerId(v === "none" ? "" : v); markDirty(); }}>
-            <SelectTrigger><SelectValue placeholder={t("details.selectCustomer")} /></SelectTrigger>
-            <SelectContent>
-              <SelectItem value="none">{t("details.none")}</SelectItem>
-              {customers.map((c) => (
-                <SelectItem key={c.id} value={c.id}>{c.name}{c.company ? ` (${c.company})` : ""}</SelectItem>
-              ))}
-            </SelectContent>
-          </Select>
+          <CustomerCombobox
+            value={customerId}
+            initialCustomer={selectedCustomer}
+            placeholder={t("details.selectCustomer")}
+            noneLabel={t("details.none")}
+            onChange={(id, customer) => {
+              setCustomerId(id);
+              setSelectedCustomer(customer);
+              markDirty();
+            }}
+          />
         </div>
         </div>
         {selectedCustomer && (
         {selectedCustomer && (
           <div className="flex items-center gap-2 rounded-md bg-muted/50 px-3 py-2">
           <div className="flex items-center gap-2 rounded-md bg-muted/50 px-3 py-2">
@@ -883,14 +888,13 @@ export function QuotePageClient({
           <DialogHeader><DialogTitle>{t("page.convertTitle")}</DialogTitle></DialogHeader>
           <DialogHeader><DialogTitle>{t("page.convertTitle")}</DialogTitle></DialogHeader>
           <div className="space-y-4">
           <div className="space-y-4">
             <p className="text-sm text-muted-foreground">{t("page.convertDescription")}</p>
             <p className="text-sm text-muted-foreground">{t("page.convertDescription")}</p>
-            <Select value={convertVehicleId} onValueChange={setConvertVehicleId}>
-              <SelectTrigger><SelectValue placeholder={t("details.selectVehicle")} /></SelectTrigger>
-              <SelectContent>
-                {vehicles.map((v) => (
-                  <SelectItem key={v.id} value={v.id}>{v.year} {v.make} {v.model}{v.licensePlate ? ` (${v.licensePlate})` : ""}</SelectItem>
-                ))}
-              </SelectContent>
-            </Select>
+            <VehicleCombobox
+              value={convertVehicleId}
+              initialVehicle={selectedVehicle}
+              placeholder={t("details.selectVehicle")}
+              noneLabel={t("details.none")}
+              onChange={(id) => setConvertVehicleId(id)}
+            />
             <div className="flex gap-2">
             <div className="flex gap-2">
               <Button type="button" onClick={handleConvert} disabled={converting || !convertVehicleId}>
               <Button type="button" onClick={handleConvert} disabled={converting || !convertVehicleId}>
                 {converting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
                 {converting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}

+ 228 - 0
src/features/quotes/Components/VehicleCombobox.tsx

@@ -0,0 +1,228 @@
+"use client";
+
+import { useState, useEffect, useRef, useCallback } from "react";
+import { Button } from "@/components/ui/button";
+import {
+  Command,
+  CommandEmpty,
+  CommandGroup,
+  CommandInput,
+  CommandItem,
+  CommandList,
+} from "@/components/ui/command";
+import {
+  Popover,
+  PopoverContent,
+  PopoverTrigger,
+} from "@/components/ui/popover";
+import { Check, ChevronsUpDown, Loader2 } from "lucide-react";
+import { cn } from "@/lib/utils";
+import { searchVehicles } from "@/features/vehicles/Actions/vehicleActions";
+
+interface VehicleOption {
+  id: string;
+  make: string;
+  model: string;
+  year: number;
+  licensePlate: string | null;
+  customerId: string | null;
+  customer: { id: string; name: string } | null;
+}
+
+const PAGE_SIZE = 50;
+
+interface VehicleComboboxProps {
+  value: string;
+  onChange: (id: string, vehicle: VehicleOption | null) => void;
+  initialVehicle?: VehicleOption | null;
+  placeholder?: string;
+  noneLabel?: string;
+}
+
+function formatVehicle(v: VehicleOption) {
+  return `${v.year} ${v.make} ${v.model}${v.licensePlate ? ` (${v.licensePlate})` : ""}`;
+}
+
+export function VehicleCombobox({
+  value,
+  onChange,
+  initialVehicle,
+  placeholder = "Select vehicle...",
+  noneLabel = "None",
+}: VehicleComboboxProps) {
+  const [open, setOpen] = useState(false);
+  const [search, setSearch] = useState("");
+  const [options, setOptions] = useState<VehicleOption[]>([]);
+  const [searching, setSearching] = useState(false);
+  const [loadingMore, setLoadingMore] = useState(false);
+  const [hasMore, setHasMore] = useState(true);
+  const [selected, setSelected] = useState<VehicleOption | null>(
+    initialVehicle ?? null
+  );
+  const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
+  const listRef = useRef<HTMLDivElement | null>(null);
+  const loadingMoreRef = useRef(false);
+
+  const mapVehicle = (v: VehicleOption) => ({
+    id: v.id,
+    make: v.make,
+    model: v.model,
+    year: v.year,
+    licensePlate: v.licensePlate,
+    customerId: v.customerId,
+    customer: v.customer,
+  });
+
+  const loadOptions = useCallback(
+    async (query?: string, offset = 0, append = false) => {
+      if (offset === 0) setSearching(true);
+      else {
+        setLoadingMore(true);
+        loadingMoreRef.current = true;
+      }
+
+      const result = await searchVehicles(query || undefined, PAGE_SIZE, offset);
+
+      if (result.success && result.data) {
+        const mapped = result.data.map(mapVehicle);
+        if (append) {
+          setOptions((prev) => [...prev, ...mapped]);
+        } else {
+          setOptions(mapped);
+        }
+        setHasMore(result.data.length === PAGE_SIZE);
+      }
+
+      setSearching(false);
+      setLoadingMore(false);
+      loadingMoreRef.current = false;
+    },
+    []
+  );
+
+  // Prefetch on mount
+  useEffect(() => {
+    loadOptions();
+  }, [loadOptions]);
+
+  // Debounced search — resets to page 0
+  useEffect(() => {
+    if (!open) return;
+    if (debounceRef.current) clearTimeout(debounceRef.current);
+    if (!search) {
+      loadOptions();
+      return;
+    }
+    debounceRef.current = setTimeout(() => {
+      loadOptions(search);
+    }, 300);
+    return () => {
+      if (debounceRef.current) clearTimeout(debounceRef.current);
+    };
+  }, [search, open, loadOptions]);
+
+  useEffect(() => {
+    if (initialVehicle) setSelected(initialVehicle);
+  }, [initialVehicle]);
+
+  // Infinite scroll
+  const handleScroll = useCallback(() => {
+    const el = listRef.current;
+    if (!el || !hasMore || loadingMoreRef.current) return;
+    const { scrollTop, scrollHeight, clientHeight } = el;
+    if (scrollHeight - scrollTop - clientHeight < 50) {
+      loadOptions(search || undefined, options.length, true);
+    }
+  }, [hasMore, search, options.length, loadOptions]);
+
+  return (
+    <Popover open={open} onOpenChange={setOpen}>
+      <PopoverTrigger asChild>
+        <Button
+          variant="outline"
+          role="combobox"
+          className="w-full justify-between font-normal"
+        >
+          <span className="truncate">
+            {selected ? formatVehicle(selected) : placeholder}
+          </span>
+          <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
+        </Button>
+      </PopoverTrigger>
+      <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
+        <Command shouldFilter={false}>
+          <CommandInput
+            placeholder={placeholder}
+            value={search}
+            onValueChange={setSearch}
+          />
+          <CommandList
+            ref={listRef}
+            onScroll={handleScroll}
+          >
+            {searching && options.length === 0 && (
+              <div className="flex items-center justify-center py-6">
+                <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
+              </div>
+            )}
+            <CommandEmpty>No vehicles found.</CommandEmpty>
+            <CommandGroup>
+              <CommandItem
+                onSelect={() => {
+                  onChange("", null);
+                  setSelected(null);
+                  setOpen(false);
+                }}
+              >
+                <Check
+                  className={cn(
+                    "mr-2 h-4 w-4",
+                    !value ? "opacity-100" : "opacity-0"
+                  )}
+                />
+                {noneLabel}
+              </CommandItem>
+              {options.map((v) => (
+                <CommandItem
+                  key={v.id}
+                  onSelect={() => {
+                    onChange(v.id, v);
+                    setSelected(v);
+                    setOpen(false);
+                  }}
+                >
+                  <Check
+                    className={cn(
+                      "mr-2 h-4 w-4",
+                      value === v.id ? "opacity-100" : "opacity-0"
+                    )}
+                  />
+                  <div>
+                    <p className="text-sm">
+                      {v.year} {v.make} {v.model}
+                      {v.licensePlate && (
+                        <span className="ml-1.5 text-muted-foreground">
+                          · {v.licensePlate}
+                        </span>
+                      )}
+                    </p>
+                    {v.customer?.name && (
+                      <p className="text-xs text-muted-foreground">
+                        {v.customer.name}
+                      </p>
+                    )}
+                  </div>
+                </CommandItem>
+              ))}
+            </CommandGroup>
+            {loadingMore && (
+              <div className="flex items-center justify-center py-2">
+                <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
+              </div>
+            )}
+          </CommandList>
+        </Command>
+      </PopoverContent>
+    </Popover>
+  );
+}

+ 63 - 9
src/features/vehicles/Actions/vehicleActions.ts

@@ -79,15 +79,25 @@ export async function getVehiclesPaginated(params: {
     const where: any = { organizationId, isArchived: params.archived ?? false };
     const where: any = { organizationId, isArchived: params.archived ?? false };
 
 
     if (params.search) {
     if (params.search) {
-      where.OR = [
-        { make: { contains: params.search, mode: "insensitive" } },
-        { model: { contains: params.search, mode: "insensitive" } },
-        { licensePlate: { contains: params.search, mode: "insensitive" } },
-        { vin: { contains: params.search, mode: "insensitive" } },
-        { customer: { name: { contains: params.search, mode: "insensitive" } } },
-      ];
-      if (!isNaN(Number(params.search))) {
-        where.OR.push({ year: Number(params.search) });
+      const words = params.search.trim().split(/\s+/).filter(Boolean);
+      const fieldMatch = (word: string) => {
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        const conditions: any[] = [
+          { make: { contains: word, mode: "insensitive" } },
+          { model: { contains: word, mode: "insensitive" } },
+          { licensePlate: { contains: word, mode: "insensitive" } },
+          { vin: { contains: word, mode: "insensitive" } },
+          { customer: { name: { contains: word, mode: "insensitive" } } },
+        ];
+        if (!isNaN(Number(word))) {
+          conditions.push({ year: Number(word) });
+        }
+        return conditions;
+      };
+      if (words.length > 1) {
+        where.AND = words.map((word: string) => ({ OR: fieldMatch(word) }));
+      } else {
+        where.OR = fieldMatch(words[0]);
       }
       }
     }
     }
 
 
@@ -256,3 +266,47 @@ export async function deleteVehicle(vehicleId: string) {
     }),
     }),
   });
   });
 }
 }
+
+export async function searchVehicles(search?: string, limit = 20, offset = 0) {
+  return withAuth(async ({ organizationId }) => {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    const where: any = { organizationId, isArchived: false };
+    if (search) {
+      const words = search.trim().split(/\s+/).filter(Boolean);
+      const fieldMatch = (word: string) => {
+        // eslint-disable-next-line @typescript-eslint/no-explicit-any
+        const conditions: any[] = [
+          { make: { contains: word, mode: "insensitive" } },
+          { model: { contains: word, mode: "insensitive" } },
+          { licensePlate: { contains: word, mode: "insensitive" } },
+          { vin: { contains: word, mode: "insensitive" } },
+          { customer: { name: { contains: word, mode: "insensitive" } } },
+        ];
+        if (!isNaN(Number(word))) {
+          conditions.push({ year: Number(word) });
+        }
+        return conditions;
+      };
+      if (words.length > 1) {
+        where.AND = words.map((word) => ({ OR: fieldMatch(word) }));
+      } else {
+        where.OR = fieldMatch(words[0]);
+      }
+    }
+    return db.vehicle.findMany({
+      where,
+      select: {
+        id: true,
+        make: true,
+        model: true,
+        year: true,
+        licensePlate: true,
+        customerId: true,
+        customer: { select: { id: true, name: true } },
+      },
+      orderBy: { updatedAt: "desc" },
+      skip: offset,
+      take: limit,
+    });
+  }, { requiredPermissions: [{ action: PermissionAction.READ, subject: PermissionSubject.VEHICLES }] });
+}

+ 25 - 76
src/features/vehicles/Components/service-edit/BasicInfoSection.tsx

@@ -1,8 +1,6 @@
 'use client'
 'use client'
 
 
-import { useMemo } from 'react'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
-import { Button } from '@/components/ui/button'
 import { Input } from '@/components/ui/input'
 import { Input } from '@/components/ui/input'
 import { Label } from '@/components/ui/label'
 import { Label } from '@/components/ui/label'
 import {
 import {
@@ -12,18 +10,10 @@ import {
   SelectTrigger,
   SelectTrigger,
   SelectValue,
   SelectValue,
 } from '@/components/ui/select'
 } from '@/components/ui/select'
-import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
-import {
-  Command,
-  CommandEmpty,
-  CommandGroup,
-  CommandInput,
-  CommandItem,
-  CommandList,
-} from '@/components/ui/command'
 import Link from 'next/link'
 import Link from 'next/link'
-import { Check, ChevronsUpDown, ExternalLink } from 'lucide-react'
-import type { InitialData, VehicleOption } from './form-types'
+import { ExternalLink } from 'lucide-react'
+import type { InitialData } from './form-types'
+import { VehicleCombobox } from '@/features/quotes/Components/VehicleCombobox'
 
 
 interface CustomerInfo {
 interface CustomerInfo {
   id: string
   id: string
@@ -37,15 +27,13 @@ interface BasicInfoSectionProps {
   vehicleName: string
   vehicleName: string
   selectedVehicleId: string
   selectedVehicleId: string
   setSelectedVehicleId: (id: string) => void
   setSelectedVehicleId: (id: string) => void
-  vehicles: VehicleOption[]
-  vehicleOpen: boolean
-  setVehicleOpen: (open: boolean) => void
   type: string
   type: string
   setType: (type: string) => void
   setType: (type: string) => void
   status: string
   status: string
   setStatus: (status: string) => void
   setStatus: (status: string) => void
   techName: string
   techName: string
   customer?: CustomerInfo | null
   customer?: CustomerInfo | null
+  initialVehicle?: { id: string; make: string; model: string; year: number; licensePlate: string | null } | null
 }
 }
 
 
 export function BasicInfoSection({
 export function BasicInfoSection({
@@ -53,79 +41,41 @@ export function BasicInfoSection({
   vehicleName,
   vehicleName,
   selectedVehicleId,
   selectedVehicleId,
   setSelectedVehicleId,
   setSelectedVehicleId,
-  vehicles,
-  vehicleOpen,
-  setVehicleOpen,
   type,
   type,
   setType,
   setType,
   status,
   status,
   setStatus,
   setStatus,
   techName,
   techName,
   customer,
   customer,
+  initialVehicle,
 }: BasicInfoSectionProps) {
 }: BasicInfoSectionProps) {
   const t = useTranslations('service.basicInfo')
   const t = useTranslations('service.basicInfo')
-  const selectedVehicleLabel = useMemo(() => {
-    if (vehicles.length === 0) return vehicleName
-    const v = vehicles.find((v) => v.id === selectedVehicleId)
-    return v?.label || vehicleName
-  }, [selectedVehicleId, vehicles, vehicleName])
 
 
   return (
   return (
     <div className="rounded-lg border p-3 space-y-3">
     <div className="rounded-lg border p-3 space-y-3">
       <h3 className="text-sm font-semibold">{t('title')}</h3>
       <h3 className="text-sm font-semibold">{t('title')}</h3>
 
 
-      {vehicles.length > 0 && (
-        <div className="space-y-1">
-          <div className="flex items-center justify-between">
-            <Label className="text-xs">{t('vehicle')}</Label>
-            <Link
-              href={`/vehicles/${selectedVehicleId}`}
-              className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
-            >
-              {t('open')}
-              <ExternalLink className="h-3 w-3" />
-            </Link>
-          </div>
-          <Popover open={vehicleOpen} onOpenChange={setVehicleOpen} modal={true}>
-            <PopoverTrigger asChild>
-              <Button
-                variant="outline"
-                role="combobox"
-                aria-expanded={vehicleOpen}
-                className="w-full justify-between font-normal"
-              >
-                <span className="truncate">{selectedVehicleLabel}</span>
-                <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
-              </Button>
-            </PopoverTrigger>
-            <PopoverContent className="w-[--radix-popover-trigger-width] p-0" align="start">
-              <Command>
-                <CommandInput placeholder={t('searchVehicles')} />
-                <CommandList className="max-h-60 overflow-y-auto">
-                  <CommandEmpty>{t('noVehicleFound')}</CommandEmpty>
-                  <CommandGroup>
-                    {vehicles.map((v) => (
-                      <CommandItem
-                        key={v.id}
-                        value={v.label}
-                        onSelect={() => {
-                          setSelectedVehicleId(v.id)
-                          setVehicleOpen(false)
-                        }}
-                      >
-                        <Check
-                          className={`mr-2 h-4 w-4 ${selectedVehicleId === v.id ? 'opacity-100' : 'opacity-0'}`}
-                        />
-                        {v.label}
-                      </CommandItem>
-                    ))}
-                  </CommandGroup>
-                </CommandList>
-              </Command>
-            </PopoverContent>
-          </Popover>
+      <div className="space-y-1">
+        <div className="flex items-center justify-between">
+          <Label className="text-xs">{t('vehicle')}</Label>
+          <Link
+            href={`/vehicles/${selectedVehicleId}`}
+            className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors"
+          >
+            {t('open')}
+            <ExternalLink className="h-3 w-3" />
+          </Link>
         </div>
         </div>
-      )}
+        <VehicleCombobox
+          value={selectedVehicleId}
+          initialVehicle={initialVehicle ? { ...initialVehicle, customerId: null, customer: null } : null}
+          placeholder={vehicleName || t('searchVehicles')}
+          noneLabel={t('noVehicleFound')}
+          onChange={(id) => {
+            if (id) setSelectedVehicleId(id)
+          }}
+        />
+      </div>
 
 
       {customer && (
       {customer && (
         <div className="flex items-center justify-between rounded-md border px-3 py-2">
         <div className="flex items-center justify-between rounded-md border px-3 py-2">
@@ -227,4 +177,3 @@ export function BasicInfoSection({
     </div>
     </div>
   )
   )
 }
 }
-

+ 3 - 6
src/features/vehicles/Components/service-page/DetailsRightColumn.tsx

@@ -9,7 +9,6 @@ import { revokePublicLink } from '@/features/vehicles/Actions/serviceActions'
 import type { useServiceFormState } from './useServiceFormState'
 import type { useServiceFormState } from './useServiceFormState'
 import type { useServiceActions } from './useServiceActions'
 import type { useServiceActions } from './useServiceActions'
 import type { ServiceDetail } from '../service-detail/types'
 import type { ServiceDetail } from '../service-detail/types'
-import type { VehicleOption } from '../service-edit/form-types'
 import type { BoardTechnicianOption } from './service-page-types'
 import type { BoardTechnicianOption } from './service-page-types'
 
 
 interface DetailsRightColumnProps {
 interface DetailsRightColumnProps {
@@ -20,7 +19,7 @@ interface DetailsRightColumnProps {
   organizationId: string
   organizationId: string
   currencyCode: string
   currencyCode: string
   taxEnabled: boolean
   taxEnabled: boolean
-  vehicles: VehicleOption[]
+  initialVehicle: { id: string; make: string; model: string; year: number; licensePlate: string | null }
   boardTechnicians: BoardTechnicianOption[]
   boardTechnicians: BoardTechnicianOption[]
 }
 }
 
 
@@ -32,7 +31,7 @@ export function DetailsRightColumn({
   organizationId,
   organizationId,
   currencyCode,
   currencyCode,
   taxEnabled,
   taxEnabled,
-  vehicles,
+  initialVehicle,
   boardTechnicians,
   boardTechnicians,
 }: DetailsRightColumnProps) {
 }: DetailsRightColumnProps) {
   const router = useRouter()
   const router = useRouter()
@@ -59,15 +58,13 @@ export function DetailsRightColumn({
         vehicleName={formState.vehicleName}
         vehicleName={formState.vehicleName}
         selectedVehicleId={formState.selectedVehicleId}
         selectedVehicleId={formState.selectedVehicleId}
         setSelectedVehicleId={formState.dirtySetSelectedVehicleId}
         setSelectedVehicleId={formState.dirtySetSelectedVehicleId}
-        vehicles={vehicles}
-        vehicleOpen={formState.vehicleOpen}
-        setVehicleOpen={formState.setVehicleOpen}
         type={formState.type}
         type={formState.type}
         setType={formState.dirtySetType}
         setType={formState.dirtySetType}
         status={formState.status}
         status={formState.status}
         setStatus={formState.dirtySetStatus}
         setStatus={formState.dirtySetStatus}
         techName={formState.techName}
         techName={formState.techName}
         customer={record.vehicle.customer}
         customer={record.vehicle.customer}
+        initialVehicle={initialVehicle}
       />
       />
       <ScheduleTimesSection
       <ScheduleTimesSection
         serviceRecordId={record.id}
         serviceRecordId={record.id}

+ 2 - 2
src/features/vehicles/Components/service-page/ServicePageClient.tsx

@@ -34,7 +34,7 @@ export function ServicePageClient({
   defaultLaborRate,
   defaultLaborRate,
   initialData,
   initialData,
   inventoryParts,
   inventoryParts,
-  vehicles,
+  initialVehicle,
   boardTechnicians = [],
   boardTechnicians = [],
   currentUserName,
   currentUserName,
   imageAttachmentsForManager,
   imageAttachmentsForManager,
@@ -111,7 +111,7 @@ export function ServicePageClient({
                 organizationId={organizationId}
                 organizationId={organizationId}
                 currencyCode={currencyCode}
                 currencyCode={currencyCode}
                 taxEnabled={taxEnabled}
                 taxEnabled={taxEnabled}
-                vehicles={vehicles}
+                initialVehicle={initialVehicle}
                 boardTechnicians={boardTechnicians}
                 boardTechnicians={boardTechnicians}
               />
               />
             }
             }

+ 3 - 3
src/features/vehicles/Components/service-page/service-page-types.ts

@@ -1,6 +1,6 @@
 import type { ServicePartInput, ServiceLaborInput } from '@/features/vehicles/Schema/serviceSchema'
 import type { ServicePartInput, ServiceLaborInput } from '@/features/vehicles/Schema/serviceSchema'
 import type { ServiceDetail } from '../service-detail/types'
 import type { ServiceDetail } from '../service-detail/types'
-import type { InitialData, InventoryPartOption, VehicleOption } from '../service-edit/form-types'
+import type { InitialData, InventoryPartOption } from '../service-edit/form-types'
 
 
 export interface BoardTechnicianOption {
 export interface BoardTechnicianOption {
   id: string
   id: string
@@ -30,7 +30,7 @@ export interface ServicePageClientProps {
   defaultLaborRate: number
   defaultLaborRate: number
   initialData: InitialData
   initialData: InitialData
   inventoryParts: InventoryPartOption[]
   inventoryParts: InventoryPartOption[]
-  vehicles: VehicleOption[]
+  initialVehicle: { id: string; make: string; model: string; year: number; licensePlate: string | null }
   boardTechnicians?: BoardTechnicianOption[]
   boardTechnicians?: BoardTechnicianOption[]
   currentUserName: string
   currentUserName: string
   imageAttachmentsForManager: Attachment[]
   imageAttachmentsForManager: Attachment[]
@@ -43,4 +43,4 @@ export interface ServicePageClientProps {
   emailEnabled?: boolean
   emailEnabled?: boolean
 }
 }
 
 
-export type { ServicePartInput, ServiceLaborInput, ServiceDetail, InitialData, InventoryPartOption, VehicleOption }
+export type { ServicePartInput, ServiceLaborInput, ServiceDetail, InitialData, InventoryPartOption }

+ 2 - 2
src/features/vehicles/Components/service-page/useServiceFormState.ts

@@ -18,7 +18,7 @@ export function useServiceFormState({
   // Form state
   // Form state
   const [loading, setLoading] = useState(false)
   const [loading, setLoading] = useState(false)
   const [selectedVehicleId, setSelectedVehicleId] = useState(vehicleId)
   const [selectedVehicleId, setSelectedVehicleId] = useState(vehicleId)
-  const [vehicleOpen, setVehicleOpen] = useState(false)
+
   const [techName] = useState(initialData.techName || currentUserName)
   const [techName] = useState(initialData.techName || currentUserName)
   const [type, setType] = useState(initialData.type || 'maintenance')
   const [type, setType] = useState(initialData.type || 'maintenance')
   const [status, setStatus] = useState(initialData.status || 'completed')
   const [status, setStatus] = useState(initialData.status || 'completed')
@@ -176,7 +176,7 @@ export function useServiceFormState({
   return {
   return {
     // State
     // State
     loading, setLoading,
     loading, setLoading,
-    selectedVehicleId, vehicleOpen, setVehicleOpen,
+    selectedVehicleId,
     techName, type, status,
     techName, type, status,
     partItems, laborItems,
     partItems, laborItems,
     taxRate, discountType, discountValue,
     taxRate, discountType, discountValue,