"use client"; import { useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Download, Link2, Loader2, RefreshCw, Save, Smile, Unlink, } from "lucide-react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { PasswordInput } from "@/components/settings/password-input"; import { TestConnectionButton } from "@/components/settings/test-connection-button"; import { useAuth } from "@/hooks/use-auth"; import { formatDateTime } from "@/lib/format"; import { useTranslations } from "@/lib/i18n/context"; import { invalidateKeys, measurementDependentKeys } from "@/lib/query-keys"; interface MoodLogStatus { configured: boolean; enabled: boolean; lastSyncedAt: string | null; entryCount: number; webhookSecret: string | null; } interface GlobalServiceAvailability { telegramGlobal: boolean; ntfyGlobal: boolean; webPushGlobal: boolean; apiGlobal: boolean; moodLogGlobal: boolean; } export function IntegrationsSection() { const { t } = useTranslations(); const { isAuthenticated } = useAuth(); const { data: globalServices } = useQuery({ queryKey: ["settings", "global-services"], queryFn: async () => { const res = await fetch("/api/settings/global-services"); if (!res.ok) throw new Error("Failed"); return (await res.json()).data as GlobalServiceAvailability; }, enabled: isAuthenticated, }); const moodLogEnabled = globalServices?.moodLogGlobal ?? true; return (

{t("settings.sections.integrations.title")}

{t("settings.sections.integrations.description")}

