import { useState, useEffect, useCallback, useMemo } from "react"; import { Plus, Zap, Globe, Folder } from "lucide-react"; import type { Routine, RoutineCreateInput } from "@fusion/core"; import { fetchRoutines, createRoutine, updateRoutine, deleteRoutine, runRoutine, } from "../api"; import { RoutineCard } from "./RoutineCard"; import { RoutineEditor } from "./RoutineEditor"; import type { ToastType } from "../hooks/useToast"; /** Polling interval for auto-refreshing the schedule/routine list (30 seconds). */ const POLL_INTERVAL_MS = 30_000; /** Scheduling scope: global (user-level) or project-scoped. */ export type SchedulingScope = "global" | "project"; interface ScheduledTasksModalProps { onClose: () => void; addToast: (message: string, type?: ToastType) => void; /** Optional project ID for project-scoped scheduling. When provided, scope defaults to "project". */ projectId?: string; } export function ScheduledTasksModal({ onClose, addToast, projectId }: ScheduledTasksModalProps) { // Scope state: defaults to "project" when projectId exists, else "global" const [activeScope, setActiveScope] = useState(() => projectId ? "project" : "global"); // Routine state const [routines, setRoutines] = useState([]); const [routineView, setRoutineView] = useState<"list" | "create" | "edit">("list"); const [editingRoutine, setEditingRoutine] = useState(); const [runningRoutineId, setRunningRoutineId] = useState(null); // Build scope options for API calls const scopeOptions = useMemo(() => ({ scope: activeScope, projectId: activeScope === "project" ? projectId : undefined, }), [activeScope, projectId]); // Load routines const loadRoutines = useCallback(async () => { try { const data = await fetchRoutines(scopeOptions); setRoutines(data); } catch (err: any) { addToast(err.message || "Failed to load routines", "error"); } }, [addToast, scopeOptions]); useEffect(() => { void loadRoutines(); }, [loadRoutines]); // Poll for updates while modal is open useEffect(() => { const interval = setInterval(() => { void loadRoutines(); }, POLL_INTERVAL_MS); return () => clearInterval(interval); }, [loadRoutines]); // Close on Escape (only when not in a sub-form) useEffect(() => { const handleKey = (e: KeyboardEvent) => { if (e.key === "Escape") { if (routineView !== "list") { setRoutineView("list"); setEditingRoutine(undefined); } else { onClose(); } } }; document.addEventListener("keydown", handleKey); return () => document.removeEventListener("keydown", handleKey); }, [onClose, routineView]); const handleOverlayClick = useCallback( (e: React.MouseEvent) => { if (e.target === e.currentTarget) onClose(); }, [onClose], ); // ── Routine CRUD handlers ─────────────────────────────────────────────── const handleCreateRoutine = useCallback( async (input: RoutineCreateInput) => { try { await createRoutine(input, scopeOptions); addToast("Routine created", "success"); setRoutineView("list"); await loadRoutines(); } catch (err: any) { addToast(err.message || "Failed to create routine", "error"); } }, [addToast, loadRoutines, scopeOptions], ); const handleEditRoutine = useCallback((routine: Routine) => { setEditingRoutine(routine); setRoutineView("edit"); }, []); const handleUpdateRoutine = useCallback( async (input: RoutineCreateInput) => { if (!editingRoutine) return; try { await updateRoutine(editingRoutine.id, input, scopeOptions); addToast("Routine updated", "success"); setRoutineView("list"); setEditingRoutine(undefined); await loadRoutines(); } catch (err: any) { addToast(err.message || "Failed to update routine", "error"); } }, [editingRoutine, addToast, loadRoutines, scopeOptions], ); const handleDeleteRoutine = useCallback( async (routine: Routine) => { try { await deleteRoutine(routine.id, scopeOptions); addToast(`Deleted "${routine.name}"`, "success"); await loadRoutines(); } catch (err: any) { addToast(err.message || "Failed to delete routine", "error"); } }, [addToast, loadRoutines, scopeOptions], ); const handleRunRoutine = useCallback( async (routine: Routine) => { setRunningRoutineId(routine.id); try { const { result } = await runRoutine(routine.id, scopeOptions); if (result.success) { addToast(`"${routine.name}" completed successfully`, "success"); } else { addToast(`"${routine.name}" failed: ${result.error || "Unknown error"}`, "error"); } await loadRoutines(); } catch (err: any) { addToast(err.message || "Failed to run routine", "error"); } finally { setRunningRoutineId(null); } }, [addToast, loadRoutines, scopeOptions], ); const handleToggleRoutine = useCallback( async (routine: Routine) => { try { await updateRoutine(routine.id, { enabled: !routine.enabled }, scopeOptions); addToast( `"${routine.name}" ${routine.enabled ? "disabled" : "enabled"}`, "success", ); await loadRoutines(); } catch (err: any) { addToast(err.message || "Failed to toggle routine", "error"); } }, [addToast, loadRoutines, scopeOptions], ); const handleRoutineCancel = useCallback(() => { setRoutineView("list"); setEditingRoutine(undefined); }, []); // ── Scope switch handler ─────────────────────────────────────────────── const handleScopeSwitch = useCallback((scope: SchedulingScope) => { setActiveScope(scope); // Reset to list view when switching scope setRoutineView("list"); setEditingRoutine(undefined); }, []); // ── Render content ───────────────────────────────────────────────────── const renderRoutinesContent = () => { if (routineView === "create") { return ; } if (routineView === "edit" && editingRoutine) { return ( ); } // List view if (routines.length === 0) { return (

No automations yet

Create an automation with a schedule, webhook, API, or manual trigger.

); } return (
{routines.map((r) => ( ))}
); }; const renderContent = () => { return renderRoutinesContent(); }; // Determine if we're in "list" view for showing the "New" button const isShowingList = routineView === "list" && routines.length > 0; return (

Automations

{/* Scope selector */}
{isShowingList && ( )}
{routines.length} automation{routines.length === 1 ? "" : "s"}
{renderContent()}
); }