108 lines
6.4 KiB
Markdown
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
|