From 2379f4e4ae239e9a59aa7eedf8285fea3484273b Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Thu, 10 Jul 2025 17:21:01 +0800 Subject: [PATCH 1/5] Create admin dashboard --- app/dashboard/page.tsx | 19 +- components/dashboard/AdminDashboard.tsx | 156 ++++++++++ package-lock.json | 389 +++++++++++++++++++++++- package.json | 3 +- 4 files changed, 560 insertions(+), 7 deletions(-) create mode 100644 components/dashboard/AdminDashboard.tsx diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index da6092a..f47d987 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,4 +1,6 @@ import { getAuthUser } from "@/lib/auth"; +import UserDashboard from "@/components/dashboard/UserDashboard"; +import AdminDashboard from "@/components/dashboard/AdminDashboard"; export default async function DashboardPage() { const user = await getAuthUser(); @@ -6,12 +8,19 @@ export default async function DashboardPage() { return null; } + const isAdmin = user?.organisation?.role === "admin"; + if (isAdmin) { + return ( +
+

Dashboard

+ +
+ ); + } return ( -
-

Welcome, {user.firstname || user.email}

-

Organisation: {user.organisation?.organisationname}

-

Role: {user.organisation.role}

-

Dashboard is currently in progress.

+
+

Dashboard

+
); } diff --git a/components/dashboard/AdminDashboard.tsx b/components/dashboard/AdminDashboard.tsx new file mode 100644 index 0000000..35a8bbe --- /dev/null +++ b/components/dashboard/AdminDashboard.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { + BarChart, + Bar, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, +} from "recharts"; + +interface Employee { + id: number; + firstname: string; + lastname: string; + totalCourses: number; + completedCourses: number; +} + +interface Enrollment { + courseName: string; + enrolledCount: number; +} + +interface AdminDashboardData { + welcome: string; + employees: Employee[]; + enrollments: Enrollment[]; +} + +export default function AdminDashboard() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetch("/api/dashboard/admin-dashboard", { credentials: "include" }) + .then(async (res) => { + if (!res.ok) throw new Error("Failed to load dashboard"); + const json = await res.json(); + const enrollments = (json.enrollments || []).map((e: any) => ({ + courseName: e.coursename, // map to camelCase + enrolledCount: Number(e.enrolledcount), // make sure it's a number + })); + const employees = (json.employees || []).map((emp: any) => ({ + id: emp.id, + firstname: emp.firstname, + lastname: emp.lastname, + totalCourses: Number(emp.totalCourses), + completedCourses: Number(emp.completedCourses), + })); + setData({ ...json, employees, enrollments }); + }) + .catch((err) => setError(err.message || "Unknown error")) + .finally(() => setLoading(false)); + }, []); + + if (loading) { + return ( +
+
+
+ ); + } + if (error || !data) { + return
{error}
; + } + + // Calculate quick stats + const totalEmployees = data.employees.length; + const totalCourses = data.enrollments.length; + const totalEnrollments = data.enrollments.reduce( + (a, e) => a + e.enrolledCount, + 0 + ); + const totalCompleted = data.employees.reduce( + (a, e) => a + e.completedCourses, + 0 + ); + + return ( +
+

{data.welcome}

+ + {/* Quick Stat Cards */} +
+ + + + +
+ + {/* Enrollment Chart Card */} +
+

Course Enrollments

+
+ + + + + + + + + +
+
+ + {/* Employee Table Card */} +
+

Employee Course Progress

+
+ + + + + + + + + + {data.employees.map((emp) => ( + + + + + + ))} + +
NameEnrolledCompleted
+ {emp.firstname} {emp.lastname} + {emp.totalCourses}{emp.completedCourses}
+
+
+
+ ); +} + +// Quick Stat Card component +function DashboardStat({ label, value }: { label: string; value: number }) { + return ( +
+
{value}
+
{label}
+
+ ); +} diff --git a/package-lock.json b/package-lock.json index 1770fc3..cb53b12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,8 @@ "next": "15.3.2", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-select": "^5.10.1" + "react-select": "^5.10.1", + "recharts": "^3.1.0" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -1224,6 +1225,32 @@ "node": ">=12.4.0" } }, + "node_modules/@reduxjs/toolkit": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", + "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^10.0.3", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1238,6 +1265,18 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -1540,6 +1579,69 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", @@ -1605,6 +1707,12 @@ "@types/react": "*" } }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.32.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz", @@ -2545,6 +2653,15 @@ "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -2649,6 +2766,127 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -2727,6 +2965,12 @@ } } }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -3028,6 +3272,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.39.7", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.7.tgz", + "integrity": "sha512-ek/wWryKouBrZIjkwW2BFf91CWOIMvoy2AE5YYgUrfWsJQM2Su1LoLtrw8uusEpN9RfqLlV/0FVNjT0WMv8Bxw==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -3466,6 +3720,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3905,6 +4165,16 @@ "node": ">= 4" } }, + "node_modules/immer": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", + "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -3946,6 +4216,15 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -5426,6 +5705,29 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-select": { "version": "5.10.1", "resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.1.tgz", @@ -5463,6 +5765,48 @@ "react-dom": ">=16.6.0" } }, + "node_modules/recharts": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.1.0.tgz", + "integrity": "sha512-NqAqQcGBmLrfDs2mHX/bz8jJCQtG2FeXfE0GqpZmIuXIjkpIwj8sd9ad0WyvKiBKPd8ZgNG0hL85c8sFDwascw==", + "license": "MIT", + "dependencies": { + "@reduxjs/toolkit": "1.x.x || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5507,6 +5851,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -6112,6 +6462,12 @@ "node": ">=18" } }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, "node_modules/tinyglobby": { "version": "0.2.13", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", @@ -6390,6 +6746,37 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index ade92c3..6be5bb1 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ "next": "15.3.2", "react": "^19.0.0", "react-dom": "^19.0.0", - "react-select": "^5.10.1" + "react-select": "^5.10.1", + "recharts": "^3.1.0" }, "devDependencies": { "@eslint/eslintrc": "^3", From 5ebfa20a88265d6500351cc00fb98db643655a03 Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Thu, 10 Jul 2025 17:21:41 +0800 Subject: [PATCH 2/5] Clean code --- components/dashboard/AdminDashboard.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/components/dashboard/AdminDashboard.tsx b/components/dashboard/AdminDashboard.tsx index 35a8bbe..23b9c2a 100644 --- a/components/dashboard/AdminDashboard.tsx +++ b/components/dashboard/AdminDashboard.tsx @@ -41,8 +41,8 @@ export default function AdminDashboard() { if (!res.ok) throw new Error("Failed to load dashboard"); const json = await res.json(); const enrollments = (json.enrollments || []).map((e: any) => ({ - courseName: e.coursename, // map to camelCase - enrolledCount: Number(e.enrolledcount), // make sure it's a number + courseName: e.coursename, + enrolledCount: Number(e.enrolledcount), })); const employees = (json.employees || []).map((emp: any) => ({ id: emp.id, @@ -84,7 +84,6 @@ export default function AdminDashboard() {

{data.welcome}

- {/* Quick Stat Cards */}
@@ -92,7 +91,6 @@ export default function AdminDashboard() {
- {/* Enrollment Chart Card */}

Course Enrollments

@@ -115,7 +113,6 @@ export default function AdminDashboard() {
- {/* Employee Table Card */}

Employee Course Progress

@@ -145,7 +142,6 @@ export default function AdminDashboard() { ); } -// Quick Stat Card component function DashboardStat({ label, value }: { label: string; value: number }) { return (
From ce8b89a2c026a051cb609eaa9933830733cce3ba Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Thu, 10 Jul 2025 17:41:02 +0800 Subject: [PATCH 3/5] Basic user dashboard --- components/dashboard/UserDashboard.tsx | 153 +++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 components/dashboard/UserDashboard.tsx diff --git a/components/dashboard/UserDashboard.tsx b/components/dashboard/UserDashboard.tsx new file mode 100644 index 0000000..0c337cd --- /dev/null +++ b/components/dashboard/UserDashboard.tsx @@ -0,0 +1,153 @@ +"use client"; + +import { useState, useEffect } from "react"; +import Link from "next/link"; + +interface Course { + id: number; + name: string; +} + +interface Module { + id: number; + title: string; +} + +interface DashboardData { + welcome: string; + currentCourse: Course | null; + currentModule: Module | null; + nextToLearn: Module[]; + toRevise: Module[]; + summaryStats: { + completed: number; + total: number; + }; +} + +export default function UserDashboard() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + fetch("/api/dashboard/user-dashboard", { credentials: "include" }) + .then(async (res) => { + if (!res.ok) throw new Error("Failed to load dashboard"); + const json = await res.json(); + setData(json); + }) + .catch((err) => setError(err.message || "Unknown error")) + .finally(() => setLoading(false)); + }, []); + + if (loading) { + return ( +
+
+
+ ); + } + if (error || !data) { + return ( +
+ {error || "No data."} +
+ ); + } + + const { + welcome, + currentCourse, + currentModule, + nextToLearn, + toRevise, + summaryStats, + } = data; + + return ( +
+
+

{welcome}

+
+

+ Continue where you left off +

+ {currentCourse && currentModule ? ( +
+
{currentCourse.name}
+
+ Module: {currentModule.title} +
+ + Resume + +
+ ) : ( +
No course in progress.
+ )} +
+
+

Next Steps For You

+
+
Learn
+ {nextToLearn && nextToLearn.length > 0 ? ( + nextToLearn.map((mod) => ( + + {mod.title} + + )) + ) : ( +
All caught up!
+ )} +
+
+
Revise
+ {toRevise && toRevise.length > 0 ? ( + toRevise.map((mod) => ( + + {mod.title} + + )) + ) : ( +
Nothing to revise.
+ )} +
+
+
+
+
+

At a glance

+
Modules Completed
+
+ {summaryStats.completed}{" "} + / {summaryStats.total} +
+
+
0 + ? (summaryStats.completed / summaryStats.total) * 100 + : 0 + }%`, + }} + >
+
+
+
+
+ ); +} From 11691744415c1e4671aa0d47533eea30759c10c6 Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Thu, 10 Jul 2025 17:47:20 +0800 Subject: [PATCH 4/5] Add better user dashboard --- components/dashboard/UserDashboard.tsx | 202 +++++++++++++++---------- 1 file changed, 122 insertions(+), 80 deletions(-) diff --git a/components/dashboard/UserDashboard.tsx b/components/dashboard/UserDashboard.tsx index 0c337cd..62b3074 100644 --- a/components/dashboard/UserDashboard.tsx +++ b/components/dashboard/UserDashboard.tsx @@ -7,12 +7,10 @@ interface Course { id: number; name: string; } - interface Module { id: number; title: string; } - interface DashboardData { welcome: string; currentCourse: Course | null; @@ -20,8 +18,8 @@ interface DashboardData { nextToLearn: Module[]; toRevise: Module[]; summaryStats: { - completed: number; - total: number; + completedModules: number; + totalModules: number; }; } @@ -66,85 +64,129 @@ export default function UserDashboard() { } = data; return ( -
-
-

{welcome}

-
-

- Continue where you left off -

- {currentCourse && currentModule ? ( -
-
{currentCourse.name}
-
- Module: {currentModule.title} +
+
+ {/* GRID */} +
+ {/* LEFT COLUMN */} +
+ {/* WELCOME */} +
+
+ {welcome} +
+
+ + {/* CONTINUE SECTION */} +
+
+ Continue where you left off +
+
+ {currentCourse && currentModule ? ( + <> +
+
+ {currentCourse.name} +
+
+ Module: {currentModule.title} +
+ + Resume + +
+ + ) : ( +
+ No course in progress. +
+ )} +
+
+ + {/* NEXT STEPS */} +
+
+ Next Steps For You +
+
+ {/* LEARN */} +
+
Learn
+ {nextToLearn && nextToLearn.length > 0 ? ( + nextToLearn.map((mod) => ( + + {mod.title} + + )) + ) : ( +
All caught up!
+ )} +
+ {/* REVISE */} +
+
Revise
+ {toRevise && toRevise.length > 0 ? ( + toRevise.map((mod) => ( + + {mod.title} + + )) + ) : ( +
Nothing to revise.
+ )} +
- - Resume -
- ) : ( -
No course in progress.
- )} -
-
-

Next Steps For You

-
-
Learn
- {nextToLearn && nextToLearn.length > 0 ? ( - nextToLearn.map((mod) => ( - - {mod.title} - - )) - ) : ( -
All caught up!
- )} -
-
-
Revise
- {toRevise && toRevise.length > 0 ? ( - toRevise.map((mod) => ( - - {mod.title} - - )) - ) : ( -
Nothing to revise.
- )} -
-
-
-
-
-

At a glance

-
Modules Completed
-
- {summaryStats.completed}{" "} - / {summaryStats.total}
-
-
0 - ? (summaryStats.completed / summaryStats.total) * 100 - : 0 - }%`, - }} - >
+ + {/* RIGHT COLUMN */} +
+ {/* AT A GLANCE */} +
+
+ At a glance +
+
+ Modules Completed +
+
+ + {summaryStats.completedModules} + + / + + {summaryStats.totalModules} + +
+ {/* Progress bar */} +
+
0 + ? (summaryStats.completedModules / + summaryStats.totalModules) * + 100 + : 0 + }%`, + }} + >
+
+
From e2aa9f2a07e632c61c4c3c146d0f9f55e320bae4 Mon Sep 17 00:00:00 2001 From: lavanyagarg112 Date: Thu, 10 Jul 2025 17:58:47 +0800 Subject: [PATCH 5/5] Update user dashboard ui --- components/dashboard/UserDashboard.tsx | 114 ++++++++++++++++--------- 1 file changed, 76 insertions(+), 38 deletions(-) diff --git a/components/dashboard/UserDashboard.tsx b/components/dashboard/UserDashboard.tsx index 62b3074..dbdb95c 100644 --- a/components/dashboard/UserDashboard.tsx +++ b/components/dashboard/UserDashboard.tsx @@ -21,6 +21,10 @@ interface DashboardData { completedModules: number; totalModules: number; }; + globalStats: { + completedModules: number; + totalModules: number; + }; } export default function UserDashboard() { @@ -61,23 +65,20 @@ export default function UserDashboard() { nextToLearn, toRevise, summaryStats, + globalStats, } = data; return ( -
+
- {/* GRID */}
- {/* LEFT COLUMN */}
- {/* WELCOME */}
{welcome}
- {/* CONTINUE SECTION */}
Continue where you left off @@ -102,19 +103,17 @@ export default function UserDashboard() { ) : (
- No course in progress. + No module in progress.
)}
- {/* NEXT STEPS */}
Next Steps For You
- {/* LEARN */}
Learn
{nextToLearn && nextToLearn.length > 0 ? ( @@ -131,7 +130,6 @@ export default function UserDashboard() {
All caught up!
)}
- {/* REVISE */}
Revise
{toRevise && toRevise.length > 0 ? ( @@ -152,39 +150,79 @@ export default function UserDashboard() {
- {/* RIGHT COLUMN */}
- {/* AT A GLANCE */} -
-
+
+
At a glance
-
- Modules Completed -
-
- - {summaryStats.completedModules} - - / - - {summaryStats.totalModules} - + +
+
+ Modules Completed{" "} + + (Current Course) + +
+
+ + {summaryStats.completedModules} + + / + + {summaryStats.totalModules} + +
+
+ {currentCourse?.name} +
+ {/* Progress bar */} +
+
0 + ? (summaryStats.completedModules / + summaryStats.totalModules) * + 100 + : 0 + }%`, + }} + >
+
- {/* Progress bar */} -
-
0 - ? (summaryStats.completedModules / - summaryStats.totalModules) * - 100 - : 0 - }%`, - }} - >
+ +
+
+ Modules Completed{" "} + (All Courses) +
+
+ + {globalStats.completedModules} + + / + + {globalStats.totalModules} + +
+
+ across all courses +
+
+
0 + ? (globalStats.completedModules / + globalStats.totalModules) * + 100 + : 0 + }%`, + }} + >
+