import type { Project, ProjectDocument, ProjectMember, ProjectRule, Task } from "@vtit-agent-coding/shared"; import { useEffect, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { ProjectRequirementsTab } from "../components/ProjectRequirementsTab"; import { Badge } from "../components/ui/badge"; import { Button } from "../components/ui/button"; import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "../components/ui/dialog"; import { Input } from "../components/ui/input"; import { Label } from "../components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../components/ui/select"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "../components/ui/tabs"; import { Textarea } from "../components/ui/textarea"; import { BoardPage } from "./BoardPage"; export function ProjectDetailPage() { const { projectId } = useParams<{ projectId: string }>(); const navigate = useNavigate(); const [project, setProject] = useState<(Project & { board_id?: string }) | null>(null); const [tasks, setTasks] = useState([]); const [rules, setRules] = useState([]); const [docs, setDocs] = useState([]); const [members, setMembers] = useState([]); const [agents, setAgents] = useState([]); const [availableAgents, setAvailableAgents] = useState([]); const [isLoading, setIsLoading] = useState(true); // Modal states for Rule and Doc const [ruleModalOpen, setRuleModalOpen] = useState(false); const [ruleFileName, setRuleFileName] = useState(""); const [ruleTitle, setRuleTitle] = useState(""); const [ruleContent, setRuleContent] = useState(""); const [docModalOpen, setDocModalOpen] = useState(false); const [docCategory, setDocCategory] = useState("Architecture"); const [docFileName, setDocFileName] = useState(""); // Agent assign modal state const [agentAssignModalOpen, setAgentAssignModalOpen] = useState(false); const [selectedAgentId, setSelectedAgentId] = useState(""); // Invite member modal state const [inviteMemberModalOpen, setInviteMemberModalOpen] = useState(false); const [inviteEmail, setInviteEmail] = useState(""); const [inviteRole, setInviteRole] = useState("READ"); // GitLab config state const [gitlabUrl, setGitlabUrl] = useState(""); const [gitlabProjectId, setGitlabProjectId] = useState(""); const [gitlabAccessToken, setGitlabAccessToken] = useState(""); const [gitlabWebhookSecret, setGitlabWebhookSecret] = useState(""); const [gitlabTestStatus, setGitlabTestStatus] = useState<"idle" | "testing" | "success" | "error">("idle"); const [gitlabTestMessage, setGitlabTestMessage] = useState(""); const [docTitle, setDocTitle] = useState(""); const [docContent, setDocContent] = useState(""); useEffect(() => { if (projectId) { loadProjectData(projectId); } }, [projectId]); async function loadProjectData(id: string) { setIsLoading(true); try { const [projRes, rulesRes, docsRes, membersRes, planRes] = await Promise.all([ fetch(`/api/projects/${id}`), fetch(`/api/projects/${id}/rules`), fetch(`/api/projects/${id}/docs`), fetch(`/api/projects/${id}/members`), fetch(`/api/projects/${id}/master-plan`), ]); if (projRes.ok) setProject(await projRes.json()); if (rulesRes.ok) { const d = await rulesRes.json(); setRules(Array.isArray(d) ? d : []); } if (docsRes.ok) { const d = await docsRes.json(); setDocs(Array.isArray(d) ? d : []); } if (membersRes.ok) { const d = await membersRes.json(); setMembers(Array.isArray(d) ? d : []); } if (planRes.ok) { const planData = (await planRes.json()) as { tasks?: Task[] }; setTasks(Array.isArray(planData.tasks) ? planData.tasks : []); } // Fetch project agents const agentsRes = await fetch(`/api/projects/${id}/agents`); if (agentsRes.ok) { const d = await agentsRes.json(); setAgents(Array.isArray(d) ? d : []); } // Fetch all available agents const allAgentsRes = await fetch("/api/agents"); if (allAgentsRes.ok) { const d = await allAgentsRes.json(); setAvailableAgents(Array.isArray(d) ? d : []); } } catch (err) { console.error("Failed to load project details", err); } finally { setIsLoading(false); } } async function handleCreateRule(e: React.FormEvent) { e.preventDefault(); if (!projectId || !ruleFileName || !ruleTitle) return; try { const res = await fetch(`/api/projects/${projectId}/rules`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ file_name: ruleFileName, title: ruleTitle, content: ruleContent }), }); if (res.ok) { setRuleModalOpen(false); setRuleFileName(""); setRuleTitle(""); setRuleContent(""); loadProjectData(projectId); } } catch (err) { console.error("Failed to create rule", err); } } async function handleCreateDoc(e: React.FormEvent) { e.preventDefault(); if (!projectId || !docFileName || !docTitle) return; try { const res = await fetch(`/api/projects/${projectId}/docs`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ category: docCategory, file_name: docFileName, title: docTitle, content: docContent, }), }); if (res.ok) { setDocModalOpen(false); setDocFileName(""); setDocTitle(""); setDocContent(""); loadProjectData(projectId); } } catch (err) { console.error("Failed to create doc", err); } } async function handleAssignAgent() { if (!projectId || !selectedAgentId) return; try { const res = await fetch(`/api/projects/${projectId}/agents`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ agent_id: selectedAgentId }), }); if (res.ok) { setAgentAssignModalOpen(false); setSelectedAgentId(""); loadProjectData(projectId); } } catch (err) { console.error("Failed to assign agent", err); } } async function handleRemoveAgent(agentId: string) { if (!projectId) return; try { const res = await fetch(`/api/projects/${projectId}/agents/${agentId}`, { method: "DELETE" }); if (res.ok) loadProjectData(projectId); } catch (err) { console.error("Failed to remove agent", err); } } async function handleInviteMember(e: React.FormEvent) { e.preventDefault(); if (!projectId || !inviteEmail) return; try { // Find user by email first const searchRes = await fetch(`/api/users/search?q=${encodeURIComponent(inviteEmail)}`); if (!searchRes.ok) throw new Error("Search user failed"); const users = await searchRes.json(); if (!Array.isArray(users) || users.length === 0) { alert("Không tìm thấy người dùng với email này!"); return; } const targetUser = users.find((u: any) => u.email.toLowerCase() === inviteEmail.toLowerCase()) || users[0]; const res = await fetch(`/api/projects/${projectId}/members`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: targetUser.id, role: inviteRole }), }); if (res.ok) { setInviteMemberModalOpen(false); setInviteEmail(""); setInviteRole("READ"); loadProjectData(projectId); } } catch (err) { console.error("Failed to invite member", err); } } async function handleChangeMemberRole(userId: string, newRole: string) { if (!projectId) return; try { const res = await fetch(`/api/projects/${projectId}/members`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId, role: newRole }), }); if (res.ok) loadProjectData(projectId); } catch (err) { console.error("Failed to change member role", err); } } async function handleRemoveMember(userId: string) { if (!projectId) return; try { const res = await fetch(`/api/projects/${projectId}/members/${userId}`, { method: "DELETE" }); if (res.ok) loadProjectData(projectId); } catch (err) { console.error("Failed to remove member", err); } } async function handleSaveGitLabConfig() { if (!projectId) return; try { await fetch(`/api/projects/${projectId}/gitlab-config`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ gitlab_url: gitlabUrl, gitlab_project_id: gitlabProjectId, access_token: gitlabAccessToken, webhook_secret: gitlabWebhookSecret, }), }); } catch (err) { console.error("Failed to save GitLab config", err); } } async function handleTestGitLabConnection() { if (!projectId) return; setGitlabTestStatus("testing"); setGitlabTestMessage(""); try { const res = await fetch(`/api/projects/${projectId}/gitlab-config/test`, { method: "POST" }); if (res.ok) { setGitlabTestStatus("success"); setGitlabTestMessage("Kết nối GitLab thành công!"); } else { const data = (await res.json().catch(() => ({}))) as any; setGitlabTestStatus("error"); setGitlabTestMessage(data.error || "Kết nối thất bại."); } } catch (_err) { setGitlabTestStatus("error"); setGitlabTestMessage("Không thể kết nối đến server."); } } function getRoleBadgeClasses(role: string) { switch (role) { case "READ": return "bg-blue-500/10 text-blue-600 border-blue-500/20"; case "DEVELOPER": return "bg-amber-500/10 text-amber-600 border-amber-500/20"; case "MAINTAINER": return "bg-red-500/10 text-red-600 border-red-500/20"; default: return "bg-primary/10 text-primary border-primary/20"; } } if (isLoading) { return
Đang tải dữ liệu dự án...
; } if (!project) { return
Không tìm thấy thông tin dự án.
; } return (
{/* Project Title Header */}
{project.code}

{project.name}

{project.description &&

{project.description}

}
{project.board_id && ( )}
🗺️ Master Plan 📋 Kanban 📜 Rules ({rules.length}) 📚 Tri Thức ({docs.length}) 🤖 Agents & Repos 📝 Yêu Cầu ⚙️ Phân Quyền & Settings {/* Tab 1: Master Plan */} Lộ Trình & Tiến Độ Kế Hoạch Tổng Thể (Master Plan) {tasks.length === 0 ? (
Chưa có công việc nào trong Master Plan. Hãy thêm công việc vào Kanban Board!
) : (
{["Phase 1", "Phase 2", "Phase 3"].map((phase) => { const phaseTasks = tasks.filter((t) => (t as any).phase === phase || (!(t as any).phase && phase === "Phase 1")); const doneCount = phaseTasks.filter((t) => t.status === "done").length; const percent = phaseTasks.length > 0 ? Math.round((doneCount / phaseTasks.length) * 100) : 0; return (

🚀 {phase} {doneCount}/{phaseTasks.length} Đã Xong ({percent}%)

{phaseTasks.map((task) => (
#{task.seq} {task.title}
{task.status}
))}
); })}
)} {/* Tab 2: Kanban */} {project.board_id ? :
Dự án này chưa được khởi tạo Kanban Board.
}
{/* Tab 3: Rules */} Quy Tắc Dự Án (Project Rules) Thêm File Quy Tắc Mới
setRuleFileName(e.target.value)} required />
setRuleTitle(e.target.value)} required />