"use client"; import React, { useState, useEffect, useCallback } from "react"; import { Plus, Trash2, RefreshCcw, Check, AlertCircle, ArrowRight, ArrowLeftRight, Pencil, X } from "lucide-react"; interface CalendarSyncRule { id: string; name: string; enabled: boolean; direction: "one-way" | "two-way"; sourceConnectionId: string; sourceCalendarId: string; targetConnectionId: string; targetCalendarId: string; syncDescription: boolean; syncLocation: boolean; syncRecurring: boolean; titlePrefix: string; lastSyncedAt?: string | null; } interface CalendarInfo { id: string; title: string; color?: string; editable?: boolean; } interface ConnectionInfo { id: string; provider: string; calendars?: CalendarInfo[]; } interface Props { connections: ConnectionInfo[]; darkMode?: boolean; t: Record; } const PROVIDER_LABELS: Record = { google: "Google", apple: "Apple", outlook: "Outlook", synology: "Synology", }; function calendarLabel(conn: ConnectionInfo, calId: string): string { const cal = conn.calendars?.find((c) => c.id === calId); return cal?.title || calId; } function providerLabel(conn: ConnectionInfo): string { return PROVIDER_LABELS[conn.provider] || conn.provider; } // Flatten all (connectionId, calendarId) pairs for picker usage function allCalendarOptions(connections: ConnectionInfo[]) { const opts: { connId: string; calId: string; label: string; connLabel: string; color?: string }[] = []; for (const conn of connections) { if (!conn.calendars) continue; for (const cal of conn.calendars) { opts.push({ connId: conn.id, calId: cal.id, label: cal.title, connLabel: providerLabel(conn), color: cal.color, }); } } return opts; } const EMPTY_FORM = { name: "", enabled: true, direction: "one-way" as "one-way" | "two-way", sourceConnectionId: "", sourceCalendarId: "", targetConnectionId: "", targetCalendarId: "", syncDescription: true, syncLocation: true, syncRecurring: false, titlePrefix: "", }; export default function CalendarSyncRulesPanel({ connections, darkMode, t }: Props) { const [rules, setRules] = useState([]); const [loading, setLoading] = useState(true); const [syncing, setSyncing] = useState(false); const [syncResults, setSyncResults] = useState<{ ruleId: string; ruleName: string; created: number; updated: number; deleted: number; error?: string }[] | null>(null); const [showForm, setShowForm] = useState(false); const [editingId, setEditingId] = useState(null); const [form, setForm] = useState({ ...EMPTY_FORM }); const [formError, setFormError] = useState(""); const [saving, setSaving] = useState(false); const [msg, setMsg] = useState<{ text: string; type: "success" | "error" } | null>(null); const calOptions = allCalendarOptions(connections); const fetchRules = useCallback(async () => { try { const res = await fetch("/api/calendar/cross-sync/rules"); const data = await res.json(); setRules(data.rules || []); } catch { // silently fail } finally { setLoading(false); } }, []); useEffect(() => { fetchRules(); }, [fetchRules]); const openNewForm = () => { setForm({ ...EMPTY_FORM }); setEditingId(null); setFormError(""); setShowForm(true); }; const openEditForm = (rule: CalendarSyncRule) => { setForm({ name: rule.name, enabled: rule.enabled, direction: rule.direction, sourceConnectionId: rule.sourceConnectionId, sourceCalendarId: rule.sourceCalendarId, targetConnectionId: rule.targetConnectionId, targetCalendarId: rule.targetCalendarId, syncDescription: rule.syncDescription, syncLocation: rule.syncLocation, syncRecurring: rule.syncRecurring, titlePrefix: rule.titlePrefix, }); setEditingId(rule.id); setFormError(""); setShowForm(true); }; const closeForm = () => { setShowForm(false); setEditingId(null); }; const saveForm = async () => { if (!form.sourceCalendarId || !form.targetCalendarId) { setFormError("Please select a source and target calendar."); return; } if (form.sourceConnectionId === form.targetConnectionId && form.sourceCalendarId === form.targetCalendarId) { setFormError("Source and target calendar must be different."); return; } setSaving(true); setFormError(""); try { const url = editingId ? `/api/calendar/cross-sync/rules/${editingId}` : "/api/calendar/cross-sync/rules"; const method = editingId ? "PATCH" : "POST"; const res = await fetch(url, { method, headers: { "Content-Type": "application/json" }, body: JSON.stringify(form), }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Failed to save"); } await fetchRules(); closeForm(); setMsg({ text: editingId ? "Rule updated." : "Rule created.", type: "success" }); setTimeout(() => setMsg(null), 3000); } catch (err: any) { setFormError(err.message || "Failed to save rule"); } finally { setSaving(false); } }; const deleteRule = async (ruleId: string) => { if (!confirm("Delete this sync rule? Existing synced events will NOT be removed.")) return; try { await fetch(`/api/calendar/cross-sync/rules/${ruleId}`, { method: "DELETE" }); setRules((prev) => prev.filter((r) => r.id !== ruleId)); setMsg({ text: "Rule deleted.", type: "success" }); setTimeout(() => setMsg(null), 3000); } catch { setMsg({ text: "Failed to delete rule.", type: "error" }); setTimeout(() => setMsg(null), 4000); } }; const toggleEnabled = async (rule: CalendarSyncRule) => { const newVal = !rule.enabled; setRules((prev) => prev.map((r) => r.id === rule.id ? { ...r, enabled: newVal } : r)); await fetch(`/api/calendar/cross-sync/rules/${rule.id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ enabled: newVal }), }); }; const runSync = async () => { setSyncing(true); setSyncResults(null); try { const res = await fetch("/api/calendar/cross-sync/run", { method: "POST" }); const data = await res.json(); setSyncResults(data.results || []); } catch { setMsg({ text: "Sync failed.", type: "error" }); setTimeout(() => setMsg(null), 4000); } finally { setSyncing(false); } }; // Derive connection for a given connectionId const connFor = (id: string) => connections.find((c) => c.id === id); const label = (s: string) => ( ); const inputStyle: React.CSSProperties = { width: "100%", padding: "7px 10px", borderRadius: "7px", border: "1px solid var(--weekly-border, #e5e7eb)", background: "var(--weekly-input-bg, var(--weekly-bg, #fff))", color: "var(--weekly-text, #333)", fontSize: "0.875rem", }; const btnStyle = (primary?: boolean): React.CSSProperties => ({ padding: "8px 16px", borderRadius: "8px", background: primary ? "var(--weekly-text, #333)" : "var(--weekly-bg, #fff)", color: primary ? "var(--weekly-bg, #fff)" : "var(--weekly-text, #333)", border: primary ? "none" : "1px solid var(--weekly-border, #e5e7eb)", fontSize: "0.875rem", fontWeight: 500, cursor: "pointer", display: "flex", alignItems: "center", gap: "6px", }); // Calendar picker select for the form const CalPicker = ({ label: pickerLabel, connId, calId, onChange }: { label: string; connId: string; calId: string; onChange: (connId: string, calId: string) => void; }) => { const value = connId && calId ? `${connId}::${calId}` : ""; return (
{label(pickerLabel)}
); }; if (loading) return

