mission-control/app/projects/page.tsx
OpenClaw Bot c1c01bd21e feat: merge Gantt Board into Mission Control
- Add Projects page with Sprint Board and Backlog views
- Copy SprintBoard and BacklogView components to components/gantt/
- Copy useTaskStore for project/task/sprint management
- Add API routes for task persistence with SQLite
- Add UI components: dialog, select, table, textarea
- Add avatar and attachment utilities
- Update sidebar with Projects navigation link
- Remove static export config to support API routes
- Add dist to .gitignore
2026-02-20 18:49:52 -06:00

230 lines
8.3 KiB
TypeScript

"use client"
import { useState, useEffect } from "react"
import { DashboardLayout } from "@/components/layout/sidebar"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { SprintBoard } from "@/components/gantt/SprintBoard"
import { BacklogView } from "@/components/gantt/BacklogView"
import { useTaskStore } from "@/stores/useTaskStore"
import {
Plus,
LayoutGrid,
ListTodo,
Calendar,
FolderKanban,
Target,
CheckCircle2,
Clock
} from "lucide-react"
import Link from "next/link"
export default function ProjectsPage() {
const [activeTab, setActiveTab] = useState("board")
const [mounted, setMounted] = useState(false)
const {
tasks,
sprints,
projects,
selectedProjectId,
selectedSprintId,
currentUser,
syncFromServer,
isLoading
} = useTaskStore()
useEffect(() => {
setMounted(true)
syncFromServer()
}, [syncFromServer])
// Calculate stats
const activeSprint = sprints.find(s => s.status === "active")
const activeSprintTasks = activeSprint
? tasks.filter(t => t.sprintId === activeSprint.id)
: []
const completedTasks = activeSprintTasks.filter(t => t.status === "done").length
const totalTasks = activeSprintTasks.length
const completionRate = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0
if (!mounted) {
return (
<DashboardLayout>
<div className="flex items-center justify-center h-64">
<div className="animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" />
</div>
</DashboardLayout>
)
}
return (
<DashboardLayout>
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-3xl font-bold tracking-tight">Projects</h1>
<p className="text-muted-foreground mt-1">
Sprint planning and project management
</p>
</div>
<div className="flex items-center gap-2">
<Link href="/tasks">
<Button variant="outline" size="sm">
<CheckCircle2 className="w-4 h-4 mr-2" />
Daily Tasks
</Button>
</Link>
<Button size="sm">
<Plus className="w-4 h-4 mr-2" />
New Sprint
</Button>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Active Sprint
</CardTitle>
<Target className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">
{activeSprint?.name || "No Active Sprint"}
</div>
<p className="text-xs text-muted-foreground mt-1">
{activeSprint
? `${new Date(activeSprint.startDate).toLocaleDateString()} - ${new Date(activeSprint.endDate).toLocaleDateString()}`
: "Create a sprint to get started"
}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Sprint Progress
</CardTitle>
<CheckCircle2 className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{completionRate}%</div>
<div className="h-2 bg-secondary rounded-full overflow-hidden mt-2">
<div
className="h-full bg-primary rounded-full transition-all"
style={{ width: `${completionRate}%` }}
/>
</div>
<p className="text-xs text-muted-foreground mt-1">
{completedTasks} of {totalTasks} tasks completed
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Total Projects
</CardTitle>
<FolderKanban className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{projects.length}</div>
<p className="text-xs text-muted-foreground mt-1">
{tasks.length} total tasks across all projects
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">
Sprints
</CardTitle>
<Clock className="w-4 h-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{sprints.length}</div>
<p className="text-xs text-muted-foreground mt-1">
{sprints.filter(s => s.status === "active").length} active, {sprints.filter(s => s.status === "planning").length} planning
</p>
</CardContent>
</Card>
</div>
{/* Main Content Tabs */}
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList className="grid w-full sm:w-auto grid-cols-2 sm:inline-flex">
<TabsTrigger value="board" className="flex items-center gap-2">
<LayoutGrid className="w-4 h-4" />
<span className="hidden sm:inline">Sprint Board</span>
<span className="sm:hidden">Board</span>
</TabsTrigger>
<TabsTrigger value="backlog" className="flex items-center gap-2">
<ListTodo className="w-4 h-4" />
<span className="hidden sm:inline">Backlog & Sprints</span>
<span className="sm:hidden">Backlog</span>
</TabsTrigger>
</TabsList>
<TabsContent value="board" className="space-y-4">
{isLoading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" />
</div>
) : (
<SprintBoard />
)}
</TabsContent>
<TabsContent value="backlog" className="space-y-4">
{isLoading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin h-8 w-8 border-2 border-primary border-t-transparent rounded-full" />
</div>
) : (
<BacklogView />
)}
</TabsContent>
</Tabs>
{/* Quick Links */}
<Card className="bg-muted/50">
<CardHeader>
<CardTitle className="text-sm font-medium">Quick Links</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
<Link href="/tasks">
<Button variant="outline" size="sm">
<CheckCircle2 className="w-4 h-4 mr-2" />
Daily Tasks
</Button>
</Link>
<Link href="/calendar">
<Button variant="outline" size="sm">
<Calendar className="w-4 h-4 mr-2" />
Calendar
</Button>
</Link>
<Link href="/activity">
<Button variant="outline" size="sm">
<Target className="w-4 h-4 mr-2" />
Activity
</Button>
</Link>
</div>
</CardContent>
</Card>
</div>
</DashboardLayout>
)
}