- 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
230 lines
8.3 KiB
TypeScript
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>
|
|
)
|
|
}
|