344 lines
12 KiB
TypeScript
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>
|
|
);
|
|
};
|