6.4 KiB
6.4 KiB
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)
ensureShareToken()called — saves quote to PocketBase if not yet saved, generates acrypto.randomUUID()token, writes it toquote.shareToken- Returns URL:
${window.location.origin}/quote/approve/${token} - Copy to clipboard:
handleShare()(line 154) — copies link, shows shareUrl UI with Email/SMS buttons - Email draft:
handleSendEmail()(line 172) — callsopenEmailDraft(to="customerPhone", subject, body)which firesmailto:link. Does NOT send from server. - SMS draft:
handleSendSms()(line 195) — callsopenSmsDraft(to="customerPhone", body)which firessms:link. Does NOT send from server. - 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 usingshareTokenas both filter and query param (PB API rule gating)- On load, stamps
viewedAton the quote record viastampViewed()— fires once per quote
Approval Flow
- Customer sees per-service Approve/Decline toggle buttons
- Must make a decision on every visible service before submitting
- Must enter their full name as signature
handleSubmitAll()(line 159) writes per-service decisions to PocketBase:customerDecision:'approved'/'declined'approved:true/falseauthorizedBy: customer's nameauthorizedAt: ISO timestampauthorizedMethod:'signature'
- Top-level quote
statuscomputed:- All approved →
'approved' - All declined →
'declined' - Mixed →
'sent'(stays "pending" in UI filters)
- All approved →
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:
CustomerInfotype lacksemail. Only repair orders and invoices havecustomerEmail. 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
onRecordBeforeUpdateRequesthook checkingquoteExpiryDaysfrom settings, but not implemented.
How the Shop Knows
Dashboard Real-Time Subscription (Dashboard.tsx:157-189)
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
localStorageasspq-emailNotifications. No backend code reads this preference. No email is ever sent regardless of toggle state. - Roadmap 4.1 describes a PB
onRecordAfterUpdateRequesthook that would auto-send email via PB SMTP when quote status changes fromsenttoapproved/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
senttoapproved/declined. Requires PB SMTP from 3.2 (already configured in Docker env per Ray). - 4.2 Quote expiry enforcement: PB hook blocks updates if
quoteExpiryDaysexceeded and request usesshareTokenquery param.
Approximate Approach for Adding Identity Verification (least-infrastructure option)
- 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 - On
QuoteApprove.tsxload, if verification code exists, show a code-entry prompt before revealing services - Customer enters the code → matches → show the approval UI
- The verification code can be communicated separately (verbally, or via SMS/email through existing channels)
- No server-side infrastructure beyond the PB record field