diff --git a/src/app/page.js b/src/app/page.js index 8515b2c..d29b047 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -8,34 +8,197 @@ import Badge from "@/components/ui/Badge"; import PageContainer from "@/components/ui/PageContainer"; import PageHeader from "@/components/ui/PageHeader"; import { LoadingState } from "@/components/ui/States"; +import { + differenceInCalendarDays, + parseISO, + formatDistanceToNow, + isAfter, + isBefore, + addDays, + startOfWeek, + endOfWeek, + format, +} from "date-fns"; export default function Home() { const [stats, setStats] = useState({ totalProjects: 0, activeProjects: 0, + completedProjects: 0, + overdueProjects: 0, + totalContracts: 0, + activeContracts: 0, pendingTasks: 0, + inProgressTasks: 0, completedTasks: 0, + overdueTasks: 0, + projectsThisWeek: 0, + tasksThisWeek: 0, + completionRate: 0, }); const [recentProjects, setRecentProjects] = useState([]); + const [recentTasks, setRecentTasks] = useState([]); + const [upcomingDeadlines, setUpcomingDeadlines] = useState([]); + const [contracts, setContracts] = useState([]); + const [tasksByStatus, setTasksByStatus] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const fetchDashboardData = async () => { try { - // Fetch projects for stats and recent projects - const projectsRes = await fetch("/api/projects"); + // Fetch all data concurrently + const [projectsRes, tasksRes, contractsRes] = await Promise.all([ + fetch("/api/projects"), + fetch("/api/all-project-tasks"), + fetch("/api/contracts"), + ]); + const projects = await projectsRes.json(); + const tasks = await tasksRes.json(); + const contractsData = await contractsRes.json(); + + const now = new Date(); + + // Calculate project statistics + const activeProjects = projects.filter( + (p) => p.finish_date && isAfter(parseISO(p.finish_date), now) + ); + const completedProjects = projects.filter( + (p) => p.project_status === "fulfilled" + ); + const overdueProjects = projects.filter( + (p) => + p.finish_date && + isBefore(parseISO(p.finish_date), now) && + p.project_status !== "fulfilled" + ); + + // Calculate contract statistics + const activeContracts = contractsData.filter( + (c) => !c.finish_date || isAfter(parseISO(c.finish_date), now) + ); + + // Calculate task statistics + const pendingTasks = tasks.filter((t) => t.status === "pending"); + const inProgressTasks = tasks.filter((t) => t.status === "in_progress"); + const completedTasks = tasks.filter((t) => t.status === "completed"); + + // Calculate overdue tasks + const overdueTasks = tasks.filter((t) => { + if (t.status === "completed" || t.status === "cancelled") + return false; + try { + const addedDate = t.date_added.includes("T") + ? parseISO(t.date_added) + : new Date(t.date_added + "T00:00:00"); + const daysElapsed = differenceInCalendarDays(now, addedDate); + const maxWaitDays = t.max_wait_days || 0; + return daysElapsed > maxWaitDays; + } catch { + return false; + } + }); + + // Get upcoming deadlines (next 14 days) + const deadlines = [ + ...projects + .filter((p) => p.finish_date) + .map((p) => ({ + type: "project", + id: p.project_id, + name: p.project_name, + date: p.finish_date, + status: p.project_status, + city: p.city, + })), + ...contractsData + .filter((c) => c.finish_date) + .map((c) => ({ + type: "contract", + id: c.contract_id, + name: c.contract_name, + date: c.finish_date, + customer: c.customer, + })), + ] + .filter((item) => { + const itemDate = parseISO(item.date); + return ( + isAfter(itemDate, now) && isBefore(itemDate, addDays(now, 14)) + ); + }) + .sort((a, b) => new Date(a.date) - new Date(b.date)) + .slice(0, 5); + + // Calculate weekly statistics + const weekStart = startOfWeek(now); + const weekEnd = endOfWeek(now); + + const projectsThisWeek = projects.filter((p) => { + if (!p.date_created) return false; + const projectDate = parseISO(p.date_created); + return projectDate >= weekStart && projectDate <= weekEnd; + }).length; + + const tasksThisWeek = tasks.filter((t) => { + if (!t.date_added) return false; + const taskDate = t.date_added.includes("T") + ? parseISO(t.date_added) + : new Date(t.date_added + "T00:00:00"); + return taskDate >= weekStart && taskDate <= weekEnd; + }).length; + + const totalTasks = tasks.length; + const completionRate = + totalTasks > 0 + ? Math.round((completedTasks.length / totalTasks) * 100) + : 0; + + // Group tasks by status for charts + const tasksByStatusData = [ + { + status: "pending", + count: pendingTasks.length, + color: "bg-yellow-500", + }, + { + status: "in_progress", + count: inProgressTasks.length, + color: "bg-blue-500", + }, + { + status: "completed", + count: completedTasks.length, + color: "bg-green-500", + }, + { + status: "overdue", + count: overdueTasks.length, + color: "bg-red-500", + }, + ]; setStats({ totalProjects: projects.length, - activeProjects: projects.filter( - (p) => new Date(p.finish_date) >= new Date() - ).length, - pendingTasks: 0, // You might want to fetch this from tasks API - completedTasks: 0, // You might want to fetch this from tasks API + activeProjects: activeProjects.length, + completedProjects: completedProjects.length, + overdueProjects: overdueProjects.length, + totalContracts: contractsData.length, + activeContracts: activeContracts.length, + pendingTasks: pendingTasks.length, + inProgressTasks: inProgressTasks.length, + completedTasks: completedTasks.length, + overdueTasks: overdueTasks.length, + projectsThisWeek, + tasksThisWeek, + completionRate, }); setRecentProjects(projects.slice(0, 5)); + setRecentTasks(tasks.slice(0, 8)); + setUpcomingDeadlines(deadlines); + setContracts(contractsData); + setTasksByStatus(tasksByStatusData); } catch (error) { console.error("Failed to fetch dashboard data:", error); } finally { @@ -46,33 +209,363 @@ export default function Home() { fetchDashboardData(); }, []); + const getProjectStatusColor = (status) => { + switch (status) { + case "fulfilled": + return "text-green-600"; + case "in_progress_design": + return "text-blue-600"; + case "in_progress_construction": + return "text-orange-600"; + case "registered": + return "text-gray-600"; + default: + return "text-gray-600"; + } + }; + + const getTaskPriorityVariant = (priority) => { + switch (priority) { + case "high": + return "danger"; + case "normal": + return "secondary"; + case "low": + return "success"; + default: + return "secondary"; + } + }; + + const getDeadlineVariant = (days) => { + if (days <= 2) return "danger"; + if (days <= 7) return "warning"; + return "primary"; + }; + + if (loading) { + return ( + + + + + ); + } return ( + description="Overview of your projects, contracts, and tasks" + > +
+ + + + + + +
+
- {loading ? ( - - ) : ( - <> - {/* Stats Cards */} -
- - -
-
-

- Total Projects -

-

- {stats.totalProjects} -

+ {/* Overview Stats - Enhanced 6 Cards */} +
+ {/* Projects Stats */} + + +
+
+

+ Total Projects +

+

+ {stats.totalProjects} +

+

+ {stats.activeProjects} active +

+
+
+ + + +
+
+
+
+ + + +
+
+

+ Contracts +

+

+ {stats.totalContracts} +

+

+ {stats.activeContracts} active +

+
+
+ + + +
+
+
+
+ + + +
+
+

+ Pending Tasks +

+

+ {stats.pendingTasks} +

+

Awaiting start

+
+
+ + + +
+
+
+
+ + + +
+
+

+ In Progress +

+

+ {stats.inProgressTasks} +

+

Active tasks

+
+
+ + + +
+
+
+
+ + + +
+
+

+ Completed +

+

+ {stats.completedTasks} +

+

Tasks done

+
+
+ + + +
+
+
+
+ + 0 ? "border-red-200 bg-red-50" : "" + }`} + > + +
+
+

+ Overdue +

+

0 ? "text-red-600" : "text-gray-900" + }`} + > + {stats.overdueTasks} +

+

+ {stats.overdueTasks > 0 ? "Need attention!" : "All on track"} +

+
+
0 ? "bg-red-100" : "bg-gray-100" + } rounded-lg flex items-center justify-center`} + > + 0 ? "text-red-600" : "text-gray-400" + }`} + fill="none" + stroke="currentColor" + viewBox="0 0 24 24" + > + + +
+
+
+
+
+ + {/* Performance Metrics Row */} +
+ {/* Task Progress Visualization */} + + +
+

+ Task Distribution +

+ + {stats.completionRate}% complete + +
+
+ +
+ {tasksByStatus.map((item) => ( +
+
+
+ + {item.status.replace("_", " ")} +
-
+ + {item.count} + +
+ ))} +
+
+
+ Total Tasks + + {tasksByStatus.reduce((sum, item) => sum + item.count, 0)} + +
+
+ + + + {/* Weekly Activity */} + + +

This Week

+
+ +
+
+
+
-
- - - - - -
-
-

- Active Projects -

-

- {stats.activeProjects} +

+

+ New Projects

+

Added this week

-
+
+ + {stats.projectsThisWeek} + +
+ +
+
+
-
- - - - - -
-
-

- Pending Tasks +

+

+ New Tasks

-

- {stats.pendingTasks} -

-
-
- - - +

Created this week

- - + + {stats.tasksThisWeek} + +
+
+ + - - -
-
-

- Completed Tasks -

-

- {stats.completedTasks} -

-
-
- - - -
+ {/* Quick Stats Summary */} + + +

+ Quick Overview +

+
+ +
+
+ Active Projects +
+
+ + {stats.activeProjects} +
- - -
- - {/* Recent Projects */} -
- - +
+
+ Overdue Projects +
+
+ + {stats.overdueProjects} + +
+
+
+ Active Contracts +
+
+ + {stats.activeContracts} + +
+
+
-

- Recent Projects -

- - - + + Completion Rate + + + {stats.completionRate}% +
- - - {loading ? ( -
-
- {[...Array(3)].map((_, i) => ( -
-
-
-
-
-
-
- ))} -
-
- ) : recentProjects.length === 0 ? ( -
-

No projects yet.

- - - -
- ) : ( -
- {recentProjects.map((project) => ( -
-
-
- +
+
+
+
+ + +
+ + {/* Main Content Grid */} +
+ {/* Upcoming Deadlines */} + + +
+

+ Upcoming Deadlines +

+ + Next 14 days + +
+
+ + {upcomingDeadlines.length === 0 ? ( +
+
+ + + +
+

No upcoming deadlines

+

+ All projects on track! +

+
+ ) : ( +
+ {upcomingDeadlines.map((deadline) => { + const daysUntil = differenceInCalendarDays( + parseISO(deadline.date), + new Date() + ); + return ( +
+
+
+
+ - {project.project_name} - -

- {project.city} • Due: {project.finish_date} -

+ {deadline.type} +
+ + {daysUntil === 0 + ? "Today" + : daysUntil === 1 + ? "Tomorrow" + : `${daysUntil} days`} +
+ + {deadline.name} + +

+ {deadline.type === "project" + ? deadline.city + : deadline.customer}{" "} + • {deadline.date} +

+
+
+
+ ); + })} +
+ )} +
+
+ + {/* Recent Projects */} + + +
+

+ Recent Projects +

+ + + +
+
+ + {recentProjects.length === 0 ? ( +
+

No projects yet.

+ + + +
+ ) : ( +
+ {recentProjects.map((project) => { + const daysRemaining = project.finish_date + ? differenceInCalendarDays( + parseISO(project.finish_date), + new Date() + ) + : null; + return ( +
+
+
+ + {project.project_name} + +
+

+ {project.city} +

+
+ {project.project_status?.replace("_", " ")} +
+
+ {project.finish_date && ( +

+ Due: {project.finish_date} +

+ )} +
+ {daysRemaining !== null && ( = new Date() - ? "success" - : "danger" + daysRemaining < 0 + ? "danger" + : daysRemaining <= 7 + ? "warning" + : "success" } size="xs" > - {new Date(project.finish_date) >= new Date() - ? "Active" - : "Overdue"} + {daysRemaining < 0 + ? `${Math.abs(daysRemaining)} days overdue` + : daysRemaining === 0 + ? "Due today" + : `${daysRemaining} days left`} + )} +
+
+ ); + })} +
+ )} +
+
+ + {/* Recent Tasks Activity */} + + +
+

+ Recent Tasks +

+ + + +
+
+ + {recentTasks.length === 0 ? ( +
+

No tasks yet.

+ + + +
+ ) : ( +
+ {recentTasks.slice(0, 6).map((task) => ( +
+
+
+
+

+ {task.task_name} +

+ + {task.status?.replace("_", " ")} + + {task.priority && ( + + {task.priority} + + )}
-
- ))} -
- )} - - - - {/* Quick Actions */} - - -

- Quick Actions -

-
- - -
-
-
- - - -
-
-

- New Project -

-

- Create a new project + {task.project_name} + +

+ {(() => { + try { + if (!task.date_added) return "No date"; + const taskDate = task.date_added.includes("T") + ? parseISO(task.date_added) + : new Date(task.date_added + "T00:00:00"); + + if (isNaN(taskDate.getTime())) + return "Invalid date"; + + return formatDistanceToNow(taskDate, { + addSuffix: true, + }); + } catch (error) { + return "Invalid date"; + } + })()}

- + ))} +
+ )} + + +
- -
-
-
- - - -
-
-

- New Contract -

-

- Add a new contract -

-
-
-
- + {/* Quick Actions Grid */} +
+ + + +
+ + + +
+

+ New Project +

+

Start a new project

+
+
+ - -
-
-
- - - -
-
-

- Task Template -

-

- Create task template -

-
-
-
- - - -
- - )} + + + +
+ + + +
+

+ New Contract +

+

Add a new contract

+
+
+ + + + + +
+ + + +
+

+ Task Template +

+

Create reusable template

+
+
+ + + + + +
+ + + +
+

+ Task Dashboard +

+

Monitor all tasks

+
+
+ +
); }