feat(quotes): staff and portal Quotes UI (PMS-675) #436
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/PMS-675-quotes-ui"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
PMS-675, the final phase of the PMS-670 Quotes epic and the only one in this repo. Adds the Quotes UI: staff list / detail / editor, and the client-facing portal view where a quote is accepted or declined.
Server side (PMS-671 through PMS-674) is already merged in
mokosh-server.Staff
/quotes, filtered by status and company, with a status column./quotes/new,/quotes/:id/edit) with add / remove line rows.Every action is gated on the same predicate the server enforces: Send appears only on an
approvedquote, Convert only on anacceptedone, and the editor is unreachable once the quote leavesdraft/rejected. Those predicates live inmodules::quotes::statusand are unit-tested against the server's rules, so a disabled control means "not in this state" rather than "this might 409".The detail page carries a one-line explainer per status, because
approved(we cleared it to go out) andaccepted(the customer signed) are one word apart and mean completely different things. Their badge colours are deliberately different too, and a test pins that.The editor's running total is explicitly a preview: it recomputes from the half-typed inputs so the number moves as you type, but the server recomputes from the saved lines and what it returns is what the detail page shows. Unparseable input contributes zero rather than erroring, since the field is still being filled in; a test covers
""and"-"mid-keystroke, and that a negative discount line subtracts.Portal
The client sees the quotes we sent them, opens one to read the scope and lines, and accepts or declines with an optional note.
Reads are already scoped server-side to the contact's company and to issued statuses, so this surface does no filtering of its own: anything it can fetch, it is allowed to show. The status palette is correspondingly narrower than the staff one.
Accepting goes through an explicit confirm step rather than firing on the first click. It authorises work at a price and the client cannot reverse it from the portal. Once decided, expired, or converted, the page shows the outcome and what it means instead of controls that would come back 409.
Notable refactor
ApprovalsSectionwas hardcoded to/tickets/{id}/approvals, but the approvals surface is polymorphic server-side. Rather than fork a quotes copy, it now takesentity_id+entity_segment+entity_noun, with the last two defaulted so the original ticket call site changes by a single word and keeps its copy. Quotes points it at the quote target.Verify
just pre-commitpasses in the pinned CI image:cargo fmt --all --check,cargo clippy --all-targets -- -D warnings,cargo check --target wasm32-unknown-unknown, andcargo test --lib(222 tests, including 6 new ones).The new tests cover the parts with real logic rather than markup: that every server status has a label and an explainer, that the action predicates match the server's state machine (send is approved-only, convert is accepted-only, content editing stops at
submitted, cancel is refused once issued), that internal and client outcomes get distinct badge colours, and the line-total preview behaviour above.Worth an eye during review: I have exercised this against the compiler and the unit tests, not yet clicked through it against a live server. The branch is checked out on the dev container so it can be driven end to end.
The approvals surface is polymorphic server-side (`target` = ticket | change_request | quote | time_entry), but this component was hardcoded to `/tickets/{id}/approvals`. Quotes need the same section, so it takes the entity rather than being forked per entity. `entity_segment` and `entity_noun` are defaulted, so the original ticket call site changes by one word and keeps its existing copy. The submit closure clones the segment per invocation for the same reason it already cloned the id: it is an `onclick` handler and must stay `FnMut`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>