refactor: decompose frontend state and medication dialog flows
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user