mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
feat(meds): widen advanced settings into a two-column layout with co-located export
Widen the medication advanced-settings dialog to the 4xl token and lay the Data, Reminders and Lifecycle groups into a responsive two-column grid on lg+ so a normal desktop viewport no longer forces a long single-column scroll. The Danger zone keeps its destructive-tinted card full-width at the bottom, separated from routine toggles. Co-locate medications CSV export beside the intake import in the Data group: a new DataPortabilityRow pairs the import button with an export button that hits the existing /api/export/medications endpoint, with an intake-history toggle mirroring the Settings export card. No new endpoint. Correct the import affordance copy to JSON — the dialog and API only parse a JSON intake array, so the prior CSV/JSON label overstated the accepted format.
This commit is contained in:
1 parent
7802ce1e11
commit
91f23bae68
9 files changed
+349
-120
No files matched your search
+12
-4
@@ -1285,10 +1285,18 @@
|
||||
"lifecycle": "Lebenszyklus",
|
||||
"danger": "Gefahrenzone"
|
||||
},
|
||||
"csvImport": {
|
||||
"title": "CSV-/JSON-Import",
|
||||
"helper": "Vergangene Einnahmen aus einer Datei oder eingefügtem JSON importieren.",
|
||||
"button": "CSV/JSON importieren"
|
||||
"dataPortability": {
|
||||
"import": {
|
||||
"title": "Einnahmen importieren",
|
||||
"helper": "Vergangene Einnahmen aus einer JSON-Datei oder eingefügtem JSON laden.",
|
||||
"button": "JSON importieren"
|
||||
},
|
||||
"export": {
|
||||
"title": "Medikament exportieren",
|
||||
"helper": "Dieses Medikament als CSV-Datei herunterladen.",
|
||||
"includeIntake": "Einnahmeverlauf einschließen",
|
||||
"button": "CSV exportieren"
|
||||
}
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
|
||||
+12
-4
@@ -1285,10 +1285,18 @@
|
||||
"lifecycle": "Lifecycle",
|
||||
"danger": "Danger zone"
|
||||
},
|
||||
"csvImport": {
|
||||
"title": "CSV / JSON import",
|
||||
"helper": "Import past intakes from a file or pasted JSON.",
|
||||
"button": "Import CSV/JSON"
|
||||
"dataPortability": {
|
||||
"import": {
|
||||
"title": "Import intakes",
|
||||
"helper": "Load past intakes from a JSON file or pasted JSON.",
|
||||
"button": "Import JSON"
|
||||
},
|
||||
"export": {
|
||||
"title": "Export medication",
|
||||
"helper": "Download this medication as a CSV file.",
|
||||
"includeIntake": "Include intake history",
|
||||
"button": "Export CSV"
|
||||
}
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
|
||||
+12
-4
@@ -1285,10 +1285,18 @@
|
||||
"lifecycle": "Ciclo de vida",
|
||||
"danger": "Zona de peligro"
|
||||
},
|
||||
"csvImport": {
|
||||
"title": "Importar CSV/JSON",
|
||||
"helper": "Importa tomas anteriores desde un archivo o JSON pegado.",
|
||||
"button": "Importar CSV/JSON"
|
||||
"dataPortability": {
|
||||
"import": {
|
||||
"title": "Importar tomas",
|
||||
"helper": "Carga tomas anteriores desde un archivo JSON o JSON pegado.",
|
||||
"button": "Importar JSON"
|
||||
},
|
||||
"export": {
|
||||
"title": "Exportar medicamento",
|
||||
"helper": "Descarga este medicamento como archivo CSV.",
|
||||
"includeIntake": "Incluir historial de tomas",
|
||||
"button": "Exportar CSV"
|
||||
}
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
|
||||
+12
-4
@@ -1285,10 +1285,18 @@
|
||||
"lifecycle": "Cycle de vie",
|
||||
"danger": "Zone de danger"
|
||||
},
|
||||
"csvImport": {
|
||||
"title": "Import CSV/JSON",
|
||||
"helper": "Importez les prises passées depuis un fichier ou du JSON collé.",
|
||||
"button": "Importer CSV/JSON"
|
||||
"dataPortability": {
|
||||
"import": {
|
||||
"title": "Importer les prises",
|
||||
"helper": "Chargez les prises passées depuis un fichier JSON ou du JSON collé.",
|
||||
"button": "Importer JSON"
|
||||
},
|
||||
"export": {
|
||||
"title": "Exporter le médicament",
|
||||
"helper": "Téléchargez ce médicament au format CSV.",
|
||||
"includeIntake": "Inclure l'historique des prises",
|
||||
"button": "Exporter CSV"
|
||||
}
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
|
||||
+12
-4
@@ -1285,10 +1285,18 @@
|
||||
"lifecycle": "Ciclo di vita",
|
||||
"danger": "Zona pericolosa"
|
||||
},
|
||||
"csvImport": {
|
||||
"title": "Importa CSV/JSON",
|
||||
"helper": "Importa le assunzioni passate da un file o da JSON incollato.",
|
||||
"button": "Importa CSV/JSON"
|
||||
"dataPortability": {
|
||||
"import": {
|
||||
"title": "Importa assunzioni",
|
||||
"helper": "Carica le assunzioni passate da un file JSON o da JSON incollato.",
|
||||
"button": "Importa JSON"
|
||||
},
|
||||
"export": {
|
||||
"title": "Esporta farmaco",
|
||||
"helper": "Scarica questo farmaco come file CSV.",
|
||||
"includeIntake": "Includi cronologia delle assunzioni",
|
||||
"button": "Esporta CSV"
|
||||
}
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
|
||||
+12
-4
@@ -1285,10 +1285,18 @@
|
||||
"lifecycle": "Cykl życia",
|
||||
"danger": "Strefa zagrożenia"
|
||||
},
|
||||
"csvImport": {
|
||||
"title": "Import CSV/JSON",
|
||||
"helper": "Importuj wcześniejsze przyjęcia z pliku lub wklejonego JSON.",
|
||||
"button": "Importuj CSV/JSON"
|
||||
"dataPortability": {
|
||||
"import": {
|
||||
"title": "Importuj przyjęcia",
|
||||
"helper": "Wczytaj wcześniejsze przyjęcia z pliku JSON lub wklejonego JSON.",
|
||||
"button": "Importuj JSON"
|
||||
},
|
||||
"export": {
|
||||
"title": "Eksportuj lek",
|
||||
"helper": "Pobierz ten lek jako plik CSV.",
|
||||
"includeIntake": "Dołącz historię przyjęć",
|
||||
"button": "Eksportuj CSV"
|
||||
}
|
||||
}
|
||||
},
|
||||
"header": {
|
||||
|
||||
@@ -8,9 +8,10 @@
|
||||
* and echoes `contentWidth`. The bodies are mocked to ordered markers.
|
||||
* The test pins:
|
||||
* - the four groups render in the documented order;
|
||||
* - the sheet opens at the `2xl` width token;
|
||||
* - the sheet opens at the `4xl` width token;
|
||||
* - `onRequestPhaseSheet` threads through to the phases row;
|
||||
* - `onOpenImport` wires the CSV-import button;
|
||||
* - `onOpenImport` wires the import button (co-located with export);
|
||||
* - the export control sits beside the import control;
|
||||
* - the lifecycle + danger bodies receive an `onAfterAction` closer;
|
||||
* - the sheet renders nothing when `open` is false.
|
||||
*/
|
||||
@@ -122,11 +123,11 @@ describe("AdvancedSettingsSheet (v1.7.0)", () => {
|
||||
expect(danger).toBeGreaterThan(lifecycle);
|
||||
});
|
||||
|
||||
it("opens at the 2xl width token", () => {
|
||||
it("opens at the 4xl width token", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<AdvancedSettingsSheet {...baseProps(true)} />,
|
||||
);
|
||||
expect(html).toContain('data-content-width="2xl"');
|
||||
expect(html).toContain('data-content-width="4xl"');
|
||||
});
|
||||
|
||||
it("threads onRequestPhaseSheet through to the phases row", () => {
|
||||
@@ -138,14 +139,30 @@ describe("AdvancedSettingsSheet (v1.7.0)", () => {
|
||||
expect(html).toContain('data-has-phase-swap="yes"');
|
||||
});
|
||||
|
||||
it("wires the CSV/JSON import button", () => {
|
||||
it("wires the import button", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<AdvancedSettingsSheet
|
||||
{...baseProps(true, { onOpenImport: () => {} })}
|
||||
/>,
|
||||
);
|
||||
expect(html).toContain("advanced-csv-import-button");
|
||||
expect(html).toContain("medications.detail.advanced.csvImport.button");
|
||||
expect(html).toContain("advanced-import-button");
|
||||
expect(html).toContain(
|
||||
"medications.detail.advanced.dataPortability.import.button",
|
||||
);
|
||||
});
|
||||
|
||||
it("co-locates an export control beside the import control", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<AdvancedSettingsSheet {...baseProps(true)} />,
|
||||
);
|
||||
expect(html).toContain("advanced-export-button");
|
||||
expect(html).toContain(
|
||||
"medications.detail.advanced.dataPortability.export.button",
|
||||
);
|
||||
const importIdx = html.indexOf("advanced-import-block");
|
||||
const exportIdx = html.indexOf("advanced-export-block");
|
||||
expect(importIdx).toBeGreaterThan(-1);
|
||||
expect(exportIdx).toBeGreaterThan(importIdx);
|
||||
});
|
||||
|
||||
it("passes an onAfterAction to the lifecycle and danger bodies so they can close the sheet", () => {
|
||||
|
||||
@@ -2,22 +2,29 @@
|
||||
|
||||
/**
|
||||
* v1.7.0 — Advanced settings sheet.
|
||||
* v1.7.2 — widened to `4xl` and re-laid into a responsive two-column
|
||||
* grid so a normal desktop viewport shows every group without forcing
|
||||
* a long single-column scroll.
|
||||
*
|
||||
* Widened to the `2xl` ResponsiveSheet token (672px desktop) and
|
||||
* regrouped from three stacked section cards into four labelled groups
|
||||
* under one consistent visual system (R-medui §5):
|
||||
* Four labelled groups under one consistent visual system (R-medui §5):
|
||||
*
|
||||
* - Data: real CSV/JSON import + external-API endpoint / token.
|
||||
* - Data: intake Import + medications CSV Export (co-located) +
|
||||
* external-API endpoint / token.
|
||||
* - Reminders: notifications switch + reminder window (grace).
|
||||
* - Lifecycle: pause/resume + end course + phases.
|
||||
* - Danger zone: purge intake history + delete medication.
|
||||
*
|
||||
* Layout: on `lg+` the first three groups flow into a two-column grid —
|
||||
* the Data group (tallest, with the export toggle + token snippets)
|
||||
* holds the left column; Reminders + Lifecycle stack down the right.
|
||||
* The Danger zone always spans the full width at the bottom, set off by
|
||||
* its own destructive-tinted card, so an irreversible action never
|
||||
* shares a column with a routine toggle. On `<lg` everything stacks in
|
||||
* the documented Data → Reminders → Lifecycle → Danger order.
|
||||
*
|
||||
* Button styling is consistent: reversible / neutral actions are
|
||||
* `outline`, destructive actions are `destructive font-semibold`,
|
||||
* toggles are switches wrapped in labels. The CSV import is promoted
|
||||
* from a one-line stub to a real button that opens the existing
|
||||
* `<IntakeImportDialog>` (mounted by the page; opened via
|
||||
* `onOpenImport`).
|
||||
* toggles are switches wrapped in labels.
|
||||
*
|
||||
* The Phasen button is sibling-swapped, not nested: `onRequestPhaseSheet`
|
||||
* closes this sheet first and lets the parent open the phase sheet so
|
||||
@@ -25,13 +32,11 @@
|
||||
* self-saves, so the footer slot stays empty.
|
||||
*/
|
||||
|
||||
import { Upload } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { SettingsGroup } from "@/components/medications/settings-group";
|
||||
import { ApiTokensRow } from "@/components/medications/sections/api-tokens-row";
|
||||
import { DataPortabilityRow } from "@/components/medications/sections/data-portability-row";
|
||||
import { NotificationsBody } from "@/components/medications/sections/notifications-section";
|
||||
import {
|
||||
GraceRow,
|
||||
@@ -62,7 +67,7 @@ export interface AdvancedSettingsSheetProps {
|
||||
onRequestPhaseSheet?: () => void;
|
||||
/**
|
||||
* v1.7.0 — opens the page-owned `<IntakeImportDialog>` from the Data
|
||||
* group's CSV/JSON import button.
|
||||
* group's import button.
|
||||
*/
|
||||
onOpenImport?: () => void;
|
||||
}
|
||||
@@ -90,89 +95,85 @@ export function AdvancedSettingsSheet({
|
||||
open={open}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t("medications.detail.advanced.title")}
|
||||
contentWidth="2xl"
|
||||
contentWidth="4xl"
|
||||
bodyClassName="gap-6"
|
||||
>
|
||||
<div className="space-y-8" data-slot="advanced-settings-sheet-body">
|
||||
{/* DATA — CSV/JSON import + external API endpoint */}
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.advanced.group.data")}
|
||||
dataSlot="advanced-group-data"
|
||||
>
|
||||
<div className="space-y-2" data-slot="advanced-csv-import-block">
|
||||
<div className="space-y-1">
|
||||
<p className="text-foreground text-sm font-medium">
|
||||
{t("medications.detail.advanced.csvImport.title")}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("medications.detail.advanced.csvImport.helper")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onOpenImport?.()}
|
||||
className="min-h-11 sm:min-h-9"
|
||||
data-slot="advanced-csv-import-button"
|
||||
<div className="space-y-6" data-slot="advanced-settings-sheet-body">
|
||||
{/* Top region: Data + Reminders + Lifecycle in a two-column grid
|
||||
on lg+, single column below. `items-start` keeps each group
|
||||
top-aligned so the shorter right column never stretches. */}
|
||||
<div className="grid items-start gap-6 lg:grid-cols-2">
|
||||
{/* DATA — import + export + external API. Tallest group → left
|
||||
column on lg+ so the right column's two shorter groups
|
||||
balance against it. */}
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.advanced.group.data")}
|
||||
dataSlot="advanced-group-data"
|
||||
>
|
||||
<DataPortabilityRow
|
||||
medicationId={medicationId}
|
||||
onOpenImport={onOpenImport}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<ApiTokensRow
|
||||
medicationId={medicationId}
|
||||
medicationName={medicationName}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
|
||||
{/* Right column: Reminders + Lifecycle stacked. */}
|
||||
<div className="space-y-6">
|
||||
{/* REMINDERS — notifications + reminder window */}
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.advanced.group.reminders")}
|
||||
dataSlot="advanced-group-reminders"
|
||||
>
|
||||
<Upload aria-hidden="true" className="h-4 w-4" />
|
||||
{t("medications.detail.advanced.csvImport.button")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<ApiTokensRow
|
||||
medicationId={medicationId}
|
||||
medicationName={medicationName}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
|
||||
{/* REMINDERS — notifications + reminder window */}
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.advanced.group.reminders")}
|
||||
dataSlot="advanced-group-reminders"
|
||||
>
|
||||
<NotificationsBody
|
||||
medicationId={medicationId}
|
||||
notificationsEnabled={notificationsEnabled}
|
||||
/>
|
||||
|
||||
<Separator />
|
||||
|
||||
<GraceRow
|
||||
medicationId={medicationId}
|
||||
reminderGraceMinutes={reminderGraceMinutes}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
|
||||
{/* LIFECYCLE — pause/resume + end course + phases */}
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.advanced.group.lifecycle")}
|
||||
dataSlot="advanced-group-lifecycle"
|
||||
>
|
||||
<LifecycleManageBody
|
||||
medicationId={medicationId}
|
||||
medicationName={medicationName}
|
||||
active={active}
|
||||
onAfterAction={() => onOpenChange(false)}
|
||||
/>
|
||||
|
||||
{isGlp1 && (
|
||||
<>
|
||||
<Separator />
|
||||
<PhasesRow
|
||||
<NotificationsBody
|
||||
medicationId={medicationId}
|
||||
treatmentClass={treatmentClass}
|
||||
startsOn={startsOn}
|
||||
endsOn={endsOn}
|
||||
onRequestPhaseSheet={onRequestPhaseSheet}
|
||||
notificationsEnabled={notificationsEnabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingsGroup>
|
||||
|
||||
{/* DANGER ZONE — purge + delete */}
|
||||
<Separator />
|
||||
|
||||
<GraceRow
|
||||
medicationId={medicationId}
|
||||
reminderGraceMinutes={reminderGraceMinutes}
|
||||
/>
|
||||
</SettingsGroup>
|
||||
|
||||
{/* LIFECYCLE — pause/resume + end course + phases */}
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.advanced.group.lifecycle")}
|
||||
dataSlot="advanced-group-lifecycle"
|
||||
>
|
||||
<LifecycleManageBody
|
||||
medicationId={medicationId}
|
||||
medicationName={medicationName}
|
||||
active={active}
|
||||
onAfterAction={() => onOpenChange(false)}
|
||||
/>
|
||||
|
||||
{isGlp1 && (
|
||||
<>
|
||||
<Separator />
|
||||
<PhasesRow
|
||||
medicationId={medicationId}
|
||||
treatmentClass={treatmentClass}
|
||||
startsOn={startsOn}
|
||||
endsOn={endsOn}
|
||||
onRequestPhaseSheet={onRequestPhaseSheet}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingsGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* DANGER ZONE — purge + delete. Full-width at the bottom, set
|
||||
off by the destructive-tinted card inside the body so an
|
||||
irreversible action never shares a column with a toggle. */}
|
||||
<SettingsGroup
|
||||
label={t("medications.detail.advanced.group.danger")}
|
||||
dataSlot="advanced-group-danger"
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* v1.7.2 — Data-portability row for the advanced-settings Data group.
|
||||
*
|
||||
* Co-locates intake Import and Export side by side so the two halves of
|
||||
* the "move my data" flow share one visual block instead of import
|
||||
* living alone in the sheet while export hides under /settings/export.
|
||||
*
|
||||
* - Import: opens the page-owned `<IntakeImportDialog>` via
|
||||
* `onOpenImport`. The dialog parses a JSON intake array.
|
||||
* - Export: downloads `/api/export/medications` as CSV. The
|
||||
* intake-history toggle mirrors the Settings → Export card, so an
|
||||
* operator can grab the medication row plus its full intake log
|
||||
* from the same place they imported it.
|
||||
*
|
||||
* The download helper spins up an off-DOM anchor — the same pattern the
|
||||
* Settings export cards use — so the response stays a same-origin GET
|
||||
* and never needs a new endpoint.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { Download, Loader2, Upload } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
export interface DataPortabilityRowProps {
|
||||
medicationId: string;
|
||||
/** Opens the page-owned `<IntakeImportDialog>`. */
|
||||
onOpenImport?: () => void;
|
||||
}
|
||||
|
||||
async function downloadFromUrl(url: string, filename: string): Promise<void> {
|
||||
const res = await fetch(url, { credentials: "include" });
|
||||
if (!res.ok) {
|
||||
throw new Error(`Download failed (${res.status})`);
|
||||
}
|
||||
const blob = await res.blob();
|
||||
const objectUrl = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = objectUrl;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
|
||||
export function DataPortabilityRow({
|
||||
medicationId,
|
||||
onOpenImport,
|
||||
}: DataPortabilityRowProps) {
|
||||
const { t } = useTranslations();
|
||||
const [includeIntake, setIncludeIntake] = useState(true);
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
async function handleExport() {
|
||||
setBusy(true);
|
||||
setError(null);
|
||||
try {
|
||||
const query = includeIntake ? "?intake=true" : "?intake=false";
|
||||
const stamp = new Date().toISOString().slice(0, 10);
|
||||
await downloadFromUrl(
|
||||
`/api/export/medications${query}`,
|
||||
`healthlog-medications-${stamp}.csv`,
|
||||
);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid gap-4 sm:grid-cols-2"
|
||||
data-slot="advanced-data-portability-row"
|
||||
>
|
||||
{/* Import — JSON intake array */}
|
||||
<div
|
||||
className="border-border bg-muted/30 flex flex-col gap-2 rounded-lg border p-3"
|
||||
data-slot="advanced-import-block"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<p className="text-foreground text-sm font-medium">
|
||||
{t("medications.detail.advanced.dataPortability.import.title")}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("medications.detail.advanced.dataPortability.import.helper")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onOpenImport?.()}
|
||||
className="mt-auto min-h-11 w-full sm:min-h-9"
|
||||
data-slot="advanced-import-button"
|
||||
>
|
||||
<Upload aria-hidden="true" className="h-4 w-4" />
|
||||
{t("medications.detail.advanced.dataPortability.import.button")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Export — medications CSV */}
|
||||
<div
|
||||
className="border-border bg-muted/30 flex flex-col gap-2 rounded-lg border p-3"
|
||||
data-slot="advanced-export-block"
|
||||
>
|
||||
<div className="space-y-1">
|
||||
<p className="text-foreground text-sm font-medium">
|
||||
{t("medications.detail.advanced.dataPortability.export.title")}
|
||||
</p>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("medications.detail.advanced.dataPortability.export.helper")}
|
||||
</p>
|
||||
</div>
|
||||
<label
|
||||
htmlFor={`advanced-export-include-intake-${medicationId}`}
|
||||
className="flex min-h-11 cursor-pointer items-center gap-2 text-xs sm:min-h-8"
|
||||
>
|
||||
<Switch
|
||||
id={`advanced-export-include-intake-${medicationId}`}
|
||||
checked={includeIntake}
|
||||
onCheckedChange={setIncludeIntake}
|
||||
data-slot="advanced-export-include-intake"
|
||||
/>
|
||||
<span className="text-muted-foreground">
|
||||
{t(
|
||||
"medications.detail.advanced.dataPortability.export.includeIntake",
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => void handleExport()}
|
||||
disabled={busy}
|
||||
aria-busy={busy || undefined}
|
||||
className="mt-auto min-h-11 w-full sm:min-h-9"
|
||||
data-slot="advanced-export-button"
|
||||
>
|
||||
{busy ? (
|
||||
<Loader2
|
||||
aria-hidden="true"
|
||||
className="h-4 w-4 animate-spin motion-reduce:animate-none"
|
||||
/>
|
||||
) : (
|
||||
<Download aria-hidden="true" className="h-4 w-4" />
|
||||
)}
|
||||
{t("medications.detail.advanced.dataPortability.export.button")}
|
||||
</Button>
|
||||
{error && (
|
||||
<p role="alert" className="text-destructive text-xs">
|
||||
{error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user