diff --git a/messages/de.json b/messages/de.json index cfea1b804..6e3af8473 100644 --- a/messages/de.json +++ b/messages/de.json @@ -720,7 +720,12 @@ "iconGroupWeather": "Wetter", "iconGroupPlaces": "Orte", "iconGroupMisc": "Sonstiges" - } + }, + "sectionMoreTags": "Weitere Tags", + "sectionFactors": "Faktoren", + "sectionNoteDetails": "Notiz & Details", + "quickTagsHelp": "Häufig genutzte Tags – ein Tipp genügt", + "quickTagsExpand": "Weitere" }, "medications": { "title": "Medikamente", @@ -5926,7 +5931,10 @@ "contraceptive": "Verhütung", "severity": "Schweregrad", "severityLevel": "Schweregrad {level} von 4", - "delete": "Löschen" + "delete": "Löschen", + "temperatureSection": "Temperatur & Eisprung", + "intimacySection": "Intimität & Verhütung", + "testsSection": "Tests" }, "settings": { "title": "Zyklus-Einstellungen", diff --git a/messages/en.json b/messages/en.json index 0e147aab4..a52e3b151 100644 --- a/messages/en.json +++ b/messages/en.json @@ -720,7 +720,12 @@ "iconGroupWeather": "Weather", "iconGroupPlaces": "Places", "iconGroupMisc": "Other" - } + }, + "sectionMoreTags": "More tags", + "sectionFactors": "Factors", + "sectionNoteDetails": "Note & details", + "quickTagsHelp": "Your frequent tags — one tap", + "quickTagsExpand": "More" }, "medications": { "title": "Medications", @@ -5926,7 +5931,10 @@ "saved": "Day saved", "saveError": "Could not save. Try again.", "periodStarted": "Period start logged", - "clear": "None" + "clear": "None", + "temperatureSection": "Temperature & ovulation", + "intimacySection": "Intimacy & contraception", + "testsSection": "Tests" }, "testResult": { "POSITIVE": "Positive", diff --git a/messages/es.json b/messages/es.json index ee2ea77c3..928349fe5 100644 --- a/messages/es.json +++ b/messages/es.json @@ -720,7 +720,12 @@ "iconGroupWeather": "Tiempo", "iconGroupPlaces": "Lugares", "iconGroupMisc": "Otros" - } + }, + "sectionMoreTags": "Más etiquetas", + "sectionFactors": "Factores", + "sectionNoteDetails": "Nota y detalles", + "quickTagsHelp": "Tus etiquetas frecuentes: un toque", + "quickTagsExpand": "Más" }, "medications": { "title": "Medicamentos", @@ -5926,7 +5931,10 @@ "contraceptive": "Anticonceptivo", "severity": "Intensidad", "severityLevel": "Intensidad {level} de 4", - "delete": "Eliminar" + "delete": "Eliminar", + "temperatureSection": "Temperatura y ovulación", + "intimacySection": "Intimidad y anticoncepción", + "testsSection": "Pruebas" }, "settings": { "title": "Ajustes del ciclo", diff --git a/messages/fr.json b/messages/fr.json index 54b3b084b..fe1526491 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -720,7 +720,12 @@ "iconGroupWeather": "Météo", "iconGroupPlaces": "Lieux", "iconGroupMisc": "Autres" - } + }, + "sectionMoreTags": "Plus de tags", + "sectionFactors": "Facteurs", + "sectionNoteDetails": "Note et détails", + "quickTagsHelp": "Vos tags fréquents — en un geste", + "quickTagsExpand": "Plus" }, "medications": { "title": "Médicaments", @@ -5926,7 +5931,10 @@ "contraceptive": "Contraception", "severity": "Intensité", "severityLevel": "Intensité {level} sur 4", - "delete": "Supprimer" + "delete": "Supprimer", + "temperatureSection": "Température et ovulation", + "intimacySection": "Intimité et contraception", + "testsSection": "Tests" }, "settings": { "title": "Réglages du cycle", diff --git a/messages/it.json b/messages/it.json index 04730edac..7e9cdff00 100644 --- a/messages/it.json +++ b/messages/it.json @@ -720,7 +720,12 @@ "iconGroupWeather": "Meteo", "iconGroupPlaces": "Luoghi", "iconGroupMisc": "Altro" - } + }, + "sectionMoreTags": "Altri tag", + "sectionFactors": "Fattori", + "sectionNoteDetails": "Nota e dettagli", + "quickTagsHelp": "I tuoi tag frequenti: un tocco", + "quickTagsExpand": "Altro" }, "medications": { "title": "Farmaci", @@ -5926,7 +5931,10 @@ "contraceptive": "Contraccettivo", "severity": "Intensità", "severityLevel": "Intensità {level} su 4", - "delete": "Elimina" + "delete": "Elimina", + "temperatureSection": "Temperatura e ovulazione", + "intimacySection": "Intimità e contraccezione", + "testsSection": "Test" }, "settings": { "title": "Impostazioni del ciclo", diff --git a/messages/pl.json b/messages/pl.json index e3915160a..948aa2432 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -720,7 +720,12 @@ "iconGroupWeather": "Pogoda", "iconGroupPlaces": "Miejsca", "iconGroupMisc": "Inne" - } + }, + "sectionMoreTags": "Więcej tagów", + "sectionFactors": "Czynniki", + "sectionNoteDetails": "Notatka i szczegóły", + "quickTagsHelp": "Twoje częste tagi — jedno dotknięcie", + "quickTagsExpand": "Więcej" }, "medications": { "title": "Leki", @@ -5926,7 +5931,10 @@ "contraceptive": "Antykoncepcja", "severity": "Nasilenie", "severityLevel": "Nasilenie {level} z 4", - "delete": "Usuń" + "delete": "Usuń", + "temperatureSection": "Temperatura i owulacja", + "intimacySection": "Intymność i antykoncepcja", + "testsSection": "Testy" }, "settings": { "title": "Ustawienia cyklu", diff --git a/src/components/cycle/__tests__/log-day-sheet.test.ts b/src/components/cycle/__tests__/log-day-sheet.test.ts index 8c9671e01..f2e5a54cf 100644 --- a/src/components/cycle/__tests__/log-day-sheet.test.ts +++ b/src/components/cycle/__tests__/log-day-sheet.test.ts @@ -3,6 +3,11 @@ import { describe, it, expect } from "vitest"; import { buildDayLogPatch, buildDayLogInput, + symptomsCount, + temperatureCount, + intimacyCount, + testsCount, + noteCount, type DayLogFormState, } from "../log-day-sheet"; @@ -136,3 +141,56 @@ describe("buildDayLogInput (new row → omit empties)", () => { expect("temperatureExcluded" in input).toBe(false); }); }); + +describe("section summary-badge counts (v1.17.0)", () => { + it("counts each logged symptom", () => { + expect(symptomsCount(blank())).toBe(0); + const s = { ...blank(), symptoms: new Map([["cramps", 2], ["headache", null]]) }; + expect(symptomsCount(s)).toBe(2); + }); + + it("counts only the rendered symptothermal sign alongside BBT and OPK", () => { + expect(temperatureCount(blank(), false)).toBe(0); + // A non-finite / empty BBT string does not count. + expect(temperatureCount({ ...blank(), bbt: "abc" }, false)).toBe(0); + const s: DayLogFormState = { + ...blank(), + bbt: "36.5", + opk: "POSITIVE_LH_SURGE", + mucus: "EGG_WHITE", + cervixPosition: "HIGH", + cervixFirmness: "SOFT", + cervixOpening: "OPEN", + }; + // Mucus surface (showCervix=false): BBT + OPK + mucus = 3; cervix signs + // are not rendered, so they don't count. + expect(temperatureCount(s, false)).toBe(3); + // Cervix surface (showCervix=true): BBT + OPK + 3 cervix signs = 5; the + // stale mucus value is not rendered, so it doesn't count. + expect(temperatureCount(s, true)).toBe(5); + }); + + it("counts intercourse and contraceptive", () => { + expect(intimacyCount(blank())).toBe(0); + expect( + intimacyCount({ ...blank(), intercourse: true, contraceptive: "ORAL" }), + ).toBe(2); + }); + + it("counts each home-test result", () => { + expect(testsCount(blank())).toBe(0); + expect( + testsCount({ + ...blank(), + pregnancyTest: "NEGATIVE", + progesteroneTest: "POSITIVE", + }), + ).toBe(2); + }); + + it("counts a note only when it carries non-whitespace content", () => { + expect(noteCount(blank())).toBe(0); + expect(noteCount({ ...blank(), note: " " })).toBe(0); + expect(noteCount({ ...blank(), note: "tired today" })).toBe(1); + }); +}); diff --git a/src/components/cycle/log-day-sheet.tsx b/src/components/cycle/log-day-sheet.tsx index 770f32c81..20b9558e4 100644 --- a/src/components/cycle/log-day-sheet.tsx +++ b/src/components/cycle/log-day-sheet.tsx @@ -42,6 +42,10 @@ import { PopoverTrigger, } from "@/components/ui/popover"; import { ResponsiveSheet } from "@/components/ui/responsive-sheet"; +import { + SheetSection, + SheetSectionCount, +} from "@/components/ui/sheet-section"; import { useTranslations } from "@/lib/i18n/context"; import { CUSTOM_SYMPTOM_ICON_ALLOWLIST } from "@/lib/cycle/custom-symptoms-shared"; import { CYCLE_SYMPTOM_CATALOG } from "./symptom-catalog"; @@ -249,6 +253,62 @@ export function buildDayLogInput( }; } +/** + * v1.17.0 — section summary-badge counts for the sectioned log-day sheet. + * + * Each helper reports how many fields a collapsed section holds, so the + * badge communicates its contents without expanding it. Pure + exported + * for the unit tests. + */ +export function symptomsCount(s: DayLogFormState): number { + return s.symptoms.size; +} + +/** + * Temperature & ovulation: BBT reading, OPK, and the symptothermal secondary + * sign. Only the sign the section actually renders is counted — mucus when + * `showCervix` is false, the three cervix observations when true — so the + * badge never reports a stale value from the surface that isn't shown (e.g. + * after the user flips the advanced-settings secondary-symptom choice). + */ +export function temperatureCount( + s: DayLogFormState, + showCervix: boolean, +): number { + let n = 0; + if (resolveBbt(s.bbt) != null) n += 1; + if (s.opk) n += 1; + if (showCervix) { + if (s.cervixPosition) n += 1; + if (s.cervixFirmness) n += 1; + if (s.cervixOpening) n += 1; + } else if (s.mucus) { + n += 1; + } + return n; +} + +/** Intimacy & contraception: intercourse logged, and any contraceptive. */ +export function intimacyCount(s: DayLogFormState): number { + let n = 0; + if (s.intercourse) n += 1; + if (s.contraceptive) n += 1; + return n; +} + +/** Tests: pregnancy + progesterone home-test results. */ +export function testsCount(s: DayLogFormState): number { + let n = 0; + if (s.pregnancyTest) n += 1; + if (s.progesteroneTest) n += 1; + return n; +} + +/** Note: 1 when the free-text note carries content. */ +export function noteCount(s: DayLogFormState): number { + return s.note.trim() ? 1 : 0; +} + export interface LogDaySheetProps { open: boolean; onOpenChange: (open: boolean) => void; @@ -421,25 +481,28 @@ export function LogDaySheet({ }); } + // The live form state, shared by the save-payload builders and the section + // summary badges (so a collapsed section reports what it holds). + const formState: DayLogFormState = { + flow, + intermenstrual, + bbt, + bbtDisturbed, + opk, + mucus, + cervixPosition, + cervixFirmness, + cervixOpening, + intercourse, + protectedSex, + pregnancyTest, + progesteroneTest, + contraceptive, + note, + symptoms, + }; + async function handleSave() { - const state: DayLogFormState = { - flow, - intermenstrual, - bbt, - bbtDisturbed, - opk, - mucus, - cervixPosition, - cervixFirmness, - cervixOpening, - intercourse, - protectedSex, - pregnancyTest, - progesteroneTest, - contraceptive, - note, - symptoms, - }; // v1.16.4 — catch so a rejected save doesn't escape as an unhandled // rejection; the inline `logDay.isError || patchDay.isError` strip in the // footer carries the visible failure signal and the sheet stays open. @@ -449,10 +512,10 @@ export function LogDaySheet({ // chip actually CLEARS (the POST merge can only add/keep — QA W-2). await patchDay.mutateAsync({ id: rowId, - patch: buildDayLogPatch(state), + patch: buildDayLogPatch(formState), }); } else { - await logDay.mutateAsync(buildDayLogInput(state, date)); + await logDay.mutateAsync(buildDayLogInput(formState, date)); } onOpenChange(false); } catch { @@ -573,7 +636,9 @@ export function LogDaySheet({ ) : null} - {/* Flow */} + {/* Quick row — Flow is the single most common period sign, so it + stays always-open at the top alongside the intermenstrual toggle. + Everything else lives in collapsible sections below. */}
{FLOW_LEVELS.map((f) => ( @@ -599,7 +664,10 @@ export function LogDaySheet({ {/* Symptoms */} - + } + >
{CYCLE_SYMPTOM_CATALOG.map((cat) => (
@@ -656,10 +724,18 @@ export function LogDaySheet({
-
+ - {/* BBT */} - + {/* Temperature & ovulation — BBT, OPK, and the symptothermal secondary + sign (mucus or cervix) grouped under one disclosure. */} + + } + > + {/* BBT */} + )} + - {/* Intercourse + protection */} - + {/* Intimacy & contraception */} + } + > + {/* Intercourse + protection */} +
) : null}
-
+
- {/* Pregnancy test */} - -
- {TEST_RESULTS.map((v) => ( - setPregnancyTest((cur) => (cur === v ? null : v))} - > - {t(`cycle.testResult.${v}`)} - - ))} -
-
+ {/* Contraceptive */} + +
+ {CONTRACEPTIVE_KINDS.map((v) => ( + + setContraceptive((cur) => (cur === v ? null : v)) + } + > + {t(`cycle.contraceptive.${v}`)} + + ))} +
+
+ - {/* Progesterone test */} - -
- {TEST_RESULTS.map((v) => ( - - setProgesteroneTest((cur) => (cur === v ? null : v)) - } - > - {t(`cycle.testResult.${v}`)} - - ))} -
-
+ {/* Tests */} + } + > + {/* Pregnancy test */} + +
+ {TEST_RESULTS.map((v) => ( + + setPregnancyTest((cur) => (cur === v ? null : v)) + } + > + {t(`cycle.testResult.${v}`)} + + ))} +
+
- {/* Contraceptive */} - -
- {CONTRACEPTIVE_KINDS.map((v) => ( - setContraceptive((cur) => (cur === v ? null : v))} - > - {t(`cycle.contraceptive.${v}`)} - - ))} -
-
+ {/* Progesterone test */} + +
+ {TEST_RESULTS.map((v) => ( + + setProgesteroneTest((cur) => (cur === v ? null : v)) + } + > + {t(`cycle.testResult.${v}`)} + + ))} +
+
+
{/* Note */} - + } + >