Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
08689aa
feat(web): convert Sales to MUI, delete .order-panel/tr.discounted (#…
mforce Sep 18, 2026
3d74d77
docs(designs): amend 822-mui-revamp.md for #831's actual shape
mforce Sep 18, 2026
2c29abc
fix(web): shrink the Status select's label on its placeholder option …
mforce Sep 18, 2026
3d4afe3
fix(e2e): fix phone.spec.ts's stale table.data locator for Sales (#831)
mforce Sep 18, 2026
04077f1
fix(web): pin nowrap on Sales orders table, wrap actions, add FilterB…
mforce Sep 19, 2026
361256c
Merge branch 'feat/831-mui-ledgers' into feat/831-mui-ledgers-sales
mforce Sep 19, 2026
f7d75b3
feat(web): redesign Sales as an order desk with settlement rail
mforce Sep 19, 2026
d4e598f
refactor(web): tidy the Sales console and shorten relocated comments
mforce Sep 19, 2026
a58a384
fix(web): keep Sales payment scroll cues readable in the dark rail
mforce Sep 19, 2026
b1e9dde
refactor(web): retain only the Sales stepper label constraint
mforce Sep 19, 2026
7f64c88
Merge branch 'feat/831-mui-ledgers' into feat/831-mui-ledgers-sales
mforce Sep 19, 2026
da9809d
fix(web): compact Orders values while preserving accessible descriptions
mforce Sep 19, 2026
94168f1
Merge branch 'feat/831-mui-ledgers' into feat/831-mui-ledgers-sales
mforce Sep 19, 2026
83e95c8
fix(web): strengthen Sales dismissal checks and retarget phone actions
mforce Sep 19, 2026
84420b4
refactor(web): remove obsolete Sales wrapping and conversion commentary
mforce Sep 19, 2026
ccb0121
Merge branch 'feat/831-mui-ledgers' into feat/831-mui-ledgers-sales
mforce Sep 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/designs/822-mui-revamp.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,15 @@ Order is by dependency, then by what each slice teaches the next. **Deletions ar

Only one CSS deletion actually landed with #832: `.dialog .confirm-body` / `.dialog .confirm-body strong` (UsersPage's disable-warning paragraph was its last consumer anywhere in the repo; `useConfirm.tsx` had already dropped it in #827). Every other class family the five screens used — `table.data`, `.page-head`, `.form-grid`/`.inline-form`, `.actions`, `tr.inactive`, `.dialog-foot`, `.numfield-field`, `.named-picker-trigger`, `.hint`, `.check`, `.cell`, `td.nowrap` — is still shared with an unconverted screen, confirmed by grep before touching each one.

**Amendment (#831).** Row 9's own scope held; six things landed beyond or narrower than its text:

- **Both `.order-panel` and `tr.discounted` deleted, not only the badge half.** Row 9 named `tr.discounted .badge-warn`; the row tint itself (`tr.discounted td { background: var(--tint-warn); }`) shares the same last consumer (Sales) and retired in the same commit. Both move to inline `sx` on `TableRow`/the chip's `Box` (`DISCOUNTED_ROW_SX`/`DISCOUNTED_BADGE_SX` in `SalesPage.tsx`), same two tokens (`--tint-warn`, `--surface`). `styles.discount.test.ts`'s cross-brand/mode contrast guard was rewritten to assert against those token names directly rather than the deleted selectors — same rigor, same invariant, new source.
- **`EntryRow`/`STEPPER_SX` extracted from `DailyEntryPage.tsx` into `web/src/components/EntryRow.tsx`**, ahead of touching History. Row 8 (#830) already named the reason — "HistoryPage's adjust dialog mirrors this layout" — but left the component `DailyEntryPage`-private; converting History's own copy of the same two-step grading grid without extracting first would have meant maintaining the alignment-critical layout (#830's own minus/plus-button lineup fix) in two places. Pure extraction, no behavior change; DailyEntryPage's 82 tests stayed green throughout.
- **`FarmThemeProvider.tsx`'s `MuiTableContainer` phone override gained the #150 scroll-shadow gradient**, alongside the `contain: layout` #832 already added. `table.data`'s own phone rule carries a real, deliberate edge-fade affordance (cueing "more columns to scroll" on a wide table) that #832's `MuiTableContainer` override never replicated — converting Stock's wide tables away from `table.data` without adding it would have silently dropped the cue on every MUI table already shipped, #832's five CRUD lists included.
- **"Seven screens call FilterBar" resolves to: Stock, History, Expenses, Feed, Water, Reports (six, here) plus Audit (#833).** Sales and Inventory do not — neither carries a bounded date-range toolbar (Sales' list filter is status + customer + a checkbox; Inventory has no list-level filter row at all), so both convert their filter/form rows under pair 11 instead. Confirmed against the actual `.toolbar` consumer list, not assumed from the row count.
- **Two PRs, not eight.** FilterBar plus Reports/Feed/Water/Stock/History/Expenses/Inventory ship together (the brief's "smaller screens may share one"); Sales ships alone, per the brief's own note that it carries 4,110 test lines. Neither PR is the strict one-screen-per-PR the row's prose could be read as defaulting to.
- **`.panel-actions` and `.actions` stay declared** — Sales' record-payment trigger and the order-detail/item-panel action rows are their last live consumers respectively, confirmed by grep; neither was in row 9's deletion list and neither is deleted here.

### D9. The precache ceiling (#825). Owner review

Ceiling **1,800 KiB** of precache, enforced in CI by extending `scripts/verify-sw.mjs` (it already extracts the manifest at L150-161; it sums the listed files' sizes from `dist/` and fails above the ceiling). Derivation: MUI plus the realistic kit measured 1632.68 KiB; D7.2 adds 118.9 KiB; that is 1751.6 KiB, and 1,800 leaves 2.7% for the small controls. The alternative is a ceiling below the unoptimised kit measurement (1,600 KiB) on the argument that this epic deletes as it adds and a ceiling above the measurement is not a ceiling. Its cost is stated plainly: #825 installs the gate at order 3, before any deletion slice, and no deletion estimate exists yet (the hand-built controls are TSX, which the precache counts as part of the JS chunk, and nobody has measured what #826 and #827 remove), so a 1,600 ceiling can go red at #828 and stay red until enough hand-built code is gone; it also forces the `opsz` fallback in D7.2. #825 measures the JS delta of deleting `NamedEntityPicker` and `Dialog` on a throwaway branch before the number is chosen, so the choice is decidable. Either way: above the ceiling a slice must retire hand-built code to land, and a slice that adds more than 10 KiB of precache without deleting code names the reason in its PR body. The check is a `pull_request` check in the `web` job, so a documentation-only PR skips it (#782), which is correct: such a PR changes no bundle input. Script-time ceiling per #674: re-measure with the record's method when #826 lands, since `Autocomplete` is the component the record named as needing its own number.
Expand Down
9 changes: 7 additions & 2 deletions tools/simulation/ui/specs/phone.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,12 @@ const PHONE_ACTION_ROWS: ReadonlyArray<{
await expect(draft, "the fixture has no draft order, so the #740 row cannot be measured")
.toBeVisible();
await draft.getByRole("button", { name: tEn("sales:open") }).click();
const row = page.locator(".order-panel .actions");
// #831 — `.order-panel` retired; the draft panel is now a named
// `role="region"` landmark (SalesPage.tsx), so scope through that
// instead of the class the CSS selector used to key on. `.actions`
// itself is unchanged — a bare hook class with a real phone-stacking
// rule (styles.css) — so the row this measures is the same one.
const row = page.getByRole("region").locator(".actions");
await expect(row).toBeVisible();
return row;
},
Expand Down Expand Up @@ -547,7 +552,6 @@ test.describe("Phone shell", { tag: "@phone" }, () => {
// FOR, and its recent-sales list is the widest intrinsic content in the
// app — a money string in a `max-content` track beside a name.
{ path: "/", content: "ul.dash-sales-list", what: "the recent-sales list" },
{ path: "/sales", content: "table.data", what: "the orders table" },
{ path: "/daily-entry", content: "footer", what: "the entry form's sticky foot" },
// #832 — Customers and Flocks moved their table onto MUI's `Table`, which
// carries no `.data` class (the whole point of the conversion: the
Expand All @@ -560,6 +564,7 @@ test.describe("Phone shell", { tag: "@phone" }, () => {
// #831 keeps grade comparisons in a named board and history in a table.
{ path: "/stock", content: `role=list[name="${tEn("stock:title")}"]`, what: "the stock board" },
{ path: "/history", content: "role=table", what: "the entry history table" },
{ path: "/sales", content: "role=table", what: "the orders table" },
];

for (const { path: route, content, what } of ROUTES) {
Expand Down
14 changes: 3 additions & 11 deletions tools/simulation/ui/src/mutants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1010,19 +1010,11 @@ export const MUTANTS: Record<string, Mutant> = {
+ "`flex-direction: row` (F134, and the confirmed #864 mockup) and outranks this rule",
caughtBy: "phone.spec.ts — no action control is taller than it is wide",
apply: (page) =>
// CSS, not a longer label, and that is the re-targeting. A full-width
// button cannot become taller than it is wide however long its label, so
// the label mutant this replaced would now survive and the harness would
// correctly report the spec as uncovered. The cause worth imitating is a
// revert of the rule #823 added.
//
// Scoped inside the same media query the rule lives in, so it is inert at
// 1280 — which `MUST_STAY_GREEN_ON` checks rather than trusts. No desktop
// spec measures this ratio at all, so what a green desktop run proves is
// narrower than it sounds: no EXISTING desktop scenario noticed.
// #831: the settlement rail's sx rule outranks plain .actions.
// Keep the stronger override inside the phone breakpoint.
insertCssRule(
page,
"@media (max-width: 900px) { .actions, .dialog .dialog-foot { flex-direction: row } }",
"@media (max-width: 900px) { .actions, .dialog .dialog-foot { flex-direction: row } aside[aria-label] .actions { flex-direction: row !important } }",
),
},

Expand Down
7 changes: 5 additions & 2 deletions web/src/components/FieldConsole.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,15 @@ export function FieldConsole({ children }: { children: ReactNode }) {
);
}

export function LedgerTableContainer({ children }: { children: ReactNode }) {
export function LedgerTableContainer({ children, alwaysShowSwipeCue = false }: {
children: ReactNode;
alwaysShowSwipeCue?: boolean;
}) {
const { t } = useTranslation("common");
return (
<Box sx={{ minWidth: 0, borderTop: "2px solid var(--ink)", borderBottom: "1px solid var(--rule)" }}>
<Typography component="p" variant="body2" sx={{
display: { xs: "block", md: "none" },
display: alwaysShowSwipeCue ? "block" : { xs: "block", md: "none" },
m: 0, py: .5, px: 1,
fontSize: ".65rem",
textAlign: "right",
Expand Down
6 changes: 6 additions & 0 deletions web/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,10 @@ export const en = {
},
// Sales pilot (Task 7, #182) — the worked pattern for the full sweep.
sales: {
manifestHeading: "Order manifest",
settlementHeading: "Settlement",
stockCommitment: "Stock commitment",

// Headings
title: "Sales",
loading: "Loading…",
Expand Down Expand Up @@ -440,6 +444,7 @@ export const en = {
unitPrice: "Unit price",
discount: "Discount",
aboveList: "Above list",
atListShort: "At list",
// #723 — the row's text marker. Colour alone fails greyscale and fails a
// colour-blind reader, so the tint never travels without this chip.
// Ends in `Badge`, so badgeCase.test.ts holds it to a capital first letter
Expand Down Expand Up @@ -3113,6 +3118,7 @@ export const en = {
+ "<strong>Discount</strong> column, so a discounted order is visible without opening it — and an "
+ "order every line of which was taken before list prices were recorded reads \"List price not "
+ "recorded\" there, never zero.",
salesCommitment: "Stock commitment previews the eggs in the order, including an unsaved quantity edit. Stock is allocated only when Confirm order succeeds.",
salesConfirming:
"<strong>Confirming</strong> an order allocates real stock — oldest lots first — and is the point "
+ "where inventory changes hands.",
Expand Down
6 changes: 6 additions & 0 deletions web/src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,10 @@ export const es = {
},

sales: {
manifestHeading: "Detalle del pedido",
settlementHeading: "Liquidación",
stockCommitment: "Huevos comprometidos",

// Headings
title: "Ventas",
loading: "Cargando…",
Expand Down Expand Up @@ -335,6 +339,7 @@ export const es = {
unitPrice: "Precio unitario",
discount: "Descuento",
aboveList: "Sobre el precio de lista",
atListShort: "Precio de lista",
belowListBadge: "Por debajo de lista",
// #727
overMaximumBadge: "Sobre el máximo",
Expand Down Expand Up @@ -2444,6 +2449,7 @@ export const es = {
+ "pídaselo a un propietario o gerente, que puede confirmarlo sin tocarlo. Los propietarios y "
+ "gerentes nunca están limitados. Solo un propietario fija el límite en Configuración de la granja como "
+ "<strong>Descuento máximo</strong>.",
salesCommitment: "Huevos comprometidos muestra los huevos del pedido, incluidos los cambios de cantidad aún sin guardar. Las existencias solo se asignan al completar Confirmar pedido.",
salesConfirming:
"<strong>Confirmar</strong> un pedido asigna existencias reales — los lotes más antiguos primero — y "
+ "es el punto donde el inventario cambia de manos.",
Expand Down
6 changes: 6 additions & 0 deletions web/src/i18n/tl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ export const tl = {
},

sales: {
manifestHeading: "Mga produkto sa order",
settlementHeading: "Pagbabayad",
stockCommitment: "Mga itlog para sa order",

// Headings
title: "Benta",
loading: "Naglo-load…",
Expand Down Expand Up @@ -351,6 +355,7 @@ export const tl = {
unitPrice: "Presyo bawat yunit",
discount: "Diskwento",
aboveList: "Higit sa presyo sa listahan",
atListShort: "Presyo sa listahan",
belowListBadge: "Mas mababa sa lista",
// #727
overMaximumBadge: "Higit sa pinakamataas",
Expand Down Expand Up @@ -2545,6 +2550,7 @@ export const tl = {
+ "kumpirmahin ito nang walang binabago. Hindi kailanman nililimitahan ang mga Owner at Manager. "
+ "Owner lang ang nagtatakda ng limitasyon sa Mga setting ng bukid bilang <strong>Pinakamataas na "
+ "diskwento</strong>.",
salesCommitment: "Ipinapakita ng Mga itlog para sa order ang bilang ng itlog sa order, kasama ang pagbabago sa dami na hindi pa nai-save. Naglalaan lang ng stock kapag matagumpay ang Kumpirmahin ang order.",
salesConfirming:
"Ang <strong>Pagkumpirma</strong> ng isang order ay naglalaan ng aktwal na stock — pinakalumang lote "
+ "muna — at ito ang sandali kung saan lumilipat ang inventory.",
Expand Down
1 change: 1 addition & 0 deletions web/src/routes/HelpPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,7 @@ export function HelpPage() {
</li>
<li>
<Trans ns="help" i18nKey="salesConfirming" components={{ strong: <strong /> }} />
<p>{t("salesCommitment")}</p>
</li>
{/* #727 — the only rule on this screen that PREVENTS rather than
reports, so it sits with Confirming rather than with the discount
Expand Down
Loading
Loading