import type { Project } from "@vtit-agent-coding/shared"; import { useEffect, useState } from "react"; import { toast } from "sonner"; import { Button } from "./ui/button"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "./ui/dialog"; import { Input } from "./ui/input"; import { Label } from "./ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; import { Textarea } from "./ui/textarea"; export interface TaskSchemaModalProps { boardId: string; projectId?: string; onTaskCreated?: () => void; trigger?: React.ReactNode; } export function TaskSchemaModal({ boardId, projectId, onTaskCreated, trigger }: TaskSchemaModalProps) { const [isOpen, setIsOpen] = useState(false); const [projectAgents, setProjectAgents] = useState([]); const [projectModules, setProjectModules] = useState([]); const [isSubmitting, setIsSubmitting] = useState(false); useEffect(() => { if (isOpen) { const fetchUrl = projectId ? `/api/projects/${projectId}/agents` : "/api/agents"; fetch(fetchUrl) .then((r) => r.json()) .then((data) => { if (Array.isArray(data) && data.length > 0) { setProjectAgents(data); } else { fetch("/api/agents") .then((r) => r.json()) .then((allData) => setProjectAgents(Array.isArray(allData) ? allData : [])) .catch(console.error); } }) .catch(() => { fetch("/api/agents") .then((r) => r.json()) .then((allData) => setProjectAgents(Array.isArray(allData) ? allData : [])) .catch(console.error); }); if (projectId) { fetch(`/api/projects/${projectId}`) .then((r) => r.json()) .then((proj) => { const project = proj as Project; if (project.modules) { try { const parsed = typeof project.modules === "string" ? JSON.parse(project.modules) : project.modules; if (Array.isArray(parsed) && parsed.length > 0) { setProjectModules(parsed); if (!parsed.includes(moduleName)) { setModuleName(parsed[0]); } } } catch (e) { console.error("Failed to parse project modules", e); } } }) .catch(console.error); } } }, [isOpen, projectId]); // Form State matching _TASK.schema.json const [key, setKey] = useState(""); const [moduleName, setModuleName] = useState("NGUOI-DUNG"); const [title, setTitle] = useState(""); const [priority, setPriority] = useState("MEDIUM"); const [assignedTo, setAssignedTo] = useState("unassigned"); const [businessContext, setBusinessContext] = useState(""); const [description, setDescription] = useState(""); const [technicalNotes, setTechnicalNotes] = useState(""); const [outOfScope, setOutOfScope] = useState(""); // Array fields const [stepsText, setStepsText] = useState("1. Tạo schema & API endpoint\n2. Viết unit test\n3. Kiểm tra QC"); const [qcChecksText, setQCChecksText] = useState("Validate input cả FE & BE\nError response đúng format HTTP status code"); const [affectedFilesText, setAffectedFilesText] = useState("apps/web/server/routes.ts (MODIFY)\napps/web/src/App.tsx (MODIFY)"); const [acceptanceTestsText, setAcceptanceTestsText] = useState("GIVEN user gửi request hợp lệ WHEN gọi API THEN trả về HTTP 200 OK"); async function handleSubmit(e: React.FormEvent) { e.preventDefault(); if (!title.trim()) { toast.error("Vui lòng nhập tiêu đề Task."); return; } if (!key.trim()) { toast.error("Vui lòng nhập Mã Task (Key)."); return; } setIsSubmitting(true); // Parse array objects from text const steps = stepsText .split("\n") .filter((s) => s.trim()) .map((s, idx) => ({ order: idx + 1, action: s.replace(/^\d+\.\s*/, "").trim(), detail: s })); const qcChecks = qcChecksText .split("\n") .filter((q) => q.trim()) .map((q) => ({ category: "CODE_QUALITY", item: q.trim(), severity: "MUST", automated: false })); const affectedFiles = affectedFilesText .split("\n") .filter((f) => f.trim()) .map((f) => { const parts = f.trim().split(" "); const path = parts[0]; const changeType = f.includes("CREATE") ? "CREATE" : "MODIFY"; return { path, changeType, summary: f }; }); const acceptanceTests = acceptanceTestsText .split("\n") .filter((a) => a.trim()) .map((a, idx) => ({ id: `AT-${key.trim()}-${String(idx + 1).padStart(2, "0")}`, scenario: a.trim(), given: "Precondition", when: "Action", then: "Expected Result", testType: "HAPPY_PATH", })); const taskPayload = { board_id: boardId, title: `[${key.trim()}] ${title.trim()}`, description: description.trim(), priority: priority.toLowerCase(), assigned_to: assignedTo !== "unassigned" ? assignedTo : undefined, metadata: { key: key.trim(), module: moduleName, priority, businessContext: businessContext.trim(), technicalNotes: technicalNotes.trim(), outOfScope: outOfScope.trim(), steps, qcChecks, affectedFiles, acceptanceTests, }, }; try { const res = await fetch(`/api/tasks`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(taskPayload), }); if (res.ok) { toast.success("Tạo Task mới thành công!"); setIsOpen(false); onTaskCreated?.(); } else { const data = (await res.json().catch(() => ({}))) as any; toast.error(data.error?.message || data.message || "Tạo Task thất bại."); } } catch (err) { console.error("Failed to create task", err); toast.error("Không thể kết nối tới máy chủ. Vui lòng thử lại sau."); } finally { setIsSubmitting(false); } } return ( {trigger || ( )} {/* Sticky Header */} 📋 Tạo Task Mới _TASK.schema.json {/* Form Container */}
{/* Fixed Tab Headers */}
📌 Thông Tin 📝 Bối Cảnh Quy Trình & QC 🧪 Files & Acceptance
{/* Scrollable Tab Content Body */}
{/* Tab 1: Basic Info */}
setKey(e.target.value)} className="bg-surface-secondary border-border text-content-primary rounded-xl focus:ring-1 focus:ring-accent" required />
setTitle(e.target.value)} className="bg-surface-secondary border-border text-content-primary rounded-xl focus:ring-1 focus:ring-accent" required />
{/* Tab 2: Context & Tech */}
Lý do nghiệp vụ & Giá trị mang lại