diff --git a/guardian-admin-dashboard/package-lock.json b/guardian-admin-dashboard/package-lock.json index 65f92b365..e943ebdf3 100644 --- a/guardian-admin-dashboard/package-lock.json +++ b/guardian-admin-dashboard/package-lock.json @@ -355,7 +355,6 @@ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "aix" @@ -372,7 +371,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -389,7 +387,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -406,7 +403,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -423,7 +419,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -440,7 +435,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -457,7 +451,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -474,7 +467,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -491,7 +483,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -508,7 +499,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -525,7 +515,6 @@ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -542,7 +531,6 @@ "loong64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -559,7 +547,6 @@ "mips64el" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -576,7 +563,6 @@ "ppc64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -593,7 +579,6 @@ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -610,7 +595,6 @@ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -627,7 +611,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -644,7 +627,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "netbsd" @@ -661,7 +643,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "netbsd" @@ -678,7 +659,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openbsd" @@ -695,7 +675,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openbsd" @@ -712,7 +691,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "openharmony" @@ -729,7 +707,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "sunos" @@ -746,7 +723,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -763,7 +739,6 @@ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -780,7 +755,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -1063,7 +1037,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -1077,7 +1050,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "android" @@ -1091,7 +1063,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -1105,7 +1076,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -1119,7 +1089,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1133,7 +1102,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1147,7 +1115,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1161,7 +1128,6 @@ "arm" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1175,7 +1141,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1189,7 +1154,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1209,20 +1173,6 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", - "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", @@ -1237,20 +1187,6 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", - "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", @@ -1259,7 +1195,6 @@ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1273,7 +1208,6 @@ "riscv64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1287,7 +1221,6 @@ "s390x" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1301,7 +1234,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1315,7 +1247,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "linux" @@ -1357,7 +1288,6 @@ "arm64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -1371,7 +1301,6 @@ "ia32" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -1391,6 +1320,32 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", @@ -1399,7 +1354,6 @@ "x64" ], "dev": true, - "license": "MIT", "optional": true, "os": [ "win32" @@ -1539,7 +1493,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, - "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1603,7 +1556,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1944,7 +1896,6 @@ "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -2281,7 +2232,6 @@ "url": "https://github.com/sponsors/RubenVerborgh" } ], - "license": "MIT", "engines": { "node": ">=4.0" }, @@ -2571,7 +2521,6 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2727,7 +2676,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2892,7 +2840,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -2901,9 +2848,9 @@ } }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", "dev": true, "funding": [ { @@ -2919,7 +2866,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3066,7 +3012,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", "dev": true, - "license": "MIT", "dependencies": { "@types/estree": "1.0.8" }, @@ -3106,6 +3051,40 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/scheduler": { "version": "0.26.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", @@ -3264,7 +3243,6 @@ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", "dev": true, - "license": "MIT", "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" @@ -3350,7 +3328,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", diff --git a/guardian-admin-dashboard/src/App.css b/guardian-admin-dashboard/src/App.css index 24b374fcb..44beba320 100644 --- a/guardian-admin-dashboard/src/App.css +++ b/guardian-admin-dashboard/src/App.css @@ -171,232 +171,4 @@ body { .org-refresh-wrap { margin-top: 22px; -} -/* Doctor Assignments Page */ - -.page-shell { - display: flex; - flex-direction: column; - gap: 24px; - padding: 24px 28px; -} - -.page-header { - display: flex; - justify-content: space-between; - align-items: flex-start; -} - -.eyebrow { - margin: 0 0 8px; - font-size: 12px; - font-weight: 700; - letter-spacing: 1.5px; - color: #2d8fca; - text-transform: uppercase; -} - -.page-header h1 { - margin: 0; - color: #07336f; - font-size: 30px; - font-weight: 800; -} - -.page-subtitle, -.card-muted { - color: #61708a; - font-size: 14px; - line-height: 1.5; -} - -.dashboard-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 18px; -} - -.dashboard-card { - background: #ffffff; - border: 1px solid #d8e5f0; - border-radius: 18px; - padding: 22px; - box-shadow: 0 8px 20px rgba(15, 42, 70, 0.05); -} - -.dashboard-card h2 { - margin-top: 0; - margin-bottom: 8px; - color: #07336f; - font-size: 20px; - font-weight: 800; -} - -.full-width-card { - width: auto; -} - -.assignment-form { - display: flex; - flex-direction: column; - gap: 14px; - margin-top: 18px; -} - -.assignment-form label { - display: flex; - flex-direction: column; - gap: 6px; - font-weight: 600; - color: #07336f; - font-size: 14px; -} - -.form-control { - width: 100%; - box-sizing: border-box; - border: 1px solid #d7e4ef; - border-radius: 12px; - padding: 12px 14px; - font-size: 14px; - outline: none; - background: #ffffff; - color: #0b1f3a; -} - -.form-control:focus { - border-color: #4aa3c7; - box-shadow: 0 0 0 3px rgba(74, 163, 199, 0.15); -} - -.primary-button, -.secondary-button, -.danger-button { - border: none; - border-radius: 12px; - padding: 11px 16px; - font-weight: 700; - cursor: pointer; - transition: 0.2s ease; -} - -.primary-button { - background: #07336f; - color: #ffffff; -} - -.primary-button:hover { - background: #052858; -} - -.secondary-button { - background: #eef6fb; - color: #07336f; -} - -.secondary-button:hover { - background: #dceff8; -} - -.danger-button { - background: #ffecec; - color: #c62828; -} - -.danger-button:hover { - background: #ffdada; -} - -.primary-button:disabled, -.secondary-button:disabled, -.danger-button:disabled { - opacity: 0.6; - cursor: not-allowed; -} - -.alert { - padding: 14px 16px; - border-radius: 12px; - font-weight: 600; - font-size: 14px; -} - -.alert-error { - background: #ffecec; - color: #b3261e; - border: 1px solid #ffc9c9; -} - -.alert-success { - background: #eaf8ef; - color: #1b7f3a; - border: 1px solid #bfe8cb; -} - -.summary-box { - margin-top: 16px; - padding: 14px; - border-radius: 12px; - background: #f4f9fd; - display: flex; - flex-direction: column; - gap: 4px; - color: #07336f; -} - -.section-header { - display: flex; - justify-content: space-between; - align-items: center; - gap: 12px; -} - -.empty-state { - margin-top: 16px; - padding: 28px; - border: 1px dashed #cddcea; - border-radius: 14px; - color: #61708a; - text-align: center; - background: #f8fbfd; -} - -.table-wrapper { - overflow-x: auto; - margin-top: 16px; -} - -.admin-table { - width: 100%; - border-collapse: collapse; - background: #ffffff; -} - -.admin-table th, -.admin-table td { - text-align: left; - padding: 14px; - border-bottom: 1px solid #edf2f7; - font-size: 14px; -} - -.admin-table th { - color: #07336f; - background: #f7fbfe; - font-weight: 800; -} - -.admin-table td { - color: #253858; -} - -@media (max-width: 900px) { - .dashboard-grid { - grid-template-columns: 1fr; - } - - .section-header { - flex-direction: column; - align-items: flex-start; - } } \ No newline at end of file diff --git a/guardian-admin-dashboard/src/App.jsx b/guardian-admin-dashboard/src/App.jsx index 56c8db969..5f7bfa734 100644 --- a/guardian-admin-dashboard/src/App.jsx +++ b/guardian-admin-dashboard/src/App.jsx @@ -53,4 +53,4 @@ export default function App() { } /> ); -} \ No newline at end of file +} diff --git a/guardian-admin-dashboard/src/components/common/ConfirmationModal.jsx b/guardian-admin-dashboard/src/components/common/ConfirmationModal.jsx new file mode 100644 index 000000000..d47da3b9b --- /dev/null +++ b/guardian-admin-dashboard/src/components/common/ConfirmationModal.jsx @@ -0,0 +1,79 @@ +import { motion, AnimatePresence } from "framer-motion"; +import { X, AlertCircle } from "lucide-react"; + +export default function ConfirmationModal({ + isOpen, + onClose, + onConfirm, + title = "Are you sure?", + message, + children, + confirmText = "Confirm", + cancelText = "Cancel", + type = "danger", + isLoading = false, + icon: CustomIcon, + maxWidth = "440px" +}) { + if (!isOpen) return null; + + const getIcon = () => { + if (CustomIcon) return ; + return ; + }; + + return ( + +
+ e.stopPropagation()} + initial={{ opacity: 0, scale: 0.95, y: 20 }} + animate={{ opacity: 1, scale: 1, y: 0 }} + exit={{ opacity: 0, scale: 0.95, y: 20 }} + transition={{ type: "spring", damping: 25, stiffness: 300 }} + > +
+
+ {getIcon()} +

