feat(contracts): editable rate cards and line items (MAPPS-160) #152

Merged
nrupard merged 3 commits from feat/mapps-160-editable-rate-cards into main 2026-06-15 18:14:09 +02:00
Owner

What

MAPPS-160: rate cards and their per-work-type rate lines are now fully editable, wired to the existing mokosh-server CRUD. Edit affordances are finance-gated (can_manage_billing) to match the server's RequireFinance write guard; non-finance users keep the read-only view. The same pages render under /rate-cards and /settings/rate-cards, so both gain editing.

Cards

  • List: finance-only New Rate Card button -> create modal (name, description, default). POST /rate-cards.
  • Detail header: Edit Card action -> modal that edits (PUT /rate-cards/{id}) and deletes (DELETE /rate-cards/{id}, confirm dialog).

Line items

  • Detail "Rates" card: Add Rate button; rows become clickable to edit (finance only).
  • Editor picks a work type and sets hourly / after-hours / emergency rates.
    • On edit the work type is locked (it is the row identity; the server upserts by work_type_id).
    • On add the picker excludes work types already on the card, so you never silently overwrite an existing rate.
  • POST /rate-cards/{id}/items (upsert), DELETE /rate-card-items/{id}.
  • Work-type UUIDs are resolved to names via GET /work-types (were shown raw before).

Implementation notes

  • Bodies use the existing typed UpsertRateCardRequest / UpsertRateCardItemRequest structs, so money serializes as JSON numbers (Decimal). Amounts are parsed and validated client-side (parse::<Decimal>, Uuid::parse_str) before save, so a bad entry shows a message instead of a raw 400.
  • Reuses the proven modal pattern from billing.rs (TaxRate) and settings.rs (lookup editors): crate::components::{Modal, Input, Select, Checkbox}, confirm-on-delete, *_resource.restart() after save.
  • No mokosh-server changes (server was already CRUD-ready).
  • Empty-state under a failed fetch is suppressed (consistent with the settings editors).

Caveats

  • Not compiled locally (no Rust toolchain here). Mirrors established editor idioms; CI (cargo clippy --all-targets -- -D warnings) gates. Reviewer pass found no compile/clippy issues.

Test plan

  • As a finance user (super_admin/admin/finance): create a rate card, edit it, set it default, delete it. Add a rate line (pick work type, hourly required), edit its rates, delete it. Confirm the work-type picker hides types already on the card and is locked when editing.
  • As a non-finance user: the New/Edit/Add affordances are absent; the list and detail render read-only.
  • Confirm a card with >25 rate lines still shows all of them (items fetched at per_page=100), and work-type names render instead of UUIDs.
## What MAPPS-160: rate cards and their per-work-type rate lines are now fully editable, wired to the existing mokosh-server CRUD. Edit affordances are **finance-gated** (`can_manage_billing`) to match the server's `RequireFinance` write guard; non-finance users keep the read-only view. The same pages render under `/rate-cards` and `/settings/rate-cards`, so both gain editing. ## Cards - List: finance-only **New Rate Card** button -> create modal (name, description, default). `POST /rate-cards`. - Detail header: **Edit Card** action -> modal that edits (`PUT /rate-cards/{id}`) and deletes (`DELETE /rate-cards/{id}`, confirm dialog). ## Line items - Detail "Rates" card: **Add Rate** button; rows become clickable to edit (finance only). - Editor picks a work type and sets hourly / after-hours / emergency rates. - On **edit** the work type is locked (it is the row identity; the server upserts by `work_type_id`). - On **add** the picker excludes work types already on the card, so you never silently overwrite an existing rate. - `POST /rate-cards/{id}/items` (upsert), `DELETE /rate-card-items/{id}`. - Work-type UUIDs are resolved to names via `GET /work-types` (were shown raw before). ## Implementation notes - Bodies use the existing typed `UpsertRateCardRequest` / `UpsertRateCardItemRequest` structs, so money serializes as JSON numbers (`Decimal`). Amounts are parsed and validated client-side (`parse::<Decimal>`, `Uuid::parse_str`) before save, so a bad entry shows a message instead of a raw 400. - Reuses the proven modal pattern from `billing.rs` (TaxRate) and `settings.rs` (lookup editors): `crate::components::{Modal, Input, Select, Checkbox}`, confirm-on-delete, `*_resource.restart()` after save. - No mokosh-server changes (server was already CRUD-ready). - Empty-state under a failed fetch is suppressed (consistent with the settings editors). ## Caveats - Not compiled locally (no Rust toolchain here). Mirrors established editor idioms; CI (`cargo clippy --all-targets -- -D warnings`) gates. Reviewer pass found no compile/clippy issues. ## Test plan - As a finance user (super_admin/admin/finance): create a rate card, edit it, set it default, delete it. Add a rate line (pick work type, hourly required), edit its rates, delete it. Confirm the work-type picker hides types already on the card and is locked when editing. - As a non-finance user: the New/Edit/Add affordances are absent; the list and detail render read-only. - Confirm a card with >25 rate lines still shows all of them (items fetched at per_page=100), and work-type names render instead of UUIDs.
feat(contracts): editable rate cards and line items
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 6s
f06b1c988d
MAPPS-160. Makes rate cards and their per-work-type rate lines fully editable, wired to the existing mokosh-server CRUD. Edit affordances are finance-gated (can_manage_billing) to match the server's RequireFinance write guard; everyone else keeps the read-only view. The same pages are surfaced under /rate-cards and /settings/rate-cards, so both gain editing.

