Răsfoiți Sursa

Search vehicles and customers on the server in pickers instead of loading them all (#499)

* Search vehicles and customers on the server in pickers instead of loading them all

The new work order picker, the vehicle form's customer selector, the
reminder form's vehicle selector and the new inspection dialog now search
as you type through searchVehicles and searchCustomers. Pages offering them
no longer load every vehicle or customer, which took seconds per open and
per keystroke for a workshop with 27,000 vehicles and 19,000 customers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Fix the review notes on server-searched pickers

- The vehicle picker keeps the chosen customer, not just its id, and hands
  it to CustomerCombobox, so Add New Vehicle after Back names the customer
  the vehicle will be saved to.
- Owner matching runs on the server in findOwnerCandidates: name and company
  are folded with translate() through the same table as foldName, so
  "Muller" finds "Müller" and the other way round; every word of two
  letters or more is searched ("Bo Li"), and customers matching the most
  words come first, so a common surname cannot push the owner past the limit.
- The reminder form drops the vehicle list, and any search still running,
  when the customer changes, and only shows that customer's vehicles.
- Save waits for the owner lookup; a newer scan, another vehicle or closing
  the form drops a late answer, so the match dialog cannot open over a
  closed form.
- A customer given by id is named through getCustomerOption, which selects
  id, name and company only.
- VehicleForm uses CustomerCombobox and NewInspectionDialog VehicleCombobox,
  which take a search placeholder, an empty label, a footer and allowNone.
- The calendar booking spec searches for the Camry; the marine and plan-gate
  specs click the Create new customer button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Levon Bragg 3 zile în urmă
părinte
comite
3d87205ec5
31 a modificat fișierele cu 724 adăugiri și 701 ștergeri
  1. 3 0
      e2e/specs/calendar/booking.spec.ts
  2. 1 2
      e2e/specs/cloud/plan-gates.spec.ts
  3. 1 2
      e2e/specs/settings/marine.spec.ts
  4. 67 18
      src/__tests__/components/vehicle-picker-dialog.test.tsx
  5. 88 0
      src/__tests__/features/vehicles/reminder-form-vehicles.test.tsx
  6. 30 1
      src/__tests__/lib/name-similarity.test.ts
  7. 1 21
      src/app/(authenticated)/calendar/page.tsx
  8. 1 3
      src/app/(authenticated)/customers/[id]/customer-detail-client.tsx
  9. 7 11
      src/app/(authenticated)/customers/[id]/page.tsx
  10. 2 20
      src/app/(authenticated)/reminders/page.tsx
  11. 0 4
      src/app/(authenticated)/vehicles/[id]/page.tsx
  12. 2 15
      src/app/(authenticated)/vehicles/[id]/vehicle-detail-client.tsx
  13. 1 4
      src/app/(authenticated)/vehicles/page.tsx
  14. 0 9
      src/app/(authenticated)/vehicles/vehicles-client.tsx
  15. 1 19
      src/app/(authenticated)/work-orders/page.tsx
  16. 0 21
      src/app/(authenticated)/work-orders/work-orders-client.tsx
  17. 5 59
      src/components/new-work-order-button.tsx
  18. 4 49
      src/components/quick-create-menu.tsx
  19. 68 58
      src/components/vehicle-picker-dialog.tsx
  20. 0 37
      src/features/calendar/Components/CalendarClient.tsx
  21. 65 17
      src/features/customers/Actions/customerActions.ts
  22. 26 102
      src/features/inspections/Components/NewInspectionDialog.tsx
  23. 22 3
      src/features/quotes/Components/CustomerCombobox.tsx
  24. 27 12
      src/features/quotes/Components/VehicleCombobox.tsx
  25. 0 7
      src/features/vehicles/Components/PlateLookupCommand.tsx
  26. 140 50
      src/features/vehicles/Components/ReminderFormDialog.tsx
  27. 1 13
      src/features/vehicles/Components/RemindersPageClient.tsx
  28. 120 110
      src/features/vehicles/Components/VehicleForm.tsx
  29. 1 1
      src/features/vehicles/Components/service-page/modern/ModernHero.tsx
  30. 0 33
      src/features/workboard/Components/WorkBoardClient.tsx
  31. 40 0
      src/lib/name-similarity.ts

+ 3 - 0
e2e/specs/calendar/booking.spec.ts

@@ -119,6 +119,9 @@ test.describe('booking a job from the calendar', () => {
     // It asks which vehicle before it can make anything.
     // It asks which vehicle before it can make anything.
     const picker = page.getByRole('dialog', { name: /select vehicle for work order/i })
     const picker = page.getByRole('dialog', { name: /select vehicle for work order/i })
     await expect(picker).toBeVisible({ timeout: 30_000 })
     await expect(picker).toBeVisible({ timeout: 30_000 })
+    // The picker opens on the most recently updated vehicles and searches the
+    // rest, so the seeded Camry is asked for by name.
+    await picker.getByPlaceholder('Search vehicles...').fill('Camry')
     await picker.getByText(/Camry/i).first().click()
     await picker.getByText(/Camry/i).first().click()
 
 
     await page.waitForURL(
     await page.waitForURL(

+ 1 - 2
e2e/specs/cloud/plan-gates.spec.ts

@@ -124,8 +124,7 @@ test.describe('the free plan', () => {
     await page.locator('#new-make').fill('Volvo')
     await page.locator('#new-make').fill('Volvo')
     await page.locator('#new-model').fill('V70')
     await page.locator('#new-model').fill('V70')
     await page.locator('#new-year').fill('2015')
     await page.locator('#new-year').fill('2015')
-    await page.getByRole('combobox').filter({ hasText: 'Select a customer (optional)' }).click()
-    await page.getByRole('option', { name: 'Create new customer' }).click()
+    await page.getByRole('button', { name: 'Create new customer' }).click()
     await page.locator('#new-customer-name').fill(`E2E Work order customer ${stamp}`)
     await page.locator('#new-customer-name').fill(`E2E Work order customer ${stamp}`)
     await page.getByRole('button', { name: 'Create & Continue' }).click()
     await page.getByRole('button', { name: 'Create & Continue' }).click()
 
 

+ 1 - 2
e2e/specs/settings/marine.spec.ts

@@ -262,8 +262,7 @@ test.describe('a workshop that picks Marine', () => {
     await page.locator('#new-make').fill('Yamaha')
     await page.locator('#new-make').fill('Yamaha')
     await page.locator('#new-model').fill(`242X ${stamp.toString(36)}`)
     await page.locator('#new-model').fill(`242X ${stamp.toString(36)}`)
     await page.locator('#new-year').fill('2019')
     await page.locator('#new-year').fill('2019')
-    await page.getByRole('combobox').filter({ hasText: 'Select a customer (optional)' }).click()
-    await page.getByRole('option', { name: 'Create new customer' }).click()
+    await page.getByRole('button', { name: 'Create new customer' }).click()
     await page.locator('#new-customer-name').fill(SKIPPER)
     await page.locator('#new-customer-name').fill(SKIPPER)
     await page.getByRole('button', { name: 'Create & Continue' }).click()
     await page.getByRole('button', { name: 'Create & Continue' }).click()
     await page.waitForURL(/\/vehicles\/[^/]+\/service\//, { timeout: 30_000 })
     await page.waitForURL(/\/vehicles\/[^/]+\/service\//, { timeout: 30_000 })

+ 67 - 18
src/__tests__/components/vehicle-picker-dialog.test.tsx

@@ -8,18 +8,40 @@
  */
  */
 
 
 import { describe, it, expect, vi } from 'vitest'
 import { describe, it, expect, vi } from 'vitest'
-import { fireEvent, render, screen } from '@testing-library/react'
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
 import { ServiceTypeProvider } from '@/components/service-type-context'
 import { ServiceTypeProvider } from '@/components/service-type-context'
 
 
 vi.mock('next/navigation', () => ({
 vi.mock('next/navigation', () => ({
   useRouter: () => ({ push: vi.fn(), refresh: vi.fn() }),
   useRouter: () => ({ push: vi.fn(), refresh: vi.fn() }),
 }))
 }))
-vi.mock('@/features/vehicles/Actions/vehicleActions', () => ({ createVehicle: vi.fn() }))
+const searchVehicles = vi.hoisted(() => vi.fn())
+vi.mock('@/features/vehicles/Actions/vehicleActions', () => ({
+  createVehicle,
+  searchVehicles,
+}))
 vi.mock('@/features/vehicles/Actions/createDraftServiceRecord', () => ({
 vi.mock('@/features/vehicles/Actions/createDraftServiceRecord', () => ({
   createDraftCounterSale: vi.fn(),
   createDraftCounterSale: vi.fn(),
 }))
 }))
+const createVehicle = vi.hoisted(() => vi.fn())
 vi.mock('@/features/customers/Actions/customerActions', () => ({ createCustomer: vi.fn() }))
 vi.mock('@/features/customers/Actions/customerActions', () => ({ createCustomer: vi.fn() }))
-vi.mock('@/features/quotes/Components/CustomerCombobox', () => ({ CustomerCombobox: () => null }))
+// Stands in for the server search: one button picks a customer, and it names
+// whoever it was handed, which is what a user sees in the real one.
+vi.mock('@/features/quotes/Components/CustomerCombobox', () => ({
+  CustomerCombobox: ({
+    initialCustomer,
+    onChange,
+  }: {
+    initialCustomer?: { name: string } | null
+    onChange: (id: string, c: { id: string; name: string; company: null }) => void
+  }) => (
+    <button
+      type="button"
+      onClick={() => onChange('c1', { id: 'c1', name: 'Ann Berg', company: null })}
+    >
+      {initialCustomer?.name ?? 'Select a customer (optional)'}
+    </button>
+  ),
+}))
 vi.mock('@/components/upgrade-gate', () => ({ handleGated: () => false }))
 vi.mock('@/components/upgrade-gate', () => ({ handleGated: () => false }))
 vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn() } }))
 vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn() } }))
 
 
@@ -35,25 +57,24 @@ const unregistered = {
 }
 }
 
 
 function renderPicker(serviceType: string, props: { title?: string; empty?: boolean } = {}) {
 function renderPicker(serviceType: string, props: { title?: string; empty?: boolean } = {}) {
+  searchVehicles.mockReset()
+  searchVehicles.mockResolvedValue({
+    success: true,
+    data: props.empty ? [] : [unregistered],
+  })
   return render(
   return render(
     <ServiceTypeProvider serviceType={serviceType}>
     <ServiceTypeProvider serviceType={serviceType}>
-      <VehiclePickerDialog
-        open
-        onOpenChange={vi.fn()}
-        vehicles={props.empty ? [] : [unregistered]}
-        customers={[]}
-        title={props.title}
-      />
+      <VehiclePickerDialog open onOpenChange={vi.fn()} title={props.title} />
     </ServiceTypeProvider>
     </ServiceTypeProvider>
   )
   )
 }
 }
 
 
 describe('VehiclePickerDialog, automotive', () => {
 describe('VehiclePickerDialog, automotive', () => {
-  it('keeps its vehicle wording on the select step', () => {
+  it('keeps its vehicle wording on the select step', async () => {
     renderPicker('automotive')
     renderPicker('automotive')
     expect(screen.getByRole('dialog', { name: 'Select Vehicle for Work Order' })).toBeVisible()
     expect(screen.getByRole('dialog', { name: 'Select Vehicle for Work Order' })).toBeVisible()
     expect(screen.getByPlaceholderText('Search vehicles...')).toBeInTheDocument()
     expect(screen.getByPlaceholderText('Search vehicles...')).toBeInTheDocument()
-    expect(screen.getByText('No plate')).toBeInTheDocument()
+    expect(await screen.findByText('No plate')).toBeInTheDocument()
     expect(screen.getByRole('button', { name: 'Add New Vehicle' })).toBeInTheDocument()
     expect(screen.getByRole('button', { name: 'Add New Vehicle' })).toBeInTheDocument()
   })
   })
 
 
@@ -62,9 +83,37 @@ describe('VehiclePickerDialog, automotive', () => {
     expect(screen.getByRole('dialog', { name: 'Select Vehicle' })).toBeVisible()
     expect(screen.getByRole('dialog', { name: 'Select Vehicle' })).toBeVisible()
   })
   })
 
 
-  it('says no vehicles were found when there are none', () => {
+  it('says no vehicles were found when there are none', async () => {
     renderPicker('automotive', { empty: true })
     renderPicker('automotive', { empty: true })
-    expect(screen.getByText('No vehicles found.')).toBeInTheDocument()
+    expect(await screen.findByText('No vehicles found.')).toBeInTheDocument()
+  })
+
+  it('asks the server for the recent vehicles, then for what was typed', async () => {
+    renderPicker('automotive')
+    await waitFor(() => expect(searchVehicles).toHaveBeenCalledWith(undefined, 30))
+    fireEvent.change(screen.getByPlaceholderText('Search vehicles...'), {
+      target: { value: ' smith ' },
+    })
+    await waitFor(() => expect(searchVehicles).toHaveBeenCalledWith('smith', 30))
+  })
+
+  it('names the customer a new vehicle will be saved to, after going back and forth', async () => {
+    renderPicker('automotive')
+    createVehicle.mockReset()
+    createVehicle.mockResolvedValue({ success: true, data: { id: 'v9' } })
+    fireEvent.click(screen.getByRole('button', { name: 'Parts-Only Sale' }))
+    fireEvent.click(screen.getByRole('button', { name: 'Select a customer (optional)' }))
+    fireEvent.click(screen.getAllByRole('button')[0]) // back
+    fireEvent.click(screen.getByRole('button', { name: 'Add New Vehicle' }))
+
+    expect(screen.getByRole('button', { name: 'Ann Berg' })).toBeInTheDocument()
+    fireEvent.change(screen.getByLabelText('Make *'), { target: { value: 'Volvo' } })
+    fireEvent.change(screen.getByLabelText('Model *'), { target: { value: 'V70' } })
+    fireEvent.change(screen.getByLabelText('Year *'), { target: { value: '2015' } })
+    fireEvent.click(screen.getByRole('button', { name: 'Create & Continue' }))
+    await waitFor(() =>
+      expect(createVehicle).toHaveBeenCalledWith(expect.objectContaining({ customerId: 'c1' }))
+    )
   })
   })
 
 
   it('keeps its vehicle wording on the create step', () => {
   it('keeps its vehicle wording on the create step', () => {
@@ -78,18 +127,18 @@ describe('VehiclePickerDialog, automotive', () => {
 })
 })
 
 
 describe('VehiclePickerDialog, marine', () => {
 describe('VehiclePickerDialog, marine', () => {
-  it('asks for a vessel on the select step, whatever title the caller passed', () => {
+  it('asks for a vessel on the select step, whatever title the caller passed', async () => {
     renderPicker('marine', { title: 'Select Vehicle' })
     renderPicker('marine', { title: 'Select Vehicle' })
     expect(screen.getByRole('dialog', { name: 'Select Vessel for Work Order' })).toBeVisible()
     expect(screen.getByRole('dialog', { name: 'Select Vessel for Work Order' })).toBeVisible()
     expect(screen.getByPlaceholderText('Search vessels...')).toBeInTheDocument()
     expect(screen.getByPlaceholderText('Search vessels...')).toBeInTheDocument()
-    expect(screen.getByText('No registration')).toBeInTheDocument()
+    expect(await screen.findByText('No registration')).toBeInTheDocument()
     expect(screen.getByRole('button', { name: 'Add New Vessel' })).toBeInTheDocument()
     expect(screen.getByRole('button', { name: 'Add New Vessel' })).toBeInTheDocument()
     expect(screen.queryByText(/vehicle/i)).toBeNull()
     expect(screen.queryByText(/vehicle/i)).toBeNull()
   })
   })
 
 
-  it('says no vessels were found when there are none', () => {
+  it('says no vessels were found when there are none', async () => {
     renderPicker('marine', { empty: true })
     renderPicker('marine', { empty: true })
-    expect(screen.getByText('No vessels found.')).toBeInTheDocument()
+    expect(await screen.findByText('No vessels found.')).toBeInTheDocument()
   })
   })
 
 
   it('asks for what a boat has on the create step', () => {
   it('asks for what a boat has on the create step', () => {

+ 88 - 0
src/__tests__/features/vehicles/reminder-form-vehicles.test.tsx

@@ -0,0 +1,88 @@
+/**
+ * The reminder form's vehicle list belongs to the customer chosen above it.
+ *
+ * Vehicles are searched on the server, so the list on screen is whatever the
+ * last answer was. After the customer changes, that answer is about somebody
+ * else: picking from it would put the reminder on another customer's vehicle
+ * and switch the customer back without a word.
+ */
+
+import { describe, it, expect, vi } from 'vitest'
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+
+// Radix measures the popover's anchor; jsdom has no ResizeObserver, and
+// nothing here needs measuring.
+// biome-ignore lint/suspicious/noEmptyBlockStatements: a stub that measures nothing
+const noop = () => {}
+vi.stubGlobal(
+  'ResizeObserver',
+  class {
+    observe = noop
+    unobserve = noop
+    disconnect = noop
+  }
+)
+Element.prototype.scrollIntoView = noop
+
+vi.mock('@/components/date-settings-context', () => ({
+  useDateSettings: () => ({ timezone: 'UTC' }),
+}))
+vi.mock('@/components/glass-modal', () => ({ useGlassModal: () => ({ open: vi.fn() }) }))
+vi.mock('@/features/vehicles/Actions/reminderActions', () => ({
+  createReminder: vi.fn(),
+  updateReminder: vi.fn(),
+}))
+vi.mock('sonner', () => ({ toast: { error: vi.fn(), success: vi.fn() } }))
+
+// One button per customer stands in for the customer search
+vi.mock('@/features/quotes/Components/CustomerCombobox', () => ({
+  CustomerCombobox: ({ onChange }: { onChange: (id: string) => void }) => (
+    <div>
+      <button type="button" onClick={() => onChange('anna')}>
+        Customer Anna
+      </button>
+      <button type="button" onClick={() => onChange('bert')}>
+        Customer Bert
+      </button>
+    </div>
+  ),
+}))
+
+const annasCar = {
+  id: 'v-anna',
+  make: 'Volvo',
+  model: 'V70',
+  year: 2015,
+  licensePlate: 'ANNA-1',
+  customerId: 'anna',
+  customer: { id: 'anna', name: 'Anna' },
+}
+
+const searchVehicles = vi.hoisted(() => vi.fn())
+vi.mock('@/features/vehicles/Actions/vehicleActions', () => ({ searchVehicles }))
+
+const { ReminderFormDialog } = await import('@/features/vehicles/Components/ReminderFormDialog')
+
+describe('ReminderFormDialog vehicle list', () => {
+  it("does not offer the previous customer's vehicles while the new list loads", async () => {
+    // Anna's vehicles come back at once; Bert's search is still on its way
+    searchVehicles.mockImplementation(
+      (_q: string | undefined, _limit: number, _offset: number, customerId?: string) =>
+        customerId === 'anna'
+          ? Promise.resolve({ success: true, data: [annasCar] })
+          : new Promise(noop)
+    )
+    render(<ReminderFormDialog open onOpenChange={vi.fn()} />)
+
+    fireEvent.click(screen.getByRole('button', { name: 'Customer Anna' }))
+    fireEvent.click(screen.getByRole('combobox'))
+    expect(await screen.findByText('2015 Volvo V70')).toBeInTheDocument()
+    fireEvent.keyDown(screen.getByPlaceholderText('Search vehicles...'), { key: 'Escape' })
+    await waitFor(() => expect(screen.queryByText('2015 Volvo V70')).toBeNull())
+
+    fireEvent.click(screen.getByRole('button', { name: 'Customer Bert' }))
+    fireEvent.click(screen.getByRole('combobox'))
+    await waitFor(() => expect(searchVehicles).toHaveBeenLastCalledWith(undefined, 50, 0, 'bert'))
+    expect(screen.queryByText('2015 Volvo V70')).toBeNull()
+  })
+})

+ 30 - 1
src/__tests__/lib/name-similarity.test.ts

@@ -8,7 +8,7 @@
  */
  */
 
 
 import { describe, it, expect } from 'vitest'
 import { describe, it, expect } from 'vitest'
-import { nameSimilarity, normalizeName } from '@/lib/name-similarity'
+import { NAME_FOLD, foldName, nameSimilarity, normalizeName } from '@/lib/name-similarity'
 
 
 const THRESHOLD = 0.9
 const THRESHOLD = 0.9
 
 
@@ -59,3 +59,32 @@ describe('nameSimilarity', () => {
     expect(nameSimilarity('', 'Manuel Lücking')).toBe(0)
     expect(nameSimilarity('', 'Manuel Lücking')).toBe(0)
   })
   })
 })
 })
+
+describe('foldName and NAME_FOLD', () => {
+  // What Postgres makes of a stored name: translate() with the table, then lower()
+  const inDatabase = (stored: string) => {
+    let out = ''
+    for (const letter of stored) {
+      const at = NAME_FOLD.from.indexOf(letter)
+      out += at === -1 ? letter : NAME_FOLD.to[at]
+    }
+    return out.toLowerCase()
+  }
+
+  it('pairs every letter with the one it becomes', () => {
+    expect([...NAME_FOLD.from]).toHaveLength([...NAME_FOLD.to].length)
+  })
+
+  it.each([
+    ['Jürgen Müller', 'Jurgen Muller'],
+    ['José Núñez', 'Jose Nunez'],
+    ['Øyvind Ærø', 'Oyvind Æro'],
+    ['ŁUKASZ Żółć', 'Lukasz Zolc'],
+  ])('finds %s in the database for papers that say %s, or the same with accents', (stored, read) => {
+    for (const papers of [stored, read]) {
+      for (const word of foldName(normalizeName(papers)).split(' ')) {
+        expect(inDatabase(stored)).toContain(word)
+      }
+    }
+  })
+})

+ 1 - 21
src/app/(authenticated)/calendar/page.tsx

@@ -1,7 +1,5 @@
 import { resolveCurrencyCode } from '@/lib/currencies'
 import { resolveCurrencyCode } from '@/lib/currencies'
 import { getCalendarEvents } from '@/features/calendar/Actions/calendarActions'
 import { getCalendarEvents } from '@/features/calendar/Actions/calendarActions'
-import { getVehicles } from '@/features/vehicles/Actions/vehicleActions'
-import { getCustomersList } from '@/features/customers/Actions/customerActions'
 import { getDisplaySettings } from '@/features/settings/Actions/settingsActions'
 import { getDisplaySettings } from '@/features/settings/Actions/settingsActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
@@ -60,25 +58,9 @@ export default async function CalendarPage({
         }
         }
   const rangeKeys = { start: toLocalDateStr(range.start), end: toLocalDateStr(range.end) }
   const rangeKeys = { start: toLocalDateStr(range.start), end: toLocalDateStr(range.end) }
 
 
-  const [eventsResult, vehiclesResult, customersResult] = await Promise.all([
-    getCalendarEvents(rangeKeys),
-    getVehicles(),
-    getCustomersList(),
-  ])
+  const eventsResult = await getCalendarEvents(rangeKeys)
 
 
   const events = eventsResult.success && eventsResult.data ? eventsResult.data : []
   const events = eventsResult.success && eventsResult.data ? eventsResult.data : []
-  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,
-          customer: v.customer,
-        }))
-      : []
-  const customers = customersResult.success && customersResult.data ? customersResult.data : []
 
 
   return (
   return (
     <>
     <>
@@ -90,8 +72,6 @@ export default async function CalendarPage({
           initialView={view}
           initialView={view}
           initialDate={toLocalDateStr(date)}
           initialDate={toLocalDateStr(date)}
           todayStr={todayStr}
           todayStr={todayStr}
-          vehicles={vehicles}
-          customers={customers}
           currencyCode={currencyCode}
           currencyCode={currencyCode}
           messageChannels={messageChannels}
           messageChannels={messageChannels}
         />
         />

+ 1 - 3
src/app/(authenticated)/customers/[id]/customer-detail-client.tsx

@@ -155,7 +155,6 @@ function oneLine(text: string): string {
 
 
 export function CustomerDetailClient({
 export function CustomerDetailClient({
   customer,
   customer,
-  customers = [],
   unitSystem = 'imperial',
   unitSystem = 'imperial',
   smsEnabled = false,
   smsEnabled = false,
   smsMessages = [],
   smsMessages = [],
@@ -173,7 +172,6 @@ export function CustomerDetailClient({
   aiEnabled = false,
   aiEnabled = false,
 }: {
 }: {
   customer: CustomerDetail
   customer: CustomerDetail
-  customers?: { id: string; name: string; company: string | null }[]
   unitSystem?: 'metric' | 'imperial'
   unitSystem?: 'metric' | 'imperial'
   smsEnabled?: boolean
   smsEnabled?: boolean
   smsMessages?: SmsMessage[]
   smsMessages?: SmsMessage[]
@@ -563,8 +561,8 @@ export function CustomerDetailClient({
             <VehicleForm
             <VehicleForm
               open={showVehicleForm}
               open={showVehicleForm}
               onOpenChange={setShowVehicleForm}
               onOpenChange={setShowVehicleForm}
-              customers={customers}
               defaultCustomerId={customer.id}
               defaultCustomerId={customer.id}
+              defaultCustomer={customer}
             />
             />
             {customer.vehicles.length === 0 ? (
             {customer.vehicles.length === 0 ? (
               <Card className="border-dashed">
               <Card className="border-dashed">

+ 7 - 11
src/app/(authenticated)/customers/[id]/page.tsx

@@ -3,7 +3,6 @@ import {
   getCustomer,
   getCustomer,
   getCustomerInvoices,
   getCustomerInvoices,
   getCustomerQuotes,
   getCustomerQuotes,
-  getCustomersList,
 } from '@/features/customers/Actions/customerActions'
 } from '@/features/customers/Actions/customerActions'
 import { getDisplaySettings } from '@/features/settings/Actions/settingsActions'
 import { getDisplaySettings } from '@/features/settings/Actions/settingsActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
@@ -19,15 +18,13 @@ import { PageHeader } from '@/components/page-header'
 
 
 export default async function CustomerDetailPage({ params }: { params: Promise<{ id: string }> }) {
 export default async function CustomerDetailPage({ params }: { params: Promise<{ id: string }> }) {
   const { id } = await params
   const { id } = await params
-  const [result, settingsResult, layoutData, customersResult, invoicesResult, quotesResult] =
-    await Promise.all([
-      getCustomer(id),
-      getDisplaySettings([SETTING_KEYS.UNIT_SYSTEM]),
-      getLayoutData(),
-      getCustomersList(),
-      getCustomerInvoices(id),
-      getCustomerQuotes(id),
-    ])
+  const [result, settingsResult, layoutData, invoicesResult, quotesResult] = await Promise.all([
+    getCustomer(id),
+    getDisplaySettings([SETTING_KEYS.UNIT_SYSTEM]),
+    getLayoutData(),
+    getCustomerInvoices(id),
+    getCustomerQuotes(id),
+  ])
 
 
   if (!result.success || !result.data) {
   if (!result.success || !result.data) {
     const t = await getTranslations('customers.detail')
     const t = await getTranslations('customers.detail')
@@ -108,7 +105,6 @@ export default async function CustomerDetailPage({ params }: { params: Promise<{
       <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
       <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
         <CustomerDetailClient
         <CustomerDetailClient
           customer={result.data}
           customer={result.data}
-          customers={customersResult.success && customersResult.data ? customersResult.data : []}
           unitSystem={
           unitSystem={
             ((settingsResult.success && settingsResult.data?.[SETTING_KEYS.UNIT_SYSTEM]) ||
             ((settingsResult.success && settingsResult.data?.[SETTING_KEYS.UNIT_SYSTEM]) ||
               'imperial') as 'metric' | 'imperial'
               'imperial') as 'metric' | 'imperial'

+ 2 - 20
src/app/(authenticated)/reminders/page.tsx

@@ -1,15 +1,13 @@
 import { getTranslations } from 'next-intl/server'
 import { getTranslations } from 'next-intl/server'
 import { getAllReminders } from '@/features/vehicles/Actions/reminderActions'
 import { getAllReminders } from '@/features/vehicles/Actions/reminderActions'
-import { getVehicles } from '@/features/vehicles/Actions/vehicleActions'
 import { getDisplaySettings } from '@/features/settings/Actions/settingsActions'
 import { getDisplaySettings } from '@/features/settings/Actions/settingsActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { RemindersPageClient } from '@/features/vehicles/Components/RemindersPageClient'
 import { RemindersPageClient } from '@/features/vehicles/Components/RemindersPageClient'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
 
 
 export default async function RemindersPage() {
 export default async function RemindersPage() {
-  const [remindersResult, vehiclesResult, settingsResult] = await Promise.all([
+  const [remindersResult, settingsResult] = await Promise.all([
     getAllReminders(),
     getAllReminders(),
-    getVehicles(),
     getDisplaySettings([SETTING_KEYS.UNIT_SYSTEM]),
     getDisplaySettings([SETTING_KEYS.UNIT_SYSTEM]),
   ])
   ])
 
 
@@ -27,28 +25,12 @@ export default async function RemindersPage() {
 
 
   const settings = settingsResult.success && settingsResult.data ? settingsResult.data : {}
   const settings = settingsResult.success && settingsResult.data ? settingsResult.data : {}
   const unitSystem = (settings[SETTING_KEYS.UNIT_SYSTEM] || 'imperial') as 'metric' | 'imperial'
   const unitSystem = (settings[SETTING_KEYS.UNIT_SYSTEM] || 'imperial') as 'metric' | 'imperial'
-  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,
-          customerName: v.customer?.name ?? null,
-          customerId: v.customer?.id ?? null,
-        }))
-      : []
 
 
   return (
   return (
     <>
     <>
       <PageHeader />
       <PageHeader />
       <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
       <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
-        <RemindersPageClient
-          reminders={remindersResult.data}
-          vehicles={vehicles}
-          unitSystem={unitSystem}
-        />
+        <RemindersPageClient reminders={remindersResult.data} unitSystem={unitSystem} />
       </div>
       </div>
     </>
     </>
   )
   )

+ 0 - 4
src/app/(authenticated)/vehicles/[id]/page.tsx

@@ -3,7 +3,6 @@ import { getTranslations } from 'next-intl/server'
 import { getVehicle } from '@/features/vehicles/Actions/vehicleActions'
 import { getVehicle } from '@/features/vehicles/Actions/vehicleActions'
 import { getServiceRecordsPaginated } from '@/features/vehicles/Actions/serviceActions'
 import { getServiceRecordsPaginated } from '@/features/vehicles/Actions/serviceActions'
 import { getNotesPaginated } from '@/features/vehicles/Actions/noteActions'
 import { getNotesPaginated } from '@/features/vehicles/Actions/noteActions'
-import { getCustomersList } from '@/features/customers/Actions/customerActions'
 import { getDisplaySettings } from '@/features/settings/Actions/settingsActions'
 import { getDisplaySettings } from '@/features/settings/Actions/settingsActions'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { SETTING_KEYS } from '@/features/settings/Schema/settingsSchema'
 import { getVehiclePredictedMileage } from '@/features/vehicles/Actions/predictedMaintenanceActions'
 import { getVehiclePredictedMileage } from '@/features/vehicles/Actions/predictedMaintenanceActions'
@@ -58,7 +57,6 @@ export default async function VehicleDetailPage({
 
 
   const [
   const [
     result,
     result,
-    customersResult,
     serviceResult,
     serviceResult,
     notesResult,
     notesResult,
     settingsResult,
     settingsResult,
@@ -70,7 +68,6 @@ export default async function VehicleDetailPage({
     tireSetsResult,
     tireSetsResult,
   ] = await Promise.all([
   ] = await Promise.all([
     getVehicle(id),
     getVehicle(id),
-    getCustomersList(),
     getServiceRecordsPaginated(id, { page, pageSize, search, type }),
     getServiceRecordsPaginated(id, { page, pageSize, search, type }),
     getNotesPaginated(id, { page: notesPage, pageSize: notesPageSize }),
     getNotesPaginated(id, { page: notesPage, pageSize: notesPageSize }),
     getDisplaySettings([SETTING_KEYS.CURRENCY_CODE, SETTING_KEYS.UNIT_SYSTEM]),
     getDisplaySettings([SETTING_KEYS.CURRENCY_CODE, SETTING_KEYS.UNIT_SYSTEM]),
@@ -203,7 +200,6 @@ export default async function VehicleDetailPage({
       <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
       <div className="flex flex-1 flex-col gap-4 p-4 pt-0">
         <VehicleDetailClient
         <VehicleDetailClient
           vehicle={result.data}
           vehicle={result.data}
-          customers={customersResult.data ?? []}
           paginatedServices={paginatedServices}
           paginatedServices={paginatedServices}
           paginatedNotes={paginatedNotes}
           paginatedNotes={paginatedNotes}
           serviceSearch={search}
           serviceSearch={search}

+ 2 - 15
src/app/(authenticated)/vehicles/[id]/vehicle-detail-client.tsx

@@ -107,12 +107,6 @@ import { formatTypeKey } from '@/features/vehicles/Lib/typeKey'
 import { useTypeKeyEnabled } from '@/components/type-key-context'
 import { useTypeKeyEnabled } from '@/components/type-key-context'
 import { sanitizeHtml } from '@/lib/sanitize-html'
 import { sanitizeHtml } from '@/lib/sanitize-html'
 
 
-interface CustomerOption {
-  id: string
-  name: string
-  company: string | null
-}
-
 interface PaginatedServices {
 interface PaginatedServices {
   records: {
   records: {
     id: string
     id: string
@@ -289,7 +283,6 @@ function InspectionProgress({
 
 
 export function VehicleDetailClient({
 export function VehicleDetailClient({
   vehicle,
   vehicle,
-  customers,
   paginatedServices,
   paginatedServices,
   paginatedNotes,
   paginatedNotes,
   serviceSearch,
   serviceSearch,
@@ -306,7 +299,6 @@ export function VehicleDetailClient({
   tireSets = [],
   tireSets = [],
 }: {
 }: {
   vehicle: VehicleDetail
   vehicle: VehicleDetail
-  customers: CustomerOption[]
   paginatedServices: PaginatedServices
   paginatedServices: PaginatedServices
   paginatedNotes: PaginatedNotes
   paginatedNotes: PaginatedNotes
   paginatedFindings: PaginatedFindings
   paginatedFindings: PaginatedFindings
@@ -1758,12 +1750,7 @@ export function VehicleDetailClient({
         </TabsContent>
         </TabsContent>
       </Tabs>
       </Tabs>
 
 
-      <VehicleForm
-        open={showEditForm}
-        onOpenChange={setShowEditForm}
-        vehicle={vehicle}
-        customers={customers}
-      />
+      <VehicleForm open={showEditForm} onOpenChange={setShowEditForm} vehicle={vehicle} />
       <NoteForm
       <NoteForm
         vehicleId={vehicle.id}
         vehicleId={vehicle.id}
         open={showNoteForm}
         open={showNoteForm}
@@ -1824,7 +1811,7 @@ export function VehicleDetailClient({
         open={showNewInspection}
         open={showNewInspection}
         onOpenChange={setShowNewInspection}
         onOpenChange={setShowNewInspection}
         templates={inspectionTemplates || []}
         templates={inspectionTemplates || []}
-        preselectedVehicleId={vehicle.id}
+        preselectedVehicle={vehicle}
       />
       />
       <ArchiveVehicleDialog
       <ArchiveVehicleDialog
         open={showArchiveDialog}
         open={showArchiveDialog}

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

@@ -2,7 +2,6 @@ import { resolveListSort } from '@/lib/list-sort-preference.server'
 import { cookies } from 'next/headers'
 import { cookies } from 'next/headers'
 import { getTranslations } from 'next-intl/server'
 import { getTranslations } from 'next-intl/server'
 import { getVehiclesPaginated } from '@/features/vehicles/Actions/vehicleActions'
 import { getVehiclesPaginated } from '@/features/vehicles/Actions/vehicleActions'
-import { getCustomersList } from '@/features/customers/Actions/customerActions'
 import { VehiclesClient } from './vehicles-client'
 import { VehiclesClient } from './vehicles-client'
 import { PageHeader } from '@/components/page-header'
 import { PageHeader } from '@/components/page-header'
 import { ListPage } from '@/components/list-page'
 import { ListPage } from '@/components/list-page'
@@ -37,7 +36,7 @@ export default async function VehiclesPage({
   const cookieStore = await cookies()
   const cookieStore = await cookies()
   const viewCookie = cookieStore.get('torqvoice-vehicles-view')?.value
   const viewCookie = cookieStore.get('torqvoice-vehicles-view')?.value
   const initialView = viewCookie === 'table' ? 'table' : viewCookie === 'grid6' ? 'grid6' : 'grid'
   const initialView = viewCookie === 'table' ? 'table' : viewCookie === 'grid6' ? 'grid6' : 'grid'
-  const [result, customersResult] = await Promise.all([
+  const [result] = await Promise.all([
     getVehiclesPaginated({
     getVehiclesPaginated({
       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,
@@ -47,7 +46,6 @@ export default async function VehiclesPage({
       sortOrder: sort.sortOrder,
       sortOrder: sort.sortOrder,
       inspectionDue,
       inspectionDue,
     }),
     }),
-    getCustomersList(),
   ])
   ])
 
 
   if (!result.success || !result.data) {
   if (!result.success || !result.data) {
@@ -69,7 +67,6 @@ export default async function VehiclesPage({
       <ListPage>
       <ListPage>
         <VehiclesClient
         <VehiclesClient
           data={result.data}
           data={result.data}
-          customers={customersResult.data ?? []}
           search={params.search || ''}
           search={params.search || ''}
           sortBy={sort.sortBy || ''}
           sortBy={sort.sortBy || ''}
           sortOrder={sort.sortOrder}
           sortOrder={sort.sortOrder}

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

@@ -94,12 +94,6 @@ interface Vehicle {
   _count: { serviceRecords: number }
   _count: { serviceRecords: number }
 }
 }
 
 
-interface CustomerOption {
-  id: string
-  name: string
-  company: string | null
-}
-
 interface PaginatedData {
 interface PaginatedData {
   vehicles: Vehicle[]
   vehicles: Vehicle[]
   total: number
   total: number
@@ -130,7 +124,6 @@ const INSPECTION_TONE_CLASS: Record<ReturnType<typeof inspectionTone>, string> =
 
 
 export function VehiclesClient({
 export function VehiclesClient({
   data,
   data,
-  customers,
   search,
   search,
   sortBy = '',
   sortBy = '',
   sortOrder = 'desc',
   sortOrder = 'desc',
@@ -141,7 +134,6 @@ export function VehiclesClient({
   hasInspectionData = false,
   hasInspectionData = false,
 }: {
 }: {
   data: PaginatedData
   data: PaginatedData
-  customers: CustomerOption[]
   search: string
   search: string
   sortBy?: string
   sortBy?: string
   sortOrder?: 'asc' | 'desc'
   sortOrder?: 'asc' | 'desc'
@@ -988,7 +980,6 @@ export function VehiclesClient({
           }
           }
         }}
         }}
         vehicle={editVehicle ?? undefined}
         vehicle={editVehicle ?? undefined}
-        customers={customers}
         defaultCustomerId={createCustomerId}
         defaultCustomerId={createCustomerId}
       />
       />
 
 

+ 1 - 19
src/app/(authenticated)/work-orders/page.tsx

@@ -6,8 +6,6 @@ import { getTranslations } from 'next-intl/server'
 import { getWorkOrders } from '@/features/vehicles/Actions/serviceActions'
 import { getWorkOrders } from '@/features/vehicles/Actions/serviceActions'
 import { getDisplaySettings } from '@/features/settings/Actions/settingsActions'
 import { getDisplaySettings } 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 { getAuthContext } from '@/lib/get-auth-context'
 import { getAuthContext } from '@/lib/get-auth-context'
 import { canEmailCustomers } from '@/features/email/Lib/emailAllowance'
 import { canEmailCustomers } from '@/features/email/Lib/emailAllowance'
 import { getFeatures } from '@/lib/features'
 import { getFeatures } from '@/lib/features'
@@ -35,7 +33,7 @@ export default async function WorkOrdersPage({
     sortOrder: 'desc',
     sortOrder: 'desc',
   })
   })
   const columns = await resolveListColumns('workOrders', WORK_ORDER_COLUMNS)
   const columns = await resolveListColumns('workOrders', WORK_ORDER_COLUMNS)
-  const [result, settingsResult, vehiclesResult, customersResult, authCtx] = await Promise.all([
+  const [result, settingsResult, authCtx] = await Promise.all([
     getWorkOrders({
     getWorkOrders({
       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,
@@ -46,8 +44,6 @@ export default async function WorkOrdersPage({
       due: params.due,
       due: params.due,
     }),
     }),
     getDisplaySettings([SETTING_KEYS.CURRENCY_CODE]),
     getDisplaySettings([SETTING_KEYS.CURRENCY_CODE]),
-    getVehicles(),
-    getCustomersList(),
     getAuthContext(),
     getAuthContext(),
   ])
   ])
 
 
@@ -69,18 +65,6 @@ export default async function WorkOrdersPage({
 
 
   const settings = settingsResult.success && settingsResult.data ? settingsResult.data : {}
   const settings = settingsResult.success && settingsResult.data ? settingsResult.data : {}
   const currencyCode = resolveCurrencyCode(settings[SETTING_KEYS.CURRENCY_CODE])
   const currencyCode = resolveCurrencyCode(settings[SETTING_KEYS.CURRENCY_CODE])
-  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,
-          customer: v.customer,
-        }))
-      : []
-  const customers = customersResult.success && customersResult.data ? customersResult.data : []
 
 
   return (
   return (
     <>
     <>
@@ -88,8 +72,6 @@ export default async function WorkOrdersPage({
       <ListPage>
       <ListPage>
         <WorkOrdersClient
         <WorkOrdersClient
           data={result.data}
           data={result.data}
-          vehicles={vehicles}
-          customers={customers}
           currencyCode={currencyCode}
           currencyCode={currencyCode}
           search={params.search || ''}
           search={params.search || ''}
           statusFilter={params.status || 'all'}
           statusFilter={params.status || 'all'}

+ 0 - 21
src/app/(authenticated)/work-orders/work-orders-client.tsx

@@ -108,21 +108,6 @@ interface WorkOrder {
   } | null
   } | null
 }
 }
 
 
-interface VehicleOption {
-  id: string
-  make: string
-  model: string
-  year: number
-  licensePlate: string | null
-  customer: { id: string; name: string; company: string | null } | null
-}
-
-interface CustomerOption {
-  id: string
-  name: string
-  company: string | null
-}
-
 interface PaginatedData {
 interface PaginatedData {
   records: WorkOrder[]
   records: WorkOrder[]
   total: number
   total: number
@@ -171,8 +156,6 @@ const statusTransitions: Record<string, { actionKey: string; target: string }[]>
 
 
 export function WorkOrdersClient({
 export function WorkOrdersClient({
   data,
   data,
-  vehicles = [],
-  customers = [],
   currencyCode = DEFAULT_CURRENCY_CODE,
   currencyCode = DEFAULT_CURRENCY_CODE,
   search,
   search,
   statusFilter,
   statusFilter,
@@ -184,8 +167,6 @@ export function WorkOrdersClient({
   emailEnabled = false,
   emailEnabled = false,
 }: {
 }: {
   data: PaginatedData
   data: PaginatedData
-  vehicles?: VehicleOption[]
-  customers?: CustomerOption[]
   currencyCode?: string
   currencyCode?: string
   search: string
   search: string
   statusFilter: string
   statusFilter: string
@@ -790,8 +771,6 @@ export function WorkOrdersClient({
       <VehiclePickerDialog
       <VehiclePickerDialog
         open={showPicker}
         open={showPicker}
         onOpenChange={setShowPicker}
         onOpenChange={setShowPicker}
-        vehicles={vehicles}
-        customers={customers}
         title={t('selectVehicle')}
         title={t('selectVehicle')}
       />
       />
 
 

+ 5 - 59
src/components/new-work-order-button.tsx

@@ -3,75 +3,21 @@
 import { useState } from 'react'
 import { useState } from 'react'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
-import { Plus, Loader2 } from 'lucide-react'
+import { Plus } from 'lucide-react'
 import { VehiclePickerDialog } from '@/components/vehicle-picker-dialog'
 import { VehiclePickerDialog } from '@/components/vehicle-picker-dialog'
-import { getVehicles } from '@/features/vehicles/Actions/vehicleActions'
-import { getCustomersList } from '@/features/customers/Actions/customerActions'
-
-interface VehicleOption {
-  id: string
-  make: string
-  model: string
-  year: number
-  licensePlate: string | null
-  customer: { id: string; name: string; company: string | null } | null
-}
-
-interface CustomerOption {
-  id: string
-  name: string
-  company: string | null
-}
 
 
 export function NewWorkOrderButton() {
 export function NewWorkOrderButton() {
   const t = useTranslations('navigation')
   const t = useTranslations('navigation')
   const [open, setOpen] = useState(false)
   const [open, setOpen] = useState(false)
-  const [loading, setLoading] = useState(false)
-  const [vehicles, setVehicles] = useState<VehicleOption[]>([])
-  const [customers, setCustomers] = useState<CustomerOption[]>([])
-
-  const handleClick = async () => {
-    setLoading(true)
-    try {
-      const [vehiclesResult, customersResult] = await Promise.all([
-        getVehicles(),
-        getCustomersList(),
-      ])
-      setVehicles(
-        vehiclesResult.success && vehiclesResult.data
-          ? vehiclesResult.data.map((v) => ({
-              id: v.id,
-              make: v.make,
-              model: v.model,
-              year: v.year,
-              licensePlate: v.licensePlate,
-              customer: v.customer,
-            }))
-          : []
-      )
-      setCustomers(customersResult.success && customersResult.data ? customersResult.data : [])
-      setOpen(true)
-    } finally {
-      setLoading(false)
-    }
-  }
 
 
+  // The picker searches vehicles itself, so there is nothing to load first.
   return (
   return (
     <>
     <>
-      <Button size="sm" onClick={handleClick} disabled={loading}>
-        {loading ? (
-          <Loader2 className="mr-1 h-3.5 w-3.5 animate-spin" />
-        ) : (
-          <Plus className="mr-1 h-3.5 w-3.5" />
-        )}
+      <Button size="sm" onClick={() => setOpen(true)}>
+        <Plus className="mr-1 h-3.5 w-3.5" />
         {t('newWorkOrder')}
         {t('newWorkOrder')}
       </Button>
       </Button>
-      <VehiclePickerDialog
-        open={open}
-        onOpenChange={setOpen}
-        vehicles={vehicles}
-        customers={customers}
-      />
+      <VehiclePickerDialog open={open} onOpenChange={setOpen} />
     </>
     </>
   )
   )
 }
 }

+ 4 - 49
src/components/quick-create-menu.tsx

@@ -25,25 +25,8 @@ import {
 } from 'lucide-react'
 } from 'lucide-react'
 import { VehiclePickerDialog } from '@/components/vehicle-picker-dialog'
 import { VehiclePickerDialog } from '@/components/vehicle-picker-dialog'
 import { NewInspectionDialog } from '@/features/inspections/Components/NewInspectionDialog'
 import { NewInspectionDialog } from '@/features/inspections/Components/NewInspectionDialog'
-import { getVehicles } from '@/features/vehicles/Actions/vehicleActions'
-import { getCustomersList } from '@/features/customers/Actions/customerActions'
 import { getTemplates } from '@/features/inspections/Actions/templateActions'
 import { getTemplates } from '@/features/inspections/Actions/templateActions'
 
 
-interface VehicleOption {
-  id: string
-  make: string
-  model: string
-  year: number
-  licensePlate: string | null
-  customer: { id: string; name: string; company: string | null } | null
-}
-
-interface CustomerOption {
-  id: string
-  name: string
-  company: string | null
-}
-
 interface TemplateOption {
 interface TemplateOption {
   id: string
   id: string
   name: string
   name: string
@@ -56,34 +39,11 @@ export function QuickCreateMenu() {
   const [vehicleDialogOpen, setVehicleDialogOpen] = useState(false)
   const [vehicleDialogOpen, setVehicleDialogOpen] = useState(false)
   const [inspectionDialogOpen, setInspectionDialogOpen] = useState(false)
   const [inspectionDialogOpen, setInspectionDialogOpen] = useState(false)
   const [loading, setLoading] = useState(false)
   const [loading, setLoading] = useState(false)
-  const [vehicles, setVehicles] = useState<VehicleOption[]>([])
-  const [customers, setCustomers] = useState<CustomerOption[]>([])
   const [templates, setTemplates] = useState<TemplateOption[]>([])
   const [templates, setTemplates] = useState<TemplateOption[]>([])
 
 
-  const handleNewWorkOrder = async () => {
-    setLoading(true)
-    try {
-      const [vehiclesResult, customersResult] = await Promise.all([
-        getVehicles(),
-        getCustomersList(),
-      ])
-      setVehicles(
-        vehiclesResult.success && vehiclesResult.data
-          ? vehiclesResult.data.map((v) => ({
-              id: v.id,
-              make: v.make,
-              model: v.model,
-              year: v.year,
-              licensePlate: v.licensePlate,
-              customer: v.customer,
-            }))
-          : []
-      )
-      setCustomers(customersResult.success && customersResult.data ? customersResult.data : [])
-      setVehicleDialogOpen(true)
-    } finally {
-      setLoading(false)
-    }
+  // The picker searches vehicles itself, so there is nothing to load first.
+  const handleNewWorkOrder = () => {
+    setVehicleDialogOpen(true)
   }
   }
 
 
   const handleNewInspection = async () => {
   const handleNewInspection = async () => {
@@ -157,12 +117,7 @@ export function QuickCreateMenu() {
           </DropdownMenuItem>
           </DropdownMenuItem>
         </DropdownMenuContent>
         </DropdownMenuContent>
       </DropdownMenu>
       </DropdownMenu>
-      <VehiclePickerDialog
-        open={vehicleDialogOpen}
-        onOpenChange={setVehicleDialogOpen}
-        vehicles={vehicles}
-        customers={customers}
-      />
+      <VehiclePickerDialog open={vehicleDialogOpen} onOpenChange={setVehicleDialogOpen} />
       <NewInspectionDialog
       <NewInspectionDialog
         open={inspectionDialogOpen}
         open={inspectionDialogOpen}
         onOpenChange={setInspectionDialogOpen}
         onOpenChange={setInspectionDialogOpen}

+ 68 - 58
src/components/vehicle-picker-dialog.tsx

@@ -1,20 +1,13 @@
 'use client'
 'use client'
 
 
-import { useState, useMemo } from 'react'
+import { useState, useEffect, useRef } from 'react'
 import { useRouter } from 'next/navigation'
 import { useRouter } from 'next/navigation'
 import { Button } from '@/components/ui/button'
 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 {
-  Select,
-  SelectContent,
-  SelectItem,
-  SelectTrigger,
-  SelectValue,
-} from '@/components/ui/select'
 import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
 import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
 import { Loader2, Plus, Car, Search, ArrowLeft, UserPlus, ShoppingCart, Ship } from 'lucide-react'
 import { Loader2, Plus, Car, Search, ArrowLeft, UserPlus, ShoppingCart, Ship } from 'lucide-react'
-import { createVehicle } from '@/features/vehicles/Actions/vehicleActions'
+import { createVehicle, searchVehicles } from '@/features/vehicles/Actions/vehicleActions'
 import { createDraftCounterSale } from '@/features/vehicles/Actions/createDraftServiceRecord'
 import { createDraftCounterSale } from '@/features/vehicles/Actions/createDraftServiceRecord'
 import { CustomerCombobox } from '@/features/quotes/Components/CustomerCombobox'
 import { CustomerCombobox } from '@/features/quotes/Components/CustomerCombobox'
 import { createCustomer } from '@/features/customers/Actions/customerActions'
 import { createCustomer } from '@/features/customers/Actions/customerActions'
@@ -29,20 +22,21 @@ interface Vehicle {
   model: string
   model: string
   year: number
   year: number
   licensePlate: string | null
   licensePlate: string | null
-  customer: { id: string; name: string; company: string | null } | null
+  customer: { id: string; name: string } | null
 }
 }
 
 
-interface Customer {
+interface CustomerOption {
   id: string
   id: string
   name: string
   name: string
   company: string | null
   company: string | null
 }
 }
 
 
+// Enough recent vehicles to pick from at a glance; anything else is a search away.
+const PAGE_SIZE = 30
+
 interface VehiclePickerDialogProps {
 interface VehiclePickerDialogProps {
   open: boolean
   open: boolean
   onOpenChange: (open: boolean) => void
   onOpenChange: (open: boolean) => void
-  vehicles: Vehicle[]
-  customers: Customer[]
   title?: string
   title?: string
   redirectQuery?: Record<string, string>
   redirectQuery?: Record<string, string>
 }
 }
@@ -50,8 +44,6 @@ interface VehiclePickerDialogProps {
 export function VehiclePickerDialog({
 export function VehiclePickerDialog({
   open,
   open,
   onOpenChange,
   onOpenChange,
-  vehicles,
-  customers,
   title,
   title,
   redirectQuery,
   redirectQuery,
 }: VehiclePickerDialogProps) {
 }: VehiclePickerDialogProps) {
@@ -70,22 +62,43 @@ export function VehiclePickerDialog({
   const [newModel, setNewModel] = useState('')
   const [newModel, setNewModel] = useState('')
   const [newYear, setNewYear] = useState('')
   const [newYear, setNewYear] = useState('')
   const [newPlate, setNewPlate] = useState('')
   const [newPlate, setNewPlate] = useState('')
-  const [selectedCustomerId, setSelectedCustomerId] = useState('')
+  // Kept as the whole customer, not just its id: the combobox remounts when the
+  // step changes and needs the customer to show who the record will be saved to.
+  const [selectedCustomer, setSelectedCustomer] = useState<CustomerOption | null>(null)
+  const selectedCustomerId = selectedCustomer?.id ?? ''
   const [showNewCustomer, setShowNewCustomer] = useState(false)
   const [showNewCustomer, setShowNewCustomer] = useState(false)
   const [newCustomerName, setNewCustomerName] = useState('')
   const [newCustomerName, setNewCustomerName] = useState('')
   const [newCustomerPhone, setNewCustomerPhone] = useState('')
   const [newCustomerPhone, setNewCustomerPhone] = useState('')
   const [creating, setCreating] = useState(false)
   const [creating, setCreating] = useState(false)
 
 
-  const filteredVehicles = useMemo(() => {
-    if (!vehicleSearch.trim()) return vehicles
-    const q = vehicleSearch.toLowerCase()
-    return vehicles.filter(
-      (v) =>
-        `${v.year} ${v.make} ${v.model}`.toLowerCase().includes(q) ||
-        v.licensePlate?.toLowerCase().includes(q) ||
-        v.customer?.name.toLowerCase().includes(q)
+  // The list is searched on the server: a workshop can have tens of thousands
+  // of vehicles, and sending them all to the browser made the picker take
+  // seconds to open and to show each typed letter.
+  const [filteredVehicles, setFilteredVehicles] = useState<Vehicle[]>([])
+  const [vehiclesLoading, setVehiclesLoading] = useState(false)
+  const requestRef = useRef(0)
+
+  useEffect(() => {
+    if (!open || pickerStep !== 'select') return
+    const query = vehicleSearch.trim()
+    const request = ++requestRef.current
+    setVehiclesLoading(true)
+    const timer = setTimeout(
+      async () => {
+        try {
+          const result = await searchVehicles(query || undefined, PAGE_SIZE)
+          if (request !== requestRef.current) return
+          setFilteredVehicles(result.success && result.data ? result.data : [])
+        } catch {
+          if (request === requestRef.current) setFilteredVehicles([])
+        } finally {
+          if (request === requestRef.current) setVehiclesLoading(false)
+        }
+      },
+      query ? 300 : 0
     )
     )
-  }, [vehicles, vehicleSearch])
+    return () => clearTimeout(timer)
+  }, [open, pickerStep, vehicleSearch])
 
 
   const resetState = () => {
   const resetState = () => {
     setPickerStep('select')
     setPickerStep('select')
@@ -94,7 +107,7 @@ export function VehiclePickerDialog({
     setNewModel('')
     setNewModel('')
     setNewYear('')
     setNewYear('')
     setNewPlate('')
     setNewPlate('')
-    setSelectedCustomerId('')
+    setSelectedCustomer(null)
     setShowNewCustomer(false)
     setShowNewCustomer(false)
     setNewCustomerName('')
     setNewCustomerName('')
     setNewCustomerPhone('')
     setNewCustomerPhone('')
@@ -229,10 +242,14 @@ export function VehiclePickerDialog({
                 />
                 />
               </div>
               </div>
               <div className="max-h-[300px] overflow-y-auto">
               <div className="max-h-[300px] overflow-y-auto">
-                {filteredVehicles.length === 0 ? (
+                {vehiclesLoading && filteredVehicles.length === 0 ? (
+                  <div className="flex justify-center py-6">
+                    <Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
+                  </div>
+                ) : filteredVehicles.length === 0 ? (
                   <div className="py-6 text-center space-y-3">
                   <div className="py-6 text-center space-y-3">
                     <p className="text-sm text-muted-foreground">
                     <p className="text-sm text-muted-foreground">
-                      {vehicles.length === 0
+                      {!vehicleSearch.trim()
                         ? isMarine
                         ? isMarine
                           ? t('emptyMarine')
                           ? t('emptyMarine')
                           : t('empty')
                           : t('empty')
@@ -312,7 +329,8 @@ export function VehiclePickerDialog({
                       value={selectedCustomerId}
                       value={selectedCustomerId}
                       placeholder={t('selectCustomer')}
                       placeholder={t('selectCustomer')}
                       noneLabel={t('none')}
                       noneLabel={t('none')}
-                      onChange={(id) => setSelectedCustomerId(id)}
+                      initialCustomer={selectedCustomer}
+                      onChange={(_id, customer) => setSelectedCustomer(customer)}
                     />
                     />
                     <Button
                     <Button
                       variant="ghost"
                       variant="ghost"
@@ -320,7 +338,7 @@ export function VehiclePickerDialog({
                       className="h-8 px-2 text-xs text-muted-foreground"
                       className="h-8 px-2 text-xs text-muted-foreground"
                       onClick={() => {
                       onClick={() => {
                         setShowNewCustomer(true)
                         setShowNewCustomer(true)
-                        setSelectedCustomerId('')
+                        setSelectedCustomer(null)
                       }}
                       }}
                     >
                     >
                       <UserPlus className="h-3.5 w-3.5 mr-1" />
                       <UserPlus className="h-3.5 w-3.5 mr-1" />
@@ -450,35 +468,27 @@ export function VehiclePickerDialog({
               <div className="space-y-1.5">
               <div className="space-y-1.5">
                 <Label>{t('customer')}</Label>
                 <Label>{t('customer')}</Label>
                 {!showNewCustomer ? (
                 {!showNewCustomer ? (
-                  <Select
-                    value={selectedCustomerId}
-                    onValueChange={(val) => {
-                      if (val === '__new__') {
+                  <>
+                    <CustomerCombobox
+                      value={selectedCustomerId}
+                      placeholder={t('selectCustomer')}
+                      noneLabel={t('none')}
+                      initialCustomer={selectedCustomer}
+                      onChange={(_id, customer) => setSelectedCustomer(customer)}
+                    />
+                    <Button
+                      variant="ghost"
+                      size="sm"
+                      className="h-8 px-2 text-xs text-muted-foreground"
+                      onClick={() => {
                         setShowNewCustomer(true)
                         setShowNewCustomer(true)
-                        setSelectedCustomerId('')
-                      } else {
-                        setSelectedCustomerId(val)
-                      }
-                    }}
-                  >
-                    <SelectTrigger className="w-full">
-                      <SelectValue placeholder={t('selectCustomer')} />
-                    </SelectTrigger>
-                    <SelectContent>
-                      <SelectItem value="__new__">
-                        <span className="flex items-center gap-1.5">
-                          <UserPlus className="h-3.5 w-3.5" />
-                          {t('createNewCustomer')}
-                        </span>
-                      </SelectItem>
-                      {customers.map((c) => (
-                        <SelectItem key={c.id} value={c.id}>
-                          {c.name}
-                          {c.company ? ` (${c.company})` : ''}
-                        </SelectItem>
-                      ))}
-                    </SelectContent>
-                  </Select>
+                        setSelectedCustomer(null)
+                      }}
+                    >
+                      <UserPlus className="h-3.5 w-3.5 mr-1" />
+                      {t('createNewCustomer')}
+                    </Button>
+                  </>
                 ) : (
                 ) : (
                   <div className="space-y-3 rounded-md border p-3">
                   <div className="space-y-3 rounded-md border p-3">
                     <div className="flex items-center justify-between">
                     <div className="flex items-center justify-between">

+ 0 - 37
src/features/calendar/Components/CalendarClient.tsx

@@ -42,21 +42,6 @@ import { TimeGridView } from './TimeGridView'
 import { useCalendarPreferences } from './useCalendarPreferences'
 import { useCalendarPreferences } from './useCalendarPreferences'
 import { YearView } from './YearView'
 import { YearView } from './YearView'
 
 
-interface Vehicle {
-  id: string
-  make: string
-  model: string
-  year: number
-  licensePlate: string | null
-  customer: { id: string; name: string; company: string | null } | null
-}
-
-interface Customer {
-  id: string
-  name: string
-  company: string | null
-}
-
 interface CalendarClientProps {
 interface CalendarClientProps {
   initialEvents: CalendarEvent[]
   initialEvents: CalendarEvent[]
   /** The days `initialEvents` covers, as YYYY-MM-DD. */
   /** The days `initialEvents` covers, as YYYY-MM-DD. */
@@ -65,8 +50,6 @@ interface CalendarClientProps {
   /** YYYY-MM-DD the calendar opens on. */
   /** YYYY-MM-DD the calendar opens on. */
   initialDate: string
   initialDate: string
   todayStr: string // YYYY-MM-DD computed on server to avoid hydration mismatch
   todayStr: string // YYYY-MM-DD computed on server to avoid hydration mismatch
-  vehicles: Vehicle[]
-  customers: Customer[]
   currencyCode: string
   currencyCode: string
   /** Channels the workshop can actually send on, resolved on the server */
   /** Channels the workshop can actually send on, resolved on the server */
   messageChannels: MessageChannel[]
   messageChannels: MessageChannel[]
@@ -98,8 +81,6 @@ export default function CalendarClient({
   initialView,
   initialView,
   initialDate,
   initialDate,
   todayStr,
   todayStr,
-  vehicles,
-  customers,
   currencyCode,
   currencyCode,
   messageChannels,
   messageChannels,
 }: CalendarClientProps) {
 }: CalendarClientProps) {
@@ -397,21 +378,6 @@ export default function CalendarClient({
     [menuDateStr]
     [menuDateStr]
   )
   )
 
 
-  // The reminder dialog picks vehicles from a flat list with the customer inlined
-  const reminderVehicles = useMemo(
-    () =>
-      vehicles.map((v) => ({
-        id: v.id,
-        make: v.make,
-        model: v.model,
-        year: v.year,
-        licensePlate: v.licensePlate,
-        customerName: v.customer?.name ?? null,
-        customerId: v.customer?.id ?? null,
-      })),
-    [vehicles]
-  )
-
   const monthDays = useMemo(
   const monthDays = useMemo(
     () => getMonthGridDays(date.getFullYear(), date.getMonth(), weekStartDay),
     () => getMonthGridDays(date.getFullYear(), date.getMonth(), weekStartDay),
     [date, weekStartDay]
     [date, weekStartDay]
@@ -597,7 +563,6 @@ export default function CalendarClient({
       <ReminderFormDialog
       <ReminderFormDialog
         open={showReminderDialog}
         open={showReminderDialog}
         onOpenChange={setShowReminderDialog}
         onOpenChange={setShowReminderDialog}
-        vehicles={reminderVehicles}
         defaultDueDate={menuDate}
         defaultDueDate={menuDate}
         defaultDueTime={menuTime}
         defaultDueTime={menuTime}
         onSaved={refresh}
         onSaved={refresh}
@@ -621,8 +586,6 @@ export default function CalendarClient({
       <VehiclePickerDialog
       <VehiclePickerDialog
         open={showPicker}
         open={showPicker}
         onOpenChange={setShowPicker}
         onOpenChange={setShowPicker}
-        vehicles={vehicles}
-        customers={customers}
         title={t('selectVehicle')}
         title={t('selectVehicle')}
         redirectQuery={workOrderQuery}
         redirectQuery={workOrderQuery}
       />
       />

+ 65 - 17
src/features/customers/Actions/customerActions.ts

@@ -12,6 +12,8 @@ import { claimWhatsappMessagesForCustomer } from '@/lib/whatsapp'
 import { claimEmailMessagesForCustomer } from '@/features/messaging/Lib/emailClaim.server'
 import { claimEmailMessagesForCustomer } from '@/features/messaging/Lib/emailClaim.server'
 import { serviceDateOrderBy } from '@/lib/date-sort'
 import { serviceDateOrderBy } from '@/lib/date-sort'
 import { clearedToNull } from '@/lib/clearable'
 import { clearedToNull } from '@/lib/clearable'
+import { Prisma } from '@/generated/prisma/client'
+import { NAME_FOLD, foldName, normalizeName } from '@/lib/name-similarity'
 
 
 /**
 /**
  * A design a customer is being pointed at has to be this workshop's and an
  * A design a customer is being pointed at has to be this workshop's and an
@@ -506,23 +508,6 @@ export async function createWorkOrderFromRequest(requestId: string) {
   )
   )
 }
 }
 
 
-export async function getCustomersList() {
-  return withAuth(
-    async ({ userId, organizationId }) => {
-      return db.customer.findMany({
-        where: { organizationId },
-        select: { id: true, name: true, company: true },
-        orderBy: { name: 'asc' },
-      })
-    },
-    {
-      requiredPermissions: [
-        { action: PermissionAction.READ, subject: PermissionSubject.CUSTOMERS },
-      ],
-    }
-  )
-}
-
 export async function searchCustomers(search?: string, limit = 20, offset = 0) {
 export async function searchCustomers(search?: string, limit = 20, offset = 0) {
   return withAuth(
   return withAuth(
     async ({ organizationId }) => {
     async ({ organizationId }) => {
@@ -566,6 +551,69 @@ export async function searchCustomers(search?: string, limit = 20, offset = 0) {
   )
   )
 }
 }
 
 
+/** One customer's name and company, for a selector that was given only the id. */
+export async function getCustomerOption(customerId: string) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const customer = await db.customer.findFirst({
+        where: { id: customerId, organizationId },
+        select: { id: true, name: true, company: true },
+      })
+      if (!customer) throw new Error('Customer not found')
+      return customer
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.CUSTOMERS },
+      ],
+    }
+  )
+}
+
+/** How many possible owners are sent back for the fuzzy comparison. */
+const OWNER_CANDIDATE_LIMIT = 50
+
+/**
+ * Customers who could be the keeper named on a scanned document, for the
+ * vehicle form to compare with `nameSimilarity`.
+ *
+ * Name and company are compared with accents and case folded by `foldName`,
+ * so papers that say "Muller" find "Müller" and the other way round. Every
+ * word of two letters or more is looked for, so "Bo Li" is found, and the
+ * customers matching the most words come first, so the right one is not
+ * pushed past the limit by others sharing a surname.
+ */
+export async function findOwnerCandidates(ownerName: string) {
+  return withAuth(
+    async ({ organizationId }) => {
+      const allWords = [...new Set(foldName(normalizeName(ownerName)).split(' ').filter(Boolean))]
+      const longer = allWords.filter((word) => word.length >= 2)
+      const words = (longer.length ? longer : allWords).slice(0, 6)
+      if (!words.length) return []
+
+      const folded = Prisma.sql`lower(translate(c.name || ' ' || coalesce(c.company, ''), ${NAME_FOLD.from}, ${NAME_FOLD.to}))`
+      // normalizeName leaves only letters and digits, so no word holds a LIKE wildcard
+      const hits = words.map((word) => Prisma.sql`(${folded} LIKE ${`%${word}%`})`)
+      return db.$queryRaw<{ id: string; name: string; company: string | null }[]>(Prisma.sql`
+        SELECT c.id, c.name, c.company
+        FROM "public"."customers" c
+        WHERE c."organizationId" = ${organizationId}
+          AND (${Prisma.join(hits, ' OR ')})
+        ORDER BY (${Prisma.join(
+          hits.map((hit) => Prisma.sql`${hit}::int`),
+          ' + '
+        )}) DESC, c.name ASC
+        LIMIT ${OWNER_CANDIDATE_LIMIT}
+      `)
+    },
+    {
+      requiredPermissions: [
+        { action: PermissionAction.READ, subject: PermissionSubject.CUSTOMERS },
+      ],
+    }
+  )
+}
+
 /// Every invoice belonging to a customer: the ones raised directly against
 /// Every invoice belonging to a customer: the ones raised directly against
 /// them (parts-only counter sales, which carry no vehicle) plus every invoice
 /// them (parts-only counter sales, which carry no vehicle) plus every invoice
 /// on a vehicle they own. Without the OR, parts-only sales are invisible from
 /// on a vehicle they own. Without the OR, parts-only sales are invisible from

+ 26 - 102
src/features/inspections/Components/NewInspectionDialog.tsx

@@ -1,6 +1,6 @@
 'use client'
 'use client'
 
 
-import { useState, useEffect, useTransition } from 'react'
+import { useState, useEffect, useMemo, useTransition } from 'react'
 import { useRouter } from 'next/navigation'
 import { useRouter } from 'next/navigation'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
 import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
 import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
@@ -15,23 +15,13 @@ 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 { Check, ChevronsUpDown, Loader2, Settings } from 'lucide-react'
-import { cn } from '@/lib/utils'
+import { Loader2, Settings } from 'lucide-react'
 import Link from 'next/link'
 import Link from 'next/link'
 import { toast } from 'sonner'
 import { toast } from 'sonner'
 import { useServiceType } from '@/components/service-type-context'
 import { useServiceType } from '@/components/service-type-context'
 import { createInspection } from '../Actions/inspectionActions'
 import { createInspection } from '../Actions/inspectionActions'
 import { getTemplates } from '../Actions/templateActions'
 import { getTemplates } from '../Actions/templateActions'
-import { getVehicles } from '@/features/vehicles/Actions/vehicleActions'
+import { VehicleCombobox } from '@/features/quotes/Components/VehicleCombobox'
 
 
 interface TemplateOption {
 interface TemplateOption {
   id: string
   id: string
@@ -51,14 +41,14 @@ export function NewInspectionDialog({
   open,
   open,
   onOpenChange,
   onOpenChange,
   templates: givenTemplates,
   templates: givenTemplates,
-  preselectedVehicleId,
+  preselectedVehicle,
   serviceRecordId,
   serviceRecordId,
 }: {
 }: {
   open: boolean
   open: boolean
   onOpenChange: (open: boolean) => void
   onOpenChange: (open: boolean) => void
   /** The workshop's templates; fetched here when the opener has none to hand. */
   /** The workshop's templates; fetched here when the opener has none to hand. */
   templates?: TemplateOption[]
   templates?: TemplateOption[]
-  preselectedVehicleId?: string
+  preselectedVehicle?: VehicleOption
   /** The booked job this inspection is started from; it is linked to the inspection. */
   /** The booked job this inspection is started from; it is linked to the inspection. */
   serviceRecordId?: string
   serviceRecordId?: string
 }) {
 }) {
@@ -66,8 +56,6 @@ export function NewInspectionDialog({
   const router = useRouter()
   const router = useRouter()
   const serviceType = useServiceType()
   const serviceType = useServiceType()
   const [isPending, startTransition] = useTransition()
   const [isPending, startTransition] = useTransition()
-  const [vehicles, setVehicles] = useState<VehicleOption[]>([])
-  const [loadingVehicles, setLoadingVehicles] = useState(false)
   const [fetchedTemplates, setFetchedTemplates] = useState<TemplateOption[]>([])
   const [fetchedTemplates, setFetchedTemplates] = useState<TemplateOption[]>([])
   const templates = givenTemplates ?? fetchedTemplates
   const templates = givenTemplates ?? fetchedTemplates
 
 
@@ -87,38 +75,26 @@ export function NewInspectionDialog({
   }, [open, givenTemplates])
   }, [open, givenTemplates])
 
 
   const defaultTemplate = templates.find((t) => t.isDefault)
   const defaultTemplate = templates.find((t) => t.isDefault)
-  const [vehicleId, setVehicleId] = useState(preselectedVehicleId || '')
+  const [vehicleId, setVehicleId] = useState(preselectedVehicle?.id || '')
   const [templateId, setTemplateId] = useState(defaultTemplate?.id || '')
   const [templateId, setTemplateId] = useState(defaultTemplate?.id || '')
   const [mileage, setMileage] = useState('')
   const [mileage, setMileage] = useState('')
-  const [vehiclePopoverOpen, setVehiclePopoverOpen] = useState(false)
-
-  useEffect(() => {
-    if (open && vehicles.length === 0) {
-      setLoadingVehicles(true)
-      getVehicles().then((result) => {
-        if (result.success && result.data) {
-          setVehicles(
-            result.data.map((v) => ({
-              id: v.id,
-              make: v.make,
-              model: v.model,
-              year: v.year,
-              licensePlate: v.licensePlate,
-            }))
-          )
-        }
-        setLoadingVehicles(false)
-      })
-    }
-  }, [open, vehicles.length])
+  // The combobox's own shape; the owner only matters to its list
+  const initialVehicle = useMemo(
+    () => (preselectedVehicle ? { ...preselectedVehicle, customerId: null, customer: null } : null),
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+    [preselectedVehicle?.id]
+  )
 
 
   useEffect(() => {
   useEffect(() => {
     if (open) {
     if (open) {
-      setVehicleId(preselectedVehicleId || '')
+      setVehicleId(preselectedVehicle?.id || '')
       setTemplateId(defaultTemplate?.id || '')
       setTemplateId(defaultTemplate?.id || '')
       setMileage('')
       setMileage('')
     }
     }
-  }, [open, preselectedVehicleId, defaultTemplate?.id])
+    // Keyed on the id: an opener that rebuilds the vehicle object each render
+    // would otherwise reset the choice while the dialog is open.
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [open, preselectedVehicle?.id, defaultTemplate?.id])
 
 
   const handleSubmit = (e: React.FormEvent) => {
   const handleSubmit = (e: React.FormEvent) => {
     e.preventDefault()
     e.preventDefault()
@@ -150,67 +126,15 @@ export function NewInspectionDialog({
         <form onSubmit={handleSubmit} className="space-y-4">
         <form onSubmit={handleSubmit} className="space-y-4">
           <div className="space-y-2">
           <div className="space-y-2">
             <Label>{t('vehicle')}</Label>
             <Label>{t('vehicle')}</Label>
-            {loadingVehicles ? (
-              <div className="flex items-center gap-2 text-sm text-muted-foreground">
-                <Loader2 className="h-4 w-4 animate-spin" />
-                {t('loadingVehicles')}
-              </div>
-            ) : (
-              <Popover open={vehiclePopoverOpen} onOpenChange={setVehiclePopoverOpen}>
-                <PopoverTrigger asChild>
-                  <Button
-                    variant="outline"
-                    role="combobox"
-                    aria-expanded={vehiclePopoverOpen}
-                    className="w-full justify-between font-normal"
-                  >
-                    {vehicleId
-                      ? (() => {
-                          const v = vehicles.find((v) => v.id === vehicleId)
-                          return v
-                            ? `${v.year} ${v.make} ${v.model}${v.licensePlate ? ` (${v.licensePlate})` : ''}`
-                            : 'Select vehicle'
-                        })()
-                      : 'Select vehicle'}
-                    <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
-                  </Button>
-                </PopoverTrigger>
-                <PopoverContent
-                  className="w-(--radix-popover-trigger-width) min-w-64 p-0"
-                  align="start"
-                >
-                  <Command>
-                    <CommandInput placeholder={t('searchVehicles')} />
-                    <CommandList>
-                      <CommandEmpty>{t('noVehicles')}</CommandEmpty>
-                      <CommandGroup>
-                        {vehicles.map((v) => {
-                          const label = `${v.year} ${v.make} ${v.model}${v.licensePlate ? ` (${v.licensePlate})` : ''}`
-                          return (
-                            <CommandItem
-                              key={v.id}
-                              value={label}
-                              onSelect={() => {
-                                setVehicleId(v.id)
-                                setVehiclePopoverOpen(false)
-                              }}
-                            >
-                              <Check
-                                className={cn(
-                                  'mr-2 h-4 w-4',
-                                  vehicleId === v.id ? 'opacity-100' : 'opacity-0'
-                                )}
-                              />
-                              {label}
-                            </CommandItem>
-                          )
-                        })}
-                      </CommandGroup>
-                    </CommandList>
-                  </Command>
-                </PopoverContent>
-              </Popover>
-            )}
+            <VehicleCombobox
+              value={vehicleId}
+              initialVehicle={initialVehicle}
+              placeholder="Select vehicle"
+              searchPlaceholder={t('searchVehicles')}
+              emptyLabel={t('noVehicles')}
+              allowNone={false}
+              onChange={(id) => setVehicleId(id)}
+            />
           </div>
           </div>
 
 
           <div className="space-y-2">
           <div className="space-y-2">

+ 22 - 3
src/features/quotes/Components/CustomerCombobox.tsx

@@ -1,6 +1,6 @@
 'use client'
 'use client'
 
 
-import { useState, useEffect, useRef, useCallback } from 'react'
+import { useState, useEffect, useRef, useCallback, type ReactNode } from 'react'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
 import {
 import {
   Command,
   Command,
@@ -28,7 +28,13 @@ interface CustomerComboboxProps {
   onChange: (id: string, customer: CustomerOption | null) => void
   onChange: (id: string, customer: CustomerOption | null) => void
   initialCustomer?: CustomerOption | null
   initialCustomer?: CustomerOption | null
   placeholder?: string
   placeholder?: string
+  /** What the search box says; the placeholder when left out */
+  searchPlaceholder?: string
   noneLabel?: string
   noneLabel?: string
+  /** What the list says when nothing matches */
+  emptyLabel?: string
+  /** Shown under the list, e.g. a way to add a customer; `close` shuts the list */
+  footer?: (close: () => void) => ReactNode
 }
 }
 
 
 export function CustomerCombobox({
 export function CustomerCombobox({
@@ -36,7 +42,10 @@ export function CustomerCombobox({
   onChange,
   onChange,
   initialCustomer,
   initialCustomer,
   placeholder = 'Select customer...',
   placeholder = 'Select customer...',
+  searchPlaceholder,
   noneLabel = 'None',
   noneLabel = 'None',
+  emptyLabel = 'No customers found.',
+  footer,
 }: CustomerComboboxProps) {
 }: CustomerComboboxProps) {
   const [open, setOpen] = useState(false)
   const [open, setOpen] = useState(false)
   const [search, setSearch] = useState('')
   const [search, setSearch] = useState('')
@@ -98,6 +107,11 @@ export function CustomerCombobox({
     if (initialCustomer) setSelected(initialCustomer)
     if (initialCustomer) setSelected(initialCustomer)
   }, [initialCustomer])
   }, [initialCustomer])
 
 
+  // Parent can clear the selection (e.g. the form was reset); drop the label too
+  useEffect(() => {
+    if (!value) setSelected(null)
+  }, [value])
+
   // Infinite scroll
   // Infinite scroll
   const handleScroll = useCallback(() => {
   const handleScroll = useCallback(() => {
     const el = listRef.current
     const el = listRef.current
@@ -126,14 +140,18 @@ export function CustomerCombobox({
       </PopoverTrigger>
       </PopoverTrigger>
       <PopoverContent className="w-(--radix-popover-trigger-width) min-w-64 p-0">
       <PopoverContent className="w-(--radix-popover-trigger-width) min-w-64 p-0">
         <Command shouldFilter={false}>
         <Command shouldFilter={false}>
-          <CommandInput placeholder={placeholder} value={search} onValueChange={setSearch} />
+          <CommandInput
+            placeholder={searchPlaceholder ?? placeholder}
+            value={search}
+            onValueChange={setSearch}
+          />
           <CommandList ref={listRef} onScroll={handleScroll}>
           <CommandList ref={listRef} onScroll={handleScroll}>
             {searching && options.length === 0 && (
             {searching && options.length === 0 && (
               <div className="flex items-center justify-center py-6">
               <div className="flex items-center justify-center py-6">
                 <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
                 <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
               </div>
               </div>
             )}
             )}
-            <CommandEmpty>No customers found.</CommandEmpty>
+            <CommandEmpty>{emptyLabel}</CommandEmpty>
             <CommandGroup>
             <CommandGroup>
               <CommandItem
               <CommandItem
                 onSelect={() => {
                 onSelect={() => {
@@ -168,6 +186,7 @@ export function CustomerCombobox({
               </div>
               </div>
             )}
             )}
           </CommandList>
           </CommandList>
+          {footer?.(() => setOpen(false))}
         </Command>
         </Command>
       </PopoverContent>
       </PopoverContent>
     </Popover>
     </Popover>

+ 27 - 12
src/features/quotes/Components/VehicleCombobox.tsx

@@ -32,7 +32,13 @@ interface VehicleComboboxProps {
   onChange: (id: string, vehicle: VehicleOption | null) => void
   onChange: (id: string, vehicle: VehicleOption | null) => void
   initialVehicle?: VehicleOption | null
   initialVehicle?: VehicleOption | null
   placeholder?: string
   placeholder?: string
+  /** What the search box says; the placeholder when left out */
+  searchPlaceholder?: string
   noneLabel?: string
   noneLabel?: string
+  /** Off where a vehicle is required, so "none" is not offered */
+  allowNone?: boolean
+  /** What the list says when nothing matches */
+  emptyLabel?: string
   /** When set, only this customer's vehicles are offered */
   /** When set, only this customer's vehicles are offered */
   customerId?: string
   customerId?: string
 }
 }
@@ -46,7 +52,10 @@ export function VehicleCombobox({
   onChange,
   onChange,
   initialVehicle,
   initialVehicle,
   placeholder = 'Select vehicle...',
   placeholder = 'Select vehicle...',
+  searchPlaceholder,
   noneLabel = 'None',
   noneLabel = 'None',
+  allowNone = true,
+  emptyLabel = 'No vehicles found.',
   customerId,
   customerId,
 }: VehicleComboboxProps) {
 }: VehicleComboboxProps) {
   const [open, setOpen] = useState(false)
   const [open, setOpen] = useState(false)
@@ -170,25 +179,31 @@ export function VehicleCombobox({
       </PopoverTrigger>
       </PopoverTrigger>
       <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
       <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
         <Command shouldFilter={false}>
         <Command shouldFilter={false}>
-          <CommandInput placeholder={placeholder} value={search} onValueChange={setSearch} />
+          <CommandInput
+            placeholder={searchPlaceholder ?? placeholder}
+            value={search}
+            onValueChange={setSearch}
+          />
           <CommandList ref={listRef} onScroll={handleScroll}>
           <CommandList ref={listRef} onScroll={handleScroll}>
             {searching && options.length === 0 && (
             {searching && options.length === 0 && (
               <div className="flex items-center justify-center py-6">
               <div className="flex items-center justify-center py-6">
                 <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
                 <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
               </div>
               </div>
             )}
             )}
-            <CommandEmpty>No vehicles found.</CommandEmpty>
+            <CommandEmpty>{emptyLabel}</CommandEmpty>
             <CommandGroup>
             <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>
+              {allowNone && (
+                <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) => (
               {options.map((v) => (
                 <CommandItem
                 <CommandItem
                   key={v.id}
                   key={v.id}

+ 0 - 7
src/features/vehicles/Components/PlateLookupCommand.tsx

@@ -20,7 +20,6 @@ import {
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
 import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog'
 import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog'
 import { PLATE_LOOKUP_EVENT, usePlateLookupAccess } from '@/components/plate-lookup-context'
 import { PLATE_LOOKUP_EVENT, usePlateLookupAccess } from '@/components/plate-lookup-context'
-import { getCustomersList } from '@/features/customers/Actions/customerActions'
 import type { VehicleLookup } from '@/features/integrations/Actions/vehicleLookupActions'
 import type { VehicleLookup } from '@/features/integrations/Actions/vehicleLookupActions'
 import { cn } from '@/lib/utils'
 import { cn } from '@/lib/utils'
 import { type PlateLookup, lookupPlate } from '../Actions/plateLookupActions'
 import { type PlateLookup, lookupPlate } from '../Actions/plateLookupActions'
@@ -84,9 +83,6 @@ export function PlateLookupCommand() {
   const [copied, setCopied] = useState(false)
   const [copied, setCopied] = useState(false)
   const [formOpen, setFormOpen] = useState(false)
   const [formOpen, setFormOpen] = useState(false)
   const [formLookup, setFormLookup] = useState<VehicleLookup | null>(null)
   const [formLookup, setFormLookup] = useState<VehicleLookup | null>(null)
-  const [customers, setCustomers] = useState<
-    { id: string; name: string; company: string | null }[]
-  >([])
   const inputRef = useRef<HTMLInputElement>(null)
   const inputRef = useRef<HTMLInputElement>(null)
   const requestRef = useRef(0)
   const requestRef = useRef(0)
 
 
@@ -178,8 +174,6 @@ export function PlateLookupCommand() {
         ? { ...registry, licensePlate: registry.licensePlate ?? result.plate }
         ? { ...registry, licensePlate: registry.licensePlate ?? result.plate }
         : { licensePlate: result.plate, source: '', connectorId: '' }
         : { licensePlate: result.plate, source: '', connectorId: '' }
     )
     )
-    const list = await getCustomersList()
-    setCustomers(list.success && list.data ? list.data : [])
     setOpen(false)
     setOpen(false)
     setFormOpen(true)
     setFormOpen(true)
   }
   }
@@ -627,7 +621,6 @@ export function PlateLookupCommand() {
           setFormOpen(next)
           setFormOpen(next)
           if (!next) setFormLookup(null)
           if (!next) setFormLookup(null)
         }}
         }}
-        customers={customers}
         initialLookup={formLookup}
         initialLookup={formLookup}
       />
       />
     </>
     </>

+ 140 - 50
src/features/vehicles/Components/ReminderFormDialog.tsx

@@ -1,6 +1,6 @@
 'use client'
 'use client'
 
 
-import { useEffect, useState } from 'react'
+import { useEffect, useRef, useState } from 'react'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
 import { useDateSettings } from '@/components/date-settings-context'
 import { useDateSettings } from '@/components/date-settings-context'
 import { zonedDateInput, zonedTimeInput } from '@/lib/timezone'
 import { zonedDateInput, zonedTimeInput } from '@/lib/timezone'
@@ -28,6 +28,7 @@ import { toast } from 'sonner'
 import { cn } from '@/lib/utils'
 import { cn } from '@/lib/utils'
 import { useGlassModal } from '@/components/glass-modal'
 import { useGlassModal } from '@/components/glass-modal'
 import { createReminder, updateReminder } from '../Actions/reminderActions'
 import { createReminder, updateReminder } from '../Actions/reminderActions'
+import { searchVehicles } from '../Actions/vehicleActions'
 import { clearableInput } from '@/lib/clearable'
 import { clearableInput } from '@/lib/clearable'
 
 
 export interface ReminderFormVehicle {
 export interface ReminderFormVehicle {
@@ -51,13 +52,22 @@ export interface ReminderFormValues {
   notifyInApp: boolean
   notifyInApp: boolean
   notifyEmail: boolean
   notifyEmail: boolean
   customer: { id: string; name: string } | null
   customer: { id: string; name: string } | null
-  vehicle: { id: string } | null
+  /** The details name the vehicle in the selector without loading every vehicle */
+  vehicle: {
+    id: string
+    make?: string
+    model?: string
+    year?: number
+    licensePlate?: string | null
+  } | null
 }
 }
 
 
+/** How many vehicles the selector shows at once; typing narrows it on the server. */
+const VEHICLE_PAGE_SIZE = 50
+
 interface ReminderFormDialogProps {
 interface ReminderFormDialogProps {
   open: boolean
   open: boolean
   onOpenChange: (open: boolean) => void
   onOpenChange: (open: boolean) => void
-  vehicles: ReminderFormVehicle[]
   /** Set to edit an existing reminder; left out, the dialog creates a new one */
   /** Set to edit an existing reminder; left out, the dialog creates a new one */
   reminder?: ReminderFormValues
   reminder?: ReminderFormValues
   /** Seeds the due date of a new reminder, e.g. the calendar day that was right-clicked */
   /** Seeds the due date of a new reminder, e.g. the calendar day that was right-clicked */
@@ -84,7 +94,6 @@ function toLocalDateStr(d: Date): string {
 export function ReminderFormDialog({
 export function ReminderFormDialog({
   open,
   open,
   onOpenChange,
   onOpenChange,
-  vehicles,
   reminder,
   reminder,
   defaultDueDate,
   defaultDueDate,
   defaultDueTime,
   defaultDueTime,
@@ -113,6 +122,13 @@ export function ReminderFormDialog({
   } | null>(null)
   } | null>(null)
   const [formLoading, setFormLoading] = useState(false)
   const [formLoading, setFormLoading] = useState(false)
   const [vehicleOpen, setVehicleOpen] = useState(false)
   const [vehicleOpen, setVehicleOpen] = useState(false)
+  // Vehicles are searched on the server: a workshop can have tens of
+  // thousands, and loading them all slowed every page that offers this form.
+  const [selectedVehicle, setSelectedVehicle] = useState<ReminderFormVehicle | null>(null)
+  const [vehicleSearch, setVehicleSearch] = useState('')
+  const [vehicleOptions, setVehicleOptions] = useState<ReminderFormVehicle[]>([])
+  const [vehiclesLoading, setVehiclesLoading] = useState(false)
+  const vehicleRequestRef = useRef(0)
   const [calendarOpen, setCalendarOpen] = useState(false)
   const [calendarOpen, setCalendarOpen] = useState(false)
 
 
   const isEdit = !!reminder
   const isEdit = !!reminder
@@ -122,6 +138,19 @@ export function ReminderFormDialog({
     if (!open) return
     if (!open) return
     if (reminder) {
     if (reminder) {
       setFormVehicleId(reminder.vehicle?.id ?? '')
       setFormVehicleId(reminder.vehicle?.id ?? '')
+      setSelectedVehicle(
+        reminder.vehicle
+          ? {
+              id: reminder.vehicle.id,
+              make: reminder.vehicle.make ?? '',
+              model: reminder.vehicle.model ?? '',
+              year: reminder.vehicle.year ?? 0,
+              licensePlate: reminder.vehicle.licensePlate ?? null,
+              customerName: reminder.customer?.name ?? null,
+              customerId: reminder.customer?.id ?? null,
+            }
+          : null
+      )
       setFormTitle(reminder.title)
       setFormTitle(reminder.title)
       setFormDescription(reminder.description || '')
       setFormDescription(reminder.description || '')
       // The workshop's day and clock, because that is how these two fields
       // The workshop's day and clock, because that is how these two fields
@@ -139,6 +168,7 @@ export function ReminderFormDialog({
       setFormCustomer(reminder.customer ? { ...reminder.customer, company: null } : null)
       setFormCustomer(reminder.customer ? { ...reminder.customer, company: null } : null)
     } else {
     } else {
       setFormVehicleId('')
       setFormVehicleId('')
+      setSelectedVehicle(null)
       setFormTitle('')
       setFormTitle('')
       setFormDescription('')
       setFormDescription('')
       setFormDueDate(defaultDueDate)
       setFormDueDate(defaultDueDate)
@@ -151,7 +181,56 @@ export function ReminderFormDialog({
     }
     }
   }, [open, reminder, defaultDueDate, defaultDueTime, timezone])
   }, [open, reminder, defaultDueDate, defaultDueTime, timezone])
 
 
-  const selectedVehicle = vehicles.find((v) => v.id === formVehicleId)
+  // Another customer's list must not stay on offer: picking from it would
+  // switch the customer back. Drop it, and any search still on its way.
+  useEffect(() => {
+    vehicleRequestRef.current++
+    setVehicleOptions([])
+  }, [formCustomerId])
+
+  // Search as the user types, within the chosen customer's vehicles if there is one
+  useEffect(() => {
+    if (!vehicleOpen) return
+    const query = vehicleSearch.trim()
+    const request = ++vehicleRequestRef.current
+    setVehiclesLoading(true)
+    const timer = setTimeout(
+      async () => {
+        try {
+          const result = await searchVehicles(
+            query || undefined,
+            VEHICLE_PAGE_SIZE,
+            0,
+            formCustomerId || undefined
+          )
+          if (request !== vehicleRequestRef.current) return
+          setVehicleOptions(
+            result.success && result.data
+              ? result.data.map((v) => ({
+                  id: v.id,
+                  make: v.make,
+                  model: v.model,
+                  year: v.year,
+                  licensePlate: v.licensePlate,
+                  customerName: v.customer?.name ?? null,
+                  customerId: v.customerId,
+                }))
+              : []
+          )
+        } catch {
+          if (request === vehicleRequestRef.current) setVehicleOptions([])
+        } finally {
+          if (request === vehicleRequestRef.current) setVehiclesLoading(false)
+        }
+      },
+      query ? 300 : 0
+    )
+    return () => clearTimeout(timer)
+  }, [vehicleOpen, vehicleSearch, formCustomerId])
+
+  const shownVehicles = formCustomerId
+    ? vehicleOptions.filter((v) => v.customerId === formCustomerId)
+    : vehicleOptions
 
 
   const handleSubmit = async (e: React.FormEvent) => {
   const handleSubmit = async (e: React.FormEvent) => {
     e.preventDefault()
     e.preventDefault()
@@ -210,9 +289,10 @@ export function ReminderFormDialog({
               onChange={(id) => {
               onChange={(id) => {
                 setFormCustomerId(id)
                 setFormCustomerId(id)
                 // A vehicle belonging to another customer no longer fits
                 // A vehicle belonging to another customer no longer fits
-                const current = vehicles.find((v) => v.id === formVehicleId)
+                const current = selectedVehicle?.id === formVehicleId ? selectedVehicle : null
                 if (id && current && current.customerId !== id) {
                 if (id && current && current.customerId !== id) {
                   setFormVehicleId('')
                   setFormVehicleId('')
+                  setSelectedVehicle(null)
                 }
                 }
               }}
               }}
             />
             />
@@ -226,22 +306,33 @@ export function ReminderFormDialog({
                   aria-expanded={vehicleOpen}
                   aria-expanded={vehicleOpen}
                   className="w-full justify-between font-normal"
                   className="w-full justify-between font-normal"
                 >
                 >
-                  {selectedVehicle
+                  {selectedVehicle && formVehicleId
                     ? `${selectedVehicle.year} ${selectedVehicle.make} ${selectedVehicle.model}${selectedVehicle.licensePlate ? ` · ${selectedVehicle.licensePlate}` : ''}`
                     ? `${selectedVehicle.year} ${selectedVehicle.make} ${selectedVehicle.model}${selectedVehicle.licensePlate ? ` · ${selectedVehicle.licensePlate}` : ''}`
                     : t('selectVehicle')}
                     : t('selectVehicle')}
                   <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
                   <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
                 </Button>
                 </Button>
               </PopoverTrigger>
               </PopoverTrigger>
               <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
               <PopoverContent className="w-[var(--radix-popover-trigger-width)] p-0">
-                <Command>
-                  <CommandInput placeholder={t('searchVehicle')} />
+                <Command shouldFilter={false}>
+                  <CommandInput
+                    placeholder={t('searchVehicle')}
+                    value={vehicleSearch}
+                    onValueChange={setVehicleSearch}
+                  />
                   <CommandList>
                   <CommandList>
-                    <CommandEmpty>{t('noVehicle')}</CommandEmpty>
+                    {vehiclesLoading && shownVehicles.length === 0 ? (
+                      <div className="flex items-center justify-center py-6">
+                        <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
+                      </div>
+                    ) : (
+                      <CommandEmpty>{t('noVehicle')}</CommandEmpty>
+                    )}
                     <CommandGroup>
                     <CommandGroup>
                       <CommandItem
                       <CommandItem
                         value="__none__"
                         value="__none__"
                         onSelect={() => {
                         onSelect={() => {
                           setFormVehicleId('')
                           setFormVehicleId('')
+                          setSelectedVehicle(null)
                           setVehicleOpen(false)
                           setVehicleOpen(false)
                         }}
                         }}
                       >
                       >
@@ -253,47 +344,46 @@ export function ReminderFormDialog({
                         />
                         />
                         {t('noneOption')}
                         {t('noneOption')}
                       </CommandItem>
                       </CommandItem>
-                      {vehicles
-                        .filter((v) => !formCustomerId || v.customerId === formCustomerId)
-                        .map((v) => (
-                          <CommandItem
-                            key={v.id}
-                            value={`${v.year} ${v.make} ${v.model} ${v.licensePlate || ''} ${v.customerName || ''}`}
-                            onSelect={() => {
-                              setFormVehicleId(v.id)
-                              // The vehicle's customer follows automatically
-                              if (v.customerId) {
-                                setFormCustomerId(v.customerId)
-                                setFormCustomer({
-                                  id: v.customerId,
-                                  name: v.customerName || '',
-                                  company: null,
-                                })
-                              }
-                              setVehicleOpen(false)
-                            }}
-                          >
-                            <Check
-                              className={cn(
-                                'mr-2 h-4 w-4',
-                                formVehicleId === 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>
+                      {shownVehicles.map((v) => (
+                        <CommandItem
+                          key={v.id}
+                          value={v.id}
+                          onSelect={() => {
+                            setFormVehicleId(v.id)
+                            setSelectedVehicle(v)
+                            // The vehicle's customer follows automatically
+                            if (v.customerId) {
+                              setFormCustomerId(v.customerId)
+                              setFormCustomer({
+                                id: v.customerId,
+                                name: v.customerName || '',
+                                company: null,
+                              })
+                            }
+                            setVehicleOpen(false)
+                          }}
+                        >
+                          <Check
+                            className={cn(
+                              'mr-2 h-4 w-4',
+                              formVehicleId === 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>
                               )}
                               )}
-                            </div>
-                          </CommandItem>
-                        ))}
+                            </p>
+                            {v.customerName && (
+                              <p className="text-xs text-muted-foreground">{v.customerName}</p>
+                            )}
+                          </div>
+                        </CommandItem>
+                      ))}
                     </CommandGroup>
                     </CommandGroup>
                   </CommandList>
                   </CommandList>
                 </Command>
                 </Command>

+ 1 - 13
src/features/vehicles/Components/RemindersPageClient.tsx

@@ -62,19 +62,8 @@ interface Reminder {
   } | null
   } | null
 }
 }
 
 
-interface VehicleOption {
-  id: string
-  make: string
-  model: string
-  year: number
-  licensePlate: string | null
-  customerName: string | null
-  customerId: string | null
-}
-
 interface RemindersPageClientProps {
 interface RemindersPageClientProps {
   reminders: Reminder[]
   reminders: Reminder[]
-  vehicles: VehicleOption[]
   unitSystem: 'metric' | 'imperial'
   unitSystem: 'metric' | 'imperial'
 }
 }
 
 
@@ -92,7 +81,7 @@ function getUrgency(r: Reminder): 'overdue' | 'due-soon' | 'normal' {
   return 'normal'
   return 'normal'
 }
 }
 
 
-export function RemindersPageClient({ reminders, vehicles, unitSystem }: RemindersPageClientProps) {
+export function RemindersPageClient({ reminders, unitSystem }: RemindersPageClientProps) {
   const t = useTranslations('reminders')
   const t = useTranslations('reminders')
   const tv = useTranslations('vehicles.reminders')
   const tv = useTranslations('vehicles.reminders')
   const tc = useTranslations('common.buttons')
   const tc = useTranslations('common.buttons')
@@ -329,7 +318,6 @@ export function RemindersPageClient({ reminders, vehicles, unitSystem }: Reminde
       <ReminderFormDialog
       <ReminderFormDialog
         open={showForm}
         open={showForm}
         onOpenChange={setShowForm}
         onOpenChange={setShowForm}
-        vehicles={vehicles}
         reminder={editingReminder}
         reminder={editingReminder}
         onSaved={() => router.refresh()}
         onSaved={() => router.refresh()}
       />
       />

+ 120 - 110
src/features/vehicles/Components/VehicleForm.tsx

@@ -3,7 +3,7 @@
 import { useEngineSizeUnit } from '@/components/engine-size-context'
 import { useEngineSizeUnit } from '@/components/engine-size-context'
 import { engineSizeHint, engineSizePlaceholder, engineSizeToSave } from '@/lib/engine-size'
 import { engineSizeHint, engineSizePlaceholder, engineSizeToSave } from '@/lib/engine-size'
 import { interactiveRow } from '@/lib/interactive-row'
 import { interactiveRow } from '@/lib/interactive-row'
-import { useState, useRef, useMemo, useEffect, useId } from 'react'
+import { useState, useRef, useEffect, useId } from 'react'
 import { useRouter } from 'next/navigation'
 import { useRouter } from 'next/navigation'
 import Image from 'next/image'
 import Image from 'next/image'
 import { Button } from '@/components/ui/button'
 import { Button } from '@/components/ui/button'
@@ -16,16 +16,7 @@ import {
   SelectTrigger,
   SelectTrigger,
   SelectValue,
   SelectValue,
 } from '@/components/ui/select'
 } from '@/components/ui/select'
-import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
 import { Checkbox } from '@/components/ui/checkbox'
 import { Checkbox } from '@/components/ui/checkbox'
-import {
-  Command,
-  CommandEmpty,
-  CommandGroup,
-  CommandInput,
-  CommandItem,
-  CommandList,
-} from '@/components/ui/command'
 import {
 import {
   Dialog,
   Dialog,
   DialogContent,
   DialogContent,
@@ -45,10 +36,15 @@ import { ScanDocumentButton } from './ScanDocumentButton'
 import { VehicleLookupButton } from './VehicleLookupButton'
 import { VehicleLookupButton } from './VehicleLookupButton'
 import type { VehicleLookup } from '@/features/integrations/Actions/vehicleLookupActions'
 import type { VehicleLookup } from '@/features/integrations/Actions/vehicleLookupActions'
 import { nameSimilarity } from '@/lib/name-similarity'
 import { nameSimilarity } from '@/lib/name-similarity'
-import { Camera, Check, ChevronsUpDown, Loader2, Plus, X } from 'lucide-react'
+import { Camera, Loader2, Plus, X } from 'lucide-react'
 import { compressImage } from '@/lib/compress-image'
 import { compressImage } from '@/lib/compress-image'
 import { CustomerForm } from '@/features/customers/Components/CustomerForm'
 import { CustomerForm } from '@/features/customers/Components/CustomerForm'
-import { createCustomer } from '@/features/customers/Actions/customerActions'
+import {
+  createCustomer,
+  findOwnerCandidates,
+  getCustomerOption,
+} from '@/features/customers/Actions/customerActions'
+import { CustomerCombobox } from '@/features/quotes/Components/CustomerCombobox'
 import { useFormatter, useTranslations } from 'next-intl'
 import { useFormatter, useTranslations } from 'next-intl'
 import { useServiceType } from '@/components/service-type-context'
 import { useServiceType } from '@/components/service-type-context'
 import type { CreateVehicleInput } from '../Schema/vehicleSchema'
 import type { CreateVehicleInput } from '../Schema/vehicleSchema'
@@ -63,8 +59,14 @@ import { handleGated } from '@/components/upgrade-gate'
  */
  */
 const OWNER_MATCH_THRESHOLD = 0.9
 const OWNER_MATCH_THRESHOLD = 0.9
 
 
+interface CustomerOption {
+  id: string
+  name: string
+  company: string | null
+}
+
 interface OwnerMatch {
 interface OwnerMatch {
-  customer: { id: string; name: string; company: string | null }
+  customer: CustomerOption
   score: number
   score: number
 }
 }
 
 
@@ -88,11 +90,14 @@ interface VehicleFormProps {
     tsn?: string | null
     tsn?: string | null
     imageUrl?: string | null
     imageUrl?: string | null
     customerId?: string | null
     customerId?: string | null
+    /** The owner, so the selector can name them without loading every customer */
+    customer?: CustomerOption | null
     inspectionStatus?: { dueAt: Date | string | null; source?: string } | null
     inspectionStatus?: { dueAt: Date | string | null; source?: string } | null
   }
   }
-  customers?: { id: string; name: string; company: string | null }[]
   /** Preselects the customer when creating a new vehicle (ignored when editing) */
   /** Preselects the customer when creating a new vehicle (ignored when editing) */
   defaultCustomerId?: string
   defaultCustomerId?: string
+  /** The preselected customer itself, when the caller has it, to name it without a lookup */
+  defaultCustomer?: CustomerOption | null
   /**
   /**
    * What a registry already said about the plate, for a form opened from the
    * What a registry already said about the plate, for a form opened from the
    * plate lookup: filled in as if the search button had been pressed.
    * plate lookup: filled in as if the search button had been pressed.
@@ -104,8 +109,8 @@ export function VehicleForm({
   open,
   open,
   onOpenChange,
   onOpenChange,
   vehicle,
   vehicle,
-  customers,
   defaultCustomerId,
   defaultCustomerId,
+  defaultCustomer,
   initialLookup,
   initialLookup,
 }: VehicleFormProps) {
 }: VehicleFormProps) {
   const serviceType = useServiceType()
   const serviceType = useServiceType()
@@ -125,9 +130,14 @@ export function VehicleForm({
   const [selectedCustomerId, setSelectedCustomerId] = useState<string>(
   const [selectedCustomerId, setSelectedCustomerId] = useState<string>(
     vehicle?.customerId || defaultCustomerId || 'none'
     vehicle?.customerId || defaultCustomerId || 'none'
   )
   )
-  const [customerOpen, setCustomerOpen] = useState(false)
   const [showCustomerForm, setShowCustomerForm] = useState(false)
   const [showCustomerForm, setShowCustomerForm] = useState(false)
-  const [localCustomers, setLocalCustomers] = useState(customers || [])
+  // The customer list is searched on the server: a workshop can have tens of
+  // thousands, and loading them all slowed every page that offers this form.
+  const initialCustomer =
+    vehicle?.customer ??
+    (!vehicle && defaultCustomer?.id === defaultCustomerId ? defaultCustomer : null) ??
+    null
+  const [selectedCustomer, setSelectedCustomer] = useState<CustomerOption | null>(initialCustomer)
   const [fuelType, setFuelType] = useState(vehicle?.fuelType ?? 'gasoline')
   const [fuelType, setFuelType] = useState(vehicle?.fuelType ?? 'gasoline')
   const defaultTransmission = vehicle?.transmission ?? (isMarine ? 'outboard' : 'automatic')
   const defaultTransmission = vehicle?.transmission ?? (isMarine ? 'outboard' : 'automatic')
   const [transmission, setTransmission] = useState(defaultTransmission)
   const [transmission, setTransmission] = useState(defaultTransmission)
@@ -149,12 +159,27 @@ export function VehicleForm({
   /** Whether saving should also create the keeper as a customer. */
   /** Whether saving should also create the keeper as a customer. */
   const [addOwner, setAddOwner] = useState(true)
   const [addOwner, setAddOwner] = useState(true)
   const [showOwnerMatch, setShowOwnerMatch] = useState(false)
   const [showOwnerMatch, setShowOwnerMatch] = useState(false)
+  // The keeper is looked up on the server. Saving waits for the answer, or a
+  // quick save would add them again; a newer scan, another vehicle or closing
+  // the form makes an answer still on its way stale, and it is dropped.
+  const [ownerLookupPending, setOwnerLookupPending] = useState(false)
+  const ownerLookupRef = useRef(0)
+  const dropOwnerLookup = () => {
+    ownerLookupRef.current++
+    setOwnerLookupPending(false)
+    setShowOwnerMatch(false)
+  }
   const fileRef = useRef<HTMLInputElement>(null)
   const fileRef = useRef<HTMLInputElement>(null)
   const formRef = useRef<HTMLFormElement>(null)
   const formRef = useRef<HTMLFormElement>(null)
 
 
   // Sync state when vehicle prop changes (e.g. opening edit for a different vehicle)
   // Sync state when vehicle prop changes (e.g. opening edit for a different vehicle)
   useEffect(() => {
   useEffect(() => {
     setSelectedCustomerId(vehicle?.customerId || defaultCustomerId || 'none')
     setSelectedCustomerId(vehicle?.customerId || defaultCustomerId || 'none')
+    setSelectedCustomer(
+      vehicle?.customer ??
+        (!vehicle && defaultCustomer?.id === defaultCustomerId ? defaultCustomer : null) ??
+        null
+    )
     setPreview(vehicle?.imageUrl ?? null)
     setPreview(vehicle?.imageUrl ?? null)
     setImageFile(null)
     setImageFile(null)
     setFuelType(vehicle?.fuelType ?? 'gasoline')
     setFuelType(vehicle?.fuelType ?? 'gasoline')
@@ -163,6 +188,7 @@ export function VehicleForm({
     setScannedOwner(null)
     setScannedOwner(null)
     setOwnerMatches([])
     setOwnerMatches([])
     setAddOwner(true)
     setAddOwner(true)
+    dropOwnerLookup()
   }, [
   }, [
     vehicle?.id,
     vehicle?.id,
     vehicle?.customerId,
     vehicle?.customerId,
@@ -171,14 +197,23 @@ export function VehicleForm({
     vehicle?.transmission,
     vehicle?.transmission,
     isMarine,
     isMarine,
     defaultCustomerId,
     defaultCustomerId,
+    defaultCustomer?.id,
+    vehicle?.customer?.id,
   ])
   ])
 
 
-  const selectedCustomerLabel = useMemo(() => {
-    if (!selectedCustomerId || selectedCustomerId === 'none') return t('noCustomer')
-    const c = localCustomers.find((c) => c.id === selectedCustomerId)
-    if (!c) return t('noCustomer')
-    return c.name + (c.company ? ` (${c.company})` : '')
-  }, [selectedCustomerId, localCustomers, t])
+  // A customer preselected by id alone (a link with ?customerId=) still needs a name
+  useEffect(() => {
+    if (!open || selectedCustomerId === 'none' || selectedCustomer?.id === selectedCustomerId)
+      return
+    let cancelled = false
+    getCustomerOption(selectedCustomerId).then((result) => {
+      if (cancelled || !result.success || !result.data) return
+      setSelectedCustomer(result.data)
+    })
+    return () => {
+      cancelled = true
+    }
+  }, [open, selectedCustomerId, selectedCustomer?.id])
 
 
   const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
   const handleImageSelect = (e: React.ChangeEvent<HTMLInputElement>) => {
     const file = e.target.files?.[0]
     const file = e.target.files?.[0]
@@ -252,22 +287,32 @@ export function VehicleForm({
     // Near-identical names are still a judgement call: two brothers at one
     // Near-identical names are still a judgement call: two brothers at one
     // address differ by a first name. Offer the close ones and let the user say.
     // address differ by a first name. Offer the close ones and let the user say.
     const ownerName = data.owner.name
     const ownerName = data.owner.name
-    const candidates = localCustomers
-      .map((c) => ({
-        customer: c,
-        // Papers naming a business may match the company field rather than the
-        // contact saved as the customer's name.
-        score: Math.max(
-          nameSimilarity(ownerName, c.name),
-          c.company ? nameSimilarity(ownerName, c.company) : 0
-        ),
-      }))
-      .filter((m) => m.score >= OWNER_MATCH_THRESHOLD)
-      .sort((a, b) => b.score - a.score)
-      .slice(0, 5)
-
-    setOwnerMatches(candidates)
-    if (candidates.length > 0) setShowOwnerMatch(true)
+    const lookup = ++ownerLookupRef.current
+    setOwnerLookupPending(true)
+    setOwnerMatches([])
+    void findOwnerCandidates(ownerName)
+      .then((result) => {
+        if (lookup !== ownerLookupRef.current) return
+        const candidates = (result.success && result.data ? result.data : [])
+          .map((c) => ({
+            customer: c,
+            // Papers naming a business may match the company field rather than the
+            // contact saved as the customer's name.
+            score: Math.max(
+              nameSimilarity(ownerName, c.name),
+              c.company ? nameSimilarity(ownerName, c.company) : 0
+            ),
+          }))
+          .filter((m) => m.score >= OWNER_MATCH_THRESHOLD)
+          .sort((a, b) => b.score - a.score)
+          .slice(0, 5)
+
+        setOwnerMatches(candidates)
+        if (candidates.length > 0) setShowOwnerMatch(true)
+      })
+      .finally(() => {
+        if (lookup === ownerLookupRef.current) setOwnerLookupPending(false)
+      })
   }
   }
 
 
   /**
   /**
@@ -325,11 +370,14 @@ export function VehicleForm({
     if (!open) {
     if (!open) {
       appliedLookupRef.current = null
       appliedLookupRef.current = null
       lookupRef.current = null
       lookupRef.current = null
+      dropOwnerLookup()
     }
     }
   }, [open])
   }, [open])
 
 
   const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
   const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
     e.preventDefault()
     e.preventDefault()
+    // The save shortcut and Enter reach here past the disabled button
+    if (ownerLookupPending) return
     const formData = new FormData(e.currentTarget)
     const formData = new FormData(e.currentTarget)
 
 
     // Caught here rather than by the server, which could only answer in English.
     // Caught here rather than by the server, which could only answer in English.
@@ -510,75 +558,36 @@ export function VehicleForm({
               {/* Customer selector */}
               {/* Customer selector */}
               <div className="space-y-2">
               <div className="space-y-2">
                 <Label>{t('customer')}</Label>
                 <Label>{t('customer')}</Label>
-                <Popover open={customerOpen} onOpenChange={setCustomerOpen} modal={true}>
-                  <PopoverTrigger asChild>
-                    <Button
-                      variant="outline"
-                      role="combobox"
-                      aria-expanded={customerOpen}
-                      className="w-full justify-between font-normal"
-                    >
-                      <span className="truncate">{selectedCustomerLabel}</span>
-                      <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
-                    </Button>
-                  </PopoverTrigger>
-                  <PopoverContent
-                    className="w-(--radix-popover-trigger-width) min-w-64 p-0"
-                    align="start"
-                  >
-                    <Command>
-                      <CommandInput placeholder={t('searchCustomers')} />
-                      <CommandList className="max-h-60 overflow-y-auto">
-                        <CommandEmpty>{t('noCustomerFound')}</CommandEmpty>
-                        <CommandGroup>
-                          <CommandItem
-                            value="no-customer"
-                            onSelect={() => {
-                              setSelectedCustomerId('none')
-                              setCustomerOpen(false)
-                            }}
-                          >
-                            <Check
-                              className={`mr-2 h-4 w-4 ${selectedCustomerId === 'none' ? 'opacity-100' : 'opacity-0'}`}
-                            />
-                            {t('noCustomer')}
-                          </CommandItem>
-                          {localCustomers.map((c) => {
-                            const label = c.name + (c.company ? ` (${c.company})` : '')
-                            return (
-                              <CommandItem
-                                key={c.id}
-                                value={label}
-                                onSelect={() => {
-                                  setSelectedCustomerId(c.id)
-                                  setCustomerOpen(false)
-                                }}
-                              >
-                                <Check
-                                  className={`mr-2 h-4 w-4 ${selectedCustomerId === c.id ? 'opacity-100' : 'opacity-0'}`}
-                                />
-                                {label}
-                              </CommandItem>
-                            )
-                          })}
-                        </CommandGroup>
-                      </CommandList>
-                      <div className="border-t p-1">
-                        <button
-                          type="button"
-                          className="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
-                          onClick={() => {
-                            setCustomerOpen(false)
-                            setShowCustomerForm(true)
-                          }}
-                        >
-                          <Plus className="h-4 w-4" />
-                          {t('addCustomer')}
-                        </button>
-                      </div>
-                    </Command>
-                  </PopoverContent>
-                </Popover>
+                <CustomerCombobox
+                  value={selectedCustomerId === 'none' ? '' : selectedCustomerId}
+                  initialCustomer={selectedCustomer}
+                  // A customer given by id alone is named once the lookup below answers
+                  placeholder={
+                    selectedCustomerId !== 'none' && !selectedCustomer ? '…' : t('noCustomer')
+                  }
+                  searchPlaceholder={t('searchCustomers')}
+                  noneLabel={t('noCustomer')}
+                  emptyLabel={t('noCustomerFound')}
+                  onChange={(id, customer) => {
+                    setSelectedCustomerId(id || 'none')
+                    setSelectedCustomer(customer)
+                  }}
+                  footer={(close) => (
+                    <div className="border-t p-1">
+                      <button
+                        type="button"
+                        className="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm hover:bg-accent"
+                        onClick={() => {
+                          close()
+                          setShowCustomerForm(true)
+                        }}
+                      >
+                        <Plus className="h-4 w-4" />
+                        {t('addCustomer')}
+                      </button>
+                    </div>
+                  )}
+                />
 
 
                 {/* Keeper read off the papers who matches no customer yet */}
                 {/* Keeper read off the papers who matches no customer yet */}
                 {scannedOwner?.name && selectedCustomerId === 'none' && (
                 {scannedOwner?.name && selectedCustomerId === 'none' && (
@@ -873,7 +882,7 @@ export function VehicleForm({
               <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
               <Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
                 {tc('cancel')}
                 {tc('cancel')}
               </Button>
               </Button>
-              <Button data-save-shortcut type="submit" disabled={loading}>
+              <Button data-save-shortcut type="submit" disabled={loading || ownerLookupPending}>
                 {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
                 {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
                 {vehicle ? tc('saveChanges') : t('addTitle')}
                 {vehicle ? tc('saveChanges') : t('addTitle')}
               </Button>
               </Button>
@@ -900,6 +909,7 @@ export function VehicleForm({
                 className="flex w-full items-center justify-between gap-3 rounded-lg border border-border p-3 text-left transition-colors hover:bg-accent"
                 className="flex w-full items-center justify-between gap-3 rounded-lg border border-border p-3 text-left transition-colors hover:bg-accent"
                 onClick={() => {
                 onClick={() => {
                   setSelectedCustomerId(customer.id)
                   setSelectedCustomerId(customer.id)
+                  setSelectedCustomer(customer)
                   setScannedOwner(null)
                   setScannedOwner(null)
                   setShowOwnerMatch(false)
                   setShowOwnerMatch(false)
                 }}
                 }}
@@ -942,7 +952,7 @@ export function VehicleForm({
         onOpenChange={setShowCustomerForm}
         onOpenChange={setShowCustomerForm}
         defaults={scannedOwner ?? undefined}
         defaults={scannedOwner ?? undefined}
         onCreated={(created) => {
         onCreated={(created) => {
-          setLocalCustomers((prev) => [...prev, created])
+          setSelectedCustomer(created)
           setSelectedCustomerId(created.id)
           setSelectedCustomerId(created.id)
           setScannedOwner(null)
           setScannedOwner(null)
         }}
         }}

+ 1 - 1
src/features/vehicles/Components/service-page/modern/ModernHero.tsx

@@ -417,7 +417,7 @@ export function ModernHero({
         <NewInspectionDialog
         <NewInspectionDialog
           open={showStartInspection}
           open={showStartInspection}
           onOpenChange={setShowStartInspection}
           onOpenChange={setShowStartInspection}
-          preselectedVehicleId={vehicle.id}
+          preselectedVehicle={vehicle}
           serviceRecordId={record.id}
           serviceRecordId={record.id}
         />
         />
       )}
       )}

+ 0 - 33
src/features/workboard/Components/WorkBoardClient.tsx

@@ -57,8 +57,6 @@ import {
 import { Users, Wrench, ClipboardCheck, Columns3, Inbox, Plus } from 'lucide-react'
 import { Users, Wrench, ClipboardCheck, Columns3, Inbox, Plus } from 'lucide-react'
 import { useTranslations } from 'next-intl'
 import { useTranslations } from 'next-intl'
 import { VehiclePickerDialog } from '@/components/vehicle-picker-dialog'
 import { VehiclePickerDialog } from '@/components/vehicle-picker-dialog'
-import { getVehicles } from '@/features/vehicles/Actions/vehicleActions'
-import { getCustomersList } from '@/features/customers/Actions/customerActions'
 import { timeToMinutes } from '../utils/datetime'
 import { timeToMinutes } from '../utils/datetime'
 import { useDateSettings } from '@/components/date-settings-context'
 import { useDateSettings } from '@/components/date-settings-context'
 
 
@@ -200,19 +198,6 @@ export function WorkBoardClient({
     unassignedJob?: { job: Record<string, unknown>; type: 'serviceRecord' | 'inspection' }
     unassignedJob?: { job: Record<string, unknown>; type: 'serviceRecord' | 'inspection' }
   } | null>(null)
   } | null>(null)
   const [vehiclePickerOpen, setVehiclePickerOpen] = useState(false)
   const [vehiclePickerOpen, setVehiclePickerOpen] = useState(false)
-  const [vehiclePickerVehicles, setVehiclePickerVehicles] = useState<
-    {
-      id: string
-      make: string
-      model: string
-      year: number
-      licensePlate: string | null
-      customer: { id: string; name: string; company: string | null } | null
-    }[]
-  >([])
-  const [vehiclePickerCustomers, setVehiclePickerCustomers] = useState<
-    { id: string; name: string; company: string | null }[]
-  >([])
   const [boardContext, setBoardContext] = useState<Record<string, string>>({})
   const [boardContext, setBoardContext] = useState<Record<string, string>>({})
 
 
   const weekDropResolver = useRef<
   const weekDropResolver = useRef<
@@ -389,22 +374,6 @@ export function WorkBoardClient({
 
 
   const openVehiclePicker = useCallback(async (context: Record<string, string>) => {
   const openVehiclePicker = useCallback(async (context: Record<string, string>) => {
     setBoardContext(context)
     setBoardContext(context)
-    const [vehiclesResult, customersResult] = await Promise.all([getVehicles(), getCustomersList()])
-    setVehiclePickerVehicles(
-      vehiclesResult.success && vehiclesResult.data
-        ? vehiclesResult.data.map((v) => ({
-            id: v.id,
-            make: v.make,
-            model: v.model,
-            year: v.year,
-            licensePlate: v.licensePlate,
-            customer: v.customer,
-          }))
-        : []
-    )
-    setVehiclePickerCustomers(
-      customersResult.success && customersResult.data ? customersResult.data : []
-    )
     setVehiclePickerOpen(true)
     setVehiclePickerOpen(true)
   }, [])
   }, [])
 
 
@@ -884,8 +853,6 @@ export function WorkBoardClient({
           setVehiclePickerOpen(open)
           setVehiclePickerOpen(open)
           if (!open) setBoardContext({})
           if (!open) setBoardContext({})
         }}
         }}
-        vehicles={vehiclePickerVehicles}
-        customers={vehiclePickerCustomers}
         redirectQuery={boardContext}
         redirectQuery={boardContext}
       />
       />
     </div>
     </div>

+ 40 - 0
src/lib/name-similarity.ts

@@ -22,6 +22,46 @@ export function normalizeName(value: string): string {
     .join(' ')
     .join(' ')
 }
 }
 
 
+/**
+ * Letters a name search treats as plain ones, as two strings of equal length
+ * for SQL `translate()`: each accented Latin letter and each capital beside
+ * the letter it becomes, plus the few that carry a stroke rather than an
+ * accent (Ł, Ø, Đ...), which papers and OCR write plain just as often.
+ *
+ * `foldName` applies the same table in the browser or on the server, so a
+ * word folded here is found in a column folded in the database: "Muller"
+ * finds "Müller", and "Lukasz" finds "Łukasz", with no extension installed.
+ */
+export const NAME_FOLD: { from: string; to: string } = (() => {
+  let from = ''
+  let to = ''
+  const strokes: Record<string, string> = { ł: 'l', ø: 'o', đ: 'd', ħ: 'h', ı: 'i', ŧ: 't' }
+  for (let code = 0xc0; code <= 0x24f; code++) {
+    const letter = String.fromCharCode(code)
+    const plain = letter
+      .normalize('NFD')
+      .replace(/[\u0300-\u036f]/g, '')
+      .toLowerCase()
+    const folded = strokes[plain] ?? plain
+    if (folded !== letter && folded.length === 1) {
+      from += letter
+      to += folded
+    }
+  }
+  return { from, to }
+})()
+
+/** Lowercases and folds letters with `NAME_FOLD`, as the database search does. */
+export function foldName(value: string): string {
+  return [...value]
+    .map((letter) => {
+      const at = NAME_FOLD.from.indexOf(letter)
+      return at === -1 ? letter : NAME_FOLD.to[at]
+    })
+    .join('')
+    .toLowerCase()
+}
+
 /** Levenshtein distance, iterative with a single row of state. */
 /** Levenshtein distance, iterative with a single row of state. */
 function editDistance(a: string, b: string): number {
 function editDistance(a: string, b: string): number {
   if (a === b) return 0
   if (a === b) return 0