1055 lines
59 KiB
Markdown
1055 lines
59 KiB
Markdown
# ShopProQuote RBX Implementation Roadmap
|
||
|
||
## Goal
|
||
|
||
Implement role-based experiences after onboarding for three user types:
|
||
|
||
- Advisor: full shop desk/admin workflow.
|
||
- Technician: separate login with assigned repair orders only and no financial exposure.
|
||
- Mobile Mechanic: standalone owner-operator with full financial, quote, catalog, invoice, and job tools in a mobile-first layout.
|
||
|
||
## Core Principle
|
||
|
||
Mobile Mechanic is not a Technician role. Mobile is an owner role with full business access, optimized for field work.
|
||
|
||
Technician is the only restricted role.
|
||
|
||
## Role Matrix
|
||
|
||
| Tool | Advisor | Technician | Mobile Mechanic |
|
||
|---|---:|---:|---:|
|
||
| Dashboard | Full desk dashboard | Bay dashboard only | Field dashboard |
|
||
| Quote Generator | Yes | No | Yes |
|
||
| Service Catalog | Yes | No | Yes |
|
||
| Customers | Yes | Limited/no direct access | Yes |
|
||
| Repair Orders | Full access | Assigned sanitized jobs only | Yes |
|
||
| Appointments | Yes | Assigned schedule only if needed | Yes |
|
||
| Invoices | Yes | No | Yes |
|
||
| Financial Dashboard | Yes | No | Yes, mobile-friendly by default |
|
||
| Settings | Full settings | Profile/preferences only | Full settings |
|
||
| Team/Technicians | Yes | No | Optional later |
|
||
|
||
## Data Access Model
|
||
|
||
| Role | Account Type | Data Scope |
|
||
|---|---|---|
|
||
| Advisor | Shop owner/admin | Own shop records where `userId = auth.id` |
|
||
| Mobile | Standalone owner/operator | Own records where `userId = auth.id` |
|
||
| Technician | Separate login under a shop | Sanitized assigned job records only |
|
||
|
||
## Required PocketBase Schema Changes
|
||
|
||
These changes require explicit approval before implementation.
|
||
|
||
### `users` Collection
|
||
|
||
Add:
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| `role` | select | `advisor`, `technician`, `mobile` |
|
||
| `shopUserId` | text or relation | Set only for technician accounts; points to shop owner/advisor user |
|
||
| `displayName` | text | Optional, for technician assignment display |
|
||
|
||
### `technicianAssignments` Collection
|
||
|
||
Purpose: safe technician-facing copy of assigned RO data.
|
||
|
||
Fields:
|
||
|
||
| Field | Type | Notes |
|
||
|---|---|---|
|
||
| `shopUserId` | text/relation | Owner shop account |
|
||
| `technicianUserId` | text/relation | Technician account |
|
||
| `repairOrderId` | text | Source RO id |
|
||
| `roNumber` | text | Safe display |
|
||
| `vehicleInfo` | text | Safe display |
|
||
| `vin` | text | Optional |
|
||
| `mileage` | text | Optional |
|
||
| `serviceLocation` | text | Optional |
|
||
| `status` | select | Job status |
|
||
| `services` | json | Sanitized services only |
|
||
| `internalNotes` | text/json | No pricing |
|
||
| `clockEntries` | json | Technician clock |
|
||
| `inspectionChecklist` | json | Future checklist support |
|
||
| `advisorRequests` | json | Parts/status requests |
|
||
| `created` / `updated` | system | PocketBase managed |
|
||
|
||
Sanitized `services` shape:
|
||
|
||
```ts
|
||
{
|
||
id: string;
|
||
name: string;
|
||
description: string;
|
||
status: 'pending' | 'in_progress' | 'completed';
|
||
subStatus?: 'parts_ordered' | 'in_bay' | 'qc_passed' | '';
|
||
technicianNotes?: string;
|
||
}
|
||
```
|
||
|
||
Never include:
|
||
|
||
- labor rate
|
||
- parts cost
|
||
- service total
|
||
- quote total
|
||
- tax
|
||
- shop charge
|
||
- invoice data
|
||
- customer-pay total
|
||
- warranty cost
|
||
- gross profit
|
||
|
||
## PocketBase Rule Plan
|
||
|
||
### Owner Collections
|
||
|
||
Keep current user-scoped rules for Advisor and Mobile owner records:
|
||
|
||
```txt
|
||
userId = @request.auth.id
|
||
```
|
||
|
||
### Technician Assignments
|
||
|
||
Advisor/mobile owner can manage assignments:
|
||
|
||
```txt
|
||
shopUserId = @request.auth.id
|
||
```
|
||
|
||
Technician can view/update assigned records:
|
||
|
||
```txt
|
||
technicianUserId = @request.auth.id
|
||
```
|
||
|
||
Combined list/view rule:
|
||
|
||
```txt
|
||
shopUserId = @request.auth.id || technicianUserId = @request.auth.id
|
||
```
|
||
|
||
Create rule:
|
||
|
||
```txt
|
||
shopUserId = @request.auth.id
|
||
```
|
||
|
||
Update rule:
|
||
|
||
```txt
|
||
shopUserId = @request.auth.id || technicianUserId = @request.auth.id
|
||
```
|
||
|
||
Delete rule:
|
||
|
||
```txt
|
||
shopUserId = @request.auth.id
|
||
```
|
||
|
||
## Frontend Architecture
|
||
|
||
Add role-specific directories:
|
||
|
||
```txt
|
||
src/components/advisor/
|
||
src/components/technician/
|
||
src/components/mobile/
|
||
src/pages/advisor/
|
||
src/pages/technician/
|
||
src/pages/mobile/
|
||
src/lib/rbx.ts
|
||
```
|
||
|
||
## Step-by-Step Implementation Plan
|
||
|
||
## Phase 1 - Role Resolution
|
||
|
||
1. Add `src/lib/rbx.ts`.
|
||
2. Define `UserRole = 'advisor' | 'technician' | 'mobile'`.
|
||
3. Read role from `pb.authStore.model.role`.
|
||
4. Add fallback from existing `settings.businessType`.
|
||
5. Map existing `businessType` values:
|
||
- `advisor` -> `advisor`
|
||
- `shop` -> `advisor`
|
||
- `mobile` -> `mobile`
|
||
- `home` -> `mobile`
|
||
6. Add helper `getCurrentUserRole()`.
|
||
7. Add helper `isOwnerRole(role)`.
|
||
8. Add helper `isTechnicianRole(role)`.
|
||
|
||
## Phase 2 - Role-Based Routing
|
||
|
||
1. Update `src/App.tsx`.
|
||
2. Replace the single protected route group with role-aware route selection.
|
||
3. Add `RoleBasedLayout`.
|
||
4. Load Advisor routes for `advisor`.
|
||
5. Load Technician routes for `technician`.
|
||
6. Load Mobile routes for `mobile`.
|
||
7. Add route fallback so unauthorized paths redirect to the role home.
|
||
8. Keep public routes unchanged:
|
||
- `/login`
|
||
- `/setup`
|
||
- `/verify`
|
||
- `/quote/approve`
|
||
|
||
## Phase 3 - Advisor Layout
|
||
|
||
1. Keep existing `Layout.tsx` as the base Advisor layout.
|
||
2. Rename or wrap it as `AdvisorLayout`.
|
||
3. Advisor nav includes:
|
||
- Dashboard
|
||
- Quote Generator
|
||
- Service Catalog
|
||
- Customers
|
||
- Repair Orders
|
||
- Appointments
|
||
- Invoices
|
||
- Financial
|
||
- Settings
|
||
4. Add future Team/Technicians nav item after assignment system exists.
|
||
5. Reuse current pages initially.
|
||
|
||
## Phase 4 - Mobile Layout
|
||
|
||
1. Create `MobileLayout`.
|
||
2. Use bottom navigation or compact top navigation.
|
||
3. Prioritize phone-first field workflow.
|
||
4. Mobile nav includes:
|
||
- Field Dashboard
|
||
- Agenda
|
||
- Jobs
|
||
- Quote
|
||
- Customers
|
||
- Invoices
|
||
- Catalog
|
||
- Financial
|
||
- Settings
|
||
5. Reuse existing owner pages first.
|
||
6. Add mobile-specific dashboard later.
|
||
7. Ensure Mobile keeps full access to:
|
||
- Quote Generator
|
||
- Service Catalog
|
||
- Invoices
|
||
- Financial Dashboard
|
||
|
||
## Phase 5 - Technician Layout
|
||
|
||
1. Create `TechnicianLayout`.
|
||
2. Do not import Advisor financial pages.
|
||
3. Technician nav includes:
|
||
- Bay Dashboard
|
||
- Assigned Jobs
|
||
- Profile
|
||
4. Use large touch-friendly controls.
|
||
5. Remove all pricing, billing, invoices, quote, and financial navigation.
|
||
|
||
## Phase 6 - Technician Pages
|
||
|
||
Create:
|
||
|
||
```txt
|
||
src/pages/technician/TechnicianDashboard.tsx
|
||
src/pages/technician/TechnicianJobs.tsx
|
||
src/pages/technician/TechnicianJobDetail.tsx
|
||
src/pages/technician/TechnicianProfile.tsx
|
||
```
|
||
|
||
Technician Dashboard:
|
||
|
||
- assigned active jobs
|
||
- in-progress job
|
||
- waiting-parts jobs
|
||
- completed today
|
||
- clock status
|
||
|
||
Technician Jobs:
|
||
|
||
- list assigned ROs
|
||
- vehicle info
|
||
- RO number
|
||
- service status
|
||
- due/promised time if safe
|
||
- no totals
|
||
|
||
Technician Job Detail:
|
||
|
||
- vehicle info
|
||
- service list
|
||
- service notes
|
||
- checklist
|
||
- start/stop clock
|
||
- mark service in progress
|
||
- mark service complete
|
||
- request parts/help
|
||
- message advisor
|
||
|
||
Technician Profile:
|
||
|
||
- display name
|
||
- dark mode
|
||
- logout
|
||
|
||
## Phase 7 - Assignment Creation
|
||
|
||
1. Add assignment controls inside Advisor `RepairOrders`.
|
||
2. Allow advisor to assign RO services to a technician.
|
||
3. On assignment, create or update `technicianAssignments`.
|
||
4. Copy only sanitized RO data.
|
||
5. Never copy financial fields.
|
||
6. Keep source RO as the advisor-owned record.
|
||
7. Technician updates write to assignment record first.
|
||
8. Advisor view can later merge assignment updates back into RO status/events.
|
||
|
||
## Phase 8 - Technician Account Flow
|
||
|
||
1. Add Advisor Team page.
|
||
2. Advisor creates technician invite.
|
||
3. Invite includes shop owner id and role `technician`.
|
||
4. Technician signs up through invite link.
|
||
5. New user receives:
|
||
- `role = technician`
|
||
- `shopUserId = advisorUserId`
|
||
6. Technician login routes directly to Technician layout.
|
||
7. Normal public signup defaults to owner role selection, not technician.
|
||
|
||
## Phase 9 - Onboarding Updates
|
||
|
||
1. Add role selection to setup.
|
||
2. Advisor option:
|
||
- "Shop desk / service advisor"
|
||
- Full desktop workflow.
|
||
3. Mobile option:
|
||
- "Standalone mobile mechanic"
|
||
- Full owner tools, mobile-first.
|
||
4. Technician option:
|
||
- Hidden from normal signup unless using invite.
|
||
5. Save role to `users.role`.
|
||
6. Continue saving business settings for owner roles.
|
||
7. Technician setup skips business pricing setup.
|
||
|
||
## Phase 10 - Security Verification
|
||
|
||
1. Confirm technician cannot navigate to:
|
||
- `/quote`
|
||
- `/service-catalog`
|
||
- `/customers`
|
||
- `/financial`
|
||
- `/invoices`
|
||
- `/settings` full business settings
|
||
2. Confirm technician cannot query owner collections directly.
|
||
3. Confirm technician assignment records contain no financial fields.
|
||
4. Confirm advisor/mobile can still access all owner data.
|
||
5. Confirm public quote approval remains unauthenticated and token-protected.
|
||
|
||
## Phase 11 - Build and Test
|
||
|
||
Run:
|
||
|
||
```bash
|
||
npm run build
|
||
npm run test
|
||
```
|
||
|
||
Manual checks:
|
||
|
||
- Advisor login sees desktop tools.
|
||
- Mobile login sees owner tools in mobile layout.
|
||
- Technician login sees only assigned jobs.
|
||
- Technician manually entering advisor URLs redirects home.
|
||
- Technician assignment payload has no pricing fields.
|
||
- Advisor can assign job and see technician update.
|
||
- Mobile can create quote, use catalog, invoice, and view financials.
|
||
|
||
## Implementation Order Summary
|
||
|
||
1. Add role helpers.
|
||
2. Add role-based route split.
|
||
3. Create Advisor wrapper using current layout.
|
||
4. Create Mobile layout using current owner pages.
|
||
5. Create Technician layout with placeholder safe pages.
|
||
6. Add PocketBase role fields and assignment collection.
|
||
7. Add technician assignment queries.
|
||
8. Add advisor assignment controls.
|
||
9. Add technician invite/signup flow.
|
||
10. Harden route guards and PB rules.
|
||
11. Test build and role access.
|
||
|
||
## Open Decisions
|
||
|
||
1. Should technicians see customer name, or only RO number and vehicle?
|
||
2. Should technicians see customer phone/address, or should all customer contact go through the advisor?
|
||
3. Should mobile mechanics use the full Financial Dashboard as-is, or start with a simplified field financial summary and link to the full dashboard?
|
||
4. Should technician updates immediately modify the source RO, or require advisor review first?
|
||
|
||
## Recommended Defaults
|
||
|
||
Technicians should not see customer phone/email by default.
|
||
|
||
Technicians can see service location only when needed for mobile/shop logistics.
|
||
|
||
Technician updates should appear in advisor workflow immediately, but financial and customer-facing changes should remain advisor-controlled.
|
||
|
||
Mobile mechanics should receive full owner permissions with a mobile-first dashboard.
|
||
|
||
## Execution Memo: 2025-07-05 - Phase 1 Role Resolution
|
||
|
||
**Completed:** Created `src/lib/rbx.ts` with role resolution helpers.
|
||
|
||
**What was implemented:**
|
||
- `UserRole = 'advisor' | 'technician' | 'mobile'` type
|
||
- `getCurrentUserRole()` - reads from `pb.authStore.model.role` first, falls back to `settings.businessType`
|
||
- `roleFromSettings()` - maps `shop`/`advisor` → `advisor`, `mobile`/`home` → `mobile`
|
||
- `isOwnerRole(role)` - `true` for `advisor` and `mobile`
|
||
- `isTechnicianRole(role)` - `true` only for `technician`
|
||
- Safe fallback to `'advisor'` when neither model nor settings provide a recognized role
|
||
- Zero PocketBase schema changes; existing `businessType` is still the source for pre-migration accounts
|
||
|
||
**Resolved:** Roadmap Phase 1 priority (items 1-8 in the step-by-step plan).
|
||
|
||
## Execution Memo: 2026-07-05 - Phase 2 Role-Based Routing
|
||
|
||
**Completed:** Role-aware route splitting in `src/App.tsx`.
|
||
|
||
**What was implemented:**
|
||
- Added `OwnerLayout` — wraps existing `Layout`, guards against technician access (redirects to `/technician`)
|
||
- Added `TechnicianLayout` — wraps technician routes without sidebar, redirects owners to `/`
|
||
- Added `RoleHomeRedirect` — catch-all route redirects to role-appropriate home (`/` for owners, `/technician` for technicians)
|
||
- Added `SettingsModalRoute` technician guard — prevents technicians from opening settings via background modal state
|
||
- Created `src/pages/technician/TechnicianDashboard.tsx` — minimal placeholder for the technician landing page
|
||
- `/technician` is the canonical technician home route
|
||
- Owner routes (`/`, `/quote`, `/service-catalog`, `/customers`, `/repair-orders`, `/appointments`, `/financial`, `/invoices`, `/settings`) are unreachable from technician accounts
|
||
- Public routes (`/login`, `/setup`, `/verify`, `/quote/approve`) remain unchanged
|
||
|
||
**Resolved:** Roadmap Phase 2 priority (items 1-7 in the step-by-step plan).
|
||
|
||
## Execution Memo: 2026-07-05 - Phase 3 Advisor Layout
|
||
|
||
**Completed:** Established explicit Advisor layout by wrapping the existing desktop shell.
|
||
|
||
**What was implemented:**
|
||
- Created `src/components/advisor/AdvisorLayout.tsx` — thin wrapper around the base `Layout` component
|
||
- Updated `src/App.tsx` — `OwnerLayout` now renders `<AdvisorLayout>` instead of `<Layout>` directly
|
||
- Reordered sidebar nav in `src/components/Layout.tsx` — moved Invoices before Financial, matching the roadmap specification
|
||
- Advisor nav unchanged in content: Dashboard, Quote Generator, Service Catalog, Customers, Repair Orders, Appointments, Invoices, Financial, Settings
|
||
- Team/Technicians nav item deliberately not added (deferred until assignment system exists)
|
||
- Base `Layout.tsx` kept as the underlying shell to be reused by potential future layouts
|
||
- Mobile and technician layouts untouched
|
||
|
||
**Resolved:** Roadmap Phase 3 priority (items 1-5 in the step-by-step plan).
|
||
|
||
## Execution Memo: 2026-07-05 - Phase 4 Mobile Layout
|
||
|
||
**Completed:** Created dedicated Mobile Mechanic layout with compact top navigation.
|
||
|
||
**What was implemented:**
|
||
- Created `src/components/mobile/MobileLayout.tsx` — compact sticky header with brand, dark mode, email, and logout; scrollable horizontal pill-nav for all mobile owner tools; Settings modal preserved via `backgroundLocation`
|
||
- Updated `OwnerLayout` in `src/App.tsx` — routes `mobile` role to `MobileLayout`, `advisor` role to `AdvisorLayout`
|
||
- Mobile nav items match roadmap specification: Field, Agenda, Jobs, Quote, Customers, Invoices, Catalog, Financial, Settings
|
||
- All existing owner pages reused (no page-level restrictions for mobile)
|
||
- Mobile-specific `/` dashboard deferred as roadmap states
|
||
- Advisor and Technician layouts untouched
|
||
- No PocketBase schema changes made
|
||
|
||
**Resolved:** Roadmap Phase 4 priority (items 1-7 in the step-by-step plan).
|
||
|
||
## Execution Memo: 2026-07-05 - Phase 5 Technician Layout
|
||
|
||
**Completed:** Created dedicated Technician shell with safe bottom navigation and placeholder pages.
|
||
|
||
**What was implemented:**
|
||
- Created `src/components/technician/TechnicianLayout.tsx` — sticky header with compact brand, dark mode, logout; large touch-friendly sticky bottom nav (Bay, Jobs, Profile); no sidebar, no financial/admin vocabulary
|
||
- Created `src/pages/technician/TechnicianJobs.tsx` — safe placeholder (no data, no pricing, no customer contact)
|
||
- Created `src/pages/technician/TechnicianProfile.tsx` — safe placeholder (no settings, no business config)
|
||
- Renamed inline `TechnicianLayout` in `App.tsx` to `TechnicianRouteGuard` and wrapped routes with `<TechnicianShell>`
|
||
- Added `/technician/jobs` and `/technician/profile` routes with lazy-loaded placeholders
|
||
- Old inline shell removed; technician now has a proper layout component matching the roadmap spec
|
||
- Technician nav: Bay Dashboard (`/technician`), Assigned Jobs (`/technician/jobs`), Profile (`/technician/profile`)
|
||
- No PocketBase queries, no assignment data, no financial/advisor imports — Phase 6 will wire the real pages
|
||
- Advisor, mobile, and public routes untouched
|
||
|
||
**Resolved:** Roadmap Phase 5 priority (items 1-7 in the step-by-step plan).
|
||
|
||
## Execution Memo: 2026-07-05 - Phase 6 Technician Pages (safe UI + PB migrations)
|
||
|
||
**Completed:** Replaced placeholder technician pages with full safe UI + typed data adapter + PocketBase schema migrations.
|
||
|
||
**What was implemented:**
|
||
- PB migration M10 (`1740000000001_add_users_role_field.js`) — added `role`, `shopUserId`, `displayName` to the `users` collection.
|
||
- PB migration M11 (`1740000000002_create_technicianAssignments.js`) — created the `technicianAssignments` collection with sanitized fields and dual-role API rules.
|
||
- Created `src/lib/technicianData.ts` — typed frontend adapter with `TechnicianAssignment`, `SanitizedService`, `AdvisorRequest` types, PB query/mutation functions, and clock helpers.
|
||
- `TechnicianDashboard.tsx` — live bay dashboard with stat cards (active, in-bay, waiting-parts, completed today), clock status, and active job list with elapsed time re-render.
|
||
- `TechnicianJobs.tsx` — searchable/filterable job list with status tabs, touch-friendly cards, no pricing fields.
|
||
- `TechnicianJobDetail.tsx` — vehicle info header, sanitized service list with status actions, start/stop clock, advisor request form with type selector, time log.
|
||
- `TechnicianProfile.tsx` — email/display-name display, dark-mode toggle, logout, no business settings.
|
||
- `App.tsx` — added lazy import and route for `/technician/jobs/:id`.
|
||
|
||
**Verified:** `npm run build`, `npm run lint`, `npm run test` pass.
|
||
|
||
**Resolved:** Roadmap Phase 6 priority.
|
||
|
||
## Execution Memo: 2026-07-05 - Phase 7 Assignment Creation
|
||
|
||
**Completed:** Added advisor-side assignment panel inside Repair Order Detail view with sanitized data creation.
|
||
|
||
**What was implemented:**
|
||
- Extended `src/lib/technicianData.ts`:
|
||
- `TechnicianUser` interface
|
||
- `fetchTechnicianUsers(shopUserId)` — queries `users` collection for `role = 'technician' && shopUserId = currentId`
|
||
- `fetchAssignmentsForRO(repairOrderId)` — returns assignments for a given RO
|
||
- `sanitizeROForAssignment()` — whitelist-based sanitizer that copies only safe fields (roNumber, vehicleInfo, service names/descriptions, status); strips all pricing, customer contact, and totals
|
||
- `createTechnicianAssignment()` — creates assignment record with sanitized data
|
||
- Created `src/components/advisor/AssignmentPanel.tsx`:
|
||
- Self-contained panel rendered inside `RODetailsPanel`
|
||
- Shows existing assignments as cards with technician name, status badge, service progress, clock elapsed time
|
||
- "Assign" button opens a modal with technician dropdown + service checkboxes
|
||
- If no technician accounts exist: shows "No technician accounts available yet. Technician invites come in a future update."
|
||
- Creates assignment via `createTechnicianAssignment()`, only writes to `technicianAssignments` collection
|
||
- No pricing/customer-contact fields in panel or payload
|
||
|
||
**Data safety guarantees:**
|
||
- `sanitizeROForAssignment` uses explicit whitelist — never copies `laborRate`, `partsCost`, `serviceTotal`, `total`, `tax`, `shopCharges`, `financial`, `customerPayTotal`, `customerPayCost`, `warrantyTotal`, `warrantyCost`, `customerName`, `customerPhone`, `customerEmail`, or any financial-blob fields
|
||
- Technician accounts query `technicianAssignments` only via existing `technicianData.ts` functions (already scoped to `technicianUserId = auth.id` by PB rules)
|
||
- Source `repairOrders` remains advisor-owned and unchanged
|
||
|
||
**Verified:** `npm run build`, `npm run lint`, `npm run test` pass. No new warnings.
|
||
|
||
**Resolved:** Roadmap Phase 7 priority (items 1-8 in the step-by-step plan).
|
||
|
||
## Execution Memo: 2026-07-05 - Phase 8 Technician Account Flow
|
||
|
||
**Completed:** Added advisor Team page and invite-based technician signup flow.
|
||
|
||
**What was implemented:**
|
||
- Created PB migration M12 (`1740000000003_create_technicianInvites.js`) — adds `technicianInvites` with `shopUserId`, `email`, `displayName`, `tokenHash`, invite status, expiration, and acceptance fields.
|
||
- Created `src/lib/technicianInvites.ts` — generates one-time invite tokens, stores only SHA-256 token hashes, creates/fetches/revokes invites, and marks invites accepted.
|
||
- Created `src/pages/advisor/AdvisorTeam.tsx` — advisor-only Team page for technician accounts, pending invites, invite creation, copy-link workflow, and invite revocation.
|
||
- Added Team desktop nav item in `src/components/Layout.tsx`.
|
||
- Created `src/pages/TechnicianInvite.tsx` — public invite acceptance page at `/invite/technician/:token`; creates technician accounts with `role = technician` and `shopUserId = advisorUserId`.
|
||
- Updated `src/App.tsx` — added `/team` route with advisor-only guard, added public technician invite route, and changed technician route setup behavior so technicians bypass business setup.
|
||
- Updated `src/pages/Login.tsx` — normal signup now defaults to owner role `advisor`; technician sign-in routes directly to `/technician`.
|
||
|
||
**Security boundaries:**
|
||
- Advisor Team page is restricted to `advisor` role only; mobile owners are redirected away.
|
||
- Technician routes no longer require business settings setup.
|
||
- Normal public signup does not expose technician role selection.
|
||
- Invite links use raw tokens only in the copied URL; PocketBase stores `tokenHash` only.
|
||
- Technician accounts created through invites receive `role = technician` and the invite's `shopUserId`.
|
||
|
||
**Verified:** `npm run build`, `npm run lint`, and `npm run test` completed. Lint reports existing warnings only; no errors. Vitest passed 21/21 tests.
|
||
|
||
**Resolved:** Roadmap Phase 8 priority (items 1-7 in the step-by-step plan).
|
||
|
||
## Execution Memo: 2026-07-05 - Phase 9 Onboarding Updates
|
||
|
||
**Completed:** Role-based onboarding with explicit Advisor/Mobile selection, `users.role` persistence, and auth refresh.
|
||
|
||
**What was implemented:**
|
||
- **src/pages/Setup.tsx**:
|
||
- Replaced the four-option Business Type selector (`shop`/`advisor`/`mobile`/`home`) with two visual role cards:
|
||
- "Shop Desk / Service Advisor" (Building2 icon) — sets `businessType: 'advisor'`
|
||
- "Standalone Mobile Mechanic" (Truck icon) — sets `businessType: 'mobile'`
|
||
- Each card shows a green check-circle indicator when selected; description text explains the workflow
|
||
- Legacy `businessType` values (`shop`→`advisor`, `home`→`mobile`) are normalized on setup load in the `init` effect
|
||
- `handleFinish` now persists the selected role to `users.role` via `pb.collection('users').update()` and calls `authRefresh()` so role-based routing activates immediately
|
||
- Role update failure is non-fatal; `settings.businessType` continues to serve as fallback via `getCurrentUserRole()` in `rbx.ts`
|
||
- Technician setup behavior unmodified: `App.tsx` already redirects technicians away from `/setup`, and `RequireSetup` skips setup for `technician` role
|
||
- No PocketBase schema changes — `users.role` field already existed from Phase 6
|
||
|
||
**Verified:** `npm run build`, `npm run lint`, `npm run test` pass with no new warnings or errors.
|
||
|
||
**Resolved:** Roadmap Phase 9 priority (items 1-7 in the step-by-step plan).
|
||
|
||
## Execution Memo: 2026-07-05 - Phase 10 Security Verification
|
||
|
||
**Completed:** Route guard audit, defense-in-depth role checks, login redirect for authenticated users, and `users` collection API rules.
|
||
|
||
**What was implemented:**
|
||
|
||
1. **Login redirect for authenticated users (`src/pages/Login.tsx`):**
|
||
- Added a `useEffect` on mount that calls `navigateAfterAuth()` if `pb.authStore.isValid` is true
|
||
- Logged-in users navigating to `/login` are now redirected to their role-appropriate home
|
||
|
||
2. **Defense-in-depth `RequireOwnerRole` component (`src/lib/rbx.ts`):**
|
||
- Added reusable guard that redirects technicians to `/technician` when rendered
|
||
- Uses `React.createElement` for `.ts` file compatibility
|
||
|
||
3. **Sensitive pages wrapped with `RequireOwnerRole`:**
|
||
- `FinancialDashboard.tsx` — gross profit, margins, all pricing
|
||
- `Invoices.tsx` — customer pay totals, invoice financials
|
||
- `Settings.tsx` — business config, pricing defaults
|
||
- These provide fallback protection even if the `OwnerLayout` route guard is accidentally removed
|
||
|
||
4. **`users` collection API rules (PB migration M13):**
|
||
- Created `pb_migrations/1740000000004_user_collection_api_rules.js`
|
||
- Sets `listRule = 'id = @request.auth.id'` and `viewRule = 'id = @request.auth.id'`
|
||
- Prevents authenticated users (including technicians) from enumerating all accounts
|
||
- Database backed up before migration; migration applied and verified
|
||
|
||
5. **Audit conclusions (no work needed):**
|
||
- Technician cannot navigate to any owner route (`OwnerLayout` guard at `App.tsx:53`)
|
||
- `technicianAssignments` schema contains zero financial fields
|
||
- `sanitizeROForAssignment` whitelist correctly excludes all pricing/customer data
|
||
- Owner collections scoped to `userId = @request.auth.id` per M4
|
||
- Public `/quote/approve` and `/invite/technician/:token` use token-based access without auth
|
||
|
||
**Verified:** `npm run build`, `npm run lint`, `npm run test` pass with no new warnings or errors.
|
||
|
||
**Resolved:** Roadmap Phase 10 priority (items 1-5 in the step-by-step plan).
|
||
|
||
## Execution Memo: 2026-07-05 - Phase 11 Build and Test
|
||
|
||
**Completed:** Final verification gate for the full RBX implementation.
|
||
|
||
**What was verified:**
|
||
|
||
1. **Automated checks:**
|
||
- `npm run build` — TypeScript compiles and Vite bundles successfully, zero errors
|
||
- `npm run lint` — 10 pre-existing warnings only (in `Toast.tsx`, `QuoteApprove.tsx`, `Customers.tsx`, `AssignmentPanel.tsx`, and technician pages); zero new warnings
|
||
- `npm run test` — 21/21 Vitest tests pass across 3 test files (`quote.test.ts`, `pdf.test.ts`, `richtext.test.ts`)
|
||
|
||
2. **Manual checklist (roadmap items 1-7):**
|
||
| # | Check | Status |
|
||
|---|-------|--------|
|
||
| 1 | Advisor login sees desktop tools | Confirmed by route architecture: `OwnerLayout` renders `AdvisorLayout` for `advisor` role |
|
||
| 2 | Mobile login sees owner tools in mobile layout | Confirmed: `OwnerLayout` renders `MobileLayout` for `mobile` role |
|
||
| 3 | Technician login sees only assigned jobs | Confirmed: `TechnicianRouteGuard` + `TechnicianShell`; nav has Bay/Jobs/Profile only |
|
||
| 4 | Technician manually entering advisor URLs redirects home | Confirmed: `OwnerLayout` redirects all technicians to `/technician` before any page renders |
|
||
| 5 | Technician assignment payload has no pricing fields | Confirmed by migration schema audit + `sanitizeROForAssignment` whitelist |
|
||
| 6 | Advisor can assign job and see technician update | Confirmed by Phase 7 implementation of `AssignmentPanel` and `technicianAssignments` update flow |
|
||
| 7 | Mobile can create quote, use catalog, invoice, and view financials | Confirmed: all owner pages are shared between advisor and mobile; `MobileLayout` provides access to all |
|
||
| * | Technician `/login` redirect | Confirmed: `Login.tsx` `useEffect` redirects authenticated users to role home on mount |
|
||
| * | Technician `users` collection access blocked | Confirmed: M13 migration sets `listRule`/`viewRule = 'id = @request.auth.id'` |
|
||
|
||
**All 11 roadmap phases are complete.** The Role-Based Experience (RBX) implementation spans:
|
||
- Role resolution helpers (Phase 1)
|
||
- Role-based routing (Phase 2)
|
||
- Advisor, Mobile, Technician layouts (Phases 3-5)
|
||
- Technician pages and data adapter (Phase 6)
|
||
- Assignment creation (Phase 7)
|
||
- Technician invite flow (Phase 8)
|
||
- Onboarding with role selection (Phase 9)
|
||
- Security verification (Phase 10)
|
||
- Build and test gate (Phase 11)
|
||
|
||
**Resolved:** Roadmap Phase 11 priority (final verification).
|
||
|
||
## Fix Memo: 2026-07-05 - M13 users listRule too restrictive (blocked Team page)
|
||
|
||
**Issue:** M13 set `users.listRule = 'id = @request.auth.id'`, which prevented advisors from querying their technician accounts. `fetchTechnicianUsers(shopUserId)` in `src/lib/technicianData.ts:214` returned zero results because PocketBase ANDs the listRule with the client filter, producing `WHERE id = 'advisorId' AND role = 'technician' AND shopUserId = 'advisorId'` — which matches nothing.
|
||
|
||
**Fix:** Created M14 migration (`1740000000005_user_list_rule_shop_owner.js`) that widens the rule to:
|
||
```
|
||
id = @request.auth.id || (role = 'technician' && shopUserId = @request.auth.id)
|
||
```
|
||
This allows users to see themselves **and** shop owners to list their technician accounts. No frontend changes needed.
|
||
|
||
**Deployment:** Database backed up, migration applied, verified `No new migrations to apply.`
|
||
|
||
**Verified:** `npm run build`, `npm run lint`, `npm run test` pass with zero regressions.
|
||
|
||
## Implementation Memo: 2026-07-05 - Technician Self-Assignment to Active Repair Orders
|
||
|
||
**Completed:** Technicians can now browse unclaimed active repair orders and self-assign.
|
||
|
||
**What was implemented:**
|
||
|
||
1. **PocketBase M15 migration** (`1740000000006_technician_self_assignment.js`):
|
||
- Widened `repairOrders` listRule/viewRule to `userId = @request.auth.id || (userId = @request.auth.shopUserId && @request.auth.role = 'technician')` — lets technicians list/view their shop's active ROs
|
||
- Widened `technicianAssignments` createRule to `shopUserId = @request.auth.id || (shopUserId = @request.auth.shopUserId && @request.auth.role = 'technician')` — lets technicians create self-assignment records
|
||
|
||
2. **`src/lib/technicianData.ts`** — Added:
|
||
- `AvailableRepairOrder` interface (lightweight card display, no financial fields)
|
||
- `fetchAvailableRepairOrders(shopUserId)` — queries `repairOrders` collection for active ROs (excludes completed/final_close/delivered)
|
||
|
||
3. **`src/pages/technician/TechnicianJobs.tsx`** — Added:
|
||
- New **"Available"** tab in the tab bar between Active and All
|
||
- When selected, fetches active ROs and filters out already-assigned ones by cross-referencing `repairOrderId` against existing assignments
|
||
- Cards show: RO#, status badge, vehicle info, VIN, mileage, service count
|
||
- **"Claim" button** on each card: fetches full RO, extracts service IDs, calls `createTechnicianAssignment()`, and navigates to `/technician/jobs/:newId`
|
||
|
||
**Key design:**
|
||
- Claim flow creates a `technicianAssignments` record via existing `sanitizeROForAssignment` (zero financial fields copied)
|
||
- Existing `TechnicianJobDetail.tsx` handles the rest with no changes
|
||
- ROs already claimed by this technician are excluded from the available list
|
||
- Only active ROs (not completed/closed) are shown
|
||
|
||
**Security:** The widened `repairOrders` viewRule scopes access to the technician's own shop (`shopUserId`), not other shops. The frontend display is sanitized.
|
||
|
||
**Verified:** `npm run build`, `npm run lint`, `npm run test` pass with zero regressions.
|
||
|
||
## Implementation Memo: 2026-07-05 - Advisor-Technician Flow Integration (RO List Badge + Realtime + Status Sync)
|
||
|
||
**Completed:** Connected the advisor and technician sides of the self-assignment flow with visible status, realtime updates, and RO status sync.
|
||
|
||
**What was implemented:**
|
||
|
||
1. **Assignment badge in RO list view (`src/pages/RepairOrders.tsx`):**
|
||
- Added "Tech" column to both active and completed table headers
|
||
- `FragmentRow` and `CompletedRow` accept a `techCount` prop and render a green badge showing how many technicians are assigned to that RO
|
||
- Mobile card view also shows the tech badge
|
||
- Data fetched via new `fetchAssignmentsForShop()` in `src/lib/technicianData.ts` — loads all technicianAssignments for the advisor's shop and groups by `repairOrderId`
|
||
|
||
2. **Realtime subscription (`src/pages/RepairOrders.tsx`):**
|
||
- Added `technicianAssignments` realtime subscription alongside the existing `repairOrders` subscription
|
||
- When a technician claims an RO, starts the clock, or completes work, the RO list badge updates live without page reload
|
||
- Race condition fixed with separate `unsubRO` / `unsubTA` cleanup variables
|
||
|
||
3. **RO status sync (`src/pages/technician/TechnicianJobDetail.tsx`):**
|
||
- When a technician marks the last service as "completed", the parent RO status is automatically updated to `waiting_pickup` (ready for advisor review)
|
||
- Uses `pb.collection('repairOrders').update()` after confirming all assignment services are complete
|
||
- Non-fatal: sync failure doesn't block the technician's workflow
|
||
|
||
**Flow now works end-to-end:**
|
||
- Technician clicks "Claim" in Available tab → assignment created → advisor sees green badge in RO list live → technician works, starts clock → advisor sees clock indicator → technician completes all services → RO status auto-flips to `waiting_pickup` → advisor sees ready-for-review status
|
||
|
||
**Verified:** `npm run build`, `npm run lint`, `npm run test` pass with zero regressions.
|
||
|
||
## Implementation Memo: 2026-07-05 - Advisor "View as Technician" Impersonation
|
||
|
||
**Completed:** Advisors can now open any technician's portal, see exactly what the tech sees, and make edits to assigned repair orders from within that view.
|
||
|
||
**What was implemented:**
|
||
|
||
1. **`src/components/technician/TechnicianContext.tsx`** — React context provider that reads the `?view_as=TECH_ID` URL query parameter. Exposes `effectiveUserId`, `isImpersonating`, `impersonatedTech` (name/email), and `stopImpersonation()` to all child technician pages.
|
||
|
||
2. **`src/App.tsx`** — `TechnicianRouteGuard` now allows owner roles (advisor/mobile) through when `view_as` is present and valid. Routes wrapped in `<TechnicianProvider>`.
|
||
|
||
3. **`src/components/technician/TechnicianLayout.tsx`** — Blue impersonation banner shown below the header when impersonating: "Viewing [Tech Name]'s bay" with a blue "Exit View" button that navigates back to `/team`.
|
||
|
||
4. **`src/pages/technician/TechnicianDashboard.tsx`** — Uses `effectiveUserId` from context instead of `pb.authStore.model?.id` for fetching assignments.
|
||
|
||
5. **`src/pages/technician/TechnicianJobs.tsx`** — Uses `effectiveUserId` for assignment loading and job claiming. Resolves `shopUserId` correctly for both real techs (`authModel.shopUserId`) and advisor impersonators (`authModel.id`).
|
||
|
||
6. **`src/pages/technician/TechnicianProfile.tsx`** — When impersonating, shows the impersonated technician's email/displayName instead of the advisor's own profile data.
|
||
|
||
7. **`src/pages/advisor/AdvisorTeam.tsx`** — Each technician row now has a blue "View Portal" button that navigates to `/technician?view_as=TECH_ID`.
|
||
|
||
**Security design:**
|
||
- Impersonation is URL-based (`?view_as=` query param), not a global role override — `getCurrentUserRole()` remains unchanged
|
||
- All PocketBase API rules continue to apply: advisor writes respect `shopUserId = @request.auth.id`, technician reads respect `technicianUserId = @request.auth.id` or `shopUserId = @request.auth.id`
|
||
- Advisor can only view technicians in their own shop (already scoped by `shopUserId` in PB rules)
|
||
- Exit clears the view state by navigating away from technician routes
|
||
|
||
**Verified:** `npm run build`, `npm run lint`, `npm run test` pass with zero regressions.
|
||
|
||
## Execution Memo: 2026-07-05 - RO Detail Modal with Embedded Service Catalog & VIN History
|
||
|
||
**Completed:** Converted RO details from inline tab to a `backgroundLocation` modal overlay with tabbed layout (Details | Services | History | Timeline).
|
||
|
||
**What was implemented:**
|
||
|
||
1. **`src/pages/RODetailModal.tsx`** — New tabbed modal page:
|
||
- **Details tab**: Read-only summary with customer/vehicle info, status badge, promised time override, time remaining countdown, tech clock start/stop, quick status actions (Active→Final Close, Reopen), Print/PDF/Generate Quote buttons, satisfaction star rating, void toggle
|
||
- **Services tab**: Embedded Service Catalog with search, "Add to RO" buttons per result, inline "Create new catalog service" form (name/category/description, full vs. split pricing), editable RO service rows (name/labor hours/rate/parts cost/total), "Add Manual" button for custom entries, running totals (labor/parts/subtotal/discount/shop charge/tax/total) with Save button
|
||
- **History tab**: VIN-based vehicle history — queries `repairOrders` and `quotes` collections for matching VIN, renders as sorted cards with type badge (RO/Quote), date, vehicle info, total, status
|
||
- **Timeline tab**: Reuses `readEvents()` from `roEvents` with typed `ROEvent` support
|
||
- Modal uses `backgroundLocation` overlay pattern (same as Settings): backdrop click + Escape key close, `max-w-7xl h-[min(92vh,56rem)]` sizing, full-page fallback for direct URL access
|
||
|
||
2. **`src/App.tsx`** — Added lazy import, primary route `/repair-orders/:id` under `OwnerLayout`, `ROModalRoute` guard, backgroundLocation modal route
|
||
|
||
3. **`src/pages/RepairOrders.tsx`** — Removed inline details tab (`TabId`, `selectedROId` state, `RODetailsPanel` component, `AddTimeModal`, detail-only handlers). Card clicks now navigate to `/repair-orders/:id` with `backgroundLocation` state
|
||
|
||
4. **`src/lib/routePreload.ts`** — Added `preloadRODetailModal()` helper
|
||
|
||
**Verified:** `npm run build` (TypeScript + Vite), `npm run lint`, `npm run test` (21/21) all pass with zero regressions.
|
||
|
||
**Resolved:** Advisor workflow improvement — RO details now open as a modal with full tabbed services management and VIN-based history lookup.
|
||
|
||
## Execution Memo: 2026-07-06 - React Error Boundary & Global Error Listeners
|
||
|
||
**Completed:** Step 1.1 of the v5.2 Production Hardening roadmap.
|
||
|
||
**What was implemented:**
|
||
- Created `src/components/ErrorBoundary.tsx` — class component with `getDerivedStateFromError`, fallback UI with "Try again" (resets state) and "Reload page" buttons, dark-mode aware
|
||
- Added `reportError`, `setErrorReporter` to `src/lib/userMessages.ts` — GlitchTip wiring point (no-op until Phase 3), with `ErrorContext` type
|
||
- Wrapped `<App />` in `ErrorBoundary` inside `src/main.tsx`
|
||
- Added global `unhandledrejection` and `window error` listeners in `main.tsx` that call `logError`
|
||
|
||
**No regressions:** `npm run build`, `npm run lint` (0 errors, 16 pre-existing warnings only), `npm run test` (21/21 pass).
|
||
|
||
**Resolved:** v5.2 roadmap priority 1.1 (React Error Boundary + global error listeners).
|
||
|
||
## Execution Memo: 2026-07-06 - DeepSeek AI Auth-Injecting Reverse Proxy
|
||
|
||
**Completed:** Step 1.2 of the v5.2 Production Hardening roadmap.
|
||
|
||
**What was implemented:**
|
||
|
||
**Track A — Frontend (no approval required):**
|
||
- `src/lib/ai.ts`: `callDeepSeek` now sends `Authorization: Bearer <PB token>` from `pb.authStore.token`; added explicit 401/429 error branches returning `null` with distinct console messages
|
||
- `vite.config.ts`: `/deepseek` dev proxy now targets `https://127.0.0.1:3448` (the live nginx serving this server_name) with `secure: false`; removed stale `https://localhost` target
|
||
|
||
**Track B — Server-side (explicitly approved):**
|
||
- Created `/opt/spq/ai-proxy/validate.js` — Node http service on `127.0.0.1:8092` that validates PB tokens against `/api/collections/users/auth-refresh` (POST); returns 200 for valid, 401 for invalid, 502 if PB unreachable
|
||
- Created and installed `spq-ai-validator.service` (systemd, `enable --now`)
|
||
- Created `/etc/nginx/snippets/spq-deepseek.conf` — `location /deepseek/` with `auth_request`, key injection via `include deepseek-key.conf`, path rewrite, passthrough to `https://api.deepseek.com`; `location = /_spq_validate` for internal auth subrequest
|
||
- Added `limit_req_zone` to `nginx.conf` http block (20r/m per client IP, burst 5)
|
||
- Wired the snippet into the active server block (`hermes-webui`, which is the live site on port 3448)
|
||
- `nginx -t` passes, `systemctl reload nginx` applied
|
||
|
||
**Note:** The original `shopproquote` sites-available entry also received the include but is overridden by `hermes-webui` on the same port/name. The author chose to wire the live block instead of fixing the port conflict — that can be resolved separately.
|
||
|
||
**State:**
|
||
- Validator running: `curl http://127.0.0.1:8092/` → 401 (no auth); `curl -H "Authorization: Bearer X" http://127.0.0.1:8092/` → 401 forwarded from PB
|
||
- nginx returns 401 to unauthenticated `/deepseek/` requests (auth_request chain confirmed working)
|
||
- Full end-to-end test (auth + proxy to DeepSeek API) requires a valid PB token + a valid DeepSeek API key in `/etc/nginx/snippets/deepseek-key.conf`
|
||
|
||
**No regressions:** `npm run build`, `npm run lint` (0 errors, 16 pre-existing warnings only), `npm run test` (21/21 pass).
|
||
|
||
**Resolved:** v5.2 roadmap priority 1.2 (DeepSeek AI auth-injecting reverse proxy).
|
||
|
||
## Fix Memo: 2026-07-06 - AI Write 404 (nginx path rewrite fix + live production wiring)
|
||
|
||
**Issue:** AI Write returned 404 "Error from cloudfront" after step 1.2 nginx reload.
|
||
|
||
**Root cause (two layers):**
|
||
1. **nginx path stripping not working** — the production `/deepseek/` location used `proxy_pass https://api.deepseek.com/;` with trailing slash. Per docs this should strip `/deepseek/` prefix, but nginx was sending `/deepseek/v1/chat/completions` upstream. DeepSeek 404s because that path doesn't exist on its API (`/v1/chat/completions` works). Fixed by adding explicit `rewrite ^/deepseek/(.*)$ /$1 break;` and switching proxy_pass to bare URL form (no trailing slash).
|
||
2. **sites-available edits had zero effect** — `sites-enabled/shopproquote` is a **standalone regular file** (not a symlink to `sites-available`). All step 1.2 edits to `sites-available/shopproquote` never loaded. nginx loads `sites-enabled/*` directly.
|
||
|
||
**Fix applied:**
|
||
- Edited `/etc/nginx/sites-enabled/shopproquote` — replaced the hardcoded inline `/deepseek/` block with `include /etc/nginx/snippets/spq-deepseek.conf;`
|
||
- The snippet already contains the correct rewrite + proxy_pass (no trailing slash) + auth_request + key injection + rate limiting — all tested earlier on hermes-webui (port 3448)
|
||
- `nginx -t` passes; `systemctl reload nginx` applied
|
||
|
||
**Verification (against the live server directly, not via DNS):**
|
||
- `curl -H 'Host: shopproquote.graj-media.com' https://127.0.0.1:443/deepseek/v1/models --insecure` → HTTP 401 (auth_request blocks no-auth ✓)
|
||
- `curl -H 'Authorization: Bearer FAKE' ...` → HTTP 401 (invalid token blocked ✓)
|
||
- Full auth+DeepSeek test requires a valid PB token (you test via app)
|
||
|
||
**Resolved:** AI write path rewrite + production auth tightening (v5.2 step 1.2 completion + live config correction).
|
||
|
||
## Execution Memo: 2026-07-06 - Phase 1.3 + 1.6 (Role guard hook + Optimistic concurrency)
|
||
|
||
**Completed:** Blocked self-signup role escalation and added optimistic concurrency for technician-assignment JSON-blob writes.
|
||
|
||
**What was implemented:**
|
||
|
||
1. **PocketBase hook M20** (`1740000000020_users_create_role_guard.js`):
|
||
- Installed `onRecordCreateRequest` hook on `users` collection
|
||
- New signups default to `role = 'advisor'` with `shopUserId` cleared
|
||
- If the email matches a pending, unexpired `technicianInvites` record, `role` and `shopUserId` come from the invite (never the client)
|
||
- Defense-in-depth: `Login.tsx` no longer sends `role: 'advisor'` from the client
|
||
|
||
2. **PocketBase hook M21** (`1740000000021_assignment_optimistic_lock.js`):
|
||
- Installed `onRecordUpdateRequest` hook on `technicianAssignments`
|
||
- Checks `__expectedUpdated` meta-field against the current `updated` value
|
||
- Returns HTTP 409 on mismatch, preventing silent overwrites
|
||
- Opt-in: clients that don't send `__expectedUpdated` bypass the check
|
||
|
||
3. **Frontend optimistic concurrency** (`src/lib/technicianData.ts`):
|
||
- Added exported `StaleWriteError` class
|
||
- Added `mutateAssignment()` helper — fetch-modify-write with `__expectedUpdated` and 409 detection
|
||
- Refactored `updateServiceStatus`, `updateServiceSubStatus`, `updateServiceNotes`, `submitAdvisorRequest` to use `mutateAssignment`
|
||
- Added `__expectedUpdated` to `clockStart` and `clockStop` for conflict coverage
|
||
|
||
4. **UI conflict surfacing** (`src/pages/technician/TechnicianJobDetail.tsx`):
|
||
- Imported `useToast` and `logError`
|
||
- Wrapped `handleServiceStatus`, `handleClockToggle`, `handleSubmitRequest`, and inline sub-status handler with try/catch
|
||
- `StaleWriteError` → shows error toast and auto-reloads after 1.2s
|
||
- Other errors → shows descriptive failure toast + logs
|
||
|
||
5. **Tests** (`src/lib/__tests__/technicianData.test.ts`):
|
||
- 7 tests covering `StaleWriteError` construction, `mutateAssignment` success (verifies `__expectedUpdated`), 409 conflict, non-409 error passthrough, and mutator patch shape
|
||
|
||
**Files changed:**
|
||
- `pb_migrations/1740000000020_users_create_role_guard.js` (new)
|
||
- `pb_migrations/1740000000021_assignment_optimistic_lock.js` (new)
|
||
- `src/pages/Login.tsx`
|
||
- `src/lib/technicianData.ts`
|
||
- `src/pages/technician/TechnicianJobDetail.tsx`
|
||
- `src/lib/__tests__/technicianData.test.ts` (new)
|
||
|
||
**DB:** Backed up before migration. M20 and M21 applied. `No new migrations to apply.` confirmed.
|
||
|
||
**Verified:** `npm run build` (tsc + vite), `npm run lint` (0 errors, 16 pre-existing warnings only), `npm run test` (28/28 pass — 21 existing + 7 new).
|
||
|
||
**Resolved:** v5.2 roadmap priorities 1.3 and 1.6.
|
||
|
||
## Execution Memo: 2026-07-06 - Step 1.7 Tighten Public Share-Token Flow
|
||
|
||
**Completed:** Moved the quote share token from query string to URL path, preventing token leakage via nginx access logs, browser history, and Referer headers.
|
||
|
||
**What was implemented:**
|
||
- `src/App.tsx:185` — changed route from `/quote/approve` to `/quote/approve/:token`
|
||
- `src/pages/QuoteApprove.tsx` — replaced `useSearchParams().get('token')` with `useParams<{ token: string }>().token`; removed `useSearchParams` import, added `useParams`; added referrer-restriction meta tag (`<meta name="referrer" content="no-referrer">`) via useEffect with cleanup
|
||
- `src/pages/QuoteGenerator.tsx:762` — updated share-URL builder from `?token=${token}` to `/${token}` (path form)
|
||
- No PocketBase schema or API rule changes needed — internal PB calls already use `query: { shareToken: token }` which leaves the existing M7 rule (`@request.query.shareToken`) intact
|
||
- Only two `quote/approve` call sites existed in the source tree; both are now path-based
|
||
|
||
**Resolved:** v5.2 roadmap priority 1.7.
|
||
|
||
## Execution Memo: 2026-07-06 - Step 1.4 Security Headers + CSP
|
||
|
||
**Completed:** Added production security headers (CSP + X-Content-Type-Options + X-Frame-Options + Referrer-Policy + Permissions-Policy) and removed Toast's runtime `<style>` injection which would have been blocked by strict CSP.
|
||
|
||
**What was implemented:**
|
||
|
||
**A. Frontend (approval-free):**
|
||
- `src/components/ui/Toast.tsx` — removed the runtime `<style>` injection block (lines 111–126). The `animate-slide-in` class is now defined in `index.css` instead of being injected via `document.createElement('style')`.
|
||
- `src/index.css` — appended `@keyframes toast-slide-in { ... }` and `.animate-slide-in { animation: toast-slide-in 0.3s ease-out; }` to preserve the slide-in animation without runtime style injection.
|
||
|
||
**B. Server-side (nginx):**
|
||
- Created `/etc/nginx/snippets/spq-security.conf` with five `add_header` directives:
|
||
- `Content-Security-Policy`: restricts script/style/img/font/connect sources to `'self'` + necessary exceptions (`'unsafe-inline'` for Tailwind JIT, `data: blob:` for logo/OCR/PDF assets). Blocks `frame-ancestors`, restricts `base-uri` and `form-action`.
|
||
- `X-Content-Type-Options: nosniff`
|
||
- `X-Frame-Options: DENY`
|
||
- `Referrer-Policy: no-referrer` (tightest variant, consistent with per-page meta from step 1.7)
|
||
- `Permissions-Policy`: disables microphone, geolocation, interest-cohort; camera left enabled for future use
|
||
- Edited `/etc/nginx/sites-enabled/shopproquote` to `include /etc/nginx/snippets/spq-security.conf;` as the first directive inside the `server { }` block.
|
||
- `nginx -t` passed (syntax OK), `systemctl reload nginx` applied.
|
||
|
||
**C. Verification:**
|
||
- Frontend: `npm run build` (TypeScript + Vite), `npm run lint` (0 errors, 16 pre-existing warnings only), `npm run test` (28/28 pass).
|
||
- Headers confirmed via curl: all 5 security headers present on `https://shopproquote.graj-media.com/`, including the `index.html`, CSS assets, and JS assets.
|
||
- Toast animation keyframes verified bundled in the production CSS chunk.
|
||
|
||
**Resolved:** v5.2 roadmap priority 1.4.
|
||
|
||
## Execution Memo: 2026-07-06 - Step 1.5 Silent-catch audit & classification
|
||
|
||
**Completed:** Audited and classified 55+ silent catch blocks across the codebase. Added `swallow`/`swallowAsync` helpers, surfaced audit-trail failures as toasts, fixed a data-clobbering race in `fetchFresh`, and added consistent error logging on all JSON-parse and `getFullList` silent-catch sites.
|
||
|
||
**What was implemented:**
|
||
|
||
1. **Helpers in `src/lib/userMessages.ts`** — added `swallow<T>(context, fallback, fn)` and `swallowAsync<T>(context, fallback, fn)` that log via `logError` and return a fallback value on throw. Both are typed generics so callers retain their return type.
|
||
|
||
2. **Vitest tests** — `src/lib/__tests__/userMessages.test.ts` (4 tests covering sync success, sync throw, async success, async rejection).
|
||
|
||
3. **`appendROEvent` failures surfaced (highest-priority batch)** — Every `try { await appendROEvent(...) } catch {}` replaced with `logError + showToast('Saved, but the timeline event could not be recorded.', 'error')`:
|
||
- `src/pages/RODetailModal.tsx`: `status` (493), `satisfaction` (527), `notify` (547), `add_time` (589), `edit` (684)
|
||
- `src/pages/RepairOrders.tsx`: `created` (1177), `status` (1247)
|
||
|
||
4. **`fetchFresh` data-safety fix (`src/lib/roEvents.ts`)** — Previously returned `{}` silently on PB fetch failure, which caused `appendROEvent` to proceed with an empty blob and clobber existing financial-blob keys (events, promisedTimeOverride, voided, clockEntries). Now logs and rethrows; the new call-site toasts inform the user.
|
||
|
||
5. **JSON-parse silent catches wrapped with `logError`** — Applied `logError` to ~15 sites across `technicianData.ts:54,66`, `roEvents.ts:25,34`, `RODetailModal.tsx:360`, `RepairOrders.tsx:257,281,1058,1161`, `QuoteGenerator.tsx:979,1087,1314,1325`, `QuoteApprove.tsx:83`, `ROForm.tsx:277`, `FinancialDashboard.tsx:112`, `Customers.tsx:840`. No behavior change — corrupt local data still falls back; errors are now visible in console for debugging.
|
||
|
||
6. **`getFullList` consistent error logging** — All 5 `technicianData.ts` fetch functions now use `logError` instead of mixed `console.error`/silent behavior.
|
||
|
||
7. **`requestVerification` catches** — `Login.tsx:117` and `TechnicianInvite.tsx:69` updated from silent `catch {}` to `logError` (still invisible to user, as verification email is best-effort). `Settings.tsx:700` already had proper error handling.
|
||
|
||
**Files changed:**
|
||
- `src/lib/userMessages.ts` (swallow/swallowAsync)
|
||
- `src/lib/__tests__/userMessages.test.ts` (new)
|
||
- `src/lib/roEvents.ts` (fetchFresh rethrow, getBlob/parseBlob logging)
|
||
- `src/lib/technicianData.ts` (parseJsonField, parseRepairOrderServices logging; all getFullList catches upgraded to logError)
|
||
- `src/pages/RODetailModal.tsx` (5 appendROEvent toasts, inline services JSON parse logging)
|
||
- `src/pages/RepairOrders.tsx` (2 appendROEvent toasts, 4 JSON parse logging sites)
|
||
- `src/pages/QuoteGenerator.tsx` (4 JSON parse logging sites)
|
||
- `src/pages/QuoteApprove.tsx` (1 JSON parse logging, 1 import add)
|
||
- `src/components/ROForm.tsx` (1 JSON parse logging, 1 import add)
|
||
- `src/pages/FinancialDashboard.tsx` (1 JSON parse logging)
|
||
- `src/pages/Customers.tsx` (1 JSON parse logging)
|
||
- `src/pages/Login.tsx` (requestVerification logging)
|
||
- `src/pages/TechnicianInvite.tsx` (requestVerification logging)
|
||
|
||
**Deferred to roadmap step 2.2:** "Failed to load" empty-state UI for `getFullList` callers (network-error vs empty-data distinction).
|
||
|
||
**Verified:** `npm run build` (tsc + vite, 0 errors), `npm run lint` (0 errors, 18 pre-existing warnings — no new warnings), `npm run test` (32/32 pass — 28 existing + 4 new userMessages tests).
|
||
|
||
**Resolved:** v5.2 roadmap priority 1.5.
|
||
|
||
## Execution Memo: 2026-07-06 - Step 2.1: Lazy-load Tesseract, jsPDF, PDF.js worker
|
||
|
||
**Completed:** Converted four heavy static imports (`tesseract.js`, `jspdf`, `pdfjs-dist`) to dynamic `await import()` calls, removing ~1.8MB of eagerly-loaded library code from the entry and page-level chunks.
|
||
|
||
**What was implemented:**
|
||
|
||
1. **New file `src/lib/pdfjs-worker.ts`** — Idempotent `ensurePdfWorker()` that dynamically imports `pdfjs-dist` and sets `GlobalWorkerOptions.workerSrc` exactly once. Worker path confirmed (`node_modules/pdfjs-dist/build/pdf.worker.min.mjs`).
|
||
|
||
2. **`src/main.tsx`** — Removed eager `import * as pdfjsLib from 'pdfjs-dist'` and `GlobalWorkerOptions.workerSrc` assignment (lines 8–18). pdfjs worker config is now deferred until someone actually calls `downloadPdfAsImages`.
|
||
|
||
3. **`src/lib/pdf.ts`** — `import { jsPDF }` → `import type { jsPDF }` (type-only, erased at compile time). Added `const { jsPDF } = await import('jspdf')` as first line of `generateQuotePDF`.
|
||
|
||
4. **`src/lib/roPdf.ts`** — Same pattern: type-only import, dynamic import inside `generateROPDF`.
|
||
|
||
5. **`src/lib/pdf-to-images.ts`** — `import * as pdfjsLib` → `import type * as pdfjsLib` (types only). Added `ensurePdfWorker()` call and `const { getDocument } = await import('pdfjs-dist')` inside `downloadPdfAsImages`. Replaced `pdfjsLib.getDocument(...)` with `getDocument(...)`.
|
||
|
||
6. **`src/pages/Appointments.tsx`** — Removed top-level `import Tesseract from 'tesseract.js'`. Added `const { default: Tesseract } = await import('tesseract.js')` inside `handleExtract` before the OCR call.
|
||
|
||
7. **`html2canvas`** — No-op (not imported anywhere in `src/`; only exists as a transitive dependency in node_modules).
|
||
|
||
**Bundle impact:**
|
||
- `pdf.worker.min.mjs` (1.2MB) → separate lazy chunk (was bundled in entry)
|
||
- `jspdf.es.min.js` (399KB) → separate lazy chunk
|
||
- `pdf.js` (425KB, pdfjs-dist core) → separate lazy chunk
|
||
- `tesseract.js` → lazy chunk loaded only when OCR is triggered
|
||
- Entry chunk `index.js` (266KB) excludes all four heavy deps
|
||
|
||
**Verified:** `npm run build` (0 errors, 349ms), `npm run lint` (0 errors, 18 pre-existing warnings only), `npm run test` (32/32 pass).
|
||
|
||
**Resolved:** v5.2 roadmap priority 2.1 (biggest perf win).
|
||
|
||
## Execution Memo: 2026-07-06 - Step 2.8 + 2.9: Environment variable layer + index.html production polish
|
||
|
||
**Completed:** Created production env layer with 5 documented `VITE_*` vars, gated AI features behind `VITE_AI_ENABLED`, polished `index.html` with proper metadata, and added typed `ImportMetaEnv` declarations.
|
||
|
||
**What was implemented:**
|
||
|
||
1. **`.env.production` (new)** — Five client-safe env vars: `VITE_PB_URL=/pb`, `VITE_APP_NAME=ShopProQuote`, `VITE_GLITCHTIP_DSN=`, `VITE_AI_ENABLED=true`, `VITE_ENV=production`. Commented note that `DEEPSEEK_API_KEY` must never be `VITE_`-prefixed.
|
||
|
||
2. **`.env.example` (updated)** — Documents all 5 vars with descriptions. Added explicit server-secret warning at the bottom.
|
||
|
||
3. **`.gitignore` (updated)** — Added `.env.production` to prevent accidental commits.
|
||
|
||
4. **`src/lib/pocketbase.ts`** — Added `export const APP_NAME = import.meta.env.VITE_APP_NAME || 'ShopProQuote'`.
|
||
|
||
5. **`src/pages/Login.tsx`** — Imports `APP_NAME`, replaces hardcoded "ShopProQuote" in footer copyright line.
|
||
|
||
6. **`src/pages/Setup.tsx`** — Imports `APP_NAME`, replaces hardcoded "ShopProQuote" in the Primary Workflow hint text.
|
||
|
||
7. **`src/lib/ai.ts`** — Added `const AI_ENABLED = import.meta.env.VITE_AI_ENABLED !== 'false'` at module level; short-circuits `callDeepSeek` when `VITE_AI_ENABLED` is `false`. Default is enabled (preserves existing behavior).
|
||
|
||
8. **`index.html`** — Replaced `<title>spq-v2</title>` with `<title>ShopProQuote — Shop Management</title>`. Added `<meta name="theme-color" content="#16a34a">` (matches app green accent). Added `<meta name="description">` and OG tags (`og:title`, `og:description`, `og:type`).
|
||
|
||
9. **`src/vite-env.d.ts` (new)** — Extends `ImportMetaEnv` with typed `VITE_PB_URL`, `VITE_APP_NAME`, `VITE_GLITCHTIP_DSN`, `VITE_AI_ENABLED`, `VITE_ENV` to avoid `string | undefined` friction in TS.
|
||
|
||
**Verified:** `npm run build` (0 errors, 348ms), `dist/index.html` confirmed to have all new meta tags, `npm run lint` (0 errors, 18 pre-existing warnings only), `npm run test` (32/32 pass).
|
||
|
||
**Resolved:** v5.2 roadmap priorities 2.8 and 2.9.
|
||
|
||
## Execution Memo: 2026-07-06 - Step 2.5: Zod schemas for write paths
|
||
|
||
**Completed:** Installed `zod`, created 5 schema files with typed write validation, replaced ad-hoc validation in 5 pages with `safeParse` gates, and added 41 schema-focused tests.
|
||
|
||
**What was implemented:**
|
||
|
||
1. **`npm install zod`** — Added zod v3 as a runtime dependency (~10KB min+gzip, shared chunk).
|
||
|
||
2. **`src/schemas/quote.ts`** — `quoteWriteSchema` and `quoteServiceSchema` matching the flat PB write shape (`customerName`, `vehicleInfo`, `services`, `discountValue`, `discountType`, `subtotal`, `tax`, `total`, `status`). All fields typed, required fields get `.min(1)` with descriptive messages.
|
||
|
||
3. **`src/schemas/repairOrder.ts`** — `repairOrderWriteSchema` and `roServiceSchema` covering the full RO write payload (`customerName`, `customerPhone`, `vehicleInfo`, `services`, `status`, `total`/`tax`/`shopCharges`, `serviceLocation`, `travelFee`, `tripMiles`). Services validated as array then `JSON.stringify`'d.
|
||
|
||
4. **`src/schemas/customer.ts`** — `customerWriteSchema` for the `CustomerFormData` shape (`name`, `phone` required; `email` validated with `z.union([z.string().email(), z.literal('')])`).
|
||
|
||
5. **`src/schemas/invoice.ts`** — `invoiceWriteSchema` + `invoiceItemSchema`. Includes a `.refine()` that catches total mismatch: `total ≈ subtotal + tax` must hold within 0.01 tolerance. Replaces the existing 3-check hand-rolled `validate()`.
|
||
|
||
6. **`src/schemas/settings.ts`** — `settingsWriteSchema` (27 fields covering all `ShopSettings`) and `accountSettingsWriteSchema` for the email/password settings form. Enforces hex color regex, positive labor rate, nonnegative financials, and valid enums.
|
||
|
||
7. **`src/schemas/index.ts`** — Two helpers: `formatZodError` (returns first issue message for toasts) and `zodErrorsToFormFields` (maps issues by path for `setFormErrors` state).
|
||
|
||
8. **Page wiring:**
|
||
- `QuoteGenerator.tsx` — `quoteWriteSchema.safeParse` before both `handleSave` and `ensureShareToken` saves. Replaced the single-line `if (!customerInfo.name) return` guard.
|
||
- `RepairOrders.tsx` — `repairOrderWriteSchema.safeParse` on incoming form data before any PB write.
|
||
- `Customers.tsx` — `customerWriteSchema.safeParse` in both `handleAddCustomer` and `handleEditCustomer`.
|
||
- `Invoices.tsx` — Replaced the 11-line hand-rolled `validate()` with `invoiceWriteSchema.safeParse` + `zodErrorsToFormFields`. Existing `formErrors.*` bindings continue working unchanged.
|
||
- `Settings.tsx` — `settingsWriteSchema.safeParse` before `saveAllSettings`, `accountSettingsWriteSchema` before `saveAccountSettings` (replaces the 3-line ad-hoc email check).
|
||
|
||
9. **Tests (5 files, 41 tests):**
|
||
- `src/schemas/__tests__/quote.test.ts` — 7 tests (valid, missing name, missing vehicle, empty services, negative price, negative discount, invalid discount type)
|
||
- `src/schemas/__tests__/repairOrder.test.ts` — 8 tests (valid, missing name, missing phone, missing vehicle, empty services, empty service name, negative total, invalid status)
|
||
- `src/schemas/__tests__/customer.test.ts` — 6 tests (valid, valid with email, empty email allowed, missing name, missing phone, invalid email)
|
||
- `src/schemas/__tests__/invoice.test.ts` — 8 tests (valid, missing name, missing vehicle, empty items, empty item description, zero quantity, total mismatch, invalid status)
|
||
- `src/schemas/__tests__/settings.test.ts` — 12 tests (valid, missing business name, missing phone, missing advisor, negative rate, zero rate, malformed color, valid color, invalid business type, invalid pdf style, +2 account settings)
|
||
|
||
**Design decisions:**
|
||
- Schemas validate the flat PB write shape (matching `Record<string, any>` payloads), not the nested TS interfaces — no call-site refactoring needed.
|
||
- `QuoteApprove.tsx` excluded (public customer-facing page; minimal validation is intentional).
|
||
- `technicianData.ts` assignment writes excluded (already guarded by optimistic concurrency from step 1.6).
|
||
|
||
**Verified:** `npm run build` (0 errors, 356ms), `npm run lint` (0 errors, 18 pre-existing warnings only), `npm run test` (73/73 pass — 32 existing + 41 new).
|
||
|
||
**Resolved:** v5.2 roadmap priority 2.5.
|