Repository navigation
feat(sales): pick the next party once a designer/owner quotation is sent; เสนอใบราคา opens the new-engine quotation - #1168
Merged
Conversation
…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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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>
This was referenced Oct 22, 2026
…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>
…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>
…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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
เสนอราคา<next party>; with two parties left it is a menu,เสนอราคาเจ้าของโครงการ/ข้ามไปเสนอราคาผู้ซื้อ. Picking one opens the price-request form with ผู้รับ pre-filled.ใบเสนอราคาผู้ออกแบบส่งแล้ว เลือกเสนอราคาเจ้าของโครงการ หรือข้ามไปผู้ซื้อ(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.ส่งคำขอราคาถึง<party>sends it to import (the panel's own ส่งให้ฝ่ายนำเข้า). With import/CEO → no main button.ถัดไป: 5. เสนอราคาเจ้าของโครงการ หรือข้ามไป 8. …. The route comes from the servedcatalog.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
ยืนยันคำสั่งซื้อ, and it confirms that request. After an acceptance nothing else is quoted, waited on or approved on the deal.ลูกค้ายอมรับon a designer or owner request first asksยืนยันว่าลูกค้ายอมรับใบเสนอราคานี้. ยกเลิก records nothing.C.
เสนอใบราคา<party>opens the new-engine quotation (UAT ruling R2)/quotations/:id; an existing draft opens with no new write.รอ ผจก.ขาย/CEO อนุมัติใบเสนอราคา <number>; ผจก.ขาย (when they may approve it) and the CEO getอนุมัติใบเสนอราคา→ the quotation. A price changed from the CEO's needs the CEO.D. A quotation to redo gets the same button
เสนอใบราคา<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
เปิดใบเสนอราคา), also when an old-engine quotation on the request is cancelled or rejected. ยืนยันคำสั่งซื้อ acts on the accepted request./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.createFromPricingRequestrefuses 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
/code-review maxagainst 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.Merge notes
TicketDetailPage.jsx, thedeal-quotation-panelwrapper): keep fix(ui): stop the deal page snapping back to its sticky header on desktop #1158'smobile:scroll-mt-[420px] outline-noneand this PR'squotationByRequestprop. The page's other in-page jumps still need fix(ui): stop the deal page snapping back to its sticky header on desktop #1158 on desktop; this PR's own CTAs no longer use them.Known gaps
🤖 Generated with Claude Code