initial commit

This commit is contained in:
ray
2026-07-12 10:17:17 -04:00
commit dab5a4ebc6
1424 changed files with 330463 additions and 0 deletions
@@ -0,0 +1,87 @@
# Session Example: Quote-to-RO Service Sync
## Project Context
- SPQ-v2 (ShopProQuote): React/Vite/TS shop management app
- Backend: PocketBase v0.39.5 (self-hosted Docker container at 127.0.0.1:8091)
- State: Zustand with `persist` middleware (localStorage key: `spq-quote`)
- API rules: All user-scoped collections use `userId = @request.auth.id`
- App user login: pocketbase collections `users` (not superuser)
## The Bug
When customer-approved recommended services from a Quote (generated from an RO) were saved, they did NOT transfer back to the originating Repair Order.
## Root Cause 1 (Structural): Transient State Not Restored on Edit
The `repairOriginId` state variable was initialized from a URL query param (`?fromRO=RO_ID`) but never restored from the persisted `repairOrderId` field when re-opening the quote for editing (`?edit=QUOTE_ID`):
```tsx
// QuoteGenerator.tsx — the fromRO effect only ran on mount with [] deps
// When editing, fromRO wasn't in the URL, so repairOriginId stayed null
```
**Fix applied by previous agent:** Added `setRepairOriginId(data.repairOrderId)` in the edit-loading `useEffect` (QuoteGenerator.tsx:523-525).
## Root Cause 2 (Auth/Ownership): API Rules Blocked Cross-User Writes
Even after the structural fix, the sync STILL failed. Investigation revealed:
1. **The app user `demo@shop.com` didn't exist in PocketBase.** The actual data belonged to `mani8994@gmail.com`. Logging in as `demo@shop.com` showed the correct data due to Zustand persist rehydrating from localStorage — but any API call (load, save) was blocked.
2. **PocketBase's `userId = @request.auth.id` rule** on the `repairOrders` collection blocked the RO update when the user didn't own the RO. The sync code's `pb.collection('repairOrders').update()` returned a 404 (record "not found" from the auth perspective), which was caught by the inner `catch` and logged as a toast.
## The Full Investigation (Multi-Layer)
### Layer 1 — Code inspection
- Confirmed sync code exists in `QuoteSummary.tsx:handleSave` (lines 145-194) and `ensureShareToken` (lines 267-308)
- Confirmed the edit-loading code restores `repairOriginId` from `data.repairOrderId`
- Confirmed `sanitizeServices` doesn't strip `approved` (only null values)
- Confirmed Zustand persist rehydration is synchronous and the edit-load effect overwrites PB data correctly
### Layer 2 — Database inspection (Docker + curLl)
- Authenticated as superuser via PocketBase CLI:
```bash
docker exec pocketbase /usr/local/bin/pocketbase superuser upsert admin@shop.com TEMP_PASS --dir=/pb_data
```
- Queried quotes and ROs via the reST API:
```
POST /api/collections/_superusers/auth-with-password
GET /api/collections/quotes/records?perPage=100&fields=id,repairOrderId,customerName,services
```
- Found 10+ quotes with `repairOrderId` populated AND approved services
- Found linked ROs had **0 services** — confirming the sync never fired successfully
- Used `filter=repairOrderId!=%27%27` to find only linked quotes efficiently
### Layer 3 — Direct API test (curl as app user)
- Authenticated as `mani8994@gmail.com` (the record owner) and performed the exact same `PATCH /api/collections/repairOrders/records/RO_ID` with the same payload the frontend would send:
```bash
curl -s -X PATCH "http://127.0.0.1:8091/api/collections/repairOrders/records/RO_ID" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"services": "[{\"name\":\"Test\",\"total\":100,\"status\":\"pending\"}]"}'
```
- The API call **SUCCEEDED** — the RO now had services. This proved the code logic is correct.
- Created fresh test data (RO + quote) owned by the same user to reproduce from scratch
### Layer 4 — Auth verification
- Confirmed `demo@shop.com` could NOT update the RO owned by `mani8994@gmail.com` (returned 404)
- Confirmed `mani8994@gmail.com` COULD update the RO (returned 200)
- Conclusion: The sync code is correct BUT the frontend must be authenticated as the record owner
### Layer 5 — ensureShareToken sync gap
- When `editId` is set (editing an existing quote), the `ensureShareToken` function skips the entire `if (!quoteId)` block — including the sync code
- This means the sync only runs for NEW quote shares, not for shares of existing quotes
- The `handleSave` path's sync runs for both new and edit saves, so Save+Share in sequence works, but Share-without-intervening-Save doesn't sync
## Key Takeaways
1. **Pattern 1 fix is necessary but not sufficient.** Adding `setRepairOriginId()` to the edit-loading effect is required, but the sync still won't fire if the user doesn't own the target record.
2. **Zustand persist masks auth issues.** LocalStorage rehydration shows correct-looking data even when the authenticated user has no backend access. Saves silently fail.
3. **Verify at the API level.** When the UI shows correct state but the backend doesn't have it, test the API call directly. curl never lies.
4. **Create test data with known ownership.** Test data created via the frontend (authenticated as the target user) avoids cross-owner issues.
5. **Multi-path sync code must be audited for gaps.** When the same sync logic is copy-pasted in `handleSave` and `ensureShareToken`, the two code paths can diverge. `ensureShareToken` only syncs on new quote creation.