{title}

+
+ {!isLoading && ( + + )} +
+ +
+ {message &&

{message}

} + {children} +
+ +
+ + +
+
+
+
+ ); +} diff --git a/guardian-admin-dashboard/src/components/dashboard/NotificationDrawer.jsx b/guardian-admin-dashboard/src/components/dashboard/NotificationDrawer.jsx new file mode 100644 index 000000000..ac8b8a61c --- /dev/null +++ b/guardian-admin-dashboard/src/components/dashboard/NotificationDrawer.jsx @@ -0,0 +1,174 @@ +import { motion, AnimatePresence } from "framer-motion"; +import { + X, + Check, + Trash2, + Clock, + Bell, + Search, + Filter +} from "lucide-react"; +import { useState } from "react"; +import { + markNotificationAsRead +} from "../../services/notificationService"; + +export default function NotificationDrawer({ + isOpen, + onClose, + notifications, + setNotifications, + onDeleteRequest, + onViewNotification, + getIcon +}) { + const [filter, setFilter] = useState("all"); // all, unread + const [search, setSearch] = useState(""); + + const filteredNotifications = notifications.filter(n => { + const matchesFilter = filter === "all" || !(n.isRead || n.read); + const matchesSearch = n.title.toLowerCase().includes(search.toLowerCase()) || + n.message.toLowerCase().includes(search.toLowerCase()); + return matchesFilter && matchesSearch; + }); + + const handleMarkAllRead = async () => { + try { + // In a real app, you'd have an API endpoint for this + // For now, we'll just map through + const unreadIds = notifications.filter(n => !(n.isRead || n.read)).map(n => n._id); + for (const id of unreadIds) { + await markNotificationAsRead(id); + } + setNotifications(prev => prev.map(n => ({ ...n, read: true, isRead: true }))); + } catch (err) { + console.error("Failed to mark all as read", err); + } + }; + + const formatTimeFull = (dateStr) => { + return new Date(dateStr).toLocaleString([], { + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }); + }; + + return ( + + {isOpen && ( + <> + + +
+
+ +

Notification Center

+
+ +
+ +
+
+ + setSearch(e.target.value)} + /> +
+
+ + +
+
+ +
+ {filteredNotifications.length} notifications + +
+ +
+ {filteredNotifications.length === 0 ? ( +
+ +

No notifications found

+
+ ) : ( + filteredNotifications.map((notif) => { + const isRead = notif.isRead || notif.read; + return ( +
onViewNotification(notif)} + style={{ cursor: 'pointer' }} + > +
+

{notif.title}

+ {formatTimeFull(notif.createdAt || notif.date)} +
+

{notif.message}

+
+
{notif.type || 'info'}
+
+ {!isRead && ( + + )} + +
+
+
+ ); + }) + )} +
+
+ + )} +
+ ); +} diff --git a/guardian-admin-dashboard/src/components/dashboard/NotificationPanel.jsx b/guardian-admin-dashboard/src/components/dashboard/NotificationPanel.jsx new file mode 100644 index 000000000..632f5aa40 --- /dev/null +++ b/guardian-admin-dashboard/src/components/dashboard/NotificationPanel.jsx @@ -0,0 +1,182 @@ +import { useEffect, useState, useCallback } from "react"; +import { + X, + Check, + Trash2, + Info, + AlertTriangle, + CheckCircle2, + XCircle, + Bell, + Clock, + ExternalLink +} from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; +import { + markNotificationAsRead, +} from "../../services/notificationService"; +import Loader from "../common/Loader"; + +export default function NotificationPanel({ + isOpen, + onClose, + notifications, + setNotifications, + refreshNotifications, + onDeleteRequest, + onViewNotification, + onViewAll +}) { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + useEffect(() => { + const load = async () => { + if (isOpen) { + setIsLoading(true); + await refreshNotifications(); + setIsLoading(false); + } + }; + load(); + }, [isOpen, refreshNotifications]); + + const handleMarkAsRead = async (id) => { + try { + await markNotificationAsRead(id); + setNotifications((prev) => + prev.map((notif) => + notif._id === id ? { ...notif, isRead: true, read: true } : notif + ) + ); + } catch (err) { + console.error("Failed to mark as read", err); + } + }; + + const handleViewDetails = (notif) => { + onViewNotification(notif); + if (!(notif.isRead || notif.read)) { + handleMarkAsRead(notif._id); + } + }; + + const getIcon = (type) => { + switch (type) { + case "success": return ; + case "warning": return ; + case "error": return ; + default: return ; + } + }; + + const formatTime = (dateStr) => { + const date = new Date(dateStr); + const now = new Date(); + const diffInHours = (now - date) / (1000 * 60 * 60); + + if (diffInHours < 24 && date.getDate() === now.getDate()) { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); + } + if (diffInHours < 48 && date.getDate() === now.getDate() - 1) { + return "Yesterday"; + } + return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); + }; + + if (!isOpen) return null; + + return ( + <> + + e.stopPropagation()} + initial={{ opacity: 0, y: 10, scale: 0.95 }} + animate={{ opacity: 1, y: 0, scale: 1 }} + exit={{ opacity: 0, y: 10, scale: 0.95 }} + transition={{ duration: 0.2, ease: "easeOut" }} + > +
+
+ +

