refactor: decompose frontend state and medication dialog flows

This commit is contained in:
Daniel Volz
2026-03-27 06:50:19 +01:00
committed by GitHub
parent b58c4fe5bb
commit f46043970f
28 changed files with 2450 additions and 1613 deletions
+9
View File
@@ -5,6 +5,14 @@ export { useCollapsedDays } from "./useCollapsedDays";
export type { UseDosesReturn } from "./useDoses";
export { useDoses } from "./useDoses";
export { useEscapeKey } from "./useEscapeKey";
export {
createMedicationEnrichmentState,
MEDICATION_ENRICHMENT_INITIAL_LIMIT,
MEDICATION_ENRICHMENT_LIMIT_STEP,
MEDICATION_ENRICHMENT_MAX_LIMIT,
type MedicationEnrichmentState,
useMedicationEnrichmentController,
} from "./useMedicationEnrichmentController";
export type { UseMedicationFormReturn } from "./useMedicationForm";
export { defaultBlister, defaultForm, useMedicationForm } from "./useMedicationForm";
export type { UseMedicationsReturn } from "./useMedications";
@@ -12,6 +20,7 @@ export { useMedications } from "./useMedications";
export { useModalHistory } from "./useModalHistory";
export type { UseRefillReturn } from "./useRefill";
export { useRefill } from "./useRefill";
export { useScheduleController } from "./useScheduleController";
export { useScrollLock } from "./useScrollLock";
export type { Settings, UseSettingsReturn } from "./useSettings";
export { useSettings } from "./useSettings";
@@ -0,0 +1,84 @@
import { useCallback, useRef, useState } from "react";
import type {
MedicationEnrichmentEnrichResponse,
MedicationEnrichmentPackageOption,
MedicationEnrichmentSearchResult,
MedicationEnrichmentStrengthOption,
} from "../types";
export const MEDICATION_ENRICHMENT_INITIAL_LIMIT = 6;
export const MEDICATION_ENRICHMENT_LIMIT_STEP = 6;
export const MEDICATION_ENRICHMENT_MAX_LIMIT = 20;
export type MedicationEnrichmentState = {
query: string;
results: MedicationEnrichmentSearchResult[];
hasMoreResults: boolean;
resultLimit: number;
isSearching: boolean;
hasSearched: boolean;
searchError: string | null;
applyingCode: string | null;
applyingPackageLabel: string | null;
activeResultCode: string | null;
appliedSelection: MedicationEnrichmentEnrichResponse["selection"] | null;
enrichError: string | null;
meta: MedicationEnrichmentEnrichResponse["meta"] | null;
strengthOptions: MedicationEnrichmentStrengthOption[];
packageOptions: MedicationEnrichmentPackageOption[];
appliedStrengthLabel: string | null;
appliedPackageLabel: string | null;
};
export function createMedicationEnrichmentState(
query = "",
resultLimit = MEDICATION_ENRICHMENT_INITIAL_LIMIT
): MedicationEnrichmentState {
return {
query,
results: [],
hasMoreResults: false,
resultLimit,
isSearching: false,
hasSearched: false,
searchError: null,
applyingCode: null,
applyingPackageLabel: null,
activeResultCode: null,
appliedSelection: null,
enrichError: null,
meta: null,
strengthOptions: [],
packageOptions: [],
appliedStrengthLabel: null,
appliedPackageLabel: null,
};
}
export function useMedicationEnrichmentController() {
const [medicationEnrichment, setMedicationEnrichment] = useState<MedicationEnrichmentState>(() =>
createMedicationEnrichmentState()
);
const medicationEnrichmentQueryRef = useRef("");
const resetMedicationEnrichment = useCallback((query = "") => {
medicationEnrichmentQueryRef.current = query;
setMedicationEnrichment(createMedicationEnrichmentState(query));
}, []);
const handleMedicationEnrichmentQueryChange = useCallback((value: string) => {
medicationEnrichmentQueryRef.current = value;
setMedicationEnrichment((previous) => ({
...previous,
query: value,
}));
}, []);
return {
medicationEnrichment,
setMedicationEnrichment,
medicationEnrichmentQueryRef,
resetMedicationEnrichment,
handleMedicationEnrichmentQueryChange,
};
}
@@ -0,0 +1,41 @@
import { useAppContext } from "../context";
export function useScheduleController() {
const ctx = useAppContext();
return {
meds: ctx.meds,
loading: ctx.loading,
settings: ctx.settings,
settingsLoading: ctx.settingsLoading,
coverage: ctx.coverage,
coverageByMed: ctx.coverageByMed,
depletionByMed: ctx.depletionByMed,
stockThresholds: ctx.stockThresholds,
scheduleDays: ctx.scheduleDays,
setScheduleDays: ctx.setScheduleDays,
showPastDays: ctx.showPastDays,
setShowPastDays: ctx.setShowPastDays,
showFutureDays: ctx.showFutureDays,
setShowFutureDays: ctx.setShowFutureDays,
pastDays: ctx.pastDays,
todayDay: ctx.todayDay,
futureDays: ctx.futureDays,
takenDoses: ctx.takenDoses,
dismissedDoses: ctx.dismissedDoses,
markDoseTaken: ctx.markDoseTaken,
undoDoseTaken: ctx.undoDoseTaken,
manuallyCollapsedDays: ctx.manuallyCollapsedDays,
manuallyExpandedDays: ctx.manuallyExpandedDays,
toggleDayCollapse: ctx.toggleDayCollapse,
missedPastDoseIds: ctx.missedPastDoseIds,
getDayStockStatus: ctx.getDayStockStatus,
getDoseId: ctx.getDoseId,
isDoseTakenAutomatically: ctx.isDoseTakenAutomatically,
openMedDetail: ctx.openMedDetail,
openUserFilter: ctx.openUserFilter,
openScheduleLightbox: ctx.openScheduleLightbox,
loadMeds: ctx.loadMeds,
loadSettings: ctx.loadSettings,
};
}
+31 -10
View File
@@ -130,6 +130,13 @@ export interface UseSettingsReturn {
export function useSettings(): UseSettingsReturn {
const { i18n } = useTranslation();
const getErrorMessage = useCallback((error: unknown): string => {
if (error instanceof Error) {
return error.message;
}
return String(error);
}, []);
const [settings, setSettings] = useState<Settings>(defaultSettings);
const [savedSettings, setSavedSettings] = useState<Settings>(defaultSettings);
const [settingsLoading, setSettingsLoading] = useState(false);
@@ -281,9 +288,13 @@ export function useSettings(): UseSettingsReturn {
credentials: "include",
keepalive: true,
body: JSON.stringify(payload),
}).catch(() => {});
}).catch((error: unknown) => {
log.warn("[useSettings] keepalive settings flush failed", {
error: getErrorMessage(error),
});
});
},
[buildSettingsPayload]
[buildSettingsPayload, getErrorMessage]
);
// Load settings function - exposed for manual refresh (e.g., after auth)
@@ -394,12 +405,16 @@ export function useSettings(): UseSettingsReturn {
),
}));
})
.catch(() => {});
.catch((error: unknown) => {
log.warn("[useSettings] reminder status refresh failed", {
error: getErrorMessage(error),
});
});
};
const interval = setInterval(refreshReminderStatus, 30000);
return () => clearInterval(interval);
}, [clearReminderMetadata, fetchWithRefresh]);
}, [clearReminderMetadata, fetchWithRefresh, getErrorMessage]);
// Internal save function (no event needed)
const performSave = useCallback(
@@ -431,7 +446,11 @@ export function useSettings(): UseSettingsReturn {
} else {
latestSavedSettingsRef.current = { ...settingsToSave };
}
} catch {
} catch (error: unknown) {
log.warn("[useSettings] settings save failed", {
error: getErrorMessage(error),
syncState,
});
if (syncState) {
setSettingsSaved(false);
// Keep UI aligned with backend truth if save failed (auth/session/network/server error).
@@ -443,7 +462,7 @@ export function useSettings(): UseSettingsReturn {
}
}
},
[buildSettingsPayload, fetchWithRefresh, loadSettings]
[buildSettingsPayload, fetchWithRefresh, getErrorMessage, loadSettings]
);
// Debounced auto-save: fires whenever settings change
@@ -541,12 +560,13 @@ export function useSettings(): UseSettingsReturn {
success: res.ok,
message: data.message || (res.ok ? "Email sent!" : "Failed to send email"),
});
} catch {
} catch (error: unknown) {
log.warn("[useSettings] test email failed", { error: getErrorMessage(error) });
setTestEmailResult({ success: false, message: "Failed to send test email" });
} finally {
setTestingEmail(false);
}
}, [fetchWithRefresh, settings.notificationEmail]);
}, [fetchWithRefresh, getErrorMessage, settings.notificationEmail]);
const testShoutrrr = useCallback(async () => {
setTestingShoutrrr(true);
@@ -562,12 +582,13 @@ export function useSettings(): UseSettingsReturn {
success: res.ok,
message: data.message || (res.ok ? "Notification sent!" : "Failed to send notification"),
});
} catch {
} catch (error: unknown) {
log.warn("[useSettings] test push notification failed", { error: getErrorMessage(error) });
setTestShoutrrrResult({ success: false, message: "Failed to send test notification" });
} finally {
setTestingShoutrrr(false);
}
}, [fetchWithRefresh, settings.shoutrrrUrl]);
}, [fetchWithRefresh, getErrorMessage, settings.shoutrrrUrl]);
// Check for unsaved changes
const hasUnsavedChanges = JSON.stringify(settings) !== JSON.stringify(savedSettings);