Loading sync rules…

; return (
{/* Enable/Disable section header */}

{t.calendarSyncTitle || "Calendar Sync"}

{t.calendarSyncDesc || "Sync events between connected calendar providers."}

{/* Status message */} {msg && (
{msg.type === "success" ? : } {msg.text}
)} {/* Sync results */} {syncResults && (
{t.syncResults || "Sync Results"} {syncResults.length === 0 ? {t.noRulesEnabled || "No enabled rules found."} : syncResults.map((r) => (
{r.ruleName || "Rule"} {r.error ? Error: {r.error} : <> {r.created > 0 && +{r.created}} {r.updated > 0 && ~{r.updated}} {r.deleted > 0 && −{r.deleted}} {r.created === 0 && r.updated === 0 && r.deleted === 0 && up to date} }
)) }
)} {/* No connections warning */} {connections.length < 2 && (
{t.syncNeedsTwo || "You need at least two connected calendars to create a sync rule."}
)} {/* Rules list */} {rules.length > 0 && (
{rules.map((rule) => { const srcConn = connFor(rule.sourceConnectionId); const tgtConn = connFor(rule.targetConnectionId); return (
{/* Toggle */}
{rule.name || `${srcConn ? providerLabel(srcConn) : "?"} → ${tgtConn ? providerLabel(tgtConn) : "?"}`}
{srcConn ? calendarLabel(srcConn, rule.sourceCalendarId) : rule.sourceCalendarId} {rule.direction === "two-way" ? : } {tgtConn ? calendarLabel(tgtConn, rule.targetCalendarId) : rule.targetCalendarId}
{rule.lastSyncedAt && (
Last synced: {new Date(rule.lastSyncedAt).toLocaleString()}
)}
); })}
)} {rules.length === 0 && !showForm && (

{t.noSyncRules || "No sync rules yet. Add one below."}

)} {/* Add new rule button */} {!showForm && connections.length >= 2 && ( )} {/* Form */} {showForm && (
{editingId ? (t.editSyncRule || "Edit Rule") : (t.newSyncRule || "New Sync Rule")}
{/* Rule name */}
{label(t.syncRuleName || "Rule Name (optional)")} setForm({ ...form, name: e.target.value })} placeholder={t.syncRuleNamePlaceholder || "e.g. Work → Personal"} style={inputStyle} />
{/* Direction */}
{label(t.syncDirection || "Direction")}
{(["one-way", "two-way"] as const).map((d) => ( ))}
{/* Source calendar */} setForm({ ...form, sourceConnectionId: connId, sourceCalendarId: calId })} /> {/* Target calendar */} setForm({ ...form, targetConnectionId: connId, targetCalendarId: calId })} /> {/* Title prefix */}
{label(t.titlePrefix || "Title Prefix (optional)")} setForm({ ...form, titlePrefix: e.target.value })} placeholder={t.titlePrefixPlaceholder || "e.g. [Work] "} style={inputStyle} />
{/* Options checkboxes */}
{([ { key: "syncDescription", label: t.syncDescription || "Sync description" }, { key: "syncLocation", label: t.syncLocation || "Sync location" }, { key: "syncRecurring", label: t.syncRecurring || "Include recurring events" }, ] as const).map(({ key, label: optLabel }) => ( ))}
{formError && (
{formError}
)}
)}
); }