Skip to content

feat(sales): pick the next party once a designer/owner quotation is sent; เสนอใบราคา opens the new-engine quotation - #1168

Merged
waritctd merged 13 commits into
developfrom
claude/stage-skip-develop-debug-e97c44
Oct 27, 2026
Merged

waritctd merged 13 commits into
developfrom
claude/stage-skip-develop-debug-e97c44

Conversation

@waritctd

@waritctd waritctd commented Oct 22, 2026 •

Copy link
Copy Markdown
Owner

Why

On the designer route, an accepted designer quotation made ยืนยันคำสั่งซื้อ the deal page's only button. Pressing it confirmed the customer and moved the deal from ขั้น 4 to ขั้น 10, then ขั้น 11 on the deposit, skipping the owner and buyer (QA deal 51).

The first version of this PR (10-22) offered the next party after an acceptance. A code review against main (10-27) found it dead-ends: the server keeps one ACCEPTED quotation per deal (R8), so the owner's and buyer's acceptances were refused.

Owner rulings 2026-10-27: hold the PR and make it safe to ship; pick the next party when the designer's or owner's quotation is sent; a quotation to redo gets เสนอใบราคา<party>; a sent designer/owner quotation's outcome is always in the ⋯ menu. Frontend only, no server change.

What changes (frontend only)

A. The route choice comes when a designer or owner quotation is sent

  • The furthest party (ผู้ออกแบบ → เจ้าของโครงการ → ผู้ซื้อ) whose quotation went out (ISSUED, SUPERSEDED, REJECTED or EXPIRED) gives เสนอราคา<next party>; with two parties left it is a menu, เสนอราคาเจ้าของโครงการ / ข้ามไปเสนอราคาผู้ซื้อ. Picking one opens the price-request form with ผู้รับ pre-filled.
  • Line in the bar: ใบเสนอราคาผู้ออกแบบส่งแล้ว เลือกเสนอราคาเจ้าของโครงการ หรือข้ามไปผู้ซื้อ (one party: …ถัดไปเสนอราคาผู้ซื้อ; a rejected or expired one reads …ไม่ผ่าน…).
  • ⋯ บันทึกผลใบเสนอราคา → the request page, whenever a sent designer/owner quotation has no outcome yet, whatever the main button is.
  • A later party's request still a DRAFT → ส่งคำขอราคาถึง<party> sends it to import (the panel's own ส่งให้ฝ่ายนำเข้า). With import/CEO → no main button.
  • Pipeline card: ถัดไป: 5. เสนอราคาเจ้าของโครงการ หรือข้ามไป 8. …. The route comes from the served catalog.routes; there is no route choice until both the route and the deal's quotation list are read.

B. An acceptance is the order, as on main

  • Any accepted, unconfirmed request → ยืนยันคำสั่งซื้อ, and it confirms that request. After an acceptance nothing else is quoted, waited on or approved on the deal.
  • On the request page, ลูกค้ายอมรับ on a designer or owner request first asks ยืนยันว่าลูกค้ายอมรับใบเสนอราคานี้. ยกเลิก records nothing.

C. เสนอใบราคา<party> opens the new-engine quotation (UAT ruling R2)

  • CEO-settled price → writes the quotation draft and opens /quotations/:id; an existing draft opens with no new write.
  • Awaiting approval: the rep reads รอ ผจก.ขาย/CEO อนุมัติใบเสนอราคา <number>; ผจก.ขาย (when they may approve it) and the CEO get อนุมัติใบเสนอราคา → the quotation. A price changed from the CEO's needs the CEO.
  • A refusal (409) shows the server's message and opens the request page; later clicks go straight there.
  • The deal page's old-engine one-click create and issue path is removed.

