Files
medassist-ng/frontend/src/components/intake-journal/IntakeJournalHistoryModal.tsx
T
Daniel Volz c78fc43083 feat(frontend): add intake journal and shared note flows (#648)
* feat(backend): add intake journal APIs and share note support

* feat(frontend): add intake journal and shared note flows
2026-05-24 14:00:30 +02:00

192 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useTranslation } from "react-i18next";
import { useEscapeKey } from "../../hooks/useEscapeKey";
import type { IntakeJournalEntry, IntakeJournalHistoryFilters } from "../../hooks/useIntakeJournal";
import { useScrollLock } from "../../hooks/useScrollLock";
import type { Medication } from "../../types";
import { formatDateTime, getNumericLocale } from "../../utils/formatters";
import { DateTimeInput } from "../DateTimeInput";
import { MedicationAvatar } from "../MedicationAvatar";
interface IntakeJournalHistoryModalProps {
isOpen: boolean;
entries: IntakeJournalEntry[];
filters: IntakeJournalHistoryFilters;
medications: Medication[];
isLoading: boolean;
error: string | null;
onClose: () => void;
onFilterChange: (patch: Partial<IntakeJournalHistoryFilters>) => void;
onReload: () => Promise<void> | void;
onResetFilters: () => void;
onReopen: (doseId: string) => Promise<void> | void;
}
function formatDisplayDateTime(value: string | null): string | null {
if (!value) {
return null;
}
return formatDateTime(value, getNumericLocale());
}
function getJournalSourceLabel(entry: IntakeJournalEntry, t: ReturnType<typeof useTranslation>["t"]): string {
if (entry.takenSource === "automatic") {
return t("journal.context.sourceAutomaticReminder");
}
return entry.markedBy ? t("journal.context.sourceSharedLink") : t("journal.context.sourceOwnerApp");
}
export function IntakeJournalHistoryModal({
isOpen,
entries,
filters,
medications,
isLoading,
error,
onClose,
onFilterChange,
onReload,
onResetFilters,
onReopen,
}: IntakeJournalHistoryModalProps) {
const { t } = useTranslation();
useScrollLock(isOpen);
useEscapeKey(isOpen, onClose);
if (!isOpen) {
return null;
}
let listContent: React.ReactNode;
if (isLoading) {
listContent = <div className="journal-modal-state">{t("journal.history.loading")}</div>;
} else if (entries.length === 0) {
listContent = <div className="journal-modal-state">{t("journal.history.empty")}</div>;
} else {
listContent = entries.map((entry) => (
<article key={entry.doseTrackingId} className="journal-history-entry">
<div className="journal-history-entry-main">
<div className="journal-history-entry-header">
<MedicationAvatar name={entry.medicationName} size="sm" />
<div>
<strong>{entry.medicationName}</strong>
<p>{formatDisplayDateTime(entry.scheduledFor) ?? t("common.notAvailable")}</p>
</div>
</div>
<p className="journal-history-note">{entry.note ?? t("journal.history.noNote")}</p>
<div className="journal-history-meta">
<span>{t(entry.dismissed ? "journal.context.statusSkipped" : "journal.context.statusTaken")}</span>
<span>{getJournalSourceLabel(entry, t)}</span>
{entry.updatedAt && (
<span>
{t("journal.history.updatedAt", {
date: formatDisplayDateTime(entry.updatedAt) ?? entry.updatedAt,
})}
</span>
)}
</div>
</div>
<button type="button" className="primary small" onClick={() => void onReopen(entry.doseId)}>
{t("journal.history.reopen")}
</button>
</article>
));
}
return (
<div
className="modal-overlay"
onClick={onClose}
onKeyDown={(event) => {
if (event.key !== "Escape") {
event.stopPropagation();
}
}}
>
<div
className="modal-content journal-history-modal"
onClick={(event) => event.stopPropagation()}
onKeyDown={(event) => {
if (event.key !== "Escape") {
event.stopPropagation();
}
}}
>
<button type="button" className="modal-close" onClick={onClose} aria-label={t("common.close")}>
×
</button>
<div className="journal-modal-header">
<h2>{t("journal.history.title")}</h2>
<p>{t("journal.history.description")}</p>
</div>
<div className="journal-history-filters">
<label className="journal-field" htmlFor="journal-history-medication">
<span>{t("journal.history.filters.medication")}</span>
<select
id="journal-history-medication"
className="select-field"
value={filters.medicationId ?? "all"}
onChange={(event) => {
const value = event.target.value;
onFilterChange({ medicationId: value === "all" ? null : Number(value) });
}}
>
<option value="all">{t("journal.history.filters.allMedications")}</option>
{medications.map((medication) => (
<option key={medication.id} value={medication.id}>
{medication.name}
</option>
))}
</select>
</label>
<div className="journal-field journal-date-filter">
<span>{t("journal.history.filters.from")}</span>
<DateTimeInput
value={filters.from}
onChange={(event) => onFilterChange({ from: event.target.value })}
step="60"
aria-label={t("journal.history.filters.from")}
placeholder={t("journal.history.filters.fromPlaceholder")}
/>
</div>
<div className="journal-field journal-date-filter">
<span>{t("journal.history.filters.to")}</span>
<DateTimeInput
value={filters.to}
onChange={(event) => onFilterChange({ to: event.target.value })}
step="60"
aria-label={t("journal.history.filters.to")}
placeholder={t("journal.history.filters.toPlaceholder")}
/>
</div>
</div>
<div className="journal-history-toolbar">
<button type="button" className="ghost small" onClick={onResetFilters}>
{t("journal.history.resetFilters")}
</button>
<button type="button" className="ghost small" onClick={() => void onReload()} disabled={isLoading}>
{t("journal.history.reload")}
</button>
</div>
{error && <div className="journal-inline-error">{error}</div>}
<div className="journal-history-list">{listContent}</div>
<div className="modal-footer journal-modal-footer">
<div className="footer-right">
<button type="button" className="ghost" onClick={onClose}>
{t("common.close")}
</button>
</div>
</div>
</div>
</div>
);
}