112 lines
4.9 KiB
Markdown
112 lines
4.9 KiB
Markdown
# Customer DB Integration in SPQ-v2 (All Forms → Customers Collection)
|
|
|
|
## Overview
|
|
|
|
When a shop creates a quote, appointment, or repair order, the customer info should be saved to the `customers` PocketBase collection and linked via `customerId` across all record types. This reference documents the full pattern as applied to Quotes, Appointments, and Repair Orders.
|
|
|
|
## Architecture
|
|
|
|
```
|
|
User types first/middle/last name + phone in form
|
|
↓
|
|
debounced client-side customer matching (400ms)
|
|
↓
|
|
Match found? ─yes─→ Show "Use Existing Customer" banner
|
|
| ↓
|
|
no User clicks "Use"
|
|
| ↓
|
|
↓ Form populated, customerId set
|
|
Auto-create on save
|
|
(ensureCustomerRecord)
|
|
↓
|
|
customers collection (PB)
|
|
↓
|
|
customerId on primary record (appointment / RO / quote)
|
|
```
|
|
|
|
## Shared Helper Library
|
|
|
|
**`src/lib/customerName.ts`** provides three reusable functions:
|
|
|
|
- **`composeName(first, middle, last)`** — joins parts into a single `name` string
|
|
- **`parseName(fullName)`** — splits into `{ firstName, middleName, lastName }` (first-word/last-word heuristic)
|
|
- **`findMatchingCustomer<T>(customers, query)`** — generic matcher: phone (exact 3+ digits) → exact full name → partial name. Generic so it preserves the input type.
|
|
|
|
### Type Changes
|
|
|
|
**`CustomerFormData`** (src/components/customers/types.ts):
|
|
- Added `firstName`, `middleName`, `lastName` alongside existing `name` field
|
|
- The `name` field is auto-composed from the three parts via `composeName()`
|
|
|
|
**`customerWriteSchema`** (src/schemas/customer.ts):
|
|
- Added optional `firstName`, `middleName`, `lastName` (defaults to '')
|
|
|
|
## Form-Specific Implementations
|
|
|
|
### AppointmentModal (src/components/appointments/AppointmentModal.tsx)
|
|
|
|
- **Inputs:** 3-column grid for First / Middle / Last Name (middle optional)
|
|
- **Customer matching:** Debounced 400ms, searches loaded customer list client-side
|
|
- **Suggestion banner:** Shows "Existing customer found — Name [Use] [Continue as New]"
|
|
- **Auto-create:** `ensureCustomerRecord()` on submit — creates customer in PB, sets `customerId`
|
|
- **Linked indicator:** Green badge "Linked to customer record: Name"
|
|
- **Props:** `customers: CustomerWithVehicles[]` (loaded by parent AppointmentsPage)
|
|
|
|
### ROForm (src/components/ROForm.tsx)
|
|
|
|
- Same 3-input layout as AppointmentModal
|
|
- Same matching + suggestion banner + use/dismiss controls
|
|
- Same `ensureCustomerRecord()` on submit
|
|
- Props: `customers?: CustomerWithVehicles[]` (optional, defaults to [])
|
|
- Threaded through `ROModal` → `RepairOrders.tsx` (parent fetches customer list)
|
|
|
|
### Customer Form (CustomerFormModal.tsx)
|
|
|
|
- Updated to use first/middle/last name inputs instead of single "Name" field
|
|
- Composes `name` field internally on save
|
|
|
|
### Quote Form (CustomerInfoPanel in QuoteGenerator)
|
|
|
|
- Same 3-name-input layout + customer search at top
|
|
- Duplicate detection via debounce + PB filter
|
|
- Auto-create via `ensureCustomerRecord()` on save/share
|
|
|
|
## Customer Matching Logic
|
|
|
|
Implemented in `findMatchingCustomer()` (customerName.ts):
|
|
|
|
1. **Phone match (exact):** If phone has 3+ digits, find customer whose phone (digits-only) equals query
|
|
2. **Exact name match:** If first + last name entered, find customer whose `name` equals composed name
|
|
3. **Partial name:** Try `startsWith` on customer name, then `includes`
|
|
|
|
**Important:** Matching is client-side on the loaded customers array — it does NOT use PB `~` filter which can 400 on some collections.
|
|
|
|
## Auto-Create on Save
|
|
|
|
`ensureCustomerRecord()` is called before saving any record when no `customerId` exists:
|
|
|
|
1. If customerId already set → return it
|
|
2. Get userId from pb.authStore
|
|
3. Compose name from first/middle/last
|
|
4. Double-check for existing customer (in case one was added since page load)
|
|
5. If found → auto-link (set customerId, return id)
|
|
6. If not found → `pb.collection('customers').create({ name, phone, email, ... })`
|
|
7. Return new id
|
|
|
|
## CustomerId Propagation
|
|
|
|
- **Appointment → Check-In:** The check-in flow reads `appointment.customerId` and passes to RO create
|
|
- **Quote → RO Conversion:** `customerId` is passed through in QuoteGenerator's RO conversion
|
|
- **Customers page detail view:** Shows linked ROs by querying `customerId` or (legacy) `customerName`
|
|
|
|
## Key Files
|
|
|
|
| File | Role |
|
|
|------|------|
|
|
| `src/lib/customerName.ts` | Shared helpers (composeName, parseName, findMatchingCustomer) |
|
|
| `src/components/appointments/AppointmentModal.tsx` | First/middle/last inputs + matching + auto-save |
|
|
| `src/components/ROForm.tsx` | Same pattern on RO creation/edit |
|
|
| `src/components/customers/CustomerFormModal.tsx` | Customer CRUD modal (also first/middle/last) |
|
|
| `src/components/customers/types.ts` | CustomerRecord, CustomerWithVehicles, CustomerFormData |
|
|
| `src/schemas/customer.ts` | Zod schema (firstName/middleName/lastName fields) |
|