WFDASH-WEBAPP/docs/superpowers/specs/2026-08-27-approve-and-send-from-list-design.md
Dave d87a007be3 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>
2026-08-27 00:33:50 +02:00

8.4 KiB

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.