D. A quotation to redo gets the same button

  • The customer asked for changes, or the buyer said no or let it expire → เสนอใบราคา<party> writes a fresh quotation from the same request (the request page's เขียนใบเสนอราคาใหม่จากคำขอราคา). An open redo draft opens with no new write. A designer or owner who said no keeps the route choice.

E. Pages and pieces

  • The quotation panel shows the request's new-engine quotation (number, status, เปิดใบเสนอราคา), also when an old-engine quotation on the request is cancelled or rejected. ยืนยันคำสั่งซื้อ acts on the accepted request.
  • The request page reads past a cancelled revision draft to the quotation it revised.
  • The dashboard reads each quoted deal's quotation list, so waits and redos show as on the deal page.
  • The เอกสาร tab links a new-engine quotation's number to /quotations/:id.
  • OverflowMenu: focus returns to the trigger without scrolling (also from a dialog and on Escape), triggerProps, triggerIcon={null}, a labelled trigger lays out with a gap, onOpenChange. The deal page's sticky header holds still while one of its menus is open.
  • mockApi.createFromPricingRequest refuses what the service refuses and allows its re-quote from QUOTATION_ISSUED.

Not changed

Backend (R8 included), stage numbers, the request chain, ยืนยันคำสั่งซื้อ itself, the direct-quotation engine's own confirm.

Tests

Written first and run red, then implemented. Existing tests were revised where they covered the old behaviour; tests that pinned impossible states or could not fail were deleted.

  • salesActions.test.js: route choice (sent ISSUED/SUPERSEDED/REJECTED/EXPIRED, furthest party, dead later request, another deal's request, off-route party, unknown route); a later DRAFT → ส่งคำขอราคาถึง…; later request with import/CEO → nothing (the designer's ISSUED or REJECTED quotation included); redo (designer REVISION_REQUESTED or DRAFT, owner after the designer, buyer REJECTED/EXPIRED/REVISION_REQUESTED, SUPERSEDED → nothing, an earlier party's redo draft never takes over the buyer's); acceptance (ยืนยันคำสั่งซื้อ with a later request SUBMITTED/APPROVED/pending; after a confirmed order nothing is quoted: stale same-party request, earlier redo draft, later approved request, later quotation awaiting approval); bucket 2 opens an existing DRAFT.
  • workState.test.js: the bar line (two parties, one party, ไม่ผ่าน); approval wait for the rep; อนุมัติใบเสนอราคา for ผจก.ขาย and the CEO, and not for a cancelled request's, a CEO-only, or an after-acceptance quotation; ⋯ บันทึกผลใบเสนอราคา beside the route choice, while the owner's request is with import/CEO, not once the designer said no.
  • TicketDetailPage.test.jsx: menu, line and form pre-fill; the ⋯ pair (บันทึกผลใบเสนอราคา, แก้ใบเสนอราคา) at the owner stage; rejected designer quotation; BUYER_DIRECT route; nothing while the stage list loads (⋯ still offers the outcome); nothing while the quotation list cannot be read; a redo draft opens with no write; the later DRAFT is submitted; an accepted designer quotation with a later approved request → ยืนยันคำสั่งซื้อ confirms the accepted one; เสนอใบราคา writes, refreshes, opens; leaving first does not navigate; existing DRAFT; approval wait; 409 vs 500; after a 409 no second write; panel rows (incl. a cancelled old-engine row); the header condenses by sentinel and holds while a menu is open.
  • PricingRequestDetailPage.test.jsx: buyer accept records at once, also behind a cancelled revision draft; designer/owner accept asks first (both engines); ยกเลิก records nothing; an outcome refreshes the deal page's lists.
  • OverflowMenu.test.jsx: focus to the trigger before the item and back from its dialog without scrolling; Escape; trigger props, no stray icon, a row with a gap.
  • SalesOverview.test.jsx: the next party follows each deal's route; a redo reads เสนอใบราคาผู้ออกแบบ.
  • quotationMeta.test.js: a cancelled draft never stands in for the quotation it revised.
  • DealStagePanel, DealDocumentRegister, mockApi.pricingRequestQuotation, mockApi.dealQuotationOutcome (re-quote, superseded 409).

Deliberately not covered: real scrolling, focus jumps and remounts (jsdom has no layout; checked in headless Chrome); the request page when the deal's list is not readable (falls back to the server's row); the session lifetime of the 409 memory; e2e-real.

Verification

  • Targeted vitest on the pushed tip (develop 50cc572 merged in): 1,126 tests in 14 files, plus 765 in 20 files that use the menu, dialog focus or header. Lint clean.
  • /code-review max against main (main 0d36985 + this PR): 15 findings, all fixed in cacff6d; an earlier Opus review of the 10-27 design approved with notes, all fixed.
  • Browser, this branch's frontend against main's backend (headless Chrome, seeded deals): route choice, owner-direct, full D → O → B route to ขั้น 10; approver and rep views of a pending quotation; phone width; redo (one write, then the same draft reopens); draft request submitted from the bar; chevron gap; menu stays open on scroll; no jump when a dialog closes.

Merge notes

Known gaps

🤖 Generated with Claude Code

waritctd and others added 2 commits October 22, 2026 13:51
…ance, and opens new-engine quotations

