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