task-board/src/components/workspace-stats.tsx
2025-06-24 14:26:42 +04:00

344 lines
12 KiB
TypeScript

"use client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { Badge } from "@/components/ui/badge";
import {
Users,
MessageSquare,
Calendar,
FolderOpen,
Clock,
CheckCircle,
AlertTriangle,
TrendingUp,
Activity,
Target
} from "lucide-react";
import { formatDistanceToNow } from "date-fns";
interface WorkspaceStatsProps {
tasks: any[];
projects: any[];
members: any[];
}
export const WorkspaceProjectStats = ({ tasks, projects, members }: WorkspaceStatsProps) => {
// Project distribution
const tasksPerProject = projects.map(project => ({
name: project.name,
taskCount: tasks.filter(task => task.projectId === project.id).length
}));
const unassignedToProject = tasks.filter(task => !task.projectId).length;
const totalTasks = tasks.length;
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FolderOpen className="h-5 w-5" />
Project Distribution
</CardTitle>
<p className="text-sm text-muted-foreground">
How tasks are distributed across your projects
</p>
</CardHeader>
<CardContent>
<div className="space-y-3">
{tasksPerProject
.filter(project => project.taskCount > 0)
.sort((a, b) => b.taskCount - a.taskCount)
.slice(0, 5)
.map((project) => (
<div key={project.name} className="flex items-center justify-between">
<div className="flex items-center gap-2 min-w-0 flex-1">
<div className="w-3 h-3 bg-muted-foreground rounded-full flex-shrink-0"></div>
<span className="text-sm font-medium truncate">{project.name}</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Progress
value={totalTasks > 0 ? (project.taskCount / totalTasks) * 100 : 0}
className="w-16"
/>
<span className="text-sm font-medium w-8 text-right">
{project.taskCount}
</span>
</div>
</div>
))}
{unassignedToProject > 0 && (
<div className="flex items-center justify-between">
<div className="flex items-center gap-2 min-w-0 flex-1">
<div className="w-3 h-3 bg-muted-foreground/50 rounded-full flex-shrink-0"></div>
<span className="text-sm font-medium text-muted-foreground">Unassigned</span>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Progress
value={totalTasks > 0 ? (unassignedToProject / totalTasks) * 100 : 0}
className="w-16"
/>
<span className="text-sm font-medium w-8 text-right">
{unassignedToProject}
</span>
</div>
</div>
)}
{totalTasks === 0 && (
<div className="text-center py-4 text-muted-foreground">
<FolderOpen className="h-6 w-6 mx-auto mb-2 opacity-50" />
<p className="text-sm">No tasks found</p>
</div>
)}
</div>
</CardContent>
</Card>
);
};
export const WorkspaceDueDateStats = ({ tasks }: { tasks: any[] }) => {
const now = new Date();
const nextWeek = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);
const overdueTasks = tasks.filter(task =>
task.dueDate && new Date(task.dueDate) < now && task.status !== 'DONE'
).length;
const dueSoonTasks = tasks.filter(task =>
task.dueDate &&
new Date(task.dueDate) >= now &&
new Date(task.dueDate) <= nextWeek &&
task.status !== 'DONE'
).length;
const noDueDateTasks = tasks.filter(task => !task.dueDate && task.status !== 'DONE').length;
const activeTasks = tasks.filter(task => task.status !== 'DONE').length;
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calendar className="h-5 w-5" />
Due Date Analysis
</CardTitle>
<p className="text-sm text-muted-foreground">
Track deadlines and time management
</p>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="text-center p-3 bg-red-50 rounded-lg border border-red-200">
<div className="flex items-center justify-center mb-1">
<AlertTriangle className="h-4 w-4 text-red-600 mr-1" />
<span className="text-2xl font-bold text-red-600">{overdueTasks}</span>
</div>
<p className="text-xs text-red-700 font-medium">Overdue</p>
</div>
<div className="text-center p-3 bg-yellow-50 rounded-lg border border-yellow-200">
<div className="flex items-center justify-center mb-1">
<Clock className="h-4 w-4 text-yellow-600 mr-1" />
<span className="text-2xl font-bold text-yellow-600">{dueSoonTasks}</span>
</div>
<p className="text-xs text-yellow-700 font-medium">Due Soon</p>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">No Due Date</span>
<div className="flex items-center gap-2">
<span className="text-sm font-bold">{noDueDateTasks}</span>
<span className="text-xs text-muted-foreground">
({activeTasks > 0 ? Math.round((noDueDateTasks / activeTasks) * 100) : 0}%)
</span>
</div>
</div>
<Progress
value={activeTasks > 0 ? (noDueDateTasks / activeTasks) * 100 : 0}
className="h-2"
/>
</div>
</div>
</CardContent>
</Card>
);
};
interface WorkspaceCommunicationStatsProps {
analytics: {
totalComments: number;
thisMonthComments: number;
commentsDifference: number;
tasksWithComments: number;
totalTasksInWorkspace: number;
avgCommentsPerTask: number;
} | null;
}
export const WorkspaceCommunicationStats = ({ analytics }: WorkspaceCommunicationStatsProps) => {
if (!analytics) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare className="h-5 w-5" />
Communication Activity
</CardTitle>
<p className="text-sm text-muted-foreground">
Track collaboration and engagement
</p>
</CardHeader>
<CardContent>
<div className="text-center py-4 text-muted-foreground">
<MessageSquare className="h-6 w-6 mx-auto mb-2 opacity-50" />
<p className="text-sm">Loading...</p>
</div>
</CardContent>
</Card>
);
}
const {
totalComments,
thisMonthComments,
commentsDifference,
tasksWithComments,
totalTasksInWorkspace,
avgCommentsPerTask
} = analytics;
const tasksWithCommentsPercentage = totalTasksInWorkspace > 0
? Math.round((tasksWithComments / totalTasksInWorkspace) * 100)
: 0;
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare className="h-5 w-5" />
Communication Activity
</CardTitle>
<p className="text-sm text-muted-foreground">
Track collaboration and engagement
</p>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="text-center p-3 bg-blue-50 rounded-lg border border-blue-200">
<div className="text-2xl font-bold text-blue-600">{totalComments}</div>
<p className="text-xs text-blue-700 font-medium">Total Comments</p>
{commentsDifference !== 0 && (
<div className={`text-xs mt-1 ${commentsDifference > 0 ? 'text-green-600' : 'text-red-600'}`}>
{commentsDifference > 0 ? '+' : ''}{commentsDifference} this month
</div>
)}
</div>
<div className="text-center p-3 bg-green-50 rounded-lg border border-green-200">
<div className="text-2xl font-bold text-green-600">{avgCommentsPerTask}</div>
<p className="text-xs text-green-700 font-medium">Avg per Task</p>
</div>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">Tasks with Comments</span>
<div className="flex items-center gap-2">
<span className="text-sm font-bold">{tasksWithComments}</span>
<span className="text-xs text-muted-foreground">
({tasksWithCommentsPercentage}%)
</span>
</div>
</div>
<Progress
value={tasksWithCommentsPercentage}
className="h-2"
/>
</div>
{totalComments === 0 && (
<div className="text-center py-2 text-muted-foreground">
<p className="text-xs">No comments yet. Start collaborating!</p>
</div>
)}
</div>
</CardContent>
</Card>
);
};
interface Epic {
id: string;
name: string;
status: string;
stats?: {
totalTasks: number;
completedTasks: number;
progressPercentage: number;
};
}
interface EpicStatsProps {
epics: Epic[];
}
export const EpicStats = ({ epics }: EpicStatsProps) => {
const totalEpics = epics.length;
const completedEpics = epics.filter(epic => epic.status === "DONE").length;
const inProgressEpics = epics.filter(epic => epic.status === "IN_PROGRESS").length;
const overallProgress = totalEpics > 0 ? Math.round((completedEpics / totalEpics) * 100) : 0;
const totalTasks = epics.reduce((sum, epic) => sum + (epic.stats?.totalTasks || 0), 0);
const completedTasks = epics.reduce((sum, epic) => sum + (epic.stats?.completedTasks || 0), 0);
const taskProgress = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Epic Progress</CardTitle>
<Target className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="space-y-4">
<div>
<div className="flex items-center justify-between text-xs mb-2">
<span className="text-muted-foreground">Epic Completion</span>
<span className="font-medium">{completedEpics}/{totalEpics} epics</span>
</div>
<Progress value={overallProgress} className="h-2" />
<div className="text-xs text-muted-foreground mt-1">{overallProgress}% complete</div>
</div>
<div>
<div className="flex items-center justify-between text-xs mb-2">
<span className="text-muted-foreground">Overall Task Progress</span>
<span className="font-medium">{completedTasks}/{totalTasks} tasks</span>
</div>
<Progress value={taskProgress} className="h-2" />
<div className="text-xs text-muted-foreground mt-1">{taskProgress}% complete</div>
</div>
<div className="flex justify-between text-xs">
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
<span>{completedEpics} Done</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-yellow-500 rounded-full"></div>
<span>{inProgressEpics} In Progress</span>
</div>
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-gray-400 rounded-full"></div>
<span>{totalEpics - completedEpics - inProgressEpics} Remaining</span>
</div>
</div>
</div>
</CardContent>
</Card>
);
};