diff --git a/client/components/ProjectStatusWidget.tsx b/client/components/ProjectStatusWidget.tsx new file mode 100644 index 00000000..89a9019b --- /dev/null +++ b/client/components/ProjectStatusWidget.tsx @@ -0,0 +1,220 @@ +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { CheckCircle, Clock, AlertCircle, Calendar, DollarSign } from "lucide-react"; + +export interface Milestone { + id: string; + title: string; + description?: string; + status: "pending" | "in_progress" | "completed"; + due_date: string; + value?: number; + progress?: number; +} + +export interface Project { + id: string; + title: string; + description?: string; + status: "active" | "completed" | "paused" | "cancelled"; + start_date: string; + end_date: string; + total_value?: number; + milestones?: Milestone[]; +} + +interface ProjectStatusWidgetProps { + project: Project | null; + accentColor?: "blue" | "cyan" | "purple"; +} + +const colorMap = { + blue: { + bg: "bg-gradient-to-br from-blue-950/40 to-blue-900/20", + border: "border-blue-500/20", + }, + cyan: { + bg: "bg-gradient-to-br from-cyan-950/40 to-cyan-900/20", + border: "border-cyan-500/20", + }, + purple: { + bg: "bg-gradient-to-br from-purple-950/40 to-purple-900/20", + border: "border-purple-500/20", + }, +}; + +export function ProjectStatusWidget({ + project, + accentColor = "cyan", +}: ProjectStatusWidgetProps) { + const colors = colorMap[accentColor]; + + if (!project) { + return ( + + + Project Status + Timeline and milestones + + +
+ +

No active project

+
+
+
+ ); + } + + const completedMilestones = project.milestones?.filter(m => m.status === "completed").length || 0; + const totalMilestones = project.milestones?.length || 0; + const completionPercentage = totalMilestones > 0 ? Math.round((completedMilestones / totalMilestones) * 100) : 0; + + const getStatusIcon = (status: string) => { + switch (status) { + case "completed": + return ; + case "in_progress": + return ; + default: + return ; + } + }; + + const getStatusColor = (status: string) => { + switch (status) { + case "completed": + return "bg-green-500"; + case "in_progress": + return "bg-blue-500"; + default: + return "bg-gray-500"; + } + }; + + const getDaysRemaining = (dueDate: string) => { + const today = new Date(); + const due = new Date(dueDate); + const diff = Math.ceil((due.getTime() - today.getTime()) / (1000 * 60 * 60 * 24)); + return diff; + }; + + return ( + + + Project Status & Timeline + Track milestones and project progress + + + {/* Project Header */} +
+

{project.title}

+ {project.description && ( +

{project.description}

+ )} + +
+
+

Start

+
+ +

{new Date(project.start_date).toLocaleDateString()}

+
+
+
+

End

+
+ +

{new Date(project.end_date).toLocaleDateString()}

+
+
+
+

Value

+
+ +

${(project.total_value || 0).toLocaleString()}

+
+
+
+

Status

+ {project.status} +
+
+ + {/* Overall Progress */} +
+
+ Overall Progress + {completionPercentage}% +
+
+
+
+

{completedMilestones} of {totalMilestones} milestones completed

+
+
+ + {/* Milestones Timeline */} +
+

Milestones

+ {project.milestones && project.milestones.length > 0 ? ( +
+ {project.milestones.map((milestone, idx) => { + const daysRemaining = getDaysRemaining(milestone.due_date); + const isOverdue = daysRemaining < 0 && milestone.status !== "completed"; + + return ( +
+ {/* Milestone Header */} +
+ {getStatusIcon(milestone.status)} +
+
+
+

{milestone.title}

+ {milestone.description && ( +

{milestone.description}

+ )} +
+
+ {milestone.value && ( +

${milestone.value.toLocaleString()}

+ )} +

+ {isOverdue ? `${Math.abs(daysRemaining)} days overdue` : `${daysRemaining} days remaining`} +

+
+
+
+
+ + {/* Milestone Progress Bar */} +
+
+ Progress + {milestone.progress || 0}% +
+
+
+
+
+
+ ); + })} +
+ ) : ( +

No milestones defined

+ )} +
+ + + ); +} + +export default ProjectStatusWidget;