// app/tasks/CardModal.tsx // New / edit card modal. Fields adapt to the selected task_type. "use client"; import { useEffect, useState } from "react"; import { TASK_TYPE_LABEL, PRIORITY_LABEL, type TaskType, type TaskPriority, type TaskStatus, } from "./columns"; import type { Task, CreateTaskInput } from "./api"; interface CardModalProps { open: boolean; onClose: () => void; onSubmit: (input: CreateTaskInput) => Promise; initial?: Partial; mode?: "create" | "edit"; } const TASK_TYPES: TaskType[] = [ "bizportal_search", "bizportal_reserve", "pdf_generate", "email_send", "custom", ]; const PRIORITIES: TaskPriority[] = ["low", "normal", "high", "urgent"]; const STATUSES: TaskStatus[] = ["backlog", "ready", "blocked"]; export default function CardModal({ open, onClose, onSubmit, initial, mode = "create" }: CardModalProps) { const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [taskType, setTaskType] = useState("bizportal_search"); const [priority, setPriority] = useState("normal"); const [status, setStatus] = useState("backlog"); const [assignee, setAssignee] = useState("agent"); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); // Payload fields per task type const [bpType, setBpType] = useState<"id" | "ck">("ck"); const [bpQuery, setBpQuery] = useState(""); const [bpNames, setBpNames] = useState(""); const [bpDryRun, setBpDryRun] = useState(false); const [pdfTemplate, setPdfTemplate] = useState(""); const [pdfSource, setPdfSource] = useState(""); const [emailTo, setEmailTo] = useState(""); const [emailSubject, setEmailSubject] = useState(""); const [emailBody, setEmailBody] = useState(""); const [customJson, setCustomJson] = useState(""); useEffect(() => { if (!open) return; setError(null); if (initial) { setTitle(initial.title ?? ""); setDescription(initial.description ?? ""); setTaskType(initial.task_type ?? "bizportal_search"); setPriority(initial.priority ?? "normal"); setStatus(initial.status ?? "backlog"); setAssignee(initial.assignee ?? "agent"); const p = (initial.payload ?? {}) as Record; setBpType(p.type === "id" ? "id" : "ck"); setBpQuery(p.query ?? ""); setBpNames((p.names ?? []).join("\n")); setBpDryRun(!!p.dry_run); setPdfTemplate(p.template ?? ""); setPdfSource(p.source ?? ""); setEmailTo(p.to ?? ""); setEmailSubject(p.subject ?? ""); setEmailBody(p.body ?? ""); setCustomJson(p.raw ? JSON.stringify(p.raw, null, 2) : ""); } else { setTitle(""); setDescription(""); setTaskType("bizportal_search"); setPriority("normal"); setStatus("backlog"); setAssignee("agent"); setBpType("ck"); setBpQuery(""); setBpNames(""); setBpDryRun(false); setPdfTemplate(""); setPdfSource(""); setEmailTo(""); setEmailSubject(""); setEmailBody(""); setCustomJson(""); } }, [open, initial]); if (!open) return null; function buildPayload(): Record { switch (taskType) { case "bizportal_search": return { type: bpType, query: bpQuery.trim() }; case "bizportal_reserve": return { names: bpNames.split("\n").map((s) => s.trim()).filter(Boolean), dry_run: bpDryRun, }; case "pdf_generate": return { template: pdfTemplate.trim(), source: pdfSource.trim() }; case "email_send": return { to: emailTo.trim(), subject: emailSubject.trim(), body: emailBody }; case "custom": try { return { raw: customJson ? JSON.parse(customJson) : null }; } catch (e: any) { throw new Error(`Custom JSON is invalid: ${e.message}`); } } } async function handleSubmit(e: React.FormEvent) { e.preventDefault(); setError(null); if (!title.trim()) { setError("Title is required"); return; } setSubmitting(true); try { const payload = buildPayload(); await onSubmit({ title: title.trim(), description: description.trim() || undefined, task_type: taskType, payload, priority, status, assignee, created_by: "user", }); onClose(); } catch (err: any) { setError(err.message ?? String(err)); } finally { setSubmitting(false); } } return (
e.stopPropagation()} >

{mode === "edit" ? "Edit task" : "New task"}

setTitle(e.target.value)} placeholder="e.g. Reserve name AMPLEFORTH PTY LTD" className="w-full px-3 py-2 rounded-md border border-border bg-bg text-fg text-sm focus:outline-none focus:ring-2 focus:ring-accent" autoFocus required />