"use client"; import { useEffect, useState } from "react"; import { Plus, X } from "lucide-react"; import { useTranslation } from "react-i18next"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { ConfirmDialog } from "@/components/ui/confirm-dialog"; import { Input } from "@/components/ui/input"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { DeleteAccountDialog } from "@/components/settings/delete-account-dialog"; import { CopyField, FieldLabel, SettingsCard, SettingsSection, ToggleRow, } from "@/components/settings/settings-parts"; import { authClient } from "@/lib/auth-client"; import { supportedLanguages } from "@/lib/i18n/config"; import { persistLanguage } from "@/lib/language"; import { SPECIALTIES, specialtyLabel } from "@/lib/staff"; import { getSettings, saveSettings, type UserPreferences, } from "@/lib/settings"; import { notify } from "@/lib/toast"; // Keys into settings.profile.notif.* — each maps to a persisted preference. const patientNotifications = [ { titleKey: "newLab", descKey: "newLabDesc" }, { titleKey: "recordUpdated", descKey: "recordUpdatedDesc" }, { titleKey: "approvalRequested", descKey: "approvalRequestedDesc" }, { titleKey: "changeApproved", descKey: "changeApprovedDesc" }, { titleKey: "newMessage", descKey: "newMessageDesc" }, { titleKey: "visitScheduled", descKey: "visitScheduledDesc" }, ] as const; const accountNotifications = [ { titleKey: "pendingApprovals", descKey: "pendingApprovalsDesc" }, { titleKey: "recordsShared", descKey: "recordsSharedDesc" }, ] as const; // All notification toggles default to on; profile extras default to empty. const DEFAULT_PREFS: UserPreferences = { "notif.newLab": true, "notif.recordUpdated": true, "notif.approvalRequested": true, "notif.changeApproved": true, "notif.newMessage": true, "notif.visitScheduled": true, "notif.pendingApprovals": true, "notif.recordsShared": true, clinic: "", contactEmail: "", specialty: "", // Professional links stored as a JSON array string (values are boolean|string). links: "", }; // Parse the stored `links` preference (a JSON array string) into an array. function parseLinks(value: unknown): string[] { try { const parsed = JSON.parse(String(value || "[]")); return Array.isArray(parsed) ? parsed.map(String) : []; } catch { return []; } } export function ProfilePanel() { const { t, i18n } = useTranslation(); const { data: session } = authClient.useSession(); const user = session?.user; // The active UI language — `i18n.changeLanguage` persists the choice to // localStorage (the detector's cache), so it survives reloads. const activeLang = i18n.resolvedLanguage ?? i18n.language; const [prefs, setPrefs] = useState(DEFAULT_PREFS); const [baseline, setBaseline] = useState(DEFAULT_PREFS); const [name, setName] = useState(""); const [baselineName, setBaselineName] = useState(""); const [saving, setSaving] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); // A language picked in the Select but not yet applied — its presence opens the // confirmation dialog. The Select stays bound to `activeLang`, so cancelling // (clearing this) automatically reverts the shown selection. const [pendingLang, setPendingLang] = useState(null); useEffect(() => { let cancelled = false; getSettings() .then((stored) => { if (cancelled) return; const merged = { ...DEFAULT_PREFS, ...stored }; setPrefs(merged); setBaseline(merged); }) .catch(() => { // Keep the defaults; the page still works and Save will retry. }); return () => { cancelled = true; }; }, []); // Seed the display name from the session once it loads. useEffect(() => { if (user?.name) { setName((prev) => prev || user.name); setBaselineName((prev) => prev || user.name); } }, [user?.name]); const setPref = (key: string, value: boolean | string) => setPrefs((prev) => ({ ...prev, [key]: value })); const links = parseLinks(prefs.links); const setLinks = (next: string[]) => setPref("links", JSON.stringify(next)); const dirty = name !== baselineName || JSON.stringify(prefs) !== JSON.stringify(baseline); const save = async () => { setSaving(true); try { // Drop blank link rows before persisting. const cleanedLinks = links.map((l) => l.trim()).filter(Boolean); const toSave: UserPreferences = { ...prefs, links: cleanedLinks.length ? JSON.stringify(cleanedLinks) : "", }; const saved = await saveSettings(toSave); const trimmed = name.trim(); if (trimmed && trimmed !== baselineName) { const { error } = await authClient.updateUser({ name: trimmed }); if (error) throw new Error(error.message ?? "updateUser failed"); setName(trimmed); setBaselineName(trimmed); } const merged = { ...DEFAULT_PREFS, ...saved }; setPrefs(merged); setBaseline(merged); notify.success( t("settings.profile.savedTitle"), t("settings.profile.savedBody"), ); } catch { notify.error( t("settings.profile.saveFailedTitle"), t("settings.profile.saveFailedBody"), ); } finally { setSaving(false); } }; const initial = (name || user?.name || "?").trim().charAt(0).toUpperCase(); const username = (user as { username?: string | null } | undefined)?.username ?? null; return ( <>
{t("settings.profile.avatar")} {initial}
{t("settings.profile.displayName")} setName(event.target.value)} value={name} />
{t("settings.profile.specialty")}
{t("settings.profile.clinic")} setPref("clinic", event.target.value)} placeholder={t("settings.profile.clinicPlaceholder")} value={String(prefs.clinic ?? "")} />
{t("settings.profile.contactEmail")} setPref("contactEmail", event.target.value) } placeholder={t("settings.profile.contactEmailPlaceholder")} value={String(prefs.contactEmail ?? "")} />
{t("settings.profile.professionalLinks")}

{t("settings.profile.professionalLinksHint")}

{links.length > 0 ? (
{links.map((link, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: rows are positional
setLinks( links.map((value, i) => i === index ? event.target.value : value, ), ) } placeholder={t("settings.profile.linkPlaceholder")} value={link} />
))}
) : null}
{t("settings.profile.language.label")}
{patientNotifications.map((item) => ( setPref(`notif.${item.titleKey}`, checked) } title={t(`settings.profile.notif.${item.titleKey}`)} /> ))}
{accountNotifications.map((item) => ( setPref(`notif.${item.titleKey}`, checked) } title={t(`settings.profile.notif.${item.titleKey}`)} /> ))}

{t("settings.profile.deleteAccount")}

{t("settings.profile.deleteAccountDescription")}

{ if (pendingLang) { void i18n.changeLanguage(pendingLang); void persistLanguage(pendingLang); } }} onOpenChange={(open) => { if (!open) setPendingLang(null); }} open={pendingLang !== null} title={t("settings.profile.language.confirmTitle")} variant="default" /> {dirty ? (

{t("settings.profile.unsavedChanges")}

) : null} ); }