mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
feat(vaccinations): add the immunization log route, nav entry and shared-record family
- register /vaccinations in the clinical nav spine, module-gated on vaccinations (default-on) and part of the shared record - join /vaccinations to the profile shared-record route family so a scoped profile grant can present the page it is allowed to read - add the page shell: authenticated, module-gated, with the standard module-off redirect the medications surface uses - add the nav label and the surface title/subtitle/empty copy in six locales
This commit is contained in:
1 parent
11a6bc40c0
commit
eefc151617
11 files changed
+176
-2
No files matched your search
@@ -354,6 +354,7 @@
|
||||
"vorsorge": "Vorsorge",
|
||||
"illness": "Krankheit",
|
||||
"documents": "Dokumente",
|
||||
"vaccinations": "Impfungen",
|
||||
"mentalWellbeing": "Seelisches Wohlbefinden"
|
||||
},
|
||||
"auth": {
|
||||
@@ -8514,6 +8515,14 @@
|
||||
"description": "Der Impfpass in strukturierter Form: Dosis, Serie, nächste Auffrischung."
|
||||
}
|
||||
},
|
||||
"vaccinations": {
|
||||
"title": "Impfungen",
|
||||
"subtitle": "Dein Impfpass, Dosis für Dosis.",
|
||||
"empty": {
|
||||
"title": "Noch keine Impfung erfasst",
|
||||
"description": "Übertrage deinen Impfpass Zeile für Zeile — Datum und Impfstoff genügen."
|
||||
}
|
||||
},
|
||||
"illness": {
|
||||
"title": "Krankheitstagebuch",
|
||||
"subtitle": "Ein rückblickendes Protokoll deiner Krankheits- und Beschwerde-Episoden.",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"vorsorge": "Checkups",
|
||||
"illness": "Illness",
|
||||
"documents": "Documents",
|
||||
"vaccinations": "Vaccinations",
|
||||
"mentalWellbeing": "Mental wellbeing"
|
||||
},
|
||||
"auth": {
|
||||
@@ -8514,6 +8515,14 @@
|
||||
"description": "The vaccination record, structured: dose, series, next booster."
|
||||
}
|
||||
},
|
||||
"vaccinations": {
|
||||
"title": "Vaccinations",
|
||||
"subtitle": "Your immunization record, dose by dose.",
|
||||
"empty": {
|
||||
"title": "No vaccination logged yet",
|
||||
"description": "Transcribe your vaccination card line by line — a date and the vaccine are enough."
|
||||
}
|
||||
},
|
||||
"illness": {
|
||||
"title": "Illness journal",
|
||||
"subtitle": "A retrospective log of your illness and condition episodes.",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"vorsorge": "Revisiones",
|
||||
"illness": "Enfermedad",
|
||||
"documents": "Documentos",
|
||||
"vaccinations": "Vacunas",
|
||||
"mentalWellbeing": "Bienestar mental"
|
||||
},
|
||||
"auth": {
|
||||
@@ -8514,6 +8515,14 @@
|
||||
"description": "La cartilla de vacunación estructurada: dosis, serie, próximo refuerzo."
|
||||
}
|
||||
},
|
||||
"vaccinations": {
|
||||
"title": "Vacunas",
|
||||
"subtitle": "Tu registro de vacunación, dosis a dosis.",
|
||||
"empty": {
|
||||
"title": "Aún no hay ninguna vacuna registrada",
|
||||
"description": "Transcribe tu cartilla de vacunación línea por línea: bastan una fecha y la vacuna."
|
||||
}
|
||||
},
|
||||
"illness": {
|
||||
"title": "Diario de enfermedad",
|
||||
"subtitle": "Un registro retrospectivo de tus episodios de enfermedad y dolencias.",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"vorsorge": "Examens",
|
||||
"illness": "Maladie",
|
||||
"documents": "Documents",
|
||||
"vaccinations": "Vaccinations",
|
||||
"mentalWellbeing": "Bien-être mental"
|
||||
},
|
||||
"auth": {
|
||||
@@ -8514,6 +8515,14 @@
|
||||
"description": "Le carnet de vaccination structuré : dose, série, prochain rappel."
|
||||
}
|
||||
},
|
||||
"vaccinations": {
|
||||
"title": "Vaccinations",
|
||||
"subtitle": "Votre carnet de vaccination, dose par dose.",
|
||||
"empty": {
|
||||
"title": "Aucune vaccination enregistrée",
|
||||
"description": "Recopiez votre carnet de vaccination ligne par ligne : une date et le vaccin suffisent."
|
||||
}
|
||||
},
|
||||
"illness": {
|
||||
"title": "Journal de maladie",
|
||||
"subtitle": "Un suivi rétrospectif de vos épisodes de maladie et d'affections.",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"vorsorge": "Controlli",
|
||||
"illness": "Malattia",
|
||||
"documents": "Documenti",
|
||||
"vaccinations": "Vaccinazioni",
|
||||
"mentalWellbeing": "Benessere mentale"
|
||||
},
|
||||
"auth": {
|
||||
@@ -8514,6 +8515,14 @@
|
||||
"description": "Il libretto vaccinale strutturato: dose, serie, prossimo richiamo."
|
||||
}
|
||||
},
|
||||
"vaccinations": {
|
||||
"title": "Vaccinazioni",
|
||||
"subtitle": "Il tuo libretto vaccinale, dose per dose.",
|
||||
"empty": {
|
||||
"title": "Nessuna vaccinazione registrata",
|
||||
"description": "Trascrivi il tuo libretto vaccinale riga per riga: bastano una data e il vaccino."
|
||||
}
|
||||
},
|
||||
"illness": {
|
||||
"title": "Diario delle malattie",
|
||||
"subtitle": "Un registro retrospettivo dei tuoi episodi di malattia e disturbi.",
|
||||
|
||||
@@ -354,6 +354,7 @@
|
||||
"vorsorge": "Badania",
|
||||
"illness": "Choroba",
|
||||
"documents": "Dokumenty",
|
||||
"vaccinations": "Szczepienia",
|
||||
"mentalWellbeing": "Dobrostan psychiczny"
|
||||
},
|
||||
"auth": {
|
||||
@@ -8514,6 +8515,14 @@
|
||||
"description": "Książeczka szczepień uporządkowana: dawka, seria, następne przypomnienie."
|
||||
}
|
||||
},
|
||||
"vaccinations": {
|
||||
"title": "Szczepienia",
|
||||
"subtitle": "Twoja karta szczepień, dawka po dawce.",
|
||||
"empty": {
|
||||
"title": "Nie zarejestrowano jeszcze żadnego szczepienia",
|
||||
"description": "Przepisz swoją kartę szczepień wiersz po wierszu — wystarczą data i szczepionka."
|
||||
}
|
||||
},
|
||||
"illness": {
|
||||
"title": "Dziennik chorób",
|
||||
"subtitle": "Retrospektywny zapis Twoich epizodów chorób i dolegliwości.",
|
||||
|
||||
@@ -47,9 +47,15 @@ describe("shared-record navigation", () => {
|
||||
expect(SHARED_RECORD_DOMAIN_ROUTE_FAMILIES.profile).toEqual([
|
||||
"/profile",
|
||||
"/checkups",
|
||||
"/vaccinations",
|
||||
]);
|
||||
expect(navigation.destinationHrefs).toEqual([
|
||||
"/profile",
|
||||
"/checkups",
|
||||
"/vaccinations",
|
||||
]);
|
||||
expect(navigation.destinationHrefs).toEqual(["/profile", "/checkups"]);
|
||||
expect(navigation.allowsPath("/profile")).toBe(true);
|
||||
expect(navigation.allowsPath("/vaccinations")).toBe(true);
|
||||
expect(navigation.allowsPath("/settings/anamnesis")).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
|
||||
import { PageAuthGate } from "@/components/ui/page-auth-gate";
|
||||
import { VaccinationsView } from "@/components/vaccinations/vaccinations-view";
|
||||
|
||||
/**
|
||||
* v1.38.0 — the immunization log entry.
|
||||
*
|
||||
* Gated on the resolved `modules.vaccinations` flag from `GET /api/auth/me`.
|
||||
* The module is default-on, so `=== false` is the only state that hides the
|
||||
* surface — an account that never touched the toggle keeps it. An
|
||||
* unauthenticated visitor is bounced to login; an authenticated account that
|
||||
* turned the module off is bounced home (the nav entry is already gone for
|
||||
* them, so this only catches a direct URL hit). The `/api/vaccinations*`
|
||||
* routes stay reachable regardless — the module gate governs the surface, not
|
||||
* the row store — so this is a UX redirect, never the security boundary.
|
||||
*/
|
||||
export default function VaccinationsPage() {
|
||||
const { user, isLoading, isAuthenticated } = useAuth();
|
||||
const { inSharedRecord } = useRecordCapabilities();
|
||||
const router = useRouter();
|
||||
|
||||
// Default-on: hidden only when the resolved map says `false` explicitly. A
|
||||
// shared-record session always sees it — the delegate's own module map does
|
||||
// not govern the owner's record.
|
||||
const enabled = inSharedRecord || user?.modules?.vaccinations !== false;
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) return;
|
||||
if (!isAuthenticated) {
|
||||
router.push("/auth/login");
|
||||
} else if (!enabled) {
|
||||
router.push("/");
|
||||
}
|
||||
}, [isLoading, isAuthenticated, enabled, router]);
|
||||
|
||||
if (isLoading || !isAuthenticated || !enabled) {
|
||||
return <PageAuthGate />;
|
||||
}
|
||||
|
||||
return <VaccinationsView />;
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Pill,
|
||||
Settings,
|
||||
Stethoscope,
|
||||
Syringe,
|
||||
Thermometer,
|
||||
Trophy,
|
||||
Waves,
|
||||
@@ -193,6 +194,20 @@ export const NAV_DESTINATIONS: ReadonlyArray<NavDestination> = [
|
||||
tourId: "nav-illness",
|
||||
requiresModule: "illness",
|
||||
},
|
||||
// v1.38.0 — the immunization log sits in the clinical spine beside Illness.
|
||||
// Born-gated: `requiresModule: "vaccinations"` reads the resolved module map,
|
||||
// so the entry is absent until the account keeps the module on (default-on —
|
||||
// it drops only when a user turns it off). SURFACE-gated: the `/api/vaccinations*`
|
||||
// data routes stay reachable so a restore / import keeps working and
|
||||
// re-enabling finds every dose intact.
|
||||
{
|
||||
href: "/vaccinations",
|
||||
sharedRecord: true,
|
||||
tKey: "nav.vaccinations",
|
||||
icon: Syringe,
|
||||
tourId: "nav-vaccinations",
|
||||
requiresModule: "vaccinations",
|
||||
},
|
||||
// v1.18.0 — Workouts and Recovery both left the left-nav: each already
|
||||
// surfaces as an Insights tab-strip pill (`/insights/workouts` gated on
|
||||
// a workout row, `/insights/recovery` always present), so neither is a
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* v1.38.0 — the immunization log surface.
|
||||
*
|
||||
* A calm, retrospective transcription of a lifetime Impfpass. The list groups
|
||||
* doses by their component antigen and shows where each series stands, with the
|
||||
* numbers resolved server-side (`src/lib/vaccinations/series.ts`) so this
|
||||
* client never re-derives "N von M". Neutral cards, status through a discreet
|
||||
* badge only — no red card, no overdue tint. It reproduces the record; it does
|
||||
* not adjudicate what is due.
|
||||
*
|
||||
* The list itself arrives with the capture surface; this shell owns the page
|
||||
* header, the empty invitation, and the load/error states every module surface
|
||||
* shares.
|
||||
*/
|
||||
import { Plus, Syringe } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { PageHeader } from "@/components/ui/page-header";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
export function VaccinationsView() {
|
||||
const { t } = useTranslations();
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title={<span data-tour-id="vaccinations-hero">{t("vaccinations.title")}</span>}
|
||||
description={t("vaccinations.subtitle")}
|
||||
/>
|
||||
|
||||
<EmptyState
|
||||
icon={<Syringe className="size-6" />}
|
||||
title={t("vaccinations.empty.title")}
|
||||
description={t("vaccinations.empty.description")}
|
||||
action={
|
||||
<Button className="min-h-11 sm:min-h-9" disabled>
|
||||
<Plus className="size-4" />
|
||||
{t("common.add")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -30,7 +30,12 @@ export const SHARED_RECORD_DOMAIN_ROUTE_FAMILIES = {
|
||||
measurements: ["/measurements", "/checkups"],
|
||||
medications: ["/medications"],
|
||||
labs: ["/labs"],
|
||||
profile: ["/profile", "/checkups"],
|
||||
// `/vaccinations` joins `profile`: an immunization history is health
|
||||
// background in the same sense as the visits already listed here, so a
|
||||
// delegate scoped to `profile` must be able to present the page the API
|
||||
// already lets them read — without this line the grant reads the data and
|
||||
// the chrome hides the only page that shows it.
|
||||
profile: ["/profile", "/checkups", "/vaccinations"],
|
||||
illness: ["/illness"],
|
||||
mind: ["/mood", "/mental-wellbeing"],
|
||||
cycle: ["/cycle"],
|
||||
|
||||
Reference in new issue
Block a user