Files
2026-07-12 10:17:17 -04:00

108 lines
6.4 KiB
Markdown

# 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