637 lines
25 KiB
TypeScript
637 lines
25 KiB
TypeScript
import { DashboardLayout } from "@/components/layout/sidebar";
|
|
import { PageHeader } from "@/components/layout/page-header";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card";
|
|
import { Badge } from "@/components/ui/badge";
|
|
// Progress component not available, using inline div progress bars
|
|
import { Separator } from "@/components/ui/separator";
|
|
import {
|
|
FolderKanban,
|
|
Target,
|
|
CheckCircle2,
|
|
Clock,
|
|
ExternalLink,
|
|
TrendingUp,
|
|
AlertCircle,
|
|
Users,
|
|
Calendar,
|
|
ArrowRight,
|
|
Zap,
|
|
Activity,
|
|
AlertTriangle,
|
|
BarChart3,
|
|
Sparkles,
|
|
Timer,
|
|
} from "lucide-react";
|
|
import Link from "next/link";
|
|
import {
|
|
fetchProjectsWithStats,
|
|
fetchActiveSprint,
|
|
countSprintsByStatus,
|
|
getProjectHealth,
|
|
getSprintDaysRemaining,
|
|
formatSprintDateRange,
|
|
ProjectStats,
|
|
Sprint,
|
|
} from "@/lib/data/projects";
|
|
import { Task } from "@/lib/data/tasks";
|
|
import {
|
|
getGanttProjectUrl,
|
|
getGanttSprintUrl,
|
|
getGanttTaskUrl,
|
|
getGanttTasksUrl,
|
|
siteUrls,
|
|
} from "@/lib/config/sites";
|
|
|
|
// Force dynamic rendering to fetch fresh data on each request
|
|
export const dynamic = "force-dynamic";
|
|
|
|
// Helper functions
|
|
function formatRelativeTime(dateString: string): string {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diffMs = now.getTime() - date.getTime();
|
|
const diffMins = Math.floor(diffMs / (1000 * 60));
|
|
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
|
|
if (diffMins < 60) return `${diffMins}m ago`;
|
|
if (diffHours < 24) return `${diffHours}h ago`;
|
|
if (diffDays < 7) return `${diffDays}d ago`;
|
|
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
}
|
|
|
|
function getPriorityColor(priority: string): string {
|
|
switch (priority) {
|
|
case "urgent":
|
|
return "bg-red-500/15 text-red-400 border-red-500/30";
|
|
case "high":
|
|
return "bg-orange-500/15 text-orange-400 border-orange-500/30";
|
|
case "medium":
|
|
return "bg-yellow-500/15 text-yellow-400 border-yellow-500/30";
|
|
default:
|
|
return "bg-blue-500/15 text-blue-400 border-blue-500/30";
|
|
}
|
|
}
|
|
|
|
function getStatusIcon(status: string) {
|
|
switch (status) {
|
|
case "done":
|
|
return <CheckCircle2 className="w-4 h-4 text-green-500" />;
|
|
case "in-progress":
|
|
return <Clock className="w-4 h-4 text-blue-500" />;
|
|
case "review":
|
|
case "validate":
|
|
return <Activity className="w-4 h-4 text-purple-500" />;
|
|
default:
|
|
return <div className="w-4 h-4 rounded-full border-2 border-muted-foreground" />;
|
|
}
|
|
}
|
|
|
|
// Stat Card Component
|
|
function StatCard({
|
|
title,
|
|
value,
|
|
subtitle,
|
|
icon: Icon,
|
|
colorClass,
|
|
trend,
|
|
}: {
|
|
title: string;
|
|
value: string | number;
|
|
subtitle?: string;
|
|
icon: React.ElementType;
|
|
colorClass: string;
|
|
trend?: "up" | "down" | "neutral";
|
|
}) {
|
|
return (
|
|
<Card className="hover:shadow-md transition-shadow">
|
|
<CardContent className="p-4 sm:p-6">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0 flex-1">
|
|
<p className="text-xs sm:text-sm font-medium text-muted-foreground truncate">{title}</p>
|
|
<p className="text-2xl sm:text-3xl font-bold mt-1">{value}</p>
|
|
{subtitle && <p className="text-xs text-muted-foreground mt-1 truncate">{subtitle}</p>}
|
|
</div>
|
|
<div className={`p-2 sm:p-3 rounded-lg shrink-0 ${colorClass}`}>
|
|
<Icon className="w-4 h-4 sm:w-5 sm:h-5" />
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// Task List Item Component
|
|
function TaskListItem({ task }: { task: Task }) {
|
|
return (
|
|
<div className="flex items-start gap-3 py-3 border-b border-border last:border-0">
|
|
<div className="mt-0.5 shrink-0">{getStatusIcon(task.status)}</div>
|
|
<div className="flex-1 min-w-0 overflow-hidden">
|
|
<Link
|
|
href={getGanttTaskUrl(task.id)}
|
|
target="_blank"
|
|
className="text-sm font-medium block truncate hover:text-primary hover:underline"
|
|
>
|
|
{task.title}
|
|
</Link>
|
|
<div className="flex flex-wrap items-center gap-2 mt-1">
|
|
<span className="text-xs text-muted-foreground">
|
|
{formatRelativeTime(task.updatedAt)}
|
|
</span>
|
|
{task.assigneeName && (
|
|
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
|
<Users className="w-3 h-3" />
|
|
{task.assigneeName}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<Badge variant="outline" className={`text-xs shrink-0 ${getPriorityColor(task.priority)}`}>
|
|
{task.priority}
|
|
</Badge>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// Project Card Component
|
|
function ProjectCard({ stats }: { stats: ProjectStats }) {
|
|
const health = getProjectHealth(stats);
|
|
const { project, progress, totalTasks, completedTasks, inProgressTasks, urgentTasks, highPriorityTasks, recentTasks } = stats;
|
|
|
|
return (
|
|
<Card className="hover:shadow-lg transition-shadow h-full flex flex-col">
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-start justify-between gap-2">
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<div
|
|
className="w-3 h-3 rounded-full shrink-0"
|
|
style={{ backgroundColor: project.color }}
|
|
/>
|
|
<CardTitle className="text-base font-semibold truncate">
|
|
{project.name}
|
|
</CardTitle>
|
|
</div>
|
|
<Badge
|
|
variant="outline"
|
|
className={`text-xs shrink-0 ${health.status === "healthy" ? "border-green-500/30 text-green-400" : health.status === "warning" ? "border-yellow-500/30 text-yellow-400" : "border-red-500/30 text-red-400"}`}
|
|
>
|
|
{health.label}
|
|
</Badge>
|
|
</div>
|
|
{project.description && (
|
|
<CardDescription className="text-xs line-clamp-2 mt-1">
|
|
{project.description}
|
|
</CardDescription>
|
|
)}
|
|
</CardHeader>
|
|
|
|
<CardContent className="space-y-4 flex-1 flex flex-col">
|
|
{/* Progress Bar */}
|
|
<div className="space-y-2">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">Progress</span>
|
|
<span className="font-medium">{progress}%</span>
|
|
</div>
|
|
<div className="h-2 bg-secondary rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full rounded-full transition-all"
|
|
style={{
|
|
width: `${progress}%`,
|
|
backgroundColor: project.color
|
|
}}
|
|
/>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground">
|
|
{completedTasks} of {totalTasks} tasks completed
|
|
</p>
|
|
</div>
|
|
|
|
{/* Task Stats */}
|
|
<div className="grid grid-cols-2 gap-2">
|
|
<div className="flex items-center gap-2 text-xs">
|
|
<div className="w-2 h-2 rounded-full bg-blue-500" />
|
|
<span className="text-muted-foreground">In Progress:</span>
|
|
<span className="font-medium">{inProgressTasks}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2 text-xs">
|
|
<div className="w-2 h-2 rounded-full bg-orange-500" />
|
|
<span className="text-muted-foreground">High Priority:</span>
|
|
<span className="font-medium">{highPriorityTasks}</span>
|
|
</div>
|
|
{urgentTasks > 0 && (
|
|
<div className="flex items-center gap-2 text-xs col-span-2">
|
|
<AlertTriangle className="w-3 h-3 text-red-500" />
|
|
<span className="text-red-400 font-medium">{urgentTasks} urgent task{urgentTasks !== 1 ? 's' : ''} need{urgentTasks === 1 ? 's' : ''} attention</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
{/* Recent Activity */}
|
|
<div className="flex-1">
|
|
<p className="text-xs font-medium text-muted-foreground mb-2">Recent Activity</p>
|
|
{recentTasks.length > 0 ? (
|
|
<div className="space-y-0">
|
|
{recentTasks.slice(0, 3).map((task) => (
|
|
<div key={task.id} className="flex items-center gap-2 py-1.5">
|
|
{getStatusIcon(task.status)}
|
|
<span className="text-xs truncate flex-1">{task.title}</span>
|
|
<span className="text-xs text-muted-foreground shrink-0">
|
|
{formatRelativeTime(task.updatedAt)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-xs text-muted-foreground italic">No recent activity</p>
|
|
)}
|
|
</div>
|
|
|
|
{/* Action Link */}
|
|
<Link
|
|
href={getGanttProjectUrl(project.id)}
|
|
target="_blank"
|
|
className="flex items-center justify-between text-xs text-primary hover:underline mt-auto pt-2"
|
|
>
|
|
<span>View in Gantt Board</span>
|
|
<ExternalLink className="w-3 h-3" />
|
|
</Link>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// Active Sprint Card Component
|
|
function ActiveSprintCard({ sprint }: { sprint: Sprint }) {
|
|
const daysRemaining = getSprintDaysRemaining(sprint);
|
|
const isOverdue = daysRemaining < 0;
|
|
|
|
return (
|
|
<Card className="border-primary/20 bg-gradient-to-br from-primary/5 to-transparent">
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-2">
|
|
<div className="p-2 rounded-lg bg-primary/10">
|
|
<Zap className="w-5 h-5 text-primary" />
|
|
</div>
|
|
<div>
|
|
<CardTitle className="text-base">Active Sprint</CardTitle>
|
|
<p className="text-xs text-muted-foreground">{sprint.name}</p>
|
|
</div>
|
|
</div>
|
|
<Badge variant="default" className="bg-primary text-primary-foreground">
|
|
{isOverdue ? `${Math.abs(daysRemaining)} days overdue` : `${daysRemaining} days left`}
|
|
</Badge>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{sprint.goal && (
|
|
<p className="text-sm text-muted-foreground">{sprint.goal}</p>
|
|
)}
|
|
|
|
<div className="flex items-center justify-between text-xs text-muted-foreground">
|
|
<span className="flex items-center gap-1">
|
|
<Calendar className="w-3 h-3" />
|
|
{formatSprintDateRange(sprint)}
|
|
</span>
|
|
</div>
|
|
|
|
<Link
|
|
href={getGanttSprintUrl(sprint.id)}
|
|
target="_blank"
|
|
>
|
|
<Button variant="outline" size="sm" className="w-full mt-2">
|
|
<ExternalLink className="w-4 h-4 mr-2" />
|
|
View Sprint Board
|
|
</Button>
|
|
</Link>
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|
|
|
|
// Main Page Component
|
|
export default async function ProjectsOverviewPage() {
|
|
// Fetch all data in parallel
|
|
let projectStats: ProjectStats[] = [];
|
|
let activeSprint: Sprint | null = null;
|
|
let sprintCounts = { planning: 0, active: 0, completed: 0, total: 0 };
|
|
let errorMessage: string | null = null;
|
|
|
|
try {
|
|
[projectStats, activeSprint, sprintCounts] = await Promise.all([
|
|
fetchProjectsWithStats(),
|
|
fetchActiveSprint(),
|
|
countSprintsByStatus(),
|
|
]);
|
|
} catch (error) {
|
|
console.error("Error fetching projects data:", error);
|
|
errorMessage = error instanceof Error ? error.message : "Failed to load projects data";
|
|
}
|
|
|
|
// Calculate overall stats
|
|
const totalTasks = projectStats.reduce((sum, p) => sum + p.totalTasks, 0);
|
|
const completedTasks = projectStats.reduce((sum, p) => sum + p.completedTasks, 0);
|
|
const overallProgress = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
|
|
const urgentTasks = projectStats.reduce((sum, p) => sum + p.urgentTasks, 0);
|
|
const highPriorityTasks = projectStats.reduce((sum, p) => sum + p.highPriorityTasks, 0);
|
|
|
|
// Sort projects by health status (critical first, then warning, then healthy)
|
|
const sortedProjects = [...projectStats].sort((a, b) => {
|
|
const healthA = getProjectHealth(a).status;
|
|
const healthB = getProjectHealth(b).status;
|
|
const order = { critical: 0, warning: 1, healthy: 2 };
|
|
return order[healthA] - order[healthB];
|
|
});
|
|
|
|
return (
|
|
<DashboardLayout>
|
|
<div className="space-y-6 sm:space-y-8">
|
|
{/* Header */}
|
|
<PageHeader
|
|
title="Projects Overview"
|
|
description={
|
|
<>
|
|
Manage all projects and track progress. Edit projects in{" "}
|
|
<Link
|
|
href={siteUrls.ganttBoard}
|
|
target="_blank"
|
|
className="text-primary hover:underline inline-flex items-center gap-1"
|
|
>
|
|
gantt-board
|
|
<ExternalLink className="w-3 h-3" />
|
|
</Link>
|
|
</>
|
|
}
|
|
>
|
|
<Link href={siteUrls.ganttBoard} target="_blank">
|
|
<Button size="sm">
|
|
<ExternalLink className="w-4 h-4 mr-2" />
|
|
Open Gantt Board
|
|
</Button>
|
|
</Link>
|
|
</PageHeader>
|
|
|
|
{/* Error Message */}
|
|
{errorMessage && (
|
|
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4 text-red-400">
|
|
<p className="font-medium">Error loading projects</p>
|
|
<p className="text-sm mt-1">{errorMessage}</p>
|
|
<p className="text-sm mt-2">Make sure you are logged into gantt-board and have access to the Supabase database.</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Stats Overview */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3 sm:gap-4">
|
|
<StatCard
|
|
title="Total Projects"
|
|
value={projectStats.length}
|
|
subtitle={`${sprintCounts.active} active sprints`}
|
|
icon={FolderKanban}
|
|
colorClass="bg-blue-500/15 text-blue-500"
|
|
/>
|
|
<StatCard
|
|
title="Overall Progress"
|
|
value={`${overallProgress}%`}
|
|
subtitle={`${completedTasks} of ${totalTasks} tasks done`}
|
|
icon={BarChart3}
|
|
colorClass="bg-green-500/15 text-green-500"
|
|
/>
|
|
<StatCard
|
|
title="Needs Attention"
|
|
value={urgentTasks + highPriorityTasks}
|
|
subtitle={`${urgentTasks} urgent, ${highPriorityTasks} high priority`}
|
|
icon={AlertTriangle}
|
|
colorClass="bg-orange-500/15 text-orange-500"
|
|
/>
|
|
<StatCard
|
|
title="Active Sprint"
|
|
value={activeSprint ? `${getSprintDaysRemaining(activeSprint)}d` : "None"}
|
|
subtitle={activeSprint ? "days remaining" : "No active sprint"}
|
|
icon={Timer}
|
|
colorClass="bg-purple-500/15 text-purple-500"
|
|
/>
|
|
</div>
|
|
|
|
{/* Active Sprint Section */}
|
|
{activeSprint && (
|
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
|
|
<div className="lg:col-span-2">
|
|
<ActiveSprintCard sprint={activeSprint} />
|
|
</div>
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-sm">
|
|
<Sparkles className="w-4 h-4" />
|
|
Sprint Stats
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-3">
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">Planning</span>
|
|
<span className="font-medium">{sprintCounts.planning}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">Active</span>
|
|
<span className="font-medium text-primary">{sprintCounts.active}</span>
|
|
</div>
|
|
<div className="flex justify-between text-sm">
|
|
<span className="text-muted-foreground">Completed</span>
|
|
<span className="font-medium">{sprintCounts.completed}</span>
|
|
</div>
|
|
</div>
|
|
<Separator />
|
|
<div className="flex justify-between text-sm font-medium">
|
|
<span>Total Sprints</span>
|
|
<span>{sprintCounts.total}</span>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{/* Projects Grid */}
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-lg font-semibold flex items-center gap-2">
|
|
<TrendingUp className="w-5 h-5" />
|
|
Project Health
|
|
</h2>
|
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
<span className="flex items-center gap-1">
|
|
<div className="w-2 h-2 rounded-full bg-green-500" />
|
|
Healthy
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<div className="w-2 h-2 rounded-full bg-yellow-500" />
|
|
At Risk
|
|
</span>
|
|
<span className="flex items-center gap-1">
|
|
<div className="w-2 h-2 rounded-full bg-red-500" />
|
|
Critical
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{projectStats.length === 0 ? (
|
|
<div className="text-center py-12 bg-secondary/20 rounded-lg border border-dashed">
|
|
<FolderKanban className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
|
|
<p className="text-muted-foreground">No projects found</p>
|
|
<p className="text-sm text-muted-foreground mt-1">
|
|
Create projects in the Gantt Board to see them here
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
{sortedProjects.map((stats) => (
|
|
<ProjectCard key={stats.project.id} stats={stats} />
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Recent Activity Section */}
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
{/* Recent Tasks Across All Projects */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-sm">
|
|
<Clock className="w-4 h-4" />
|
|
Recently Updated Tasks
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{(() => {
|
|
const allRecentTasks = projectStats
|
|
.flatMap(p => p.recentTasks.map(t => ({ ...t, projectName: p.project.name, projectColor: p.project.color })))
|
|
.sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
|
.slice(0, 5);
|
|
|
|
return allRecentTasks.length > 0 ? (
|
|
<div className="divide-y divide-border">
|
|
{allRecentTasks.map((task) => (
|
|
<div key={task.id} className="flex items-start gap-3 py-3">
|
|
<div className="mt-0.5 shrink-0">{getStatusIcon(task.status)}</div>
|
|
<div className="flex-1 min-w-0 overflow-hidden">
|
|
<Link
|
|
href={getGanttTaskUrl(task.id)}
|
|
target="_blank"
|
|
className="text-sm font-medium block truncate hover:text-primary hover:underline"
|
|
>
|
|
{task.title}
|
|
</Link>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<div
|
|
className="w-2 h-2 rounded-full"
|
|
style={{ backgroundColor: task.projectColor }}
|
|
/>
|
|
<span className="text-xs text-muted-foreground">{task.projectName}</span>
|
|
<span className="text-xs text-muted-foreground">•</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{formatRelativeTime(task.updatedAt)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<Badge variant="outline" className={`text-xs shrink-0 ${getPriorityColor(task.priority)}`}>
|
|
{task.priority}
|
|
</Badge>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-sm text-muted-foreground text-center py-8">
|
|
No recent activity
|
|
</p>
|
|
);
|
|
})()}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Quick Actions */}
|
|
<Card>
|
|
<CardHeader className="pb-3">
|
|
<CardTitle className="flex items-center gap-2 text-sm">
|
|
<ArrowRight className="w-4 h-4" />
|
|
Quick Actions
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
|
<Link href={siteUrls.ganttBoard} target="_blank" className="block">
|
|
<Button variant="outline" className="w-full justify-start h-auto py-3">
|
|
<div className="p-2 rounded-lg bg-blue-500/15 mr-3 shrink-0">
|
|
<ExternalLink className="w-4 h-4 text-blue-500" />
|
|
</div>
|
|
<div className="text-left min-w-0">
|
|
<p className="font-medium text-sm truncate">Open Gantt Board</p>
|
|
<p className="text-xs text-muted-foreground truncate">Manage all projects</p>
|
|
</div>
|
|
</Button>
|
|
</Link>
|
|
|
|
<Link href={getGanttTasksUrl({ priority: "urgent,high" })} target="_blank" className="block">
|
|
<Button variant="outline" className="w-full justify-start h-auto py-3">
|
|
<div className="p-2 rounded-lg bg-orange-500/15 mr-3 shrink-0">
|
|
<AlertTriangle className="w-4 h-4 text-orange-500" />
|
|
</div>
|
|
<div className="text-left min-w-0">
|
|
<p className="font-medium text-sm truncate">High Priority</p>
|
|
<p className="text-xs text-muted-foreground truncate">View urgent tasks</p>
|
|
</div>
|
|
</Button>
|
|
</Link>
|
|
|
|
<Link href={getGanttTasksUrl({ status: "in-progress" })} target="_blank" className="block">
|
|
<Button variant="outline" className="w-full justify-start h-auto py-3">
|
|
<div className="p-2 rounded-lg bg-green-500/15 mr-3 shrink-0">
|
|
<CheckCircle2 className="w-4 h-4 text-green-500" />
|
|
</div>
|
|
<div className="text-left min-w-0">
|
|
<p className="font-medium text-sm truncate">In Progress</p>
|
|
<p className="text-xs text-muted-foreground truncate">Active tasks</p>
|
|
</div>
|
|
</Button>
|
|
</Link>
|
|
|
|
{activeSprint && (
|
|
<Link href={getGanttSprintUrl(activeSprint.id)} target="_blank" className="block">
|
|
<Button variant="outline" className="w-full justify-start h-auto py-3">
|
|
<div className="p-2 rounded-lg bg-purple-500/15 mr-3 shrink-0">
|
|
<Zap className="w-4 h-4 text-purple-500" />
|
|
</div>
|
|
<div className="text-left min-w-0">
|
|
<p className="font-medium text-sm truncate">Active Sprint</p>
|
|
<p className="text-xs text-muted-foreground truncate">View sprint board</p>
|
|
</div>
|
|
</Button>
|
|
</Link>
|
|
)}
|
|
</div>
|
|
|
|
<Separator className="my-4" />
|
|
|
|
<div className="flex flex-col sm:flex-row gap-4 items-start sm:items-center justify-between">
|
|
<div className="min-w-0">
|
|
<p className="font-medium text-sm">Project Management</p>
|
|
<p className="text-xs text-muted-foreground">
|
|
Mission Control is read-only. Create and edit projects in gantt-board.
|
|
</p>
|
|
</div>
|
|
<Link href={siteUrls.ganttBoard} target="_blank" className="shrink-0">
|
|
<Button variant="secondary" size="sm">
|
|
Go to gantt-board
|
|
<ArrowRight className="w-4 h-4 ml-2" />
|
|
</Button>
|
|
</Link>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</DashboardLayout>
|
|
);
|
|
}
|