Notifications

+
+ +
+ +
+ {isLoading ? ( +
+ +
+ ) : error ? ( +
+ +

{error}

+ +
+ ) : notifications.length === 0 ? ( +
+ +

Everything caught up!

+
+ ) : ( + + {notifications.slice(0,3).map((notif) => { + const isRead = notif.isRead || notif.read; + + return ( + handleViewDetails(notif)} + style={{ cursor: 'pointer' }} + > +
+
+ {getIcon(notif.type)} +

{notif.title}

+
+
+ + {formatTime(notif.createdAt || notif.date)} +
+
+

+ {notif.message} +

+
+ ); + })} +
+ )} +
+ + {notifications.length > 0 && ( +
+ +
+ )} +
+ + + ); +} diff --git a/guardian-admin-dashboard/src/components/dashboard/Topbar.jsx b/guardian-admin-dashboard/src/components/dashboard/Topbar.jsx index 4065d12ca..0f8a5bcdc 100644 --- a/guardian-admin-dashboard/src/components/dashboard/Topbar.jsx +++ b/guardian-admin-dashboard/src/components/dashboard/Topbar.jsx @@ -1,39 +1,83 @@ -import { Bell, Search, UserCircle2 } from "lucide-react"; -import { getAdminUser } from "../../utils/storage"; - -export default function Topbar() { - const admin = getAdminUser() || { - fullname: "Guardian Admin", - role: "admin", - }; - - return ( -
-
-
-

Administrator Workspace

-

Dashboard Overview

-
-
- -
-
- - -
- - - -
- -
- {admin.fullname || "Guardian Admin"} - {admin.role || "admin"} -
-
-
-
- ); -} \ No newline at end of file +import { useState, useEffect, useCallback } from "react"; +import { Bell, Search, UserCircle2 } from "lucide-react"; +import { getAdminUser } from "../../utils/storage"; +import NotificationPanel from "./NotificationPanel"; +import { + getNotifications, + deleteNotification +} from "../../services/notificationService"; + +export default function Topbar({ + notifications, + onRefreshNotifications, + onDeleteRequest, + onOpenDrawer, + setNotifications, + onViewNotification +}) { + const [isNotificationsOpen, setIsNotificationsOpen] = useState(false); + + const admin = getAdminUser() || { + fullname: "Guardian Admin", + role: "admin", + }; + + const unreadCount = notifications.filter(n => !(n.isRead || n.read)).length; + + return ( +
+
+
+

Administrator Workspace

+

Dashboard Overview

+
+
+ +
+
+ + +
+ +
+ + + setIsNotificationsOpen(false)} + notifications={notifications} + setNotifications={setNotifications} + refreshNotifications={onRefreshNotifications} + onDeleteRequest={onDeleteRequest} + onViewNotification={onViewNotification} + onViewAll={() => { + setIsNotificationsOpen(false); + onOpenDrawer(); + }} + /> +
+ +
+ +
+ {admin.fullname || "Guardian Admin"} + {admin.role || "admin"} +
+
+
+
+ ); +} + \ No newline at end of file diff --git a/guardian-admin-dashboard/src/index.css b/guardian-admin-dashboard/src/index.css index ecb05fdf5..4080aa131 100644 --- a/guardian-admin-dashboard/src/index.css +++ b/guardian-admin-dashboard/src/index.css @@ -113,6 +113,21 @@ button:focus-visible { --radius-md: 18px; } +.text-primary { + color: var(--primary); +} + +.text-success { + color: var(--success); +} + +.text-warning { + color: var(--warning); +} + +.text-danger { + color: var(--danger); +} body.dark-theme { --primary: #6cb8df; --primary-dark: #d7ebfb; @@ -1342,4 +1357,852 @@ a { height: 44px; font-size: 1rem; } +} + + +/* Notifications Dropdown */ +.notification-dropdown-overlay { + position: fixed; + inset: 0; + z-index: 99; +} + +.notification-dropdown { + position: absolute; + top: calc(100% + 14px); + right: 0; + width: 380px; + background: rgba(255, 255, 255, 0.94); + backdrop-filter: blur(12px) saturate(160%); + border: 1px solid rgba(255, 255, 255, 0.6); + border-radius: var(--radius-xl); + box-shadow: + 0 10px 25px -5px rgba(20, 61, 116, 0.1), + 0 8px 10px -6px rgba(20, 61, 116, 0.1), + 0 0 0 1px rgba(20, 61, 116, 0.05); + z-index: 100; + display: flex; + flex-direction: column; + max-height: 520px; + overflow: hidden; +} + +.notification-dropdown-header { + padding: 18px 20px; + border-bottom: 1px solid rgba(216, 229, 238, 0.6); + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(248, 252, 255, 0.5); +} + +.notification-dropdown-header h3 { + margin: 0; + font-size: 1.05rem; + font-weight: 800; + color: var(--primary-dark); + letter-spacing: -0.01em; +} + +.notification-dropdown-content { + overflow-y: auto; + padding: 12px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.notification-item { + padding: 12px 14px; + border-radius: 18px; + background: var(--white); + border: 1px solid rgba(216, 229, 238, 0.5); + position: relative; + min-height: fit-content; + transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + box-shadow: 0 2px 4px rgba(20, 61, 116, 0.02); +} + +.notification-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(20, 61, 116, 0.06); + border-color: var(--border); +} + +.notification-item.unread { + background: linear-gradient(145deg, #ffffff, #fcfdfe); + border-color: rgba(79, 160, 200, 0.2); +} + +.notification-item::before { + content: ''; + position: absolute; + left: 0; + top: 14px; + bottom: 14px; + width: 4px; + border-radius: 0 4px 4px 0; + background: var(--border); + transition: background 0.2s ease; +} + +.notification-item.unread::before { + background: var(--primary); +} + +.notification-item.type-success.unread::before { + background: var(--success); +} + +.notification-item.type-warning.unread::before { + background: var(--warning); +} + +.notification-item.type-error.unread::before { + background: var(--danger); +} + +.notification-item-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 6px; +} + +.notification-item-title-wrap { + display: flex; + align-items: center; + gap: 8px; +} + +.notification-item-title { + margin: 0; + font-size: 0.92rem; + font-weight: 700; + color: var(--primary-dark); +} + +.notification-item.unread .notification-item-title { + color: var(--primary); +} + +.notification-item-date { + font-size: 0.7rem; + color: var(--text-muted); + display: flex; + align-items: center; + font-weight: 500; +} + +.notification-item-message { + margin: 0 0 10px; + font-size: 0.85rem; + color: var(--text-muted); + line-height: 1.5; + padding-left: 24px; +} + +.notification-item.unread .notification-item-message { + color: var(--text); +} + +.notification-item-actions { + display: flex; + gap: 8px; + justify-content: flex-end; + opacity: 0.6; + transition: opacity 0.2s ease; +} + +.notification-item:hover .notification-item-actions { + opacity: 1; +} + +.notification-action-btn { + background: transparent; + border: none; + font-size: 0.75rem; + font-weight: 700; + cursor: pointer; + padding: 5px 10px; + border-radius: 8px; + transition: all 0.2s ease; + display: flex; + align-items: center; +} + +.notification-badge { + position: absolute; + top: -4px; + right: -4px; + background: var(--danger); + color: white; + font-size: 10px; + font-weight: 800; + min-width: 18px; + height: 18px; + padding: 0 4px; + border-radius: 9px; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid var(--white); + box-shadow: 0 2px 4px rgba(228, 98, 111, 0.3); + animation: badgePop 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275); +} + +@keyframes badgePop { + 0% { + transform: scale(0); + } + + 100% { + transform: scale(1); + } +} +/* Global Modal Overlay */ +.modal-overlay { + position: fixed; + inset: 0; + background: rgba(20, 61, 116, 0.4); + z-index: 3000; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.modal-content { + background: var(--white); + border-radius: 28px; + padding: 32px; + width: 100%; + max-width: 440px; + box-shadow: var(--shadow-lg); + border: 1px solid rgba(255, 255, 255, 0.6); +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.modal-title-wrap { + display: flex; + align-items: center; + gap: 12px; +} + +.modal-header h2 { + margin: 0; + color: var(--primary-dark); + font-size: 1.4rem; + font-weight: 800; + letter-spacing: -0.02em; +} + +.modal-body p { + margin: 0; + color: var(--text-muted); + font-size: 1rem; + line-height: 1.6; +} + +.modal-footer { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 32px; +} + +/* Modal Types */ +.modal-primary .ui-button:last-child { + background: var(--primary); + box-shadow: 0 4px 12px rgba(79, 160, 200, 0.25); + color: white; +} + +.modal-success .ui-button:last-child { + background: var(--success); + box-shadow: 0 4px 12px rgba(40, 167, 69, 0.25); + color: white; +} + +.modal-warning .ui-button:last-child { + background: var(--warning); + box-shadow: 0 4px 12px rgba(255, 193, 7, 0.25); + color: white; +} + +.ui-button.danger-btn { + background: var(--danger); + box-shadow: 0 4px 12px rgba(228, 98, 111, 0.25); + color: white; +} + +.ui-button.danger-btn:hover { + background: #d65561; + transform: translateY(-1px); +} + +.ui-button.secondary { + background: var(--background); + color: var(--text); + border: 1px solid var(--border); + box-shadow: none; +} + +.ui-button.secondary:hover { + background: #edf2f7; +} + +/* ========================================================================== + NOTIFICATION SYSTEM + ========================================================================== */ + +/* Dropdown specific */ +.notification-dropdown-overlay { + position: fixed; + inset: 0; + z-index: 400; + background: transparent; +} + +.notification-dropdown { + position: absolute; + top: calc(100% + 15px); + right: 0; + width: 380px; + max-height: 500px; + background: rgba(255, 255, 255, 0.95); + backdrop-filter: blur(15px); + border-radius: 24px; + box-shadow: 0 15px 40px rgba(20, 61, 116, 0.15); + border: 1px solid rgba(255, 255, 255, 0.6); + z-index: 500; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.notification-dropdown-header { + padding: 16px 20px; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(255, 255, 255, 0.5); +} + +.notification-dropdown-content { + flex: 1; + overflow-y: auto; + padding: 12px; + display: flex; + flex-direction: column; +} + +.notification-dropdown-footer { + padding: 12px; + border-top: 1px solid var(--border); + display: flex; + justify-content: center; + background: #f8fcff; +} + +.view-all-btn { + width: 100%; + padding: 10px; + border: none; + background: transparent; + color: var(--primary); + font-weight: 700; + font-size: 0.85rem; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + border-radius: 12px; + transition: all 0.2s ease; +} + +.view-all-btn:hover { + background: rgba(79, 160, 200, 0.1); + gap: 12px; +} + +/* Notification Items */ +.notification-item { + padding: 16px; + border-radius: 16px; + margin-bottom: 10px; + background: var(--white); + border: 1px solid var(--border); + transition: all 0.2s ease; + position: relative; + overflow: hidden; +} + +.notification-item.unread { + background: #f8fcff; + border-color: rgba(79, 160, 200, 0.15); +} + +.notification-item.compact { + padding: 12px 14px; +} + +.notification-item::before { + content: ''; + position: absolute; + left: 0; + top: 12px; + bottom: 12px; + width: 3px; + border-radius: 0 4px 4px 0; + background: transparent; + transition: all 0.2s ease; +} + +.notification-item.unread::before { + background: var(--primary); +} + +.notification-item.type-success.unread::before { + background: var(--success); +} + +.notification-item.type-warning.unread::before { + background: var(--warning); +} + +.notification-item.type-error.unread::before { + background: var(--danger); +} + +.notification-item-header { + display: flex; + justify-content: space-between; + margin-bottom: 4px; +} + +.notification-item-title-wrap { + display: flex; + align-items: center; + gap: 8px; +} + +.notification-item-title { + margin: 0; + font-size: 0.9rem; + font-weight: 700; + color: var(--primary-dark); +} + +.notification-item-date { + font-size: 0.7rem; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 4px; +} + +.notification-item-message { + margin: 0 0 8px; + font-size: 0.82rem; + color: var(--text-muted); + line-height: 1.4; + padding-left: 24px; + /* Align with icon */ +} + +.notification-item.unread .notification-item-message { + color: var(--text); +} + +.notification-item-message.truncate { + display: -webkit-box; + -webkit-line-clamp: 1; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; +} + +.notification-item-actions { + display: flex; + gap: 8px; + justify-content: flex-end; + margin-top: 4px; +} + +.notification-action-btn { + background: transparent; + border: none; + font-size: 0.75rem; + font-weight: 700; + cursor: pointer; + padding: 5px 10px; + border-radius: 8px; + transition: all 0.2s ease; + display: flex; + align-items: center; +} + +.notification-action-btn.read-btn { + color: var(--primary); + background: rgba(79, 160, 200, 0.08); +} + +.notification-action-btn.delete-btn { + color: var(--text-muted); +} + +.notification-action-btn:hover { + background: rgba(0, 0, 0, 0.05); +} + +/* Side Drawer (View All) */ +.drawer-overlay { + position: fixed; + inset: 0; + background: rgba(20, 61, 116, 0.4); + z-index: 1000; +} + +.notification-drawer { + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: 100%; + max-width: 480px; + background: var(--white); + z-index: 1100; + display: flex; + flex-direction: column; + box-shadow: -15px 0 45px rgba(20, 61, 116, 0.12); +} + +.drawer-header { + padding: 24px 32px; + border-bottom: 1px solid var(--border); + display: flex; + justify-content: space-between; + align-items: center; +} + +.drawer-title-wrap { + display: flex; + align-items: center; + gap: 12px; +} + +.drawer-header h2 { + margin: 0; + font-size: 1.4rem; + font-weight: 800; + color: var(--primary-dark); +} + +.drawer-filters { + padding: 20px 32px; + background: #f8fcff; + border-bottom: 1px solid var(--border); + display: flex; + flex-direction: column; + gap: 16px; +} + +.search-bar-mini { + display: flex; + align-items: center; + gap: 12px; + background: var(--white); + border: 1px solid var(--border); + border-radius: 14px; + padding: 10px 16px; + box-shadow: 0 2px 4px rgba(20, 61, 116, 0.02); +} + +.search-bar-mini input { + border: none; + background: transparent; + outline: none; + font-size: 0.95rem; + width: 100%; + color: var(--text); +} + +.filter-tabs { + display: flex; + gap: 10px; +} + +.filter-tab { + padding: 8px 20px; + border-radius: 20px; + font-size: 0.88rem; + font-weight: 700; + cursor: pointer; + border: 1px solid var(--border); + background: var(--white); + color: var(--text-muted); + transition: all 0.2s ease; +} + +.filter-tab.active { + background: var(--primary); + color: var(--white); + border-color: var(--primary); + box-shadow: 0 4px 12px rgba(79, 160, 200, 0.25); +} + +.drawer-actions-bar { + padding: 14px 32px; + display: flex; + justify-content: space-between; + align-items: center; + font-size: 0.9rem; + background: var(--white); + border-bottom: 1px solid var(--border); +} + +.count-label { + color: var(--text-muted); + font-weight: 600; +} + +.text-button { + background: transparent; + border: none; + color: var(--primary); + font-weight: 700; + display: flex; + align-items: center; + gap: 6px; + cursor: pointer; + padding: 4px 8px; + border-radius: 8px; +} + +.text-button:hover { + background: rgba(79, 160, 200, 0.08); +} + +.drawer-content { + flex: 1; + overflow-y: auto; + padding: 24px 32px; + display: flex; + flex-direction: column; + gap: 20px; + margin-right: 10px; +} + +.drawer-item { + padding: 24px; + border-radius: 22px; + background: var(--white); + border: 1px solid var(--border); + position: relative; + transition: all 0.25s ease; + box-shadow: 0 4px 12px rgba(20, 61, 116, 0.03); +} + +.drawer-item:hover { + transform: translateY(-2px); + box-shadow: 0 8px 20px rgba(20, 61, 116, 0.06); +} + +.drawer-item.unread { + background: #f8fcff; + border-color: rgba(79, 160, 200, 0.2); +} + +.drawer-item::before { + content: ''; + position: absolute; + left: 0; + top: 24px; + bottom: 24px; + width: 4px; + border-radius: 0 4px 4px 0; + background: var(--border); +} + +.drawer-item.unread::before { + background: var(--primary); +} + +.drawer-item.type-success.unread::before { + background: var(--success); +} + +.drawer-item.type-warning.unread::before { + background: var(--warning); +} + +.drawer-item.type-error.unread::before { + background: var(--danger); +} + +.drawer-item-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 12px; +} + +.drawer-item-title { + margin: 0; + font-size: 1.1rem; + font-weight: 800; + color: var(--primary-dark); + line-height: 1.4; +} + +.drawer-item-date { + font-size: 0.8rem; + color: var(--text-muted); + white-space: nowrap; +} + +.drawer-item-message { + margin: 0 0 20px; + font-size: 0.98rem; + line-height: 1.6; + color: var(--text); +} + +.drawer-item-footer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.type-tag { + text-transform: uppercase; + font-size: 0.7rem; + font-weight: 800; + letter-spacing: 0.06em; + color: var(--text-muted); + background: var(--background); + padding: 5px 12px; + border-radius: 12px; +} + +.type-tag.color-success { + color: var(--success); + background: rgba(23, 166, 115, 0.1); +} + +.type-tag.color-warning { + color: var(--warning); + background: rgba(232, 163, 23, 0.1); +} + +.type-tag.color-error { + color: var(--danger); + background: rgba(228, 98, 111, 0.1); +} + +.type-tag.color-info { + color: var(--primary); + background: rgba(79, 160, 200, 0.1); +} + +.drawer-item-actions { + display: flex; + gap: 10px; +} + +.action-icon-btn { + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 12px; + border: 1px solid var(--border); + background: var(--white); + color: var(--text-muted); + cursor: pointer; + transition: all 0.2s ease; +} + +.action-icon-btn:hover { + background: var(--primary); + color: var(--white); + border-color: var(--primary); +} + +.action-icon-btn.delete:hover { + background: var(--danger); + border-color: var(--danger); +} + +.drawer-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 120px 20px; + text-align: center; + color: var(--text-muted); +} + +/* Detail Modal specific */ +.notification-detail-modal { + max-width: 500px !important; +} + +.detail-message { + font-size: 1rem; + line-height: 1.6; + color: var(--text); + background: var(--background); + padding: 20px; + border-radius: 16px; + border: 1px solid var(--border); + white-space: pre-wrap; +} + +.detail-meta { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; +} + +.detail-type-badge { + text-transform: uppercase; + font-size: 0.7rem; + font-weight: 800; + padding: 4px 10px; + border-radius: 20px; + background: rgba(0, 0, 0, 0.05); +} + +.button-loader { + width: 18px; + height: 18px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-radius: 50%; + border-top-color: #fff; + animation: spin 0.8s linear infinite; + margin: 0 auto; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.ui-button:disabled { + opacity: 0.7; + cursor: not-allowed; + transform: none !important; } \ No newline at end of file diff --git a/guardian-admin-dashboard/src/layout/AdminLayout.jsx b/guardian-admin-dashboard/src/layout/AdminLayout.jsx index 31f324ef2..3aa294bc1 100644 --- a/guardian-admin-dashboard/src/layout/AdminLayout.jsx +++ b/guardian-admin-dashboard/src/layout/AdminLayout.jsx @@ -1,14 +1,54 @@ -import { useEffect, useState } from "react"; +import { useEffect, useState, useCallback } from "react"; import { Outlet } from "react-router-dom"; import Sidebar from "../components/dashboard/Sidebar"; import Topbar from "../components/dashboard/Topbar"; +import ConfirmationModal from "../components/common/ConfirmationModal"; +import NotificationDrawer from "../components/dashboard/NotificationDrawer"; +import Modal from "../components/common/Modal"; +import { Trash2, Clock } from "lucide-react"; +import { + getNotifications, + deleteNotification +} from "../services/notificationService"; export default function AdminLayout() { const [sidebarCollapsed, setSidebarCollapsed] = useState(false); const [isMobile, setIsMobile] = useState(window.innerWidth < 1100); const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false); + // Notifications State + const [notifications, setNotifications] = useState([]); + const [isDrawerOpen, setIsDrawerOpen] = useState(false); + const [detailModal, setDetailModal] = useState({ + isOpen: false, + notification: null, + title: "Notification Detail", + description: "", + confirmText: "Delete Notification", + cancelText: "Close" + }); + + // Global Confirmation Modal State + const [confirmModal, setConfirmModal] = useState({ + isOpen: false, + title: "", + message: "", + confirmText: "", + onConfirm: () => {}, + type: "danger" + }); + + const fetchNotifications = useCallback(async () => { + try { + const data = await getNotifications(); + setNotifications(Array.isArray(data) ? data : data?.notifications || []); + } catch (err) { + console.error("Failed to fetch notifications", err); + } + }, []); + useEffect(() => { + fetchNotifications(); const handleResize = () => { const mobile = window.innerWidth < 1100; setIsMobile(mobile); @@ -22,7 +62,7 @@ export default function AdminLayout() { handleResize(); return () => window.removeEventListener("resize", handleResize); - }, []); + }, [fetchNotifications]); const handleToggleSidebar = () => { if (isMobile) { @@ -32,6 +72,48 @@ export default function AdminLayout() { } }; + const showConfirm = (options) => { + setConfirmModal({ + ...options, + isOpen: true + }); + }; + + const hideConfirm = () => { + setConfirmModal(prev => ({ ...prev, isOpen: false })); + }; + + const handleDeleteRequest = (id) => { + showConfirm({ + title: "Delete Notification", + message: "Are you sure you want to remove this alert? This action cannot be reversed.", + confirmText: "Delete Alert", + onConfirm: async () => { + try { + await deleteNotification(id); + setNotifications(prev => prev.filter(n => n._id !== id)); + } catch (err) { + console.error("Failed to delete notification", err); + } + } + }); + }; + + const handleViewNotification = (notif, options = {}) => { + setDetailModal({ + isOpen: true, + notification: notif, + title: options.title || "Notification Detail", + description: options.description || "", + confirmText: options.confirmText || "Delete Notification", + cancelText: options.cancelText || "Close" + }); + }; + + const closeDetailModal = () => { + setDetailModal(prev => ({ ...prev, isOpen: false })); + }; + return (
- + setIsDrawerOpen(true)} + onViewNotification={handleViewNotification} + />
- +
+ + setIsDrawerOpen(false)} + notifications={notifications} + setNotifications={setNotifications} + onDeleteRequest={handleDeleteRequest} + onViewNotification={handleViewNotification} + /> + + + + + + } + > + {detailModal.notification && ( + <> +
+ + {detailModal.notification.type || 'information'} + +
+ + {new Date(detailModal.notification.createdAt || detailModal.notification.date).toLocaleString([], { + dateStyle: 'medium', + timeStyle: 'short' + })} +
+
+ + {detailModal.description && ( +

+ {detailModal.description} +

+ )} + +
+ {detailModal.notification.message} +
+ + )} +
+ + { + confirmModal.onConfirm(); + hideConfirm(); + }} + title={confirmModal.title} + message={confirmModal.message} + confirmText={confirmModal.confirmText} + type={confirmModal.type} + /> ); } \ No newline at end of file diff --git a/guardian-admin-dashboard/src/services/notificationService.js b/guardian-admin-dashboard/src/services/notificationService.js new file mode 100644 index 000000000..f032e8fc8 --- /dev/null +++ b/guardian-admin-dashboard/src/services/notificationService.js @@ -0,0 +1,17 @@ +import api from "./api"; + + +export async function getNotifications() { + const response = await api.get("/notifications"); + return response.data; +} + +export async function markNotificationAsRead(id) { + const response = await api.patch(`/notifications/${id}/read`); + return response.data; +} + +export async function deleteNotification(id) { + const response = await api.delete(`/notifications/${id}`); + return response.data; +}