Cards: a finance-only "New Rate Card" button on the list opens a create modal (name, description, default); the detail page gains an "Edit Card" action whose modal also deletes. POST/PUT /rate-cards, DELETE /rate-cards/{id}.

Line items: the detail "Rates" card gains an "Add Rate" button and rows become clickable to edit. The editor picks a work type (locked when editing, since the server upserts by work_type_id; add-mode excludes work types already on the card to avoid silently overwriting one) and sets hourly / after-hours / emergency rates. POST /rate-cards/{id}/items (upsert), DELETE /rate-card-items/{id}. Work-type UUIDs are now resolved to names via GET /work-types instead of shown raw.

Bodies use the existing typed UpsertRateCard*Request structs, so money is sent as JSON numbers (Decimal), parsed and validated client-side before save.

Not compiled locally (no Rust toolchain in this environment); mirrors the proven billing TaxRate and settings lookup editors and is gated by CI (cargo clippy --all-targets -- -D warnings).

#MAPPS-160

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Code-review follow-ups on the rate-card editor:

- Reject negative rates client-side. `min="0"` on the rate inputs is advisory only (paste/programmatic input bypasses it), so the item editor now refuses a negative hourly/after-hours/emergency value with a message instead of POSTing it.

- Avoid the create dead-end. Creating a rate card from the list now navigates straight to the new card's detail page (the POST already returns the card; its id was being discarded) so the user can add rates immediately instead of landing back on the list with an empty card to hunt for.

- Disable "Add Rate" when there is nothing to add. When every work type already has a rate on the card (or no work types are defined), the button is disabled rather than opening a modal whose picker is empty and whose save always errors.

#MAPPS-160

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
refactor(components): promote SettingFormModal to a shared component
Some checks failed
Check / clippy + fmt + tests (pull_request) Failing after 6s
Create release / Create release from merged PR (pull_request) Has been skipped
4d32e34ba6
Code-review follow-up. The create/edit modal chrome (Delete / Cancel / Save footer + error banner) was private to src/pages/settings.rs, so the new rate-card modals in contracts.rs hand-rolled the same footer and the identical error-banner markup. Move SettingFormModal (and its Props) to src/components/setting_form_modal.rs, export it, and reuse it:

- settings.rs: drop the local component, import it from crate::components (its nine call sites are unchanged).
- contracts.rs: replace the two hand-rolled Modal+footer blocks in RateCardFormModal / RateCardItemFormModal with SettingFormModal, passing the form fields as children. Removes ~70 lines of duplicated footer/banner and leaves one source of truth for the modal chrome.

No behavior change.

#MAPPS-160

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/mapps-160-editable-rate-cards 2026-06-15 18:14:09 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-apps!152
No description provided.