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 (
+
+ );
+}
+
\ 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;
+}