'use client' import { useState, useEffect, useCallback } from 'react' import { useFocusTrap } from '@/lib/use-focus-trap' import { Button } from '@/components/ui/button' interface Project { id: number name: string slug: string description?: string ticket_prefix: string status: 'active' | 'archived' github_repo?: string deadline?: number color?: string github_sync_enabled?: boolean github_default_branch?: string task_count?: number assigned_agents?: string[] } interface Agent { id: number name: string role: string status: string } const COLOR_PALETTE = [ '#3b82f6', // blue '#10b981', // emerald '#f59e0b', // amber '#ef4444', // red '#8b5cf6', // violet '#ec4899', // pink '#06b6d4', // cyan '#f97316', // orange ] export function ProjectManagerModal({ onClose, onChanged }: { onClose: () => void onChanged?: () => Promise }) { const [projects, setProjects] = useState([]) const [agents, setAgents] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [form, setForm] = useState({ name: '', ticket_prefix: '', description: '' }) const [editingId, setEditingId] = useState(null) const [editForm, setEditForm] = useState<{ description: string github_repo: string deadline: string color: string assigned_agents: string[] github_sync_enabled: boolean github_default_branch: string }>({ description: '', github_repo: '', deadline: '', color: '', assigned_agents: [], github_sync_enabled: false, github_default_branch: 'main' }) const load = useCallback(async () => { try { setLoading(true) const [projectsRes, agentsRes] = await Promise.all([ fetch('/api/projects?includeArchived=1'), fetch('/api/agents') ]) const projectsData = await projectsRes.json() if (!projectsRes.ok) throw new Error(projectsData.error || 'Failed to load projects') setProjects(projectsData.projects || []) if (agentsRes.ok) { const agentsData = await agentsRes.json() setAgents(agentsData.agents || []) } setError(null) } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load projects') } finally { setLoading(false) } }, []) useEffect(() => { load() }, [load]) const createProject = async (e: React.FormEvent) => { e.preventDefault() if (!form.name.trim()) return try { const response = await fetch('/api/projects', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: form.name, ticket_prefix: form.ticket_prefix, description: form.description }) }) const data = await response.json() if (!response.ok) throw new Error(data.error || 'Failed to create project') setForm({ name: '', ticket_prefix: '', description: '' }) await load() await onChanged?.() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to create project') } } const archiveProject = async (project: Project) => { try { const response = await fetch(`/api/projects/${project.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ status: project.status === 'active' ? 'archived' : 'active' }) }) const data = await response.json() if (!response.ok) throw new Error(data.error || 'Failed to update project') await load() await onChanged?.() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to update project') } } const deleteProject = async (project: Project) => { if (!confirm(`Delete project "${project.name}"? Existing tasks will be moved to General.`)) return try { const response = await fetch(`/api/projects/${project.id}?mode=delete`, { method: 'DELETE' }) const data = await response.json() if (!response.ok) throw new Error(data.error || 'Failed to delete project') await load() await onChanged?.() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to delete project') } } const startEditing = (project: Project) => { if (editingId === project.id) { setEditingId(null) return } setEditingId(project.id) setEditForm({ description: project.description || '', github_repo: project.github_repo || '', deadline: project.deadline ? new Date(project.deadline * 1000).toISOString().split('T')[0] : '', color: project.color || '', assigned_agents: project.assigned_agents || [], github_sync_enabled: !!project.github_sync_enabled, github_default_branch: project.github_default_branch || 'main', }) } const saveEdit = async (project: Project) => { try { const body: Record = { description: editForm.description, github_repo: editForm.github_repo || null, color: editForm.color || null, deadline: editForm.deadline ? Math.floor(new Date(editForm.deadline).getTime() / 1000) : null, github_sync_enabled: editForm.github_sync_enabled ? 1 : 0, github_default_branch: editForm.github_default_branch || 'main', } const response = await fetch(`/api/projects/${project.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) const data = await response.json() if (!response.ok) throw new Error(data.error || 'Failed to update project') // Sync agent assignments const currentAgents = project.assigned_agents || [] const newAgents = editForm.assigned_agents const toAdd = newAgents.filter(a => !currentAgents.includes(a)) const toRemove = currentAgents.filter(a => !newAgents.includes(a)) for (const agentName of toAdd) { await fetch(`/api/projects/${project.id}/agents`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ agent_name: agentName }) }) } for (const agentName of toRemove) { await fetch(`/api/projects/${project.id}/agents?agent_name=${encodeURIComponent(agentName)}`, { method: 'DELETE' }) } setEditingId(null) await load() await onChanged?.() } catch (err) { setError(err instanceof Error ? err.message : 'Failed to update project') } } const toggleAgentAssignment = (agentName: string) => { setEditForm(prev => ({ ...prev, assigned_agents: prev.assigned_agents.includes(agentName) ? prev.assigned_agents.filter(a => a !== agentName) : [...prev.assigned_agents, agentName] })) } const dialogRef = useFocusTrap(onClose) return (
{ if (e.target === e.currentTarget) onClose() }}>

Project Management

{error &&
{error}
}
setForm((prev) => ({ ...prev, name: e.target.value }))} placeholder="Project name" className="bg-surface-1 text-foreground border border-border rounded-md px-3 py-2" required /> setForm((prev) => ({ ...prev, ticket_prefix: e.target.value }))} placeholder="Ticket prefix (e.g. PA)" className="bg-surface-1 text-foreground border border-border rounded-md px-3 py-2" />