"use client"; import { useState } from "react"; import { useQuery, useQueryClient } from "@tanstack/react-query"; import { BookOpenCheck, Loader2, Trash2 } from "lucide-react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; import { ResearchModeAcknowledgmentDialog } from "@/components/medications/research-mode-acknowledgment-dialog"; import { formatDateTime } from "@/lib/format"; import { SettingsCard } from "@/components/settings/settings-card"; import { SettingsCardHeader } from "@/components/settings/_card-header"; import { useTranslations } from "@/lib/i18n/context"; import { queryKeys } from "@/lib/query-keys"; import { type ResearchModeStatus, researchModeGateState, } from "@/lib/medications/research-mode-types"; import { apiFetchRaw } from "@/lib/api/api-fetch"; /** * Settings → Advanced. * * v1.4.16 phase B7: every export path (CSV, JSON, doctor-report PDF) * moved out into the dedicated `` under `/settings/export`. * What stays here is the irreversible danger-zone — the "wipe all my * data" surface that should never live next to a single-click export * button. * * v1.4.25 W19c-Frontend adds the Research Mode toggle on this page. * Research Mode and the data-reset are both opt-in / version-gated * controls; they share a semantic shelf even though they touch * different parts of the user record. Toggle ON opens the * acknowledgment dialog (the user reads + confirms before the server * stamps the version). Toggle OFF fires DELETE directly. A separate * amber banner above the toggle catches the version-mismatch case * (server bumped the disclaimer while the user was still * acknowledged) and surfaces a Re-acknowledge CTA that re-opens the * dialog. */ export function AdvancedSection() { // v1.18.6 (W9) — the visible heading + subtitle now come from the shared // `` in the route; this body is the advanced cards. // v1.18.6.1 — the guided tour replay card was removed: the tour now exists // only as the first-time auto-start after onboarding. This page is the // research-mode + data/account destructive controls. return (
); } /** * v1.4.25 W21 Fix-N (simp-H3) — pull the nested ternary that decided * the toggle's caption out of the JSX. The four-way ladder (loading / * enabled-open / enabled-stale / disabled) is dense to read inline, * and the surface stays open to a fifth state ("never queried") when * we wire the offline shell in v1.4.26. */ function researchModeStatusLabel( status: ResearchModeStatus | null | undefined, isLoading: boolean, t: (key: string, params?: Record) => string, ): string { if (isLoading) return t("common.loading"); const gate = researchModeGateState(status); if (gate === "off") return t("settings.researchMode.disabledStatus"); if (gate === "stale") return t("settings.researchMode.enabledStaleStatus"); return t("settings.researchMode.acknowledgedOn", { date: status?.acknowledgedAt ? formatDateTime(status.acknowledgedAt) : "—", }); } function ResearchModeCard() { const { t } = useTranslations(); const queryClient = useQueryClient(); const [dialogOpen, setDialogOpen] = useState(false); const [toggleBusy, setToggleBusy] = useState(false); const [errorMessage, setErrorMessage] = useState(null); const { data: status, isLoading } = useQuery({ queryKey: queryKeys.researchMode(), queryFn: async () => { const res = await apiFetchRaw("/api/auth/me/research-mode"); if (!res.ok) return null; const json = await res.json(); return json.data as ResearchModeStatus; }, staleTime: 60 * 1000, }); const gateState = researchModeGateState(status); const showRePrompt = gateState === "stale"; // The Switch's `checked` mirrors the server flag exactly — even when // the version is stale we keep the toggle "on" so the user sees that // their previous choice is preserved; the banner above the toggle // explains why the chart isn't painting. const switchChecked = !!status?.enabled; async function handleToggle(next: boolean) { if (toggleBusy) return; setErrorMessage(null); if (next) { // Toggle ON → open the acknowledgment dialog. The dialog owns // the POST; on success it invalidates the `research-mode` query // and the toggle reflects the new state. setDialogOpen(true); return; } // Toggle OFF → fire DELETE directly. setToggleBusy(true); try { const res = await apiFetchRaw("/api/auth/me/research-mode", { method: "DELETE", }); if (!res.ok) { setErrorMessage(t("settings.researchMode.disableError")); return; } queryClient.invalidateQueries({ queryKey: queryKeys.researchMode() }); } catch { setErrorMessage(t("settings.researchMode.disableError")); } finally { setToggleBusy(false); } } return ( {showRePrompt && (

{t("settings.researchMode.rePromptTitle")}

{t("settings.researchMode.rePromptBody")}

)}

{t("settings.researchMode.toggleLabel")}

{researchModeStatusLabel(status, isLoading, t)}

{ void handleToggle(next); }} aria-label={t("settings.researchMode.toggleLabel")} data-slot="settings-research-mode-toggle" />
{errorMessage && (

{errorMessage}

)}
); } function DataResetCard() { const { t } = useTranslations(); const queryClient = useQueryClient(); const [deleting, setDeleting] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); const [msg, setMsg] = useState(null); const [msgType, setMsgType] = useState<"success" | "error" | null>(null); async function handleDeleteAllData() { if (deleting) return; setDeleting(true); setMsg(null); setMsgType(null); try { const res = await apiFetchRaw("/api/settings/data", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ confirm: "DELETE" }), }); const json = await res.json(); if (!res.ok) { setMsg(json.error || t("settings.dangerZoneDeleteFailed")); setMsgType("error"); return; } await queryClient.invalidateQueries(); setMsg(t("settings.dangerZoneSuccess")); setMsgType("success"); setConfirmOpen(false); } catch { setMsg(t("settings.dangerZoneDeleteFailed")); setMsgType("error"); } finally { setDeleting(false); } } return ( {/* v1.4.43 QoL (L5) — dropped the `AlertTriangle` icon and neutralised the title colour so the danger-zone shaping is GitHub-style (red CTA only) rather than red-on-red-on-red. The protective gate (confirmation dialog) is unchanged; this is purely a visual-tone fix per the v1.4.43 audit. */}

{t("settings.dangerZone")}

{t("settings.dangerZoneDescription")}

{ // Hold the dialog open while the destructive mutation is in // flight so the in-dialog pending state stays visible and a // stray backdrop tap can't dismiss it mid-request. if (deleting) return; setConfirmOpen(open); }} > {t("settings.dangerZoneConfirm")} {t("settings.dangerZoneConfirmDescription")} {t("common.cancel")} { e.preventDefault(); void handleDeleteAllData(); }} disabled={deleting} aria-busy={deleting || undefined} data-slot="settings-data-reset-confirm" > {deleting && ( )} {t("settings.finalDelete")}
{msg && (

{msg}

)}
); } /** * v1.4.43 QoL (M3) — separate destructive action for the full account * delete. Pre-fix, the only "danger zone" CTA wiped the user's health * data but left the user row, passkeys, audit log, and sessions * intact — half of what a user reading "Danger Zone" expects per * GDPR Article 17. The route `DELETE /api/settings/account` already * cascades User + passkeys + audit log + sessions (and is rate-limited * by the API handler); this card is its UI front door. * * Visually the card mirrors the data-reset shaping (neutral title, * red CTA only) but the dialog copy is explicit: this deletes the * account, not just the data, and the user will be signed out * immediately. */ function AccountDeleteCard() { const { t } = useTranslations(); const [deleting, setDeleting] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); const [msg, setMsg] = useState(null); const [msgType, setMsgType] = useState<"success" | "error" | null>(null); async function handleDeleteAccount() { if (deleting) return; setDeleting(true); setMsg(null); setMsgType(null); try { const res = await apiFetchRaw("/api/settings/account", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ confirm: "DELETE_ACCOUNT" }), }); if (!res.ok) { try { const json = await res.json(); setMsg(json.error || t("settings.deleteAccountFailed")); } catch { setMsg(t("settings.deleteAccountFailed")); } setMsgType("error"); setDeleting(false); return; } setMsg(t("settings.deleteAccountSuccess")); setMsgType("success"); // The route destroyed every session before deleting the row; // give the toast a beat to paint, then bounce to the login page. // Leave `deleting` set so the confirm button keeps its pending // state through the redirect — the row is gone, nothing to undo. setTimeout(() => { window.location.href = "/auth/login"; }, 1_500); } catch { setMsg(t("settings.deleteAccountFailed")); setMsgType("error"); setDeleting(false); } } return (

{t("settings.deleteAccountCardTitle")}

{t("settings.deleteAccountCardDescription")}

{ // Hold the dialog open while the irreversible delete runs so // the pending state stays on screen and a stray dismissal // can't fire a second request. if (deleting) return; setConfirmOpen(open); }} > {t("settings.deleteAccountConfirmTitle")} {t("settings.deleteAccountConfirmDescription")} {t("common.cancel")} { e.preventDefault(); void handleDeleteAccount(); }} disabled={deleting} aria-busy={deleting || undefined} data-slot="settings-account-delete-confirm" > {deleting && ( )} {t("settings.deleteAccountFinal")}
{msg && (

{msg}

)}
); }