From fa20b1fe3a50fc44b6a81ee2ef3c86811c28f019 Mon Sep 17 00:00:00 2001 From: haritowa <6107211+haritowa@users.noreply.github.com> Date: Sat, 24 Oct 2026 17:04:26 +0000 Subject: [PATCH] refactor(medications): give End its own component End and its confirmation now live in EndCourseControl, which renders only while the course runs. When End stops being offered, the open dialog and its state unmount with it, so the state no longer has to be reset during render. --- .../sections/destructive-zone-section.tsx | 214 ++++++++++-------- 1 file changed, 115 insertions(+), 99 deletions(-) diff --git a/src/components/medications/sections/destructive-zone-section.tsx b/src/components/medications/sections/destructive-zone-section.tsx index f627a22b4..541d4ad8e 100644 --- a/src/components/medications/sections/destructive-zone-section.tsx +++ b/src/components/medications/sections/destructive-zone-section.tsx @@ -98,24 +98,9 @@ export function LifecycleManageBody({ }) { const { t } = useTranslations(); const queryClient = useQueryClient(); - const { user } = useAuth(); - const userTz = user?.timezone || DEFAULT_TIMEZONE; - const canEnd = canEndCourse(courseStatus); const [paused, setPaused] = useState(!active); const [tier1Busy, setTier1Busy] = useState(false); - const [endDialogOpen, setEndDialogOpen] = useState(false); - const [tier2Busy, setTier2Busy] = useState(false); - - // A refetch can stop offering End while its confirmation is open (the - // course ended in another tab). Drop the open state with it, or the dialog - // would pop up by itself once a later course offers End again. Adjusted - // during render, React's pattern for state that follows a prop. - const [offeredEnd, setOfferedEnd] = useState(canEnd); - if (offeredEnd !== canEnd) { - setOfferedEnd(canEnd); - if (!canEnd) setEndDialogOpen(false); - } async function tier1Toggle(pauseNext: boolean) { if (tier1Busy) return; @@ -143,27 +128,6 @@ export function LifecycleManageBody({ } } - async function tier2End() { - if (tier2Busy) return; - setTier2Busy(true); - try { - await apiPut( - `/api/medications/${medicationId}`, - endCourseBody(new Date(), userTz), - ); - await invalidateMedicationReads(queryClient); - toast.success(t("medications.detail.zone.end.toast")); - setEndDialogOpen(false); - onAfterAction?.(); - } catch (failure) { - showErrorToast(t("medications.detail.zone.end.failed"), { - cause: failure, - }); - } finally { - setTier2Busy(false); - } - } - return (
- {canEnd ? ( - <> - - -
-
-

- {t("medications.detail.zone.end.title")} -

-

- {t("medications.detail.zone.end.helper")} -

-
- -
- - {/* Tier 2 — Beenden confirm */} - - - - - {t("medications.detail.zone.end.dialogTitle", { - name: medicationName, - })} - - - {t("medications.detail.zone.end.dialogBody")} - - - - {t("common.cancel")} - { - e.preventDefault(); - void tier2End(); - }} - disabled={tier2Busy} - aria-busy={tier2Busy || undefined} - className="font-semibold" - > - {tier2Busy && ( - - - - - - ) : null} + {canEndCourse(courseStatus) && ( + + )}
); } +/** + * Tier 2: End the course today, behind a confirmation. Rendered only while + * the course runs, so when End stops being offered (the course ended in + * another tab) the open dialog and its state go away with it. + */ +function EndCourseControl({ + medicationId, + medicationName, + onAfterAction, +}: { + medicationId: string; + medicationName: string; + onAfterAction?: () => void; +}) { + const { t } = useTranslations(); + const queryClient = useQueryClient(); + const { user } = useAuth(); + const userTz = user?.timezone || DEFAULT_TIMEZONE; + + const [endDialogOpen, setEndDialogOpen] = useState(false); + const [tier2Busy, setTier2Busy] = useState(false); + + async function tier2End() { + if (tier2Busy) return; + setTier2Busy(true); + try { + await apiPut( + `/api/medications/${medicationId}`, + endCourseBody(new Date(), userTz), + ); + await invalidateMedicationReads(queryClient); + toast.success(t("medications.detail.zone.end.toast")); + setEndDialogOpen(false); + onAfterAction?.(); + } catch (failure) { + showErrorToast(t("medications.detail.zone.end.failed"), { + cause: failure, + }); + } finally { + setTier2Busy(false); + } + } + + return ( + <> + + +
+
+

+ {t("medications.detail.zone.end.title")} +

+

+ {t("medications.detail.zone.end.helper")} +

+
+ +
+ + {/* Tier 2 — Beenden confirm */} + + + + + {t("medications.detail.zone.end.dialogTitle", { + name: medicationName, + })} + + + {t("medications.detail.zone.end.dialogBody")} + + + + {t("common.cancel")} + { + e.preventDefault(); + void tier2End(); + }} + disabled={tier2Busy} + aria-busy={tier2Busy || undefined} + className="font-semibold" + > + {tier2Busy && ( + + + + + + ); +} + /** * v1.7.0 — irreversible danger zone: purge intake history + delete the * medication. v1.16.1 — the body is bare rows (the caller's