diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aecef073..b9ee96f0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,15 @@ - A failed load of your share links now says so, with a way to retry, instead of quietly showing "no active share links" as if you had never shared anything. The same held for the API and connector token lists on the security screens. - A read that fails now shows a clear, recoverable error across the app instead of an empty list or a silent gap. The sharing and token screens, the ECG detail and list, the cycle and mood insights, the dashboard preventive-care tile, the checkups list, and the admin console all say when a read failed and offer a retry, rather than reading as "nothing here". - The AI consent and connector notes in Settings render as normal text now, not faint fine print, so the copy you are meant to read before turning a feature on is legible. +- The record-a-visit action on the checkups list now appears only where there is a real practice visit behind the reminder. A reminder you satisfy yourself in the app, like weighing in or a mood check-in, no longer offers to file a visit it never involved. +- The practices and doctors address book searches by specialty now, so typing "Zahnmedizin" finds the dentist even when the name and practice hold nothing of the sort. The list groups its entries under their specialty, with the ones you left unspecified gathered at the end, and the visit form's picker shows each contact's field beside the practice. +- Linking documents, lab results and illnesses to a visit is searchable now instead of a flat list you had to scroll. Each link block shows what you have picked as removable chips over an add button that opens a searchable sheet; lab results gather under their panel and sample date so you can attach a whole day's panel in one tap, and documents gather by month. The vaccination form uses the same picker for its document link. - The respiratory-rate tile on the dashboard shows its unit in your own language instead of a fixed English label, and the personalized greeting carries its comma. +### Removed + +- Logging water by hand inside the app is gone. The water entry on the capture menu, the dashboard quick-add and the hydration card have all been removed. Water that syncs in from another app through Apple Health still arrives and still shows on the hydration card with its daily total, history and reference line. + ## [1.37.3] — 2026-08-09 ### Added diff --git a/e2e/mobile-viewport.spec.ts b/e2e/mobile-viewport.spec.ts index 1565fa86d..ac6a0a8b3 100644 --- a/e2e/mobile-viewport.spec.ts +++ b/e2e/mobile-viewport.spec.ts @@ -210,7 +210,7 @@ test.describe("mobile-viewport smoke", () => { // 1) The center capture action opens the capture picker. await page.getByTestId("bottom-nav-capture").click(); await expect(page.getByTestId("capture-picker-options")).toBeVisible(); - for (const kind of ["measurement", "medication", "mood", "water"]) { + for (const kind of ["measurement", "medication", "mood"]) { await expect(page.getByTestId(`capture-picker-${kind}`)).toBeVisible(); } // Dismiss the picker before opening the hub. diff --git a/e2e/setup/global-setup.ts b/e2e/setup/global-setup.ts index 3619bfaa7..44ac59a20 100644 --- a/e2e/setup/global-setup.ts +++ b/e2e/setup/global-setup.ts @@ -310,7 +310,7 @@ export default async function globalSetup(config: FullConfig): Promise { // out (50+ failed CI runs since v1.4.13). Specs that need the // tour can opt back in by mocking `/api/auth/me`. // Nutrients is intentionally opt-in in production. The shared fixture - // enables it because the water-capture specs exercise that real gate. + // enables it so the nutrients/hydration display specs render the card. await pool.query( `INSERT INTO users (id, username, email, password_hash, role, diff --git a/e2e/water-capture.spec.ts b/e2e/water-capture.spec.ts deleted file mode 100644 index d16fa8eb9..000000000 --- a/e2e/water-capture.spec.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { expect, test } from "./setup/test"; - -import { STORAGE_STATE_PATH } from "./setup/global-setup"; -import { openMenu } from "./open-menu"; -import { - mockDashboardSnapshot, - WEIGHT_ONLY_SUMMARIES, -} from "./utils/mock-dashboard-snapshot"; - -test.describe("water capture", () => { - test.use({ storageState: STORAGE_STATE_PATH }); - - test("opens globally, waits for success, and retains failed input", async ({ - page, - }, testInfo) => { - await mockDashboardSnapshot(page, { summaries: WEIGHT_ONLY_SUMMARIES }); - await page.route(/\/api\/analytics(\?|$)/, (route) => - route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - data: { summaries: {}, bpInTargetPct: null, glucoseByContext: {} }, - error: null, - }), - }), - ); - await page.route("**/api/mood/analytics", (route) => - route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ - data: { entries: [], summary: { count: 0 } }, - error: null, - }), - }), - ); - - let writeCount = 0; - let confirmSuccess: (() => void) | undefined; - const successConfirmed = new Promise((resolve) => { - confirmSuccess = resolve; - }); - await page.route("**/api/nutrients/water", async (route) => { - writeCount += 1; - if (writeCount === 1) { - await successConfirmed; - await route.fulfill({ - status: 200, - contentType: "application/json", - body: JSON.stringify({ data: { amountMl: 250 }, error: null }), - }); - return; - } - await route.fulfill({ - status: 500, - contentType: "application/json", - body: JSON.stringify({ data: null, error: "write failed" }), - }); - }); - - await page.goto("/", { waitUntil: "domcontentloaded" }); - await page.waitForLoadState("networkidle"); - - const openWater = async (toastMayOverlap = false) => { - if (testInfo.project.name === "chromium-mobile") { - const capture = page.getByTestId("bottom-nav-capture"); - if (toastMayOverlap) { - await capture.dispatchEvent("click"); - } else { - await openMenu(page, capture); - } - const option = page.getByTestId("capture-picker-water"); - await expect(option).toBeVisible(); - const box = await option.boundingBox(); - expect(box?.height ?? 0).toBeGreaterThanOrEqual(44); - if (toastMayOverlap) { - await option.dispatchEvent("click"); - } else { - await option.click(); - } - } else { - await openMenu( - page, - page.locator('[data-tour-id="dashboard-quick-add"]'), - ); - await page.getByRole("menuitem", { name: "Log water" }).click(); - } - await expect( - page.getByRole("heading", { name: "Add water" }), - ).toBeVisible(); - if (testInfo.project.name === "chromium-mobile") { - const actions = page - .getByRole("dialog") - .locator( - '[data-slot="water-quick-add-chips"] button, form button[type="submit"]', - ); - await expect(actions).toHaveCount(4); - for (const action of await actions.all()) { - await expect(action).toHaveAccessibleName(/.+/); - const actionBox = await action.boundingBox(); - expect(actionBox?.height ?? 0).toBeGreaterThanOrEqual(44); - } - } - }; - - await openWater(); - const amount = page.getByRole("spinbutton", { - name: "Custom amount (mL)", - }); - await amount.fill("250"); - const submit = page - .getByRole("dialog") - .getByRole("button", { name: "Add", exact: true }); - await amount.press("Enter"); - - await expect(submit).toBeDisabled(); - await expect(amount).toHaveValue("250"); - await expect(page.getByText("250 mL water added.")).toHaveCount(0); - - confirmSuccess?.(); - await expect(page.getByText("250 mL water added.")).toBeVisible(); - await expect(page.getByRole("heading", { name: "Add water" })).toHaveCount( - 0, - ); - expect(writeCount).toBe(1); - - await openWater(true); - const failedAmount = page.getByRole("spinbutton", { - name: "Custom amount (mL)", - }); - await expect(failedAmount).toHaveValue(""); - await failedAmount.fill("375"); - await page - .getByRole("dialog") - .getByRole("button", { name: "Add", exact: true }) - .click(); - - await expect( - page.getByText("Couldn't save that — try again."), - ).toBeVisible(); - await expect(failedAmount).toHaveValue("375"); - await expect( - page.getByRole("heading", { name: "Add water" }), - ).toBeVisible(); - await expect(page.getByText("375 mL water added.")).toHaveCount(0); - expect(writeCount).toBe(2); - }); -}); diff --git a/messages/de.json b/messages/de.json index d0f074f51..b6ea1dddd 100644 --- a/messages/de.json +++ b/messages/de.json @@ -349,9 +349,7 @@ "medication": "Medikament", "medicationDescription": "Eine Dosis als genommen markieren", "mood": "Stimmung", - "moodDescription": "Stimmung und Tags erfassen", - "water": "Wasser", - "waterDescription": "Wasseraufnahme schnell hinzufügen" + "moodDescription": "Stimmung und Tags erfassen" }, "cycle": "Zyklus", "labs": "Laborwerte", @@ -455,7 +453,6 @@ "quickAddMeasurement": "Messung erfassen", "quickAddMood": "Stimmung erfassen", "quickAddMedicationIntake": "Einnahme erfassen", - "quickAddWater": "Wasser erfassen", "customizeDashboard": "Dashboard anpassen", "medicationIntakeQuickAdd": { "sheetTitle": "Medikamenteneinnahme erfassen", @@ -9654,17 +9651,11 @@ "dayInvalid": "Die Einträge trugen ein Datum, das der Server nicht akzeptieren konnte. Das kann passieren, wenn Uhrzeit oder Zeitzone auf der sendenden Seite falsch eingestellt sind — das lohnt sich zu prüfen.", "upsertFailed": "Diese Einträge haben den Server erreicht, aber ein Problem auf unserer Seite hat verhindert, dass sie gespeichert wurden. Du musst nichts weiter tun.", "unknown": "Diese Einträge sind angekommen, konnten aber nicht gespeichert werden." - } + }, + "moduleEnableError": "Das Nährstoffmodul konnte nicht aktiviert werden. Versuch es noch mal." }, "hydration": { - "referenceMeta": "Richtwert {value} {unit}/Tag · EFSA", - "quickAddTitle": "Wasser hinzufügen", - "quickAddCustomLabel": "Eigene Menge (ml)", - "quickAddCustomPlaceholder": "250", - "quickAddSubmit": "Hinzufügen", - "quickAddError": "Konnte nicht gespeichert werden — versuch's noch mal.", - "quickAddSuccess": "{amount} ml Wasser hinzugefügt.", - "editTotal": "Heutige Summe bearbeiten" + "referenceMeta": "Richtwert {value} {unit}/Tag · EFSA" }, "caffeine": { "ceilingMeta": "Sichere Obergrenze {value} {unit}/Tag · EFSA" @@ -10285,7 +10276,7 @@ "description": "Dein eigenes Adressbuch — einmal tippen, überall auswählen.", "linkLabel": "Adressbuch", "add": "Eintrag", - "searchPlaceholder": "Name oder Praxis suchen", + "searchPlaceholder": "Name, Praxis oder Fachrichtung suchen", "loadError": "Adressbuch konnte nicht geladen werden.", "emptyTitle": "Noch kein Eintrag", "emptyDescription": "Lege eine Praxis an, damit du sie beim Termin auswählen kannst.", @@ -10304,6 +10295,17 @@ "saveFailed": "Eintrag konnte nicht gespeichert werden.", "deleteTitle": "Eintrag löschen?", "deleteDescription": "Deine bisherigen Termine bleiben erhalten und behalten diesen Namen.", - "deleteFailed": "Eintrag konnte nicht gelöscht werden." + "deleteFailed": "Eintrag konnte nicht gelöscht werden.", + "noSpecialtyGroup": "Ohne Fachrichtung" + }, + "links": { + "picker": { + "add": "Hinzufügen", + "done": "Fertig", + "selectAll": "Alle auswählen", + "searchPlaceholder": "Suchen", + "searchNoMatch": "Kein Treffer", + "remove": "Entfernen" + } } } diff --git a/messages/en.json b/messages/en.json index 75193bb2c..32cc4f893 100644 --- a/messages/en.json +++ b/messages/en.json @@ -349,9 +349,7 @@ "medication": "Medication", "medicationDescription": "Mark a dose as taken", "mood": "Mood", - "moodDescription": "Log your mood and tags", - "water": "Water", - "waterDescription": "Quickly add your water intake" + "moodDescription": "Log your mood and tags" }, "cycle": "Cycle", "labs": "Labs", @@ -455,7 +453,6 @@ "quickAddMeasurement": "Log measurement", "quickAddMood": "Log mood", "quickAddMedicationIntake": "Log medication intake", - "quickAddWater": "Log water", "customizeDashboard": "Customize dashboard", "medicationIntakeQuickAdd": { "sheetTitle": "Log medication intake", @@ -9654,17 +9651,11 @@ "dayInvalid": "The entries carried a date the server couldn't accept. This can happen when the clock or time zone on the sending side is wrong — it's worth checking.", "upsertFailed": "These entries reached the server, but a problem on this end kept them from being saved. There's nothing you need to do.", "unknown": "These entries arrived but couldn't be stored." - } + }, + "moduleEnableError": "The nutrients module couldn't be turned on. Please try again." }, "hydration": { - "referenceMeta": "Reference intake {value} {unit}/day · EFSA", - "quickAddTitle": "Add water", - "quickAddCustomLabel": "Custom amount (mL)", - "quickAddCustomPlaceholder": "250", - "quickAddSubmit": "Add", - "quickAddError": "Couldn't save that — try again.", - "quickAddSuccess": "{amount} mL water added.", - "editTotal": "Edit today's total" + "referenceMeta": "Reference intake {value} {unit}/day · EFSA" }, "caffeine": { "ceilingMeta": "Safe-level ceiling {value} {unit}/day · EFSA" @@ -10285,7 +10276,7 @@ "description": "Your own address book — type a practice once, pick it everywhere.", "linkLabel": "Address book", "add": "Entry", - "searchPlaceholder": "Search name or practice", + "searchPlaceholder": "Search name, practice or specialty", "loadError": "The address book could not be loaded.", "emptyTitle": "No entry yet", "emptyDescription": "Add a practice so you can pick it when you record a visit.", @@ -10304,6 +10295,17 @@ "saveFailed": "The entry could not be saved.", "deleteTitle": "Delete this entry?", "deleteDescription": "Your existing visits stay, and they keep this name.", - "deleteFailed": "The entry could not be deleted." + "deleteFailed": "The entry could not be deleted.", + "noSpecialtyGroup": "No specialty" + }, + "links": { + "picker": { + "add": "Add", + "done": "Done", + "selectAll": "Select all", + "searchPlaceholder": "Search", + "searchNoMatch": "No match", + "remove": "Remove" + } } } diff --git a/messages/es.json b/messages/es.json index 0ba944055..209c6eeb5 100644 --- a/messages/es.json +++ b/messages/es.json @@ -349,9 +349,7 @@ "medication": "Medicamento", "medicationDescription": "Marcar una dosis como tomada", "mood": "Estado de ánimo", - "moodDescription": "Registra tu estado de ánimo y etiquetas", - "water": "Agua", - "waterDescription": "Añade rápidamente tu consumo de agua" + "moodDescription": "Registra tu estado de ánimo y etiquetas" }, "cycle": "Ciclo", "labs": "Análisis", @@ -455,7 +453,6 @@ "quickAddMeasurement": "Registrar medición", "quickAddMood": "Registrar estado de ánimo", "quickAddMedicationIntake": "Registrar toma", - "quickAddWater": "Registrar agua", "customizeDashboard": "Personalizar panel", "medicationIntakeQuickAdd": { "sheetTitle": "Registrar toma de medicación", @@ -9654,17 +9651,11 @@ "dayInvalid": "Las entradas llevaban una fecha que el servidor no pudo aceptar. Esto puede ocurrir cuando el reloj o la zona horaria del lado que envía los datos están mal configurados; merece la pena revisarlo.", "upsertFailed": "Estas entradas llegaron al servidor, pero un problema de nuestro lado impidió guardarlas. No tienes que hacer nada.", "unknown": "Estas entradas llegaron, pero no se pudieron guardar." - } + }, + "moduleEnableError": "No se pudo activar el módulo de nutrientes. Inténtalo de nuevo." }, "hydration": { - "referenceMeta": "Ingesta de referencia {value} {unit}/día · EFSA", - "quickAddTitle": "Añadir agua", - "quickAddCustomLabel": "Cantidad personalizada (mL)", - "quickAddCustomPlaceholder": "250", - "quickAddSubmit": "Añadir", - "quickAddError": "No se pudo guardar. Inténtalo de nuevo.", - "quickAddSuccess": "Se añadieron {amount} mL de agua.", - "editTotal": "Editar el total de hoy" + "referenceMeta": "Ingesta de referencia {value} {unit}/día · EFSA" }, "caffeine": { "ceilingMeta": "Límite de seguridad {value} {unit}/día · EFSA" @@ -10285,7 +10276,7 @@ "description": "Tu propia agenda: escribe una consulta una vez y elígela en todas partes.", "linkLabel": "Agenda", "add": "Entrada", - "searchPlaceholder": "Buscar nombre o consulta", + "searchPlaceholder": "Buscar nombre, consulta o especialidad", "loadError": "No se pudo cargar la agenda.", "emptyTitle": "Todavía no hay entradas", "emptyDescription": "Añade una consulta para poder elegirla al registrar una cita.", @@ -10304,6 +10295,17 @@ "saveFailed": "No se pudo guardar la entrada.", "deleteTitle": "¿Eliminar esta entrada?", "deleteDescription": "Tus citas anteriores se mantienen y conservan este nombre.", - "deleteFailed": "No se pudo eliminar la entrada." + "deleteFailed": "No se pudo eliminar la entrada.", + "noSpecialtyGroup": "Sin especialidad" + }, + "links": { + "picker": { + "add": "Añadir", + "done": "Listo", + "selectAll": "Seleccionar todo", + "searchPlaceholder": "Buscar", + "searchNoMatch": "Sin coincidencias", + "remove": "Quitar" + } } } diff --git a/messages/fr.json b/messages/fr.json index 4f8344eb7..dee4a0860 100644 --- a/messages/fr.json +++ b/messages/fr.json @@ -349,9 +349,7 @@ "medication": "Médicament", "medicationDescription": "Marquer une dose comme prise", "mood": "Humeur", - "moodDescription": "Enregistrez votre humeur et des étiquettes", - "water": "Eau", - "waterDescription": "Ajoutez rapidement votre consommation d’eau" + "moodDescription": "Enregistrez votre humeur et des étiquettes" }, "cycle": "Cycle", "labs": "Analyses", @@ -455,7 +453,6 @@ "quickAddMeasurement": "Saisir une mesure", "quickAddMood": "Saisir une humeur", "quickAddMedicationIntake": "Saisir une prise", - "quickAddWater": "Saisir de l’eau", "customizeDashboard": "Personnaliser le tableau de bord", "medicationIntakeQuickAdd": { "sheetTitle": "Saisir une prise de médicament", @@ -9654,17 +9651,11 @@ "dayInvalid": "Les entrées portaient une date que le serveur n'a pas pu accepter. Cela peut arriver quand l'horloge ou le fuseau horaire du côté qui envoie les données est incorrect — cela vaut la peine de vérifier.", "upsertFailed": "Ces entrées sont arrivées jusqu'au serveur, mais un problème de notre côté a empêché leur enregistrement. Tu n'as rien à faire.", "unknown": "Ces entrées sont arrivées mais n'ont pas pu être enregistrées." - } + }, + "moduleEnableError": "Le module nutriments n'a pas pu être activé. Réessaie." }, "hydration": { - "referenceMeta": "Apport de référence {value} {unit}/jour · EFSA", - "quickAddTitle": "Ajouter de l'eau", - "quickAddCustomLabel": "Quantité personnalisée (mL)", - "quickAddCustomPlaceholder": "250", - "quickAddSubmit": "Ajouter", - "quickAddError": "Échec de l'enregistrement — réessayez.", - "quickAddSuccess": "{amount} mL d’eau ajoutés.", - "editTotal": "Modifier le total du jour" + "referenceMeta": "Apport de référence {value} {unit}/jour · EFSA" }, "caffeine": { "ceilingMeta": "Plafond de sécurité {value} {unit}/jour · EFSA" @@ -10285,7 +10276,7 @@ "description": "Votre carnet d'adresses : saisissez un cabinet une fois, choisissez-le partout.", "linkLabel": "Carnet d'adresses", "add": "Entrée", - "searchPlaceholder": "Rechercher un nom ou un cabinet", + "searchPlaceholder": "Rechercher un nom, un cabinet ou une spécialité", "loadError": "Impossible de charger le carnet d'adresses.", "emptyTitle": "Aucune entrée", "emptyDescription": "Ajoutez un cabinet pour pouvoir le choisir lors d'un rendez-vous.", @@ -10304,6 +10295,17 @@ "saveFailed": "L'entrée n'a pas pu être enregistrée.", "deleteTitle": "Supprimer cette entrée ?", "deleteDescription": "Vos rendez-vous existants restent et conservent ce nom.", - "deleteFailed": "L'entrée n'a pas pu être supprimée." + "deleteFailed": "L'entrée n'a pas pu être supprimée.", + "noSpecialtyGroup": "Sans spécialité" + }, + "links": { + "picker": { + "add": "Ajouter", + "done": "Terminé", + "selectAll": "Tout sélectionner", + "searchPlaceholder": "Rechercher", + "searchNoMatch": "Aucun résultat", + "remove": "Retirer" + } } } diff --git a/messages/it.json b/messages/it.json index 09ab45195..db1fccb71 100644 --- a/messages/it.json +++ b/messages/it.json @@ -349,9 +349,7 @@ "medication": "Farmaco", "medicationDescription": "Segna una dose come assunta", "mood": "Umore", - "moodDescription": "Registra umore e tag", - "water": "Acqua", - "waterDescription": "Aggiungi rapidamente l’acqua bevuta" + "moodDescription": "Registra umore e tag" }, "cycle": "Ciclo", "labs": "Esami", @@ -455,7 +453,6 @@ "quickAddMeasurement": "Registra misurazione", "quickAddMood": "Registra umore", "quickAddMedicationIntake": "Registra assunzione", - "quickAddWater": "Registra acqua", "customizeDashboard": "Personalizza dashboard", "medicationIntakeQuickAdd": { "sheetTitle": "Registra assunzione del farmaco", @@ -9654,17 +9651,11 @@ "dayInvalid": "Le voci riportavano una data che il server non ha potuto accettare. Questo può succedere quando l'orologio o il fuso orario dal lato che invia i dati non sono corretti: vale la pena controllare.", "upsertFailed": "Queste voci hanno raggiunto il server, ma un problema da questa parte ne ha impedito il salvataggio. Non devi fare nulla.", "unknown": "Queste voci sono arrivate ma non è stato possibile salvarle." - } + }, + "moduleEnableError": "Non è stato possibile attivare il modulo nutrienti. Riprova." }, "hydration": { - "referenceMeta": "Apporto di riferimento {value} {unit}/giorno · EFSA", - "quickAddTitle": "Aggiungi acqua", - "quickAddCustomLabel": "Quantità personalizzata (mL)", - "quickAddCustomPlaceholder": "250", - "quickAddSubmit": "Aggiungi", - "quickAddError": "Salvataggio non riuscito. Riprova.", - "quickAddSuccess": "Aggiunti {amount} mL di acqua.", - "editTotal": "Modifica il totale di oggi" + "referenceMeta": "Apporto di riferimento {value} {unit}/giorno · EFSA" }, "caffeine": { "ceilingMeta": "Limite di sicurezza {value} {unit}/giorno · EFSA" @@ -10285,7 +10276,7 @@ "description": "La tua rubrica: scrivi uno studio una volta e selezionalo ovunque.", "linkLabel": "Rubrica", "add": "Voce", - "searchPlaceholder": "Cerca nome o studio", + "searchPlaceholder": "Cerca nome, studio o specializzazione", "loadError": "Impossibile caricare la rubrica.", "emptyTitle": "Nessuna voce", "emptyDescription": "Aggiungi uno studio per poterlo scegliere quando registri un appuntamento.", @@ -10304,6 +10295,17 @@ "saveFailed": "Non è stato possibile salvare la voce.", "deleteTitle": "Eliminare questa voce?", "deleteDescription": "I tuoi appuntamenti esistenti restano e conservano questo nome.", - "deleteFailed": "Non è stato possibile eliminare la voce." + "deleteFailed": "Non è stato possibile eliminare la voce.", + "noSpecialtyGroup": "Senza specializzazione" + }, + "links": { + "picker": { + "add": "Aggiungi", + "done": "Fatto", + "selectAll": "Seleziona tutto", + "searchPlaceholder": "Cerca", + "searchNoMatch": "Nessun risultato", + "remove": "Rimuovi" + } } } diff --git a/messages/pl.json b/messages/pl.json index 473e7c6fa..608997b21 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -349,9 +349,7 @@ "medication": "Lek", "medicationDescription": "Oznacz dawkę jako przyjętą", "mood": "Nastrój", - "moodDescription": "Zapisz nastrój i tagi", - "water": "Woda", - "waterDescription": "Szybko dodaj ilość wypitej wody" + "moodDescription": "Zapisz nastrój i tagi" }, "cycle": "Cykl", "labs": "Wyniki badań", @@ -455,7 +453,6 @@ "quickAddMeasurement": "Zarejestruj pomiar", "quickAddMood": "Zarejestruj nastrój", "quickAddMedicationIntake": "Zarejestruj przyjęcie", - "quickAddWater": "Zapisz wodę", "customizeDashboard": "Dostosuj pulpit", "medicationIntakeQuickAdd": { "sheetTitle": "Zarejestruj przyjęcie leku", @@ -9654,17 +9651,11 @@ "dayInvalid": "Wpisy zawierały datę, której serwer nie mógł zaakceptować. Może się to zdarzyć, gdy zegar lub strefa czasowa po stronie wysyłającej są nieprawidłowe — warto to sprawdzić.", "upsertFailed": "Te wpisy dotarły do serwera, ale problem po naszej stronie uniemożliwił ich zapisanie. Nie musisz nic robić.", "unknown": "Te wpisy dotarły, ale nie udało się ich zapisać." - } + }, + "moduleEnableError": "Nie udało się włączyć modułu składników odżywczych. Spróbuj ponownie." }, "hydration": { - "referenceMeta": "Zalecane spożycie {value} {unit}/dzień · EFSA", - "quickAddTitle": "Dodaj wodę", - "quickAddCustomLabel": "Własna ilość (mL)", - "quickAddCustomPlaceholder": "250", - "quickAddSubmit": "Dodaj", - "quickAddError": "Nie udało się zapisać — spróbuj ponownie.", - "quickAddSuccess": "Dodano {amount} mL wody.", - "editTotal": "Edytuj dzisiejszą sumę" + "referenceMeta": "Zalecane spożycie {value} {unit}/dzień · EFSA" }, "caffeine": { "ceilingMeta": "Bezpieczny limit {value} {unit}/dzień · EFSA" @@ -10285,7 +10276,7 @@ "description": "Twoja własna książka adresowa — wpisz gabinet raz, wybieraj go wszędzie.", "linkLabel": "Książka adresowa", "add": "Wpis", - "searchPlaceholder": "Szukaj nazwiska lub gabinetu", + "searchPlaceholder": "Szukaj nazwiska, gabinetu lub specjalizacji", "loadError": "Nie udało się wczytać książki adresowej.", "emptyTitle": "Brak wpisów", "emptyDescription": "Dodaj gabinet, aby móc go wybrać przy zapisywaniu wizyty.", @@ -10304,6 +10295,17 @@ "saveFailed": "Nie udało się zapisać wpisu.", "deleteTitle": "Usunąć ten wpis?", "deleteDescription": "Twoje dotychczasowe wizyty pozostają i zachowują tę nazwę.", - "deleteFailed": "Nie udało się usunąć wpisu." + "deleteFailed": "Nie udało się usunąć wpisu.", + "noSpecialtyGroup": "Bez specjalizacji" + }, + "links": { + "picker": { + "add": "Dodaj", + "done": "Gotowe", + "selectAll": "Zaznacz wszystko", + "searchPlaceholder": "Szukaj", + "searchNoMatch": "Brak wyników", + "remove": "Usuń" + } } } diff --git a/src/__tests__/success-affordance-guard.test.ts b/src/__tests__/success-affordance-guard.test.ts index 3114e8a16..a7a984d9e 100644 --- a/src/__tests__/success-affordance-guard.test.ts +++ b/src/__tests__/success-affordance-guard.test.ts @@ -193,9 +193,6 @@ const PINNED_AFFORDANCES: Record< "src/components/insights/mood/mood-tag-metric-crosstab.tsx": { "text-success": 1, }, - "src/components/insights/nutrients/water-quick-add-sheet.tsx": { - "toast.success": 2, - }, "src/components/insights/personal-record-badge.tsx": { "text-success": 1 }, "src/components/insights/recommendation-feedback.tsx": { "text-success": 3 }, "src/components/labs/biomarker-form.tsx": { "toast.success": 1 }, diff --git a/src/app/__tests__/quick-add-labels.test.ts b/src/app/__tests__/quick-add-labels.test.ts index 1d24e4b70..24d04508d 100644 --- a/src/app/__tests__/quick-add-labels.test.ts +++ b/src/app/__tests__/quick-add-labels.test.ts @@ -40,7 +40,6 @@ interface Messages { * Hinzufügen, Hinzufügen, Hinzufügen" with no way to discriminate. */ quickAddMedicationIntake: string; - quickAddWater: string; }; } @@ -69,23 +68,18 @@ describe("dashboard quick-add submenu labels", () => { const measurement = messages.dashboard.quickAddMeasurement; const mood = messages.dashboard.quickAddMood; const medicationIntake = messages.dashboard.quickAddMedicationIntake; - const water = messages.dashboard.quickAddWater; const trigger = messages.common.add; // Non-empty expect(measurement.trim().length).toBeGreaterThan(0); expect(mood.trim().length).toBeGreaterThan(0); expect(medicationIntake.trim().length).toBeGreaterThan(0); - expect(water.trim().length).toBeGreaterThan(0); // Distinct from each other — the icon is decorative (aria-hidden), // so the only thing distinguishing the rows is the visible text. expect(measurement).not.toBe(mood); expect(measurement).not.toBe(medicationIntake); expect(mood).not.toBe(medicationIntake); - expect(measurement).not.toBe(water); - expect(mood).not.toBe(water); - expect(medicationIntake).not.toBe(water); // Distinct from the trigger label. The trigger sits ABOVE the menu // and announces itself first; if a menu item then repeats the same @@ -93,7 +87,6 @@ describe("dashboard quick-add submenu labels", () => { expect(measurement).not.toBe(trigger); expect(mood).not.toBe(trigger); expect(medicationIntake).not.toBe(trigger); - expect(water).not.toBe(trigger); }, ); }); diff --git a/src/app/api/practitioners/route.ts b/src/app/api/practitioners/route.ts index bac66da4b..cbd8fcabc 100644 --- a/src/app/api/practitioners/route.ts +++ b/src/app/api/practitioners/route.ts @@ -50,10 +50,11 @@ export const GET = apiHandler(async (request: NextRequest) => { where: { userId: user.id, deletedAt: null, - // Both plaintext columns, because a person looks a practice up by - // whichever of the two they remember: the doctor's name on the referral - // or the practice name on the door. Searching only the name made the - // second attempt return nothing and read as "not in the address book". + // Three plaintext columns, because a person looks a practice up by + // whichever they remember: the doctor's name on the referral, the + // practice name on the door, or the field they need ("Zahnmedizin"). + // Searching only name + practice made the specialty attempt return + // nothing and read as "not in the address book". ...(parsed.data.q ? { OR: [ @@ -69,6 +70,12 @@ export const GET = apiHandler(async (request: NextRequest) => { mode: "insensitive" as const, }, }, + { + specialty: { + contains: parsed.data.q, + mode: "insensitive" as const, + }, + }, ], } : {}), diff --git a/src/app/insights/nutrients/page.tsx b/src/app/insights/nutrients/page.tsx index 0790865e5..7e8b03008 100644 --- a/src/app/insights/nutrients/page.tsx +++ b/src/app/insights/nutrients/page.tsx @@ -100,7 +100,7 @@ export default function InsightsNutrientsPage() { onSuccess: () => { void queryClient.invalidateQueries({ queryKey: queryKeys.authMe() }); }, - onError: () => toast.error(t("nutrients.hydration.quickAddError")), + onError: () => toast.error(t("nutrients.page.moduleEnableError")), }); if (!nutrientsEnabled) { diff --git a/src/components/__tests__/delegated-write-affordances.test.tsx b/src/components/__tests__/delegated-write-affordances.test.tsx index c8a14fd45..9e800346c 100644 --- a/src/components/__tests__/delegated-write-affordances.test.tsx +++ b/src/components/__tests__/delegated-write-affordances.test.tsx @@ -369,33 +369,27 @@ describe("linking a document to an illness episode", () => { }); describe("the capture picker's kinds", () => { - const ALL = ["measurement", "medication", "mood", "water"] as const; + const ALL = ["measurement", "medication", "mood"] as const; it("offers everything in the caller's own record", () => { expect( - visibleCaptureKinds({ canAdd: true, canManage: true }, true, [...ALL]), - ).toEqual(["measurement", "medication", "mood", "water"]); + visibleCaptureKinds({ canAdd: true, canManage: true }, [...ALL]), + ).toEqual(["measurement", "medication", "mood"]); }); it("offers a delegate only what the delegation admits", () => { - // A reading and a dose are admitted verbs. A mood entry and a glass of - // water are not, and the server refuses them under a switch. + // A reading and a dose are admitted verbs. A mood entry is not, and the + // server refuses it under a switch. expect( - visibleCaptureKinds({ canAdd: true, canManage: false }, true, [...ALL]), + visibleCaptureKinds({ canAdd: true, canManage: false }, [...ALL]), ).toEqual(["measurement", "medication"]); }); it("offers a read-only delegate nothing", () => { expect( - visibleCaptureKinds({ canAdd: false, canManage: false }, true, [...ALL]), + visibleCaptureKinds({ canAdd: false, canManage: false }, [...ALL]), ).toEqual([]); }); - - it("still honours the module gate for the owner", () => { - expect( - visibleCaptureKinds({ canAdd: true, canManage: true }, false, [...ALL]), - ).toEqual(["measurement", "medication", "mood"]); - }); }); /* -------------------------------------------------------------------------- */ @@ -471,13 +465,10 @@ describe("a form opened before the record answered", () => { * top of the file applies. */ it("withdraws a capture form the shrunken offer no longer holds", () => { - // The chooser offered four; the answer arrives and offers two. + // The chooser offered three; the answer arrives and offers two. expect(admittedCaptureKind("mood", ["measurement", "medication"])).toBe( null, ); - expect(admittedCaptureKind("water", ["measurement", "medication"])).toBe( - null, - ); // …and leaves an admitted one exactly where it was. expect( admittedCaptureKind("medication", ["measurement", "medication"]), @@ -488,7 +479,6 @@ describe("a form opened before the record answered", () => { it("withdraws a dashboard quick-entry sheet the delegation does not admit", () => { const DELEGATE = { canAdd: true, canManage: false }; expect(admittedQuickEntry("mood", DELEGATE)).toBe(null); - expect(admittedQuickEntry("water", DELEGATE)).toBe(null); expect(admittedQuickEntry("measurement", DELEGATE)).toBe("measurement"); expect(admittedQuickEntry("medicationIntake", DELEGATE)).toBe( "medicationIntake", @@ -497,24 +487,14 @@ describe("a form opened before the record answered", () => { it("withdraws every quick-entry sheet from a read-only delegate", () => { const READER = { canAdd: false, canManage: false }; - for (const sheet of [ - "measurement", - "mood", - "medicationIntake", - "water", - ] as const) { + for (const sheet of ["measurement", "mood", "medicationIntake"] as const) { expect(admittedQuickEntry(sheet, READER), sheet).toBe(null); } }); it("leaves the owner's own sheets alone", () => { const OWNER_CAPS = { canAdd: true, canManage: true }; - for (const sheet of [ - "measurement", - "mood", - "medicationIntake", - "water", - ] as const) { + for (const sheet of ["measurement", "mood", "medicationIntake"] as const) { expect(admittedQuickEntry(sheet, OWNER_CAPS), sheet).toBe(sheet); } expect(admittedQuickEntry(null, OWNER_CAPS)).toBe(null); diff --git a/src/components/dashboard/__tests__/dashboard-header.test.tsx b/src/components/dashboard/__tests__/dashboard-header.test.tsx index 14cd4c252..4f7ae8422 100644 --- a/src/components/dashboard/__tests__/dashboard-header.test.tsx +++ b/src/components/dashboard/__tests__/dashboard-header.test.tsx @@ -16,13 +16,6 @@ import { renderToStaticMarkup } from "react-dom/server"; import { I18nProvider } from "@/lib/i18n/context"; -const moduleGate = vi.hoisted(() => ({ nutrientsEnabled: true })); - -vi.mock("@/hooks/use-module-enabled", () => ({ - useModuleEnabled: (moduleKey: string) => - moduleKey === "nutrients" ? moduleGate.nutrientsEnabled : true, -})); - vi.mock("@/components/ui/dropdown-menu", () => ({ DropdownMenu: ({ children }: { children: React.ReactNode }) => ( <>{children} @@ -77,29 +70,18 @@ describe(" — greeting", () => { }); }); -describe(" — nutrients module gate", () => { - it("hides only the water quick-add item when nutrients are disabled", () => { - moduleGate.nutrientsEnabled = false; - +describe(" — quick-add menu", () => { + it("offers measurement, mood and medication, and no water entry", () => { const html = renderSSR( undefined} />, "en", ); - expect(html).not.toContain("Log water"); expect(html).toContain("Log measurement"); expect(html).toContain("Log mood"); expect(html).toContain("Log medication intake"); - }); - - it("keeps the water quick-add item available when nutrients are enabled", () => { - moduleGate.nutrientsEnabled = true; - - const html = renderSSR( - undefined} />, - "en", - ); - - expect(html).toContain("Log water"); + // Water logging was removed from the app; the dashboard quick-add offers + // no water entry. + expect(html).not.toContain("Log water"); }); }); diff --git a/src/components/dashboard/dashboard-header.tsx b/src/components/dashboard/dashboard-header.tsx index 868359f06..e502f9456 100644 --- a/src/components/dashboard/dashboard-header.tsx +++ b/src/components/dashboard/dashboard-header.tsx @@ -12,7 +12,7 @@ */ import { useMemo } from "react"; import Link from "next/link"; -import { Activity, GlassWater, Pill, Plus, Waves, Wrench } from "lucide-react"; +import { Activity, Pill, Plus, Waves, Wrench } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -24,7 +24,6 @@ import { PageHeader } from "@/components/ui/page-header"; import { useTranslations } from "@/lib/i18n/context"; import { useAuth } from "@/hooks/use-auth"; import { useMounted } from "@/hooks/use-mounted"; -import { useModuleEnabled } from "@/hooks/use-module-enabled"; import { useRecordCapabilities } from "@/hooks/use-record-capabilities"; import { getHourForTimeZone } from "@/components/dashboard/range-display"; import type { QuickEntryDialog } from "@/components/dashboard/quick-entry-sheets"; @@ -37,11 +36,10 @@ export function DashboardHeader({ const { t } = useTranslations(); const { user } = useAuth(); const mounted = useMounted(); - const nutrientsEnabled = useModuleEnabled("nutrients"); - // v1.36.x — the dashboard quick-add offers four kinds; a delegation admits - // two of them (a reading, a dose). Mood and water stay with the account - // whose record it is, and the customize shortcut points at a settings page - // sharing does not cover at all. + // v1.36.x — the dashboard quick-add offers three kinds; a delegation admits + // two of them (a reading, a dose). Mood stays with the account whose record + // it is, and the customize shortcut points at a settings page sharing does + // not cover at all. const { canAdd, canManage } = useRecordCapabilities(); // The pre-hero greeting derivation, kept hydration-safe: `user` comes @@ -166,12 +164,6 @@ export function DashboardHeader({