feat: track number of prescription repeats (#193)

* feat: track prescription repeats and refill reminders

* test: align backend and frontend suites with current prescription and UI behavior

* test: update frontend and backend expectations for latest reminders and refill flow
This commit is contained in:
Daniel Volz
2026-02-14 19:07:36 +01:00
committed by GitHub
parent edf42bb068
commit 8273b07231
37 changed files with 3331 additions and 4673 deletions
+69 -4
View File
@@ -43,6 +43,11 @@ export const defaultForm = (): FormState => ({
doseUnit: "mg",
expiryDate: "",
notes: "",
prescriptionEnabled: false,
prescriptionAuthorizedRefills: "",
prescriptionRemainingRefills: "",
prescriptionLowRefillThreshold: "1",
prescriptionExpiryDate: "",
intakeRemindersEnabled: false,
blisters: [defaultBlister()],
intakes: [defaultIntake()],
@@ -78,7 +83,7 @@ export interface UseMedicationFormReturn {
removeIntake: (idx: number) => void;
startEdit: (med: Medication, openEditModal: () => void) => void;
resetForm: () => void;
handleValueChange: <K extends keyof FormState>(key: K, value: string) => void;
handleValueChange: <K extends keyof FormState>(key: K, value: FormState[K]) => void;
addTakenByPerson: (name: string) => void;
removeTakenByPerson: (name: string) => void;
handleTakenByKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
@@ -96,6 +101,12 @@ export function useMedicationForm(): UseMedicationFormReturn {
const [pendingImagePreview, setPendingImagePreview] = useState<string | null>(null);
const [takenByInput, setTakenByInput] = useState("");
const parseNonNegativeInt = useCallback((value: string): number => {
const parsed = Number.parseInt(value, 10);
if (Number.isNaN(parsed) || parsed < 0) return 0;
return parsed;
}, []);
// Validate form fields
const validateField = useCallback(
(field: keyof FieldErrors, value: string | string[]): string | undefined => {
@@ -199,6 +210,10 @@ export function useMedicationForm(): UseMedicationFormReturn {
intakeRemindersEnabled: med.intakeRemindersEnabled ?? false,
}));
const authorizedRefills = Math.max(0, med.prescriptionAuthorizedRefills ?? 0);
const remainingRefills = Math.min(Math.max(0, med.prescriptionRemainingRefills ?? 0), authorizedRefills);
const lowRefillThreshold = Math.min(Math.max(0, med.prescriptionLowRefillThreshold ?? 1), authorizedRefills);
const editForm: FormState = {
name: med.name,
genericName: med.genericName ?? "",
@@ -217,6 +232,11 @@ export function useMedicationForm(): UseMedicationFormReturn {
doseUnit: med.doseUnit ?? "mg",
expiryDate: med.expiryDate ? med.expiryDate.slice(0, 10) : "",
notes: med.notes ?? "",
prescriptionEnabled: med.prescriptionEnabled ?? false,
prescriptionAuthorizedRefills: med.prescriptionAuthorizedRefills != null ? String(authorizedRefills) : "",
prescriptionRemainingRefills: med.prescriptionRemainingRefills != null ? String(remainingRefills) : "",
prescriptionLowRefillThreshold: String(lowRefillThreshold),
prescriptionExpiryDate: med.prescriptionExpiryDate ? med.prescriptionExpiryDate.slice(0, 10) : "",
intakeRemindersEnabled: med.intakeRemindersEnabled ?? false,
blisters: med.blisters.map((s) => ({
usage: String(s.usage),
@@ -246,9 +266,54 @@ export function useMedicationForm(): UseMedicationFormReturn {
setOriginalForm(newForm);
}, []);
const handleValueChange = useCallback(<K extends keyof FormState>(key: K, value: string) => {
setForm((prev) => ({ ...prev, [key]: value }));
}, []);
const handleValueChange = useCallback(
<K extends keyof FormState>(key: K, value: FormState[K]) => {
setForm((prev) => {
const next = { ...prev, [key]: value } as FormState;
if (key === "prescriptionAuthorizedRefills") {
const raw = String(value);
next.prescriptionAuthorizedRefills = raw === "" ? "" : String(parseNonNegativeInt(raw));
}
if (key === "prescriptionRemainingRefills") {
const raw = String(value);
next.prescriptionRemainingRefills = raw === "" ? "" : String(parseNonNegativeInt(raw));
}
if (key === "prescriptionLowRefillThreshold") {
const raw = String(value);
next.prescriptionLowRefillThreshold = raw === "" ? "" : String(parseNonNegativeInt(raw));
}
if (!next.prescriptionEnabled) {
return next;
}
const authorizedRefills = parseNonNegativeInt(next.prescriptionAuthorizedRefills);
if (key === "prescriptionAuthorizedRefills") {
next.prescriptionRemainingRefills = String(
Math.min(parseNonNegativeInt(next.prescriptionRemainingRefills), authorizedRefills)
);
next.prescriptionLowRefillThreshold = String(
Math.min(parseNonNegativeInt(next.prescriptionLowRefillThreshold), authorizedRefills)
);
}
if (key === "prescriptionRemainingRefills") {
next.prescriptionRemainingRefills = String(Math.min(parseNonNegativeInt(String(value)), authorizedRefills));
}
if (key === "prescriptionLowRefillThreshold") {
next.prescriptionLowRefillThreshold = String(Math.min(parseNonNegativeInt(String(value)), authorizedRefills));
}
return next;
});
},
[parseNonNegativeInt]
);
// Tag input helpers for "Taken By" field
const addTakenByPerson = useCallback(