"use client"; import { useEffect, useMemo, useState } from "react"; import useSWR from "swr"; import { useRouter } from "next/navigation"; import AppShell from "@/components/AppShell"; import { EmptyState, FormError, LoadError, Modal, PageHeader, SkeletonRows, StatusBadge, } from "@/components/ui"; import { useToast } from "@/components/toast"; import { api, fetcher } from "@/lib/api"; import { useAuth, roleAtLeast } from "@/lib/auth"; import { TaskPreview } from "@/lib/task-types"; const ACTIVE_STATUSES = ["running", "queued", "waiting_approval"]; const STATUS_FILTERS = ["all", "queued", "running", "waiting_approval", "succeeded", "failed", "cancelled"]; const TEMPLATES = [ { label: "Proxmox + pfSense status", title: "Proxmox and pfSense status", issue: "check status of proxmox and pfsense", }, { label: "Full vessel health", title: "Full vessel health check", issue: "run a full stack health check on all systems", }, { label: "Firewall rules", title: "pfSense firewall rules", issue: "list firewall rules, NAT, gateways, and interfaces for pfsense", }, { label: "VoIP registration", title: "VoIP registration check", issue: "check pfsense SIP/NAT and asterisk registrations", }, ]; type Task = { id: number; title: string; issue: string; status: string; vessel_id: number | null; vessel_name?: string | null; created_at: string; }; type VesselDetail = { id: number; name: string; devices?: { catalog_key: string | null; device_type: string; name: string }[]; }; export default function TasksPage() { const { user } = useAuth(); const router = useRouter(); const toast = useToast(); const { data: tasks, error: tasksError, isLoading, mutate, } = useSWR("/api/tasks", fetcher, { refreshInterval: (data) => (data || []).some((t) => ACTIVE_STATUSES.includes(t.status)) ? 5000 : 0, }); const { data: vessels } = useSWR("/api/vessels", fetcher); const [open, setOpen] = useState(false); const [form, setForm] = useState({ title: "", issue: "", vessel_id: "" }); const [error, setError] = useState(null); const [query, setQuery] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); const [creating, setCreating] = useState(false); const [preview, setPreview] = useState(null); const [previewLoading, setPreviewLoading] = useState(false); const canCreate = roleAtLeast(user?.role, "support"); const selectedVessel = useMemo( () => vessels?.find((v) => String(v.id) === form.vessel_id), [vessels, form.vessel_id] ); const filteredTasks = useMemo(() => { const q = query.trim().toLowerCase(); return (tasks || []).filter((t) => { if (statusFilter !== "all" && t.status !== statusFilter) return false; if (!q) return true; return ( t.title.toLowerCase().includes(q) || t.issue.toLowerCase().includes(q) || (t.vessel_name || "").toLowerCase().includes(q) ); }); }, [tasks, query, statusFilter]); useEffect(() => { const issue = form.issue.trim(); if (!open || !form.vessel_id || issue.length < 4) { setPreview(null); setPreviewLoading(false); return; } let cancelled = false; const timer = setTimeout(async () => { setPreviewLoading(true); try { const data = await api("/api/tasks/preview", { method: "POST", body: JSON.stringify({ title: form.title, issue, vessel_id: Number(form.vessel_id), }), }); if (!cancelled) setPreview(data); } catch { if (!cancelled) setPreview(null); } finally { if (!cancelled) setPreviewLoading(false); } }, 350); return () => { cancelled = true; clearTimeout(timer); }; }, [form.issue, form.title, form.vessel_id, open]); const create = async (e: React.FormEvent | null, watch = true) => { e?.preventDefault(); setError(null); if (!form.vessel_id) { setError("Select a vessel"); return; } if (form.issue.trim().length < 5) { setError("Describe the issue in a bit more detail"); return; } setCreating(true); try { const payload = { title: form.title, issue: form.issue, vessel_id: Number(form.vessel_id), }; const created = await api("/api/tasks", { method: "POST", body: JSON.stringify(payload), }); setOpen(false); setForm({ title: "", issue: "", vessel_id: "" }); setPreview(null); toast.success("Task created"); mutate(); router.push(watch ? `/tasks/${created.id}` : "/"); } catch (err: any) { setError(err.message); toast.error(err.message || "Could not create task"); } finally { setCreating(false); } }; return ( setOpen(true)}> + New task ) } /> {tasks && tasks.length > 0 && (
setQuery(e.target.value)} aria-label="Search tasks" />
{STATUS_FILTERS.map((s) => ( ))}
)} {isLoading && !tasks ? ( ) : tasksError ? ( mutate()} /> ) : (tasks || []).length === 0 ? ( setOpen(true)}> + New task ) } /> ) : filteredTasks.length === 0 ? ( ) : (
{filteredTasks.map((t) => ( ))}
)} setOpen(false)} title="New troubleshooting task">
create(e, true)} className="space-y-4">
Quick templates
{TEMPLATES.map((template) => ( ))}
setForm({ ...form, title: e.target.value })} placeholder="e.g. GeneseasX health check" required />