Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
106 changes: 106 additions & 0 deletions docs/designs/674-daily-entry-redesign/daily-entry-direction-lab.html

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion web/src/components/NumberField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ interface NumberFieldProps {
id: string;
/** Names the two buttons — "Increase total eggs", not "Increase". */
label: string;
/** Points the input at supporting text rendered beside its label. */
describedBy?: string | undefined;
value: number;
/** Takes React's setState directly: the repeat MUST use the updater form. */
onChange: Dispatch<SetStateAction<number>>;
Expand Down Expand Up @@ -51,7 +53,8 @@ interface NumberFieldProps {
// vanishes entirely on touch, so counts get typed on a phone keypad one digit
// at a time. These are thumb-sized, and holding one accelerates.
export function NumberField({
id, label, value, onChange, step = 1, max = Number.POSITIVE_INFINITY, min = 0, disabled = false,
id, label, describedBy, value, onChange, step = 1,
max = Number.POSITIVE_INFINITY, min = 0, disabled = false,
}: NumberFieldProps) {
const { t } = useTranslation("numberField");
const timer = useRef<number | null>(null);
Expand Down Expand Up @@ -152,6 +155,7 @@ export function NumberField({
</button>
<input
id={id}
aria-describedby={describedBy}
type="number"
// -Infinity means "no floor" (signed adjustments); min="-Infinity" is
// not a valid HTML constraint, so the attribute is omitted entirely.
Expand Down
15 changes: 14 additions & 1 deletion web/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -629,7 +629,7 @@ export const en = {
// (the entry-locked banner's status word) goes through the `enums`
// `statusLabel` helper, not a key here — see DailyEntryPage.tsx.
dailyEntry: {
title: "Daily entry",
title: "Count and grade today's eggs",

// Imperative messages (event handlers / promise callbacks — see
// CONTRIBUTING-i18n.md's imperative i18n.t() pattern).
Expand Down Expand Up @@ -676,6 +676,10 @@ export const en = {
// the grading pane's gradesSum read the other way from GradingChip's own
// count-DOWN wording.
gradedCountCaption: "Counted {{graded}}",
collectionHeading: "1 · Count the collection",
collectionCaption: "Collection and set-aside counts",
gradingWorkbenchHeading: "2 · Grade the sellable eggs",
gradingTargetCaption: "Target {{target}}",

// #830 — the attention line under the title (mockup: daily-entry.html
// `.attn`). i18next selects the plural form on {{count}} (same pattern
Expand Down Expand Up @@ -703,12 +707,21 @@ export const en = {
dirtyLabel: "Dirty",
discardedLabel: "Discarded",
mortalityLabel: "Mortality",
totalEggsCaption: "All eggs collected today",
mortalityEventCaption: "Flock event · birds",

// Reconciliation readouts (counts pane)
countsExceedTotalMessage:
"Cracked + dirty + discarded ({{losses}}) exceed total eggs ({{total}}).",
sellableLabel: "Sellable",
sellableFormula: "{{total}} − {{cracked}} − {{dirty}} − {{discarded}}",
collectionTotalsLabel: "Collection totals",
gradingTotalsLabel: "Grading totals",
setAsideLabel: "Set aside",
sellableTargetLabel: "Sellable target",
gradedLabel: "Graded",
stillToGradeLabel: "Still to grade",
gradingProgress: "{{graded}} of {{target}}",
deactivatedGradeSuffix: " (deactivated)",

// F134 remainder-assignment gesture (grading pane)
Expand Down
15 changes: 14 additions & 1 deletion web/src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -470,7 +470,7 @@ export const es = {
// The entry-locked banner's status word goes through the `enums`
// statusLabel helper, not a key here.
dailyEntry: {
title: "Registro diario",
title: "Cuente y clasifique los huevos de hoy",

// Imperative messages
loadFlocksGradesFailed: "No se pudieron cargar los lotes/grados. ¿Está la API activa?",
Expand Down Expand Up @@ -512,6 +512,10 @@ export const es = {
eggCountsHeading: "Conteo de huevos",
gradingHeading: "Clasificación",
gradedCountCaption: "Contados {{graded}}",
collectionHeading: "1 · Cuente la recolección",
collectionCaption: "Recolección y conteos apartados",
gradingWorkbenchHeading: "2 · Clasifique los huevos vendibles",
gradingTargetCaption: "Objetivo {{target}}",

attentionHeading_one: "{{count}} cosa.",
attentionHeading_other: "{{count}} cosas.",
Expand All @@ -525,12 +529,21 @@ export const es = {
dirtyLabel: "Sucios",
discardedLabel: "Descartados",
mortalityLabel: "Mortalidad",
totalEggsCaption: "Todos los huevos recogidos hoy",
mortalityEventCaption: "Evento del lote · aves",

// Reconciliation readouts (counts pane)
countsExceedTotalMessage:
"Rotos + sucios + descartados ({{losses}}) superan el total de huevos ({{total}}).",
sellableLabel: "Vendibles",
sellableFormula: "{{total}} − {{cracked}} − {{dirty}} − {{discarded}}",
collectionTotalsLabel: "Totales de recolección",
gradingTotalsLabel: "Totales de clasificación",
setAsideLabel: "Apartados",
sellableTargetLabel: "Objetivo vendible",
gradedLabel: "Clasificados",
stillToGradeLabel: "Faltan por clasificar",
gradingProgress: "{{graded}} de {{target}}",
deactivatedGradeSuffix: " (desactivado)",

// Remainder-assignment gesture (grading pane)
Expand Down
15 changes: 14 additions & 1 deletion web/src/i18n/tl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -490,7 +490,7 @@ export const tl = {
// The entry-locked banner's status word goes through the `enums`
// statusLabel helper, not a key here.
dailyEntry: {
title: "Araw-araw na Tala",
title: "Bilangin at i-grade ang mga itlog ngayong araw",

// Imperative messages
loadFlocksGradesFailed: "Hindi na-load ang mga kawan/grado. Gumagana ba ang API?",
Expand Down Expand Up @@ -533,6 +533,10 @@ export const tl = {
eggCountsHeading: "Bilang ng Itlog",
gradingHeading: "Pag-grade",
gradedCountCaption: "Binilang {{graded}}",
collectionHeading: "1 · Bilangin ang nakolekta",
collectionCaption: "Nakolekta at ibinukod na bilang",
gradingWorkbenchHeading: "2 · I-grade ang mga itlog na nabebenta",
gradingTargetCaption: "Target {{target}}",

attentionHeading_one: "{{count}} bagay.",
attentionHeading_other: "{{count}} bagay.",
Expand All @@ -546,13 +550,22 @@ export const tl = {
dirtyLabel: "Madumi",
discardedLabel: "Itinapon",
mortalityLabel: "Pagkamatay",
totalEggsCaption: "Lahat ng itlog na nakolekta ngayon",
mortalityEventCaption: "Pangyayari sa kawan · ibon",

// Reconciliation readouts (counts pane)
countsExceedTotalMessage:
"Ang basag + madumi + itinapon ({{losses}}) ay lumagpas sa kabuuang "
+ "itlog ({{total}}).",
sellableLabel: "Nabebenta",
sellableFormula: "{{total}} − {{cracked}} − {{dirty}} − {{discarded}}",
collectionTotalsLabel: "Kabuuan ng nakolekta",
gradingTotalsLabel: "Kabuuan ng pag-grade",
setAsideLabel: "Ibinukod",
sellableTargetLabel: "Target na nabebenta",
gradedLabel: "Na-grade",
stillToGradeLabel: "Iga-grade pa",
gradingProgress: "{{graded}} sa {{target}}",
deactivatedGradeSuffix: " (naka-deactivate)",

// Remainder-assignment gesture (grading pane)
Expand Down
77 changes: 55 additions & 22 deletions web/src/routes/DailyEntryPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ function submitBtn() {
// (component untouched by #830, still shared with HistoryPage) and needs no
// change at all.
function countsSection() {
return screen.getByRole("heading", { name: /Egg counts/ }).closest("section") as HTMLElement;
return screen.getByRole("heading", { name: /Count the collection/ }).closest("section") as HTMLElement;
}
function sellableReadout() {
const section = countsSection();
Expand Down Expand Up @@ -170,15 +170,15 @@ describe("DailyEntryPage grading pane excludes counter-fed grades", () => {
// on this screen: Egg counts has a Cracked *counter*, which must stay — an
// unscoped query matches that and passes whatever the Grading pane does.
// (The first version of this test did exactly that and had to be tightened.)
const grading = screen.getByRole("heading", { name: /Grading/ })
const grading = screen.getByRole("heading", { name: /Grade the sellable eggs/ })
.closest("section") as HTMLElement;

expect(within(grading).getByLabelText("Grade A")).toBeInTheDocument();
expect(within(grading).getByLabelText("Grade B")).toBeInTheDocument();
expect(within(grading).queryByLabelText("Cracked")).not.toBeInTheDocument();

// ...while the counter it IS fed by stays where it belongs.
const counts = screen.getByRole("heading", { name: /Egg counts/ })
const counts = screen.getByRole("heading", { name: /Count the collection/ })
.closest("section") as HTMLElement;
expect(within(counts).getByLabelText("Cracked")).toBeInTheDocument();
});
Expand Down Expand Up @@ -275,6 +275,7 @@ describe("DailyEntryPage accuracy gating", () => {
const msg = within(countsSection()).getByRole("alert");
expect(msg).toHaveTextContent("Cracked + dirty + discarded (11) exceed total eggs (10)");
expect(msg.textContent).not.toMatch(/-\d/);
expect(within(countsSection()).queryByRole("status")).toBeNull();
expect(remainingChip()).toHaveTextContent("Fix the counts first");
expect(submitBtn()).toBeDisabled();
expect(saveDraftBtn()).toBeDisabled();
Expand Down Expand Up @@ -588,34 +589,66 @@ describe("DailyEntryPage submit confirmation", () => {
});
});

// F134: the screen is one undifferentiated pile of fields no more — three
// numbered steps in the order the work actually happens, with the
// reconciliation line and both saves pinned in a footer.
// Collection and grading are distinct work steps, with their reconciliation
// beside the fields and both saves pinned in the footer.
describe("DailyEntryPage structure", () => {
it("labels the three steps without speaking the numerals twice", async () => {
it("names the two workbench steps in the order the collection is counted", async () => {
await renderReady();

// Two steps, not three: choosing a flock and a date says WHICH day is being
// recorded, it is not part of recording it. #830 dropped the visible
// "Step n" pill from this page's own headings (the mockup shows a plain
// "Egg counts"/"Grading" heading; `.step-n`'s CSS and its visible pill
// stay for HistoryPage's mirror of this layout) — "Step n of 2" is now
// entirely sr-only, but accessible-name computation does not care whether
// a contributing span is visually clipped, so the full name is unchanged.
expect(screen.getByRole("heading", { name: "Step 1 of 2: Egg counts" })).toBeInTheDocument();
expect(screen.getByRole("heading", { name: "Step 2 of 2: Grading" })).toBeInTheDocument();
const collection = screen.getByRole("heading", { name: "1 · Count the collection" });
const grading = screen.getByRole("heading", { name: "2 · Grade the sellable eggs" });
const workbenchHeadings = screen.getAllByRole("heading")
.filter((heading) => heading === collection || heading === grading);
expect(workbenchHeadings).toEqual([collection, grading]);
expect(screen.queryByRole("heading", { name: /Flock/ })).toBeNull();
});

it("shows collection and grading totals without requiring subtraction", async () => {
await renderReady();
setNum("Total eggs", 100);
setNum("Cracked", 2);
setNum("Dirty", 3);
setNum("Discarded", 5);
setNum("Grade A", 60);
setNum("Grade B", 25);

const collection = screen.getByRole("group", { name: "Collection totals" });
expect(collection).toHaveTextContent("Set aside10");
expect(collection).toHaveTextContent("Sellable target90");
expect(within(collection).getByRole("status")).toHaveTextContent("Sellable target90");

const grading = screen.getByRole("group", { name: "Grading totals" });
expect(grading).toHaveTextContent("Graded85");
expect(grading).toHaveTextContent("Still to grade5");
expect(grading).toHaveTextContent("85 of 90");
});

it("describes captioned count fields to assistive technology", async () => {
const deactivatedGrade = { ...GRADES[0], active: false };
const draft: DailyEntry = {
...NO_RECORD_HISTORY,
id: "de-caption", farmId: "farm1", houseId: "h1", flockId: "f1",
date: todayIso(), status: "Draft", totalEggs: 1, crackedEggs: 0,
dirtyEggs: 0, discardedEggs: 0, mortalityCount: 0,
crackedGradeId: null, dirtyGradeId: null,
grades: [{ eggGradeId: deactivatedGrade.id, quantity: 1 }],
version: 1, adjustReason: null, voidReason: null,
lockedAtUtc: null, adjustedFrom: null,
};
mockListEggGrades.mockResolvedValue([deactivatedGrade, GRADES[1]]);
mockListDailyEntries.mockResolvedValue([draft]);

await renderReady();

expect(screen.getByLabelText("Mortality")).toHaveAccessibleDescription("Flock event · birds");
expect(screen.getByLabelText("Grade A")).toHaveAccessibleDescription("(deactivated)");
});

it("puts each readout with the fields it describes, and the saves in the footer", async () => {
await renderReady();

// Sellable belongs to the counts that produce it; the remainder belongs to
// the grades that consume it. Reading one while the other was a screen away
// was the whole complaint. #830: pane-scoped by heading instead of
// `.entry-pane` (the class stays for HistoryPage, but #830's own markup no
// longer carries it).
const gradingSection = screen.getByRole("heading", { name: /Grading/ }).closest("section") as HTMLElement;
// Each derived readout stays in the section containing its inputs.
const gradingSection = screen.getByRole("heading", { name: /Grade the sellable eggs/ }).closest("section") as HTMLElement;
expect(within(countsSection()).getByRole("status")).toBeInTheDocument();
expect(gradingSection.querySelector(".entry-chip")).not.toBeNull();

Expand Down
Loading
Loading