Owner rulings 2026-10-22. After a designer or owner accepts a quotation the
deal page used to offer only ยืนยันคำสั่งซื้อ, which confirmed the customer and
jumped the deal from ขั้น 4 to ขั้น 10 (then 11 on the deposit). Now:

- A. After a non-last-party acceptance the sticky primary is
  เสนอราคา<next party> with a menu of the remaining on-route parties
  (เสนอราคาเจ้าของโครงการ / ข้ามไปเสนอราคาผู้ซื้อ), an info line in the action
  bar, and the pipeline card's ถัดไป line naming both choices. Picking one
  opens the price-request form with ผู้รับ pre-filled; the deal moves when the
  quotation is created, as before. ยืนยันคำสั่งซื้อ only when no on-route party
  remains. A later party's live request means neither is offered.
- B. When the CEO has settled the price, เสนอใบราคา<party> creates the
  quotation draft in the new engine and opens /quotations/:id (UAT ruling R2
  of 2026-10-18). The deal page's old-engine one-click create+issue path is
  removed.
- C. Quotation rows in the deal's เอกสาร tab link to their quotation page.
- บันทึกผลใบเสนอราคา opens the price-request page, where the outcome is
  recorded for both engines, instead of a panel that knew only the old one.

Frontend only. Tests revised first and shown red; 5 tests pinning the removed
old-engine path deleted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- confirmOrderGatePasses takes the deal's own requests, already filtered.
- DealQuotationPanel reads PARTY_LABEL instead of keeping its own copy.
- TicketDetailPage derives off-route parties from stageCatalog's offRouteCodes
  (now exported) instead of re-filtering stageDecisions.
- The banner ternary loses a redundant branch.
- OverflowMenu spreads triggerProps first, so a caller can never override the
  menu's own handlers or ARIA.

No behaviour change; clean-review pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
demo-glr Ready Ready Preview Oct 27, 2026 4:05pm UTC

Request Review

waritctd and others added 2 commits October 22, 2026 14:03
GHSA-qxg6-89mg-rqv8 and GHSA-xp26-3454-mjgp on vitest / @vitest/mocker
(moderate) fail `npm run audit`; develop's own Frontend CI fails the same
job at 9ff4283. 4.1.12 is the fix release. Tickets, pricing-request and
dashboard suites: 1578/1578 on the new version.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The test drives three saves with per-row waits and takes 5–17 s on a loaded
CI runner (it timed out at 5699 ms on #1168's first run, and passes alone in
under 5 s). It is slow, not hung.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…97c44

Conflicts resolved, keeping both sides:
- frontend/package.json, package-lock.json: develop's version. It carries
  the same vitest 4.1.12 bump this PR made, plus zod 4.6.7 and the undici
  override from #1177.
- TicketDetailPage.jsx root element: develop's `relative` + the sticky-chrome
  sentinels (#1162), with this PR's mobile:pb-36 for the taller action bar.

Lint clean; tickets, pricing-request, dashboard, app and layout suites
1732/1733 green, the one failure the slow modal test under load (passes
alone, 66/66).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
waritctd and others added 7 commits October 27, 2026 15:57
…ute never dead-ends (owner ruling 2026-10-27)

The server allows one ACCEPTED quotation per deal (R8), so "pick the next
party after the designer/owner accepts" could never finish: the next
party's acceptance always 409s. Owner ruling 2026-10-27: choose the next
party when the designer's/owner's quotation is SENT; acceptance and
ยืนยันคำสั่งซื้อ come from the buyer's quotation. Frontend only.

- Route choice (เสนอราคา<next> ▾ / ข้ามไปเสนอราคา<later>) anchors on the
  furthest party whose quotation went out (issued, superseded, rejected or
  expired); banner "ใบเสนอราคา<party>ส่งแล้ว …" / "…ไม่ผ่าน …"; ⋯ keeps
  บันทึกผลใบเสนอราคา for that quotation. A later party's DRAFT gives
  ส่งคำขอราคาถึง<party>.
- Any accepted request gives ยืนยันคำสั่งซื้อ exactly as on main (safety
  net). The request page and the deal page's panel ask before accepting a
  designer/owner quotation.
- The approved request's quotation: DRAFT opens it, PENDING_APPROVAL shows
  the waiting line (approvers get อนุมัติใบเสนอราคา); a 409 opens the
  request page. บันทึกผลใบเสนอราคา only for the buyer's issued quotation.
- The deal's route comes from the served channel route (deal page and
  dashboard); no route choice until it is known.
- Quotation panel shows the new-engine quotation; ถัดไป line built from the
  parties' own stages; register quotation number is a plain link (rows no
  longer paint over the sticky header); menu returns focus to its trigger;
  cache refresh after the new round trips; mock mirrors Java's 409s.

Tests revised first (62 red on assertions), stale and vacuous ones deleted;
tickets, pricingRequests, dashboard, OverflowMenu and mock suites 1651/1651,
lint clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ge; a cancelled request's pending quotation no longer waits (owner ruling 2026-10-27)

Opus review of 8c96888 found two states with no button at all (N-1, N-2): the buyer said no
or let the quotation expire, or a sent quotation is being redone. Owner ruling 2026-10-27:
"เสนอใบราคา<party>", the same button as after the CEO prices the request.

- salesActions: after บันทึกผลใบเสนอราคา (a live quotation's outcome comes first), a
  QUOTATION_ISSUED request whose newest quotation is DRAFT (the redo draft: opened, no write)
  or REVISION_REQUESTED, or a buyer's REJECTED/EXPIRED one, gets ISSUE_QUOTATION. The request
  furthest along the route wins. A designer or owner who said no keeps the route choice and is
  never quoted again. The re-quote is DealQuotationService#createFromPricingRequest's own
  escape hatch, the same call as the request page's เขียนใบเสนอราคาใหม่จากคำขอราคา.
- N-3: the approval wait and the approver's อนุมัติใบเสนอราคา read only live requests
  (pendingRequestQuotation, shared by salesActions and workState). A cancelled request keeps its
  quotation PENDING_APPROVAL on the server; approving it from the deal page issued a quotation
  on a cancelled request.
- N-7: OverflowMenu returns focus to the trigger with preventScroll. Measured in headless Chrome
  on the deal page: the plain focus() scrolled the page up 348px (the shell's scroll-padding-top).
- mockApi.createFromPricingRequest mirrors the service's re-quote from QUOTATION_ISSUED and its
  superseded-latest 409.

Tests (written first, 14 red for the missing behaviour, then green):
- salesActions.test.js: designer redo (REVISION_REQUESTED fresh, DRAFT opens /quotations/<id>);
  later party with import/CEO -> nothing, the designer's REJECTED included; buyer REJECTED /
  EXPIRED / REVISION_REQUESTED -> เสนอใบราคาผู้ซื้อ; buyer SUPERSEDED -> nothing; owner redo after
  the designer's went out; an earlier party's redo draft never takes over the buyer's quotation.
- workState.test.js: a cancelled request's pending quotation is ignored by the rep and ผจก.ขาย.
- TicketDetailPage.test.jsx: a designer revision draft -> เสนอใบราคาผู้ออกแบบ opens it, no write.
- OverflowMenu.test.jsx: focus returns to the trigger with { preventScroll: true }.
- mockApi.dealQuotationOutcome.test.js: re-quote after REJECTED / REVISION_REQUESTED; superseded 409.
Not covered: the page-level re-quote click (same issue_quotation handler, already pinned; checked
in the browser), a recipient with no party, EXPIRED in the mock (no expiry sweep), the dashboard
(no quotation list, never sees a redo), the scroll itself (jsdom has no layout).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- routeChoiceAction: drop the unreachable 'later party said no' carve-out (that request is the
  anchor itself) and check for parties left before reading the holding requests.
- newestQuotationByRequest moves from the deal page into salesActions, next to the resolver that
  reads its Map; DealQuotationPanel reads the request quotation's status label once.
- The bucket-count comments no longer say 7.

Same behaviour; 389 tests in the four affected files green before and after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nk test after what it pins

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… nothing is quoted after an acceptance

Clean-review findings on 8c96888..708d983:
- newestQuotationByRequest skips CANCELLED rows. A revision draft cancelled after ⋯ แก้ใบเสนอราคา
  became the request's quotation: the buyer lost บันทึกผลใบเสนอราคา, a designer/owner request
  stopped anchoring the route choice, and the deal page had no button at all.
- An acceptance is the order (spec B1): no route choice and no redo once any request is
  QUOTATION_ACCEPTED. A stale request of the same party (or an earlier party's open redo draft)
  offered a quotation again after the order was confirmed.

Tests first (3 red, then green): 'after a confirmed order nothing is quoted again' (a stale
same-party request; an earlier party's redo draft) and 'a cancelled draft never stands in for
the quotation it revised' (salesActions.test.js). 345 tests in salesActions, workState,
SalesOverview and TicketDetailPage green; lint clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r an acceptance, confirm the accepted request, ⋯ always records the outcome (owner ruling 2026-10-27)

/code-review max against main (snapshot main 0d36985 + this PR) left 15 findings; all fixed,
frontend only. Owner ruling 2026-10-27 for the one spec question: a sent designer/owner
quotation's บันทึกผลใบเสนอราคา is always in the ⋯ menu, whatever the primary is.

- V1 nothing is quoted after an acceptance: bucket 2 (เสนอใบราคา<party>) is skipped once any
  request is accepted, and pendingRequestQuotation waits on / offers approval of nothing then.
- V2 ยืนยันคำสั่งซื้อ confirms the accepted request: DealQuotationPanel picks the accepted request
  over a later party's newer one.
- V3 + G2 + V5 outcomeSecondaryAction: ⋯ บันทึกผลใบเสนอราคา for the sent designer/owner quotation
  whose outcome is open (also while a later request is with import/CEO, or the route/list is not
  read); the deal page holds the route choice until its quotation list is read.
- V4 the request page skips a cancelled revision draft (newest row that is not CANCELLED, read from
  the deal's list; newestQuotationByRequest moves to quotationMeta, shared by both pages).
- V6 a dead old-engine quotation no longer hides the request's new-engine quotation in the panel.
- V7 useDialogFocus and OverflowMenu's Escape restore focus with preventScroll (closing a dialog
  opened from the sticky menu pulled the page up; headless Chrome: 320 → 320 now).
- V8 ส่งคำขอราคาถึง<party> submits that draft through PricingRequestPanel's ref (new `submit`
  handle) instead of a scroll jump the page pulls back until #1158.
- V9 the labelled menu trigger lays its label out in a row with a gap (chevron 7px, centred).
- V10 the sticky header holds still while one of its menus is open (OverflowMenu onOpenChange);
  condensing remounted the trigger and closed the menu.
- V11 the dashboard reads each quoted deal's quotation list (same query as the deal page), so
  approval waits and redos read as they do on the deal page.
- V12 after a refusal (409) the next เสนอใบราคา click goes straight to the request page (session flag
  in the query cache), no second write or error.
- G1 ผจก.ขาย gets no อนุมัติใบเสนอราคา for a quotation that needs the CEO (canApproveDealQuotationNow).
- V15 the triggerIcon={null} trigger and the route choice + ⋯ แก้ใบเสนอราคา pair are now pinned.

Tests first (20 cases red for the missing behaviour, then green); revised existing tests where
they fit, 3 new: a sent designer quotation's ⋯ outcome (workState), the unreadable quotation list
(deal page), the second click after a 409 (deal page). The cancelled-draft map test moved to
quotationMeta.test.js with its helper. Green: salesActions, workState, OverflowMenu, SalesOverview,
quotationMeta (432); TicketDetailPage, PricingRequestDetailPage, DealStagePanel,
DealDocumentRegister, ConfirmDialog, mockApi quotation tests (644); 20 other files using the menu,
dialog focus or header (765). Lint clean. Browser (headless Chrome, main's backend): chevron gap,
menu held open on scroll, no jump on dialog close, draft CTA submits PCR 40.
Not covered: real scroll/remount in jsdom (checked in the browser); the request page when the deal
list is not readable (falls back to the server's row); the 409 flag's lifetime.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@waritctd waritctd changed the title feat(sales): deal page offers the next party to quote after an acceptance; เสนอใบราคา opens the new-engine quotation feat(sales): pick the next party once a designer/owner quotation is sent; เสนอใบราคา opens the new-engine quotation Oct 27, 2026
…h, 409 is info not memory, refused draft submit says why, held condense applies on menu close

- The deal page's route choice waits for the quotation list to be read once (data), not for the
  latest fetch to succeed: a failed refetch keeps the rows it read.
- A 409 from เสนอใบราคา is the way to the request page: shown as info, asked again on every click
  (the session memory, gc'd after 5 minutes and never cleared, is removed).
- ส่งคำขอราคาถึง<party> tells the rep when the panel will not send the draft.
- A condense that arrives while a header menu is open is applied when the menu closes.
- The dashboard's per-deal quotation rows come through a stable useQueries combine, so the
  worklist memo holds (no test: performance only).

Tests first, revised in place (4 red, then green; the 409-memory test deleted with the memory):
the unreadable-list test (failed refetch keeps the route choice), the 409/500 toast tone rows,
the draft-submit rows (a refused draft says why), the condense test (closing the menu applies the
held condense). 390 tests in 6 files green; lint clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — 4b6d48f9 Deployed Oct 27, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant