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 && (
-
- )}
- {t("medications.detail.zone.end.button")}
-
-
-
-
- >
- ) : 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 && (
+
+ )}
+ {t("medications.detail.zone.end.button")}
+
+
+
+
+ >
+ );
+}
+
/**
* v1.7.0 — irreversible danger zone: purge intake history + delete the
* medication. v1.16.1 — the body is bare rows (the caller's