Show the injection site in the intake history

An injected dose already stores where it went, but the per-medication
history only showed when it was taken and where the entry came from.
The list now renders the site as a chip next to the status, on the
desktop table and the mobile cards, so the rotation can be read back
from the history. Rows without a site are unchanged.
This commit is contained in:
Florian Maetschke committed 2026-11-03 11:46:07 +01:00
1 parent 540f4ff84f
commit 5aac158b12
11 files changed
+109

No files matched your search

+1
View File
@@ -1895,6 +1895,7 @@
"intakeHistoryColScheduled": "Geplant",
"intakeHistoryColStatus": "Status",
"intakeHistoryColSource": "Quelle",
"intakeHistoryInjectionSite": "Injektionsstelle: {site}",
"intakeHistoryStatusTaken": "Eingenommen",
"intakeHistoryStatusSkipped": "Übersprungen",
"intakeHistorySortByTaken": "Nach Einnahme-Zeitpunkt sortieren",
+1
View File
@@ -1895,6 +1895,7 @@
"intakeHistoryColScheduled": "Scheduled",
"intakeHistoryColStatus": "Status",
"intakeHistoryColSource": "Source",
"intakeHistoryInjectionSite": "Injection site: {site}",
"intakeHistoryStatusTaken": "Taken",
"intakeHistoryStatusSkipped": "Skipped",
"intakeHistorySortByTaken": "Sort by taken time",
+1
View File
@@ -1895,6 +1895,7 @@
"intakeHistoryColScheduled": "Programada",
"intakeHistoryColStatus": "Estado",
"intakeHistoryColSource": "Fuente",
"intakeHistoryInjectionSite": "Lugar de inyección: {site}",
"intakeHistoryStatusTaken": "Tomada",
"intakeHistoryStatusSkipped": "Omitida",
"intakeHistorySortByTaken": "Ordenar por hora de toma",
+1
View File
@@ -1895,6 +1895,7 @@
"intakeHistoryColScheduled": "Programmée",
"intakeHistoryColStatus": "Statut",
"intakeHistoryColSource": "Source",
"intakeHistoryInjectionSite": "Site d'injection : {site}",
"intakeHistoryStatusTaken": "Prise",
"intakeHistoryStatusSkipped": "Ignorée",
"intakeHistorySortByTaken": "Trier par heure de prise",
+1
View File
@@ -1895,6 +1895,7 @@
"intakeHistoryColScheduled": "Programmata",
"intakeHistoryColStatus": "Stato",
"intakeHistoryColSource": "Sorgente",
"intakeHistoryInjectionSite": "Sito di iniezione: {site}",
"intakeHistoryStatusTaken": "Assunta",
"intakeHistoryStatusSkipped": "Saltata",
"intakeHistorySortByTaken": "Ordina per ora di assunzione",
+1
View File
@@ -1895,6 +1895,7 @@
"intakeHistoryColScheduled": "예정",
"intakeHistoryColStatus": "상태",
"intakeHistoryColSource": "출처",
"intakeHistoryInjectionSite": "주사 부위: {site}",
"intakeHistoryStatusTaken": "복용함",
"intakeHistoryStatusSkipped": "건너뜀",
"intakeHistorySortByTaken": "복용 시각순 정렬",
+1
View File
@@ -1895,6 +1895,7 @@
"intakeHistoryColScheduled": "Gepland",
"intakeHistoryColStatus": "Status",
"intakeHistoryColSource": "Bron",
"intakeHistoryInjectionSite": "Injectieplaats: {site}",
"intakeHistoryStatusTaken": "Ingenomen",
"intakeHistoryStatusSkipped": "Overgeslagen",
"intakeHistorySortByTaken": "Sorteren op innametijd",
+1
View File
@@ -1895,6 +1895,7 @@
"intakeHistoryColScheduled": "Zaplanowany",
"intakeHistoryColStatus": "Status",
"intakeHistoryColSource": "Źródło",
"intakeHistoryInjectionSite": "Miejsce wstrzyknięcia: {site}",
"intakeHistoryStatusTaken": "Przyjęty",
"intakeHistoryStatusSkipped": "Pominięty",
"intakeHistorySortByTaken": "Sortuj wg czasu przyjęcia",
+1
View File
@@ -1895,6 +1895,7 @@
"intakeHistoryColScheduled": "Programada",
"intakeHistoryColStatus": "Status",
"intakeHistoryColSource": "Fonte",
"intakeHistoryInjectionSite": "Local da injeção: {site}",
"intakeHistoryStatusTaken": "Tomada",
"intakeHistoryStatusSkipped": "Pulada",
"intakeHistorySortByTaken": "Ordenar por horário da tomada",
@@ -46,6 +46,7 @@ type IntakeRow = {
takenAt: string | null;
skipped: boolean;
source: "WEB" | "API" | "REMINDER" | "IMPORT";
injectionSite?: string | null;
createdAt: string;
};
@@ -347,3 +348,50 @@ describe("<IntakeHistoryListV2> — sort indicators", () => {
expect(html).not.toContain(">Next<");
});
});
describe("<IntakeHistoryListV2> — injection site", () => {
const rows: IntakeRow[] = [
{
id: "evt-site",
medicationId: "med-1",
scheduledFor: "2026-05-15T08:00:00.000Z",
takenAt: "2026-05-15T08:02:00.000Z",
skipped: false,
source: "WEB",
injectionSite: "THIGH_LEFT",
createdAt: "2026-05-15T08:02:00.000Z",
},
{
id: "evt-nosite",
medicationId: "med-1",
scheduledFor: "2026-05-14T08:00:00.000Z",
takenAt: "2026-05-14T08:02:00.000Z",
skipped: false,
source: "WEB",
injectionSite: null,
createdAt: "2026-05-14T08:02:00.000Z",
},
];
it("renders the localised site next to the status of an injected dose", () => {
const client = makeClient();
seedIntake(client, "med-1", rows);
const html = render(<IntakeHistoryListV2 medicationId="med-1" />, client);
expect(html).toContain("Left thigh");
expect(html).toContain('aria-label="Injection site: Left thigh"');
// One chip for the injected dose, none for the dose without a site.
expect(html.match(/data-slot="intake-injection-site"/g)).toHaveLength(1);
});
it("speaks German when the locale is de", () => {
const client = makeClient();
seedIntake(client, "med-1", rows);
const html = render(
<IntakeHistoryListV2 medicationId="med-1" />,
client,
"de",
);
expect(html).toContain("Linker Oberschenkel");
expect(html).toContain("Injektionsstelle: Linker Oberschenkel");
});
});
@@ -14,7 +14,12 @@ import {
Check,
SkipForward,
MoreHorizontal,
MapPin,
} from "lucide-react";
import {
describeInjectionSite,
type InjectionSiteKey,
} from "@/lib/medications/injection-sites";
import { useAuth } from "@/hooks/use-auth";
import { useIsMobile } from "@/hooks/use-is-mobile";
@@ -94,6 +99,12 @@ export interface IntakeEvent {
source: IntakeSourceValue;
/** v1.42.2 — recorded by the server (`autoMarkTaken`), not the person. */
autoRecorded?: boolean;
/**
* Where an injected dose went. Set only for medications with
* `trackInjectionSites` on and only when the person picked a site; the
* list renders it as a badge so the rotation is visible in the history.
*/
injectionSite?: InjectionSiteKey | null;
createdAt: string;
}
@@ -420,6 +431,12 @@ export function IntakeHistoryListV2({
{t("medications.intakeHistoryStatusSkipped")}
</Badge>
) : null}
{event.injectionSite ? (
<InjectionSiteBadge
site={event.injectionSite}
className="ml-1.5"
/>
) : null}
</TableCell>
<TableCell>
<Badge
@@ -529,6 +546,11 @@ export function IntakeHistoryListV2({
{t("medications.intakeHistoryStatusSkipped")}
</Badge>
) : null}
{event.injectionSite ? (
<InjectionSiteBadge
site={event.injectionSite}
/>
) : null}
<Badge
variant="outline"
className="text-muted-foreground text-xs font-normal"
@@ -686,3 +708,33 @@ function SortHeaderButton({
</button>
);
}
/**
* Injection-site chip for one history row. The visible text is the
* localised site name (same vocabulary as the body-map picker); the
* accessible name prefixes it with "Injection site" so a screen reader
* does not announce a bare "Left thigh" next to the status.
*/
function InjectionSiteBadge({
site,
className,
}: {
site: InjectionSiteKey;
className?: string;
}) {
const { t } = useTranslations();
const siteName = t(describeInjectionSite(site));
return (
<Badge
variant="outline"
className={`text-muted-foreground gap-1 text-xs font-normal ${className ?? ""}`.trim()}
aria-label={t("medications.intakeHistoryInjectionSite", {
site: siteName,
})}
data-slot="intake-injection-site"
>
<MapPin aria-hidden="true" className="h-3 w-3" />
{siteName}
</Badge>
);
}