diff --git a/app/api/tasks/route.ts b/app/api/tasks/route.ts index ea7c17f..653c2f5 100644 --- a/app/api/tasks/route.ts +++ b/app/api/tasks/route.ts @@ -56,7 +56,11 @@ export async function GET(req: NextRequest) { const result = await fetchCollection(COLLECTION, { page: 1, perPage: Math.min(limit, 500), - sort: "-priority,-created", + // PB on this instance doesn't support sort on the auto-managed + // `created` column. Use priority (desc) then id (desc) — id is + // a nanoid prefix so newer tasks come last when sorted desc, and + // the worker can poll deterministically. + sort: "-priority,-id", filter: filters.length > 0 ? filters.join(" && ") : undefined, }); return NextResponse.json(result); diff --git a/app/bizportal-2/BizPortal2Explorer.tsx b/app/bizportal-2/BizPortal2Explorer.tsx index e8ee7f9..da161e3 100644 --- a/app/bizportal-2/BizPortal2Explorer.tsx +++ b/app/bizportal-2/BizPortal2Explorer.tsx @@ -167,6 +167,10 @@ export default function BizPortal2Explorer() {

No results found.

{data.log && data.log.length > 0 && } +
)} @@ -225,6 +229,53 @@ function LogBlock({ log }: { log: string[] }) { ); } +function SendToKanbanButton({ type, query }: { type: SearchType; query: string }) { + const [sending, setSending] = useState(false); + const [done, setDone] = useState(false); + async function send() { + setSending(true); + try { + const res = await fetch("/api/tasks", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + title: `BizPortal ${type.toUpperCase()} search — ${query}`, + task_type: "bizportal_search", + payload: { type, query }, + priority: "normal", + status: "ready", + created_by: "bizportal-2", + }), + }); + if (!res.ok) throw new Error(`HTTP ${res.status}`); + setDone(true); + } catch (e) { + // best-effort + } finally { + setSending(false); + } + } + if (done) { + return ( + + ✅ Task queued — open Kanban → + + ); + } + return ( + + ); +} + // ── Result display ─────────────────────────────────────────────────────── interface DoneViewProps { @@ -485,6 +536,7 @@ function DoneView({ results, activeTab, query, durationMs, log }: DoneViewProps) )}
+ {selectedCount > 0 ? ( <> {selectedCount} selected diff --git a/app/tasks/CardModal.tsx b/app/tasks/CardModal.tsx new file mode 100644 index 0000000..826dadb --- /dev/null +++ b/app/tasks/CardModal.tsx @@ -0,0 +1,402 @@ +// 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 + /> + + + +