# Quote Sharing & Customer Approval Architecture ## Overview The quote → share → customer approve/decline → shop notified flow. Covers v2 React/Vite frontend only. ## Key Files | File | Role | |------|------| | `src/components/quoteGenerator/QuoteSummary.tsx` | "Share with Customer" button + email/SMS draft links | | `src/pages/QuoteApprove.tsx` | Public customer-facing approval page (no auth) | | `src/store/quote.ts` | Zustand store with `approvedSubtotal()`, decision state | | `src/lib/contactLinks.ts` | `openEmailDraft()` and `openSmsDraft()` — local mailto:/sms: links only | | `src/lib/totals.ts` | Billable subtotal logic: approved-only vs all-non-declined | | `src/pages/Dashboard.tsx` | Real-time subscription on quotes collection (line 162) | | `src/schemas/quote.ts` | Zod validation — `shareToken`, `status`, service `customerDecision` | ## Share Flow (QuoteSummary.tsx:104-170) 1. `ensureShareToken()` called — saves quote to PocketBase if not yet saved, generates a `crypto.randomUUID()` token, writes it to `quote.shareToken` 2. Returns URL: `${window.location.origin}/quote/approve/${token}` 3. **Copy to clipboard**: `handleShare()` (line 154) — copies link, shows shareUrl UI with Email/SMS buttons 4. **Email draft**: `handleSendEmail()` (line 172) — calls `openEmailDraft(to="customerPhone", subject, body)` which fires `mailto:` link. Does NOT send from server. 5. **SMS draft**: `handleSendSms()` (line 195) — calls `openSmsDraft(to="customerPhone", body)` which fires `sms:` link. Does NOT send from server. 6. Quote status set to `'sent'` on first share. **Important**: `customerEmail` does NOT exist on `CustomerInfo` or the quote schema (`quoteWriteSchema`). Only `customerPhone` is captured. The `mailto:` link uses phone number as the `to` field (a no-op in most clients). ## Customer Approval Page (QuoteApprove.tsx) ### Loading - Public route: `/quote/approve/:token` — no auth required - `loadQuote()` queries PocketBase using `shareToken` as both filter and query param (PB API rule gating) - On load, stamps `viewedAt` on the quote record via `stampViewed()` — fires once per quote ### Approval Flow 1. Customer sees per-service Approve/Decline toggle buttons 2. Must make a decision on every visible service before submitting 3. Must enter their full name as signature 4. `handleSubmitAll()` (line 159) writes per-service decisions to PocketBase: - `customerDecision`: `'approved'` / `'declined'` - `approved`: `true` / `false` - `authorizedBy`: customer's name - `authorizedAt`: ISO timestamp - `authorizedMethod`: `'signature'` 5. Top-level quote `status` computed: - All approved → `'approved'` - All declined → `'declined'` - Mixed → `'sent'` (stays "pending" in UI filters) ### Security Gaps - **No identity verification**: The share token (UUID) is the only gate. Anyone with the URL can approve/decline. No phone/email verification, no PIN, no expiry. - **No customer email field**: `CustomerInfo` type lacks `email`. Only repair orders and invoices have `customerEmail`. No way to send the share link to an email address even if email sending were implemented. - **No expiry**: Roadmap item 4.2 suggests PB `onRecordBeforeUpdateRequest` hook checking `quoteExpiryDays` from settings, but not implemented. ## How the Shop Knows ### Dashboard Real-Time Subscription (Dashboard.tsx:157-189) ```ts unsub = await pb.collection('quotes').subscribe('*', (data) => { // Delta-applies quote changes to the in-memory list // Stats (approved count, pending review) update reactively }); ``` - **Live on Dashboard only**: Stats update without refresh while on that page - **No proactive notification anywhere**: No toast popup, no browser Notification API, no sound, no email/SMS sent to the shop - **Other pages**: Quotes page (`QuoteGenerator.tsx`) must be manually refreshed ### Toggle in Settings - Settings → Notifications tab has "Email Notifications" toggle (description: "Receive email notifications for quote updates and approvals") - **Placard only**: Saved to `localStorage` as `spq-emailNotifications`. No backend code reads this preference. No email is ever sent regardless of toggle state. - Roadmap 4.1 describes a PB `onRecordAfterUpdateRequest` hook that would auto-send email via PB SMTP when quote status changes from `sent` to `approved`/`declined` — not implemented. ## Verified Capabilities & Gaps Matrix | Capability | Status | Where | |-----------|--------|-------| | Generate share link with UUID token | ✅ Done | QuoteSummary.tsx:104-146 | | Copy link to clipboard | ✅ Done | QuoteSummary.tsx:164 | | Open local email/SMS draft | ⚠️ mailto:/sms: only, no server sending | contactLinks.ts | | Customer views & stamps viewedAt | ✅ Done | QuoteApprove.tsx:56-67 | | Per-service approve/decline | ✅ Done | QuoteApprove.tsx:155-157 | | Per-service authorizedBy/authorizedAt tracking | ✅ Done | QuoteApprove.tsx:180-192 | | Quote status update (approved/declined/sent) | ✅ Done | QuoteApprove.tsx:198-205 | | Real-time Dashboard updates | ✅ Done | Dashboard.tsx:162-183 | | **Toast/notification when customer responds** | ❌ Missing | — | | **Browser Notification API alert** | ❌ Missing | — | | **Email sent to shop on approval** | ❌ Missing | Planned at roadmap 4.1 | | **Phone/last-4 verification** | ❌ Missing | — | | **Quote expiry** | ❌ Missing | Planned at roadmap 4.2 | | **customerEmail field on quote form** | ❌ Missing | Only on RO/Invoice schemas | | **Server-side email sending** | ❌ Missing | No SMTP util in codebase | | **Server-side SMS sending** | ❌ Missing | Would need Twilio/etc | ## Related Roadmap Items - **4.1 Quote approval notifications**: PB hook sends email when status flips from `sent` to `approved`/`declined`. Requires PB SMTP from 3.2 (already configured in Docker env per Ray). - **4.2 Quote expiry enforcement**: PB hook blocks updates if `quoteExpiryDays` exceeded and request uses `shareToken` query param. ## Approximate Approach for Adding Identity Verification (least-infrastructure option) 1. Add `verificationCode` (short random string, e.g. last-4-of-phone or 4-digit PIN) to the quote PB record — set it when generating the share link 2. On `QuoteApprove.tsx` load, if verification code exists, show a code-entry prompt before revealing services 3. Customer enters the code → matches → show the approval UI 4. The verification code can be communicated separately (verbally, or via SMS/email through existing channels) 5. No server-side infrastructure beyond the PB record field