docs(quotes): add approve-and-send-from-list design spec
Lifts the detail-screen "Approve & send email" button behaviour onto the pending-approvals list rows. Replaces the list's existing two-step approve + drill-in flow with the existing combined review-approve endpoint behind the same SendQuoteModal the detail page already uses. Out of scope: "approve without send" toggle in the modal, inline markup-edit on a row. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
721b0f6bf7
commit
d87a007be3
1 changed files with 152 additions and 0 deletions
|
|
@ -0,0 +1,152 @@
|
|||
# Approve & Send from Pending-Approvals List — design spec
|
||||
|
||||
**Status:** Approved (verbal, 2026-08-27; awaiting written-spec review)
|
||||
**Date:** 2026-08-27
|
||||
**Owner:** Dave
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
The Pending Approvals list page (`/quotes/approvals`) currently exposes "Approve" and "Approve partial" buttons per row that **stop short of sending** — they only flip the status to `approved` and tell the operator via toast to drill in to send. The detail-screen already has a one-step "Approve & send email" button that goes straight through `review-approve`. Lift that detail-screen behaviour onto the list rows. Operator clicks the row button once, the same `SendQuoteModal` opens, they confirm, and the email goes.
|
||||
|
||||
This trims one round-trip for the common case where the rep already trusts the quote.
|
||||
|
||||
---
|
||||
|
||||
## Motivation
|
||||
|
||||
Today, on the approvals list, the operator must:
|
||||
1. Click "Approve" on the row → row leaves the list with status `approved`, toast says "proceed to send from the quote page"
|
||||
2. Drill into the quote detail (now under `/quotes/[id]` or the orders page) and send separately
|
||||
|
||||
Most of the time the rep is ready to approve *and* send at the same moment. The detail page already supports this via "Approve & send email". The list should match.
|
||||
|
||||
---
|
||||
|
||||
## Decisions log
|
||||
|
||||
Resolved during brainstorming on 2026-08-27:
|
||||
|
||||
| Question | Decision |
|
||||
|---|---|
|
||||
| Which detail-screen button gets lifted? | The "Approve & send email" button — the one that opens `SendQuoteModal` and submits via `review-approve action: "approve"`. |
|
||||
| Replace or supplement the existing list button? | Replace. "We never just approve it" — the standalone approve path on the list is no longer useful once the combined path exists. |
|
||||
| Same treatment for partial rows? | Yes. The current "Approve partial" button also gets the combined treatment. `review-approve` already handles `partial_approval_pending` (flips to `partial_approved → sending → emailed`). The narrower `/partial-approve` endpoint is no longer called from the list. |
|
||||
| Modal opens over list or replaces list? | Opens over the list (same modal component, no UI to clear it). Same affordance as today on the detail page. |
|
||||
| Inline markup-edit affordance from list row? | Out of scope for v1. If the rep needs to nudge markup, they drill in (existing escape hatch preserved). |
|
||||
| "Approve without send" toggle? | Deferred. Reuses the future `SendQuoteModal` toggle mentioned in conversation; not in this commit. |
|
||||
|
||||
---
|
||||
|
||||
## UI
|
||||
|
||||
### Files touched
|
||||
|
||||
- `src/app/(app)/quotes/approvals/page.tsx` — only file.
|
||||
|
||||
### Per-row button changes
|
||||
|
||||
| Row state | Current button | New button |
|
||||
|---|---|---|
|
||||
| `approval_pending` | "Approve" → `pb.collection.update` direct write | "Approve & send" → opens `SendQuoteModal`, then calls `review-approve action: "approve"` |
|
||||
| `partial_approval_pending` | "Approve partial" → POST `/partial-approve` | "Approve & send" → opens `SendQuoteModal`, then calls `review-approve action: "approve"` (server handles partials) |
|
||||
| Both | "Reject" → existing handler | "Reject" (unchanged) |
|
||||
|
||||
Button style matches the detail page's existing "Approve & send email" button — same label, same visual treatment (secondary button group typically). The detail page already passes `submitLabel="Approve & send"` to `SendQuoteModal`; the list passes the same.
|
||||
|
||||
### `SendQuoteModal` integration
|
||||
|
||||
The modal is reused unchanged. The list page needs:
|
||||
- A piece of state per row: `openSendModalForRowId: string | null` (or a Map keyed by row id)
|
||||
- A single `sendApprove` callback lifted to page level (same shape as the detail page's `sendApprove`), callable per row
|
||||
- On success: refresh the list (`router.refresh()` or local state update), toast same as detail page
|
||||
|
||||
### Toast behaviour
|
||||
|
||||
Same as detail page — branched:
|
||||
- Full approve success → `"Approved — email sent to the customer."`
|
||||
- Partial approve success → `"Partial approved — email sent for the priced items."`
|
||||
- Mail send failure (422) → `"Saved — but email send failed: <msg>. Retry from the quote page."`
|
||||
- Race / already-approved (422 invalid_status) → parse `{error}` body, surface `"This quote was already actioned — refreshing."` (then refresh the list)
|
||||
|
||||
The existing toasts `"Quote approved — proceed to send from the quote page"` and `"<N> items marked approved — open the quote to send the email."` are removed; they describe the old broken two-step flow.
|
||||
|
||||
---
|
||||
|
||||
## API surface
|
||||
|
||||
### Reused (no new endpoints)
|
||||
|
||||
- `POST /api/v1/quotes/[id]/review-approve` with `action: "approve"`, optional `email{}`, optional `itemChanges[]` — already atomically approves and sends. Already handles full + partial quotes. Already rejects on `placeholder_customer`, `invalid_status`, mail-fail. The detail page calls this; the list page will too.
|
||||
|
||||
### No longer called from the list
|
||||
|
||||
- `POST /api/v1/quotes/[id]/partial-approve` — the list's "Approve partial" button used this; replaced by `review-approve`. The endpoint stays (used elsewhere in the app).
|
||||
- Direct `pb.collection("quotes").update(...)` writes from the list — replaced by `review-approve`.
|
||||
|
||||
---
|
||||
|
||||
## Error handling
|
||||
|
||||
| Failure | Behaviour |
|
||||
|---|---|
|
||||
| `placeholder_customer` (422) | Modal disables the submit button on render (matches detail's `customerIsPlaceholder` gate). Operator drills in to fix. |
|
||||
| `invalid_status` (422) — already approved by another rep, race | Surface toast: `"This quote was already actioned — refreshing."` then `router.refresh()`. |
|
||||
| Mail send failure (422 — email sub-object) | Toast: `"Saved — but email send failed: <msg>. Retry from the quote page."` Row stays in list with `mail_failed` status (server flips it). |
|
||||
| Network/5xx | Toast: `"Couldn't approve. Try again."` Keep modal open. |
|
||||
| `customer` not found / record deleted (404) | Toast: `"This quote no longer exists. Refreshing."` then `router.refresh()`. |
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
Component-level tests for the list page (`src/app/(app)/quotes/approvals/page.test.tsx` if it exists; if not, create a focused spec file that does NOT replicate the entire list page render — only exercises the button + modal interaction).
|
||||
|
||||
| # | Test |
|
||||
|---|---|
|
||||
| 1 | Clicking "Approve & send" on an `approval_pending` row opens `SendQuoteModal` with the quote pre-loaded |
|
||||
| 2 | Submitting the modal calls `POST /api/v1/quotes/[id]/review-approve` with `action: "approve"` and an `email{}` payload taken from the modal |
|
||||
| 3 | On 200, the list removes the row (filter no longer matches) and shows the success toast |
|
||||
| 4 | On 422 `invalid_status`, the list refreshes and shows a toast |
|
||||
| 5 | On 422 mail-failed, the toast reads "Saved — but email send failed..." |
|
||||
| 6 | Clicking "Approve & send" on a `partial_approval_pending` row invokes the same endpoint and shows the partial-success toast |
|
||||
|
||||
`SendQuoteModal` itself is not re-tested — it's the existing component, unchanged.
|
||||
|
||||
`review-approve` endpoint tests already cover the success / placeholder / mail-fail branches in `src/app/api/v1/quotes/[id]/review-approve/route.test.ts` — re-run those after the merge to confirm no regression.
|
||||
|
||||
---
|
||||
|
||||
## Out of scope (deferred)
|
||||
|
||||
- "Approve without send" checkbox on `SendQuoteModal` — Dave mentioned this as a future toggle. Adds a payload flag and a separate code path on `review-approve`. Not in this commit.
|
||||
- Inline markup-edit affordance on a list row — would require a new modal/dialog. Punt.
|
||||
- Removing the standalone "Approve and don't send" path from the entire app (wherever else the operator "approves without sending" is a separate concern).
|
||||
- Visual indicator on a list row showing whether the rep already nudged markup on the detail page (i.e. a hint that the quote is "trustworthy" for one-click). Deferrable polish.
|
||||
|
||||
---
|
||||
|
||||
## Rollout
|
||||
|
||||
Single deploy:
|
||||
|
||||
1. Replace the two handler blocks and two button labels in `src/app/(app)/quotes/approvals/page.tsx`.
|
||||
2. Manual smoke: queue a quote, click "Approve & send" on the list, confirm modal opens with the right fields, submit, verify email goes out, verify row disappears from the list.
|
||||
3. Same smoke for partial row.
|
||||
|
||||
---
|
||||
|
||||
## File structure
|
||||
|
||||
| File | Touched | Responsibility |
|
||||
|---|---|---|
|
||||
| `src/app/(app)/quotes/approvals/page.tsx` | Yes — only file modified | Replace per-row handlers + button labels; lift `sendApprove`; render modal at page level keyed by row id |
|
||||
| `src/components/quotes/send-quote-modal.tsx` | No | Reused unchanged |
|
||||
| `src/app/api/v1/quotes/[id]/review-approve/route.ts` | No | Endpoint already supports the call |
|
||||
|
||||
---
|
||||
|
||||
## Open questions
|
||||
|
||||
None at design time.
|
||||
Loading…
Reference in a new issue