{moodLogEnabled && }
); } function WithingsCard({ isAuthenticated }: { isAuthenticated: boolean }) { const { t } = useTranslations(); const [syncing, setSyncing] = useState(false); const [syncMsg, setSyncMsg] = useState(null); const [syncMsgType, setSyncMsgType] = useState<"success" | "error" | null>( null, ); const [clientId, setClientId] = useState(""); const [clientSecret, setClientSecret] = useState(""); const [credsSaving, setCredsSaving] = useState(false); const [credsMsg, setCredsMsg] = useState(null); const [credsMsgType, setCredsMsgType] = useState<"success" | "error" | null>( null, ); const queryClient = useQueryClient(); const { data: status } = useQuery({ queryKey: ["withings", "status"], queryFn: async () => { const res = await fetch("/api/withings/status"); if (!res.ok) throw new Error("Failed"); const json = await res.json(); return json.data as { connected: boolean; configured: boolean; lastSyncedAt?: string | null; connectedAt?: string; tokenExpired?: boolean; }; }, enabled: isAuthenticated, }); const disconnect = useMutation({ mutationFn: async () => { const res = await fetch("/api/withings/disconnect", { method: "POST" }); if (!res.ok) throw new Error("Failed"); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["withings"] }); }, }); async function handleSync(fullSync = false) { setSyncing(true); setSyncMsg(null); setSyncMsgType(null); try { const res = await fetch("/api/withings/sync", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fullSync }), }); const json = await res.json(); if (res.ok) { setSyncMsg( fullSync ? t("settings.withingsFullSyncResult", { count: json.data.imported, }) : t("settings.withingsSyncResult", { count: json.data.imported }), ); setSyncMsgType("success"); void invalidateKeys(queryClient, measurementDependentKeys); } else { setSyncMsg(json.error || t("settings.withingsSyncFailed")); setSyncMsgType("error"); } } catch { setSyncMsg(t("settings.withingsSyncFailed")); setSyncMsgType("error"); } finally { setSyncing(false); } } async function handleSaveCredentials(e: React.FormEvent) { e.preventDefault(); setCredsSaving(true); setCredsMsg(null); setCredsMsgType(null); try { const res = await fetch("/api/withings/credentials", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ clientId: clientId.trim(), clientSecret: clientSecret.trim(), }), }); if (res.ok) { setCredsMsg(t("settings.withingsCredentialsSaved")); setCredsMsgType("success"); setClientId(""); setClientSecret(""); queryClient.invalidateQueries({ queryKey: ["withings"] }); } else { try { const json = await res.json(); setCredsMsg(json.error || t("settings.savingError")); } catch { setCredsMsg(t("settings.savingError")); } setCredsMsgType("error"); } } catch { setCredsMsg(t("common.networkError")); setCredsMsgType("error"); } setCredsSaving(false); } return (

{t("settings.withings")}

{status?.configured && !status?.connected && ( {t("settings.configured")} )} {status?.connected && ( {t("settings.withingsConnected")} )} {status?.lastSyncedAt && ( {t("settings.withingsLastSync")}:{" "} {formatDateTime(status.lastSyncedAt)} )} {status?.tokenExpired && ( {t("settings.withingsTokenExpired")} )}

{t("settings.withingsDescription")}

{t("settings.withingsCredentials")}

setClientId(e.target.value)} placeholder={ status?.configured ? t("settings.withingsCredentialsSavedPlaceholder") : t("settings.withingsClientId") } maxLength={200} />
setClientSecret(e.target.value)} placeholder={ status?.configured ? t("settings.withingsCredentialsSavedPlaceholderSecret") : t("settings.withingsClientSecret") } maxLength={200} />
{credsMsg && (

{credsMsg}

)}
{status?.connected ? ( <>
{t("settings.withingsFullSyncTitle")} {t("settings.withingsFullSyncDescription")} {t("common.cancel")} handleSync(true)}> {t("settings.withingsSynchronize")} {t("settings.withingsDisconnectTitle")} {t("settings.withingsDisconnectDescription")} {t("common.cancel")} disconnect.mutate()} > {t("settings.withingsDisconnect")}
{syncMsg && (

{syncMsg}

)} ) : status?.configured ? ( ) : (
{t("settings.withingsNoCredentials")}
)}
); } function MoodLogCard() { const { t } = useTranslations(); const [url, setUrl] = useState(""); const [apiKey, setApiKey] = useState(""); const [saving, setSaving] = useState(false); const [syncing, setSyncing] = useState(false); const [msg, setMsg] = useState(null); const [msgType, setMsgType] = useState<"success" | "error" | null>(null); const { data: status, refetch: refetchStatus } = useQuery({ queryKey: ["moodlog-status"], queryFn: async () => { const res = await fetch("/api/integrations/moodlog/status"); if (!res.ok) throw new Error("Failed"); return (await res.json()).data as MoodLogStatus; }, }); async function handleSave(e: React.FormEvent) { e.preventDefault(); setSaving(true); setMsg(null); const res = await fetch("/api/settings/moodlog", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: url.trim(), apiKey: apiKey.trim() }), }); if (res.ok) { setMsg(t("settings.moodLogSaved")); setMsgType("success"); setUrl(""); setApiKey(""); await refetchStatus(); } else { const json = await res.json(); setMsg(json.error || t("settings.savingError")); setMsgType("error"); } setSaving(false); } async function handleSync(fullSync = false) { setSyncing(true); setMsg(null); try { const res = await fetch("/api/integrations/moodlog/sync", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fullSync }), }); if (res.ok) { const json = await res.json(); setMsg( t("settings.moodLogSyncResult").replace( "{count}", String(json.data.imported), ), ); setMsgType("success"); await refetchStatus(); } else { setMsg(t("settings.moodLogSyncFailed")); setMsgType("error"); } } catch { setMsg(t("settings.moodLogSyncFailed")); setMsgType("error"); } setSyncing(false); } async function handleDisconnect() { const res = await fetch("/api/settings/moodlog", { method: "DELETE" }); if (res.ok) { setMsg(t("settings.moodLogDisconnected")); setMsgType("success"); await refetchStatus(); } } return (
{status?.configured && ( {status.enabled ? t("settings.withingsConnected") : t("settings.configured")} )}

{t("settings.moodLogDescription")}

setUrl(e.target.value)} />
setApiKey(e.target.value)} />
{status?.configured && (
{status.webhookSecret && (

{t("settings.moodLogWebhookSecretHelp")}

)}
{status.lastSyncedAt && ( {t("settings.moodLogLastSync")}:{" "} {formatDateTime(status.lastSyncedAt)} )} {t("settings.moodLogEntries")}: {status.entryCount}
{t("settings.moodLogFullSyncTitle")} {t("settings.moodLogFullSyncDescription")} {t("common.cancel")} handleSync(true)}> {t("settings.moodLogFullSync")} {/* v1.4.3: aligned with the Withings disconnect button — outline + text-destructive instead of solid destructive so the trigger reads as a reversible action; the actual "yes, disconnect" confirmation inside the dialog keeps its solid-red treatment. */} {t("settings.moodLogDisconnectTitle")} {t("settings.moodLogDisconnectDescription")} {t("common.cancel")} {t("settings.moodLogDisconnect")}
)} {msg && (

{msg}

)}
); }