diff --git a/client/package-lock.json b/client/package-lock.json index 9cee02a..5158c3e 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,11 +8,15 @@ "name": "client", "version": "0.0.0", "dependencies": { + "@hookform/resolvers": "^5.4.0", "@tailwindcss/vite": "^4.3.0", "@tanstack/react-query": "^5.100.11", "axios": "^1.16.1", "clsx": "^2.1.1", "framer-motion": "^12.39.0", + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.8", + "lucide-react": "^1.21.0", "react": "^19.2.6", "react-dom": "^19.2.6", "react-hook-form": "^7.76.0", @@ -298,7 +302,6 @@ "version": "7.29.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -682,6 +685,18 @@ } } }, + "node_modules/@hookform/resolvers": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.4.0.tgz", + "integrity": "sha512-EIsqr/t/qbinPIhGjMdtvutIN1Kk4uwbROE9/UQ93CAVGR7GkA7Y92+fX80OzXi/OB67jVFYwKGO1WzkxmkFZw==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -1093,6 +1108,12 @@ "dev": true, "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/@tailwindcss/node": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", @@ -1531,6 +1552,19 @@ "undici-types": "~7.16.0" } }, + "node_modules/@types/pako": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", + "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", + "license": "MIT" + }, + "node_modules/@types/raf": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", + "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/react": { "version": "19.2.15", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", @@ -1551,6 +1585,13 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.59.4", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.4.tgz", @@ -2058,6 +2099,16 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/baseline-browser-mapping": { "version": "2.10.31", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz", @@ -2162,6 +2213,26 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvg": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz", + "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@babel/runtime": "^7.12.5", + "@types/raf": "^3.4.0", + "core-js": "^3.8.3", + "raf": "^3.4.1", + "regenerator-runtime": "^0.13.7", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "svg-pathdata": "^6.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -2213,6 +2284,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2228,6 +2311,16 @@ "node": ">= 8" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-tree": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", @@ -2337,6 +2430,16 @@ "license": "MIT", "peer": true }, + "node_modules/dompurify": { + "version": "3.4.11", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz", + "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optional": true, + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -2698,6 +2801,17 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-png": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/fast-png/-/fast-png-6.4.0.tgz", + "integrity": "sha512-kAqZq1TlgBjZcLr5mcN6NP5Rv4V2f22z00c3g8vRrwkcqjerx7BEhPbOnWCPqaHUl2XWQBJQvOT/FQhdMT7X/Q==", + "license": "MIT", + "dependencies": { + "@types/pako": "^2.0.3", + "iobuffer": "^5.3.2", + "pako": "^2.1.0" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2715,6 +2829,12 @@ } } }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3012,6 +3132,20 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "optional": true, + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/https-proxy-agent": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", @@ -3055,6 +3189,12 @@ "node": ">=8" } }, + "node_modules/iobuffer": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/iobuffer/-/iobuffer-5.4.0.tgz", + "integrity": "sha512-DRebOWuqDvxunfkNJAlc3IzWIPD5xVxwUNbHr7xKB8E6aLJxIPfNX3CoMJghcFjpv6RWQsrcJbghtEwSPoJqMA==", + "license": "MIT" + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3206,6 +3346,32 @@ "node": ">=6" } }, + "node_modules/jspdf": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-4.2.1.tgz", + "integrity": "sha512-YyAXyvnmjTbR4bHQRLzex3CuINCDlQnBqoSYyjJwTP2x9jDLuKDzy7aKUl0hgx3uhcl7xzg32agn5vlie6HIlQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.6", + "fast-png": "^6.2.0", + "fflate": "^0.8.1" + }, + "optionalDependencies": { + "canvg": "^3.0.11", + "core-js": "^3.6.0", + "dompurify": "^3.3.1", + "html2canvas": "^1.0.0-rc.5" + } + }, + "node_modules/jspdf-autotable": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/jspdf-autotable/-/jspdf-autotable-5.0.8.tgz", + "integrity": "sha512-Hy05N86yBO7CXBrnSLOge7i1ZYpKH2DjQ94iybaP7vBhSInjvRBgDc99ngKzSbSO8Jc98ZCally8I6n0tj2RJQ==", + "license": "MIT", + "peerDependencies": { + "jspdf": "^2 || ^3 || ^4" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3517,6 +3683,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.21.0.tgz", + "integrity": "sha512-reEZMXq8Qdd5jg5XYkQ5TR1fB/GiQ7ih4vcrthYDtgjSDwh0i6/YLiGjsWsIwgN49gpAnd4J2elSNzncMEEUUQ==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -3714,6 +3889,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==", + "license": "(MIT AND Zlib)" + }, "node_modules/parse5": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", @@ -3754,6 +3935,13 @@ "dev": true, "license": "MIT" }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "license": "MIT", + "optional": true + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -3908,6 +4096,16 @@ "node": ">=6" } }, + "node_modules/raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "license": "MIT", + "optional": true, + "dependencies": { + "performance-now": "^2.1.0" + } + }, "node_modules/react": { "version": "19.2.6", "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", @@ -4005,6 +4203,13 @@ "node": ">=8" } }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "license": "MIT", + "optional": true + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -4015,6 +4220,16 @@ "node": ">=0.10.0" } }, + "node_modules/rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==", + "license": "MIT OR SEE LICENSE IN FEEL-FREE.md", + "optional": true, + "engines": { + "node": ">= 0.8.15" + } + }, "node_modules/rolldown": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", @@ -4139,6 +4354,16 @@ "dev": true, "license": "MIT" }, + "node_modules/stackblur-canvas": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz", + "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.14" + } + }, "node_modules/std-env": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", @@ -4159,6 +4384,16 @@ "node": ">=8" } }, + "node_modules/svg-pathdata": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz", + "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -4185,6 +4420,16 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "optional": true, + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -4402,6 +4647,16 @@ "punycode": "^2.1.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "optional": true, + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/vite": { "version": "8.0.13", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", diff --git a/client/package.json b/client/package.json index d7bb636..9bb8943 100644 --- a/client/package.json +++ b/client/package.json @@ -10,11 +10,15 @@ "preview": "vite preview" }, "dependencies": { + "@hookform/resolvers": "^5.4.0", "@tailwindcss/vite": "^4.3.0", "@tanstack/react-query": "^5.100.11", "axios": "^1.16.1", "clsx": "^2.1.1", "framer-motion": "^12.39.0", + "jspdf": "^4.2.1", + "jspdf-autotable": "^5.0.8", + "lucide-react": "^1.21.0", "react": "^19.2.6", "react-dom": "^19.2.6", "react-hook-form": "^7.76.0", diff --git a/client/public/favicon.svg b/client/public/favicon.svg deleted file mode 100644 index 6893eb1..0000000 --- a/client/public/favicon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/public/icons.svg b/client/public/icons.svg deleted file mode 100644 index e952219..0000000 --- a/client/public/icons.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/features/admin/components/AdminLayout.tsx b/client/src/features/admin/components/AdminLayout.tsx index 60f984e..43699f1 100644 --- a/client/src/features/admin/components/AdminLayout.tsx +++ b/client/src/features/admin/components/AdminLayout.tsx @@ -1,7 +1,7 @@ -import React, { useState, useEffect, useRef } from "react"; +import React, { useState, useEffect, useRef, useCallback } from "react"; import { Outlet, NavLink, useNavigate, useLocation } from "react-router-dom"; import { useAuthStore } from "../../../store/authStore"; -import { motion } from "framer-motion"; +import { motion, AnimatePresence } from "framer-motion"; // Lucide Icons matching your clean, balanced stroke layouts import { @@ -11,6 +11,7 @@ import { LogOut, Library, User, + ChevronUp } from "lucide-react"; export const AdminLayout: React.FC = () => { @@ -18,16 +19,50 @@ export const AdminLayout: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); - // State engine managing navigation sidebar width expansion configuration matching the reference dashboard + // State engine managing navigation sidebar width expansion configuration const [sidebarExpanded, setSidebarExpanded] = useState(false); + + // Track if the scroll container is at the absolute top position (scrollTop === 0) + const [isAtAbsoluteTop, setIsAtAbsoluteTop] = useState(true); + const mainScrollContainerRef = useRef(null); - // Handle immediate viewport layout reset on route change - useEffect(() => { + // Pure scroll engine tracking absolute position offsets across all pages + const handleContainerScroll = useCallback(() => { + if (!mainScrollContainerRef.current) return; + const currentScrollTop = mainScrollContainerRef.current.scrollTop; + + // Header vanishes completely if you scroll away from the top + setIsAtAbsoluteTop(currentScrollTop === 0); + }, []); + + // Action dispatcher that smoothly resets viewport back to coordinate 0 + const scrollToTop = () => { if (mainScrollContainerRef.current) { - mainScrollContainerRef.current.scrollTop = 0; + mainScrollContainerRef.current.scrollTo({ + top: 0, + behavior: "smooth" + }); } - }, [location.pathname]); + }; + + // Handle immediate viewport layout reset on route change and bind listener + useEffect(() => { + const container = mainScrollContainerRef.current; + + if (container) { + container.scrollTop = 0; + setIsAtAbsoluteTop(true); + setSidebarExpanded(false); // Reset expansion footprint upon switching subviews + container.addEventListener("scroll", handleContainerScroll); + } + + return () => { + if (container) { + container.removeEventListener("scroll", handleContainerScroll); + } + }; + }, [location.pathname, handleContainerScroll]); const handleSignOut = () => { logout(); @@ -53,57 +88,42 @@ export const AdminLayout: React.FC = () => { ]; return ( -
+
- {/* Institutional Top Application Header Bar - Always visible, persistent foundation layout */} -
- - {/* Core Navigation Brand Click Area */} -
setSidebarExpanded(!sidebarExpanded)} - className="flex items-center gap-4 cursor-pointer group select-none" - title={sidebarExpanded ? "Collapse Navigation Menu" : "Expand Navigation Menu"} - > -
- -
- -
- - LMS - - - Admin Portal - -
-
- - {/* User Identity Matrix */} -
-
-

- ROLE: {user?.role || "ADMIN"} -

-
- -
- -
-
-
- {/* Main Structural Application Framework Split */} -
+
- {/* Persistent Left Icon/Expanded Navigation Sidebar Tracking Shell */} + {/* Persistent left sidebar — Always visible at 80px, expands to 288px on hover */} setSidebarExpanded(true)} + onMouseLeave={() => setSidebarExpanded(false)} + className="h-full bg-[#4b6993] border-r border-white/10 shadow-2xl flex flex-col justify-between text-white shrink-0 z-40 overflow-hidden" > -
+
+ + {/* Upper Branding Header Block */} +
+
+ +
+ {sidebarExpanded && ( + + Menu + + )} +
+ {/* Navigation Routing Links */} -
{/* Action Area Sidebar Footer */} -
+
); @@ -162,8 +182,19 @@ export const ManageLibrarians: React.FC = () => { {totalPages > 0 && (
- Page {currentPage} of {totalPages} - | Total {totalCount} Librarians + Page{" "} + + {currentPage} + {" "} + of{" "} + + {totalPages} + + | Total{" "} + + {totalCount} + {" "} + Librarians
); -}; \ No newline at end of file +}; diff --git a/client/src/features/admin/components/ManageUsers.tsx b/client/src/features/admin/components/ManageUsers.tsx index 3b72abd..597a10f 100644 --- a/client/src/features/admin/components/ManageUsers.tsx +++ b/client/src/features/admin/components/ManageUsers.tsx @@ -2,13 +2,7 @@ import React, { useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { axiosClient } from "../../../api/axiosClient"; import { useAuthStore } from "../../../store/authStore"; -import { - Plus, - Search, - Users, - X, - RotateCcw, -} from "lucide-react"; +import { Plus, Search, Users, X, RotateCcw } from "lucide-react"; import { UserModal } from "./UserModal"; import { UserDetailsModal } from "./UserDetailsModal"; @@ -94,10 +88,9 @@ export const ManageUsers: React.FC = () => { setCurrentPage(1); }; - return ( + return ( /* Standardized corporate canvas padding with exact alignment configurations */
- {/* ==================== ZONES A & B: HEADER & TRACKER ==================== */}
@@ -108,7 +101,8 @@ export const ManageUsers: React.FC = () => { Users Database Management

- Click any system row ledger to view access settings, check full operational logs, or customize user system parameters. + Click any system row ledger to view access settings, check full + operational logs, or customize user system parameters.

@@ -148,9 +142,12 @@ export const ManageUsers: React.FC = () => { className="bg-transparent border-0 outline-none w-full text-xs font-medium text-[#1A365D] placeholder-[#A0AEC0] p-0 focus:ring-0 focus:outline-none" /> {searchQuery && ( -
+ )} + + )} +
+
+ +
{/* Content grid container close */} +
{/* Modal body inner layout container close */} +
{/* Fixed layout black backdrop overlay close */} + + setIsDeleteOpen(false)} + onConfirm={handleConfirmDeletion} + bookTitle={displayTitle} + /> + + ); +}; \ No newline at end of file diff --git a/client/src/features/books/components/BookModal.tsx b/client/src/features/books/components/BookModal.tsx index ff273f3..a54ab0d 100644 --- a/client/src/features/books/components/BookModal.tsx +++ b/client/src/features/books/components/BookModal.tsx @@ -2,12 +2,12 @@ import { useEffect, useState } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { BookFormSchema, type BookFormValues } from "../schemas/bookSchema"; -import type { BookCategory, BookInventoryItem } from "../../../types/books"; +import type { BookCategory, EditingBookInventoryItem } from "../../../types/books"; import { axiosClient } from "../../../api/axiosClient"; import { toast } from "sonner"; // Editorial Visual Assets -import { Sparkles, BookOpen, Layers } from "lucide-react"; +import { Sparkles, BookOpen, Layers, Hash } from "lucide-react"; interface BookAiInsights { category: string; @@ -19,7 +19,7 @@ interface BookModalProps { onClose: () => void; onSubmit: (data: BookFormValues) => void; categories: BookCategory[]; - editingBook?: BookInventoryItem | null; + editingBook?: EditingBookInventoryItem | null; } export const BookModal = ({ @@ -31,7 +31,6 @@ export const BookModal = ({ }: BookModalProps) => { const [isScanning, setIsScanning] = useState(false); const [aiInsights, setAiInsights] = useState(null); - const [scanCounter, setScanCounter] = useState(() => { const saved = localStorage.getItem("dev_scan_counter"); return saved ? parseInt(saved, 10) : 0; @@ -51,6 +50,7 @@ export const BookModal = ({ language: "", totalCopies: 1, categoryId: "", + isbn: "", // 🚀 Initialize default state empty string }, }); @@ -58,21 +58,36 @@ export const BookModal = ({ if (isOpen) { if (editingBook) { reset({ - title: editingBook.title, - author: editingBook.author, + title: editingBook.book_name, + author: editingBook.book_author, language: editingBook.language || "", - totalCopies: editingBook.totalCopies, - categoryId: editingBook.categoryId, + totalCopies: editingBook.total_copies, + categoryId: editingBook.category?.category_id, + isbn: editingBook.isbn || "", // 🚀 Hydrate value on edit click action trigger }); } else { - reset({ title: "", author: "", language: "", totalCopies: 1, categoryId: "" }); + reset({ + title: "", + author: "", + language: "", + totalCopies: 1, + categoryId: "", + isbn: "", // 🚀 Reset on creation profile layouts context + }); } } }, [editingBook, isOpen, reset]); const handleCloseModal = () => { setAiInsights(null); - reset({ title: "", author: "", language: "", totalCopies: 1, categoryId: "" }); + reset({ + title: "", + author: "", + language: "", + totalCopies: 1, + categoryId: "", + isbn: "", + }); onClose(); }; @@ -91,9 +106,7 @@ export const BookModal = ({ try { setIsScanning(true); setAiInsights(null); - toast.loading("Analyzing book layout structures...", { - id: "azure-scan", - }); + toast.loading("Analyzing book layout structures...", { id: "azure-scan" }); const response = await axiosClient.post("/ai/scan-cover", formData, { headers: { "Content-Type": "multipart/form-data" }, @@ -113,10 +126,12 @@ export const BookModal = ({ setValue("title", payload.title || ""); setValue("author", payload.author || ""); setValue("language", payload.language || ""); - + // 🚀 Populate ISBN value if the AI manages to detect it from barcode scans + if (payload.isbn) setValue("isbn", payload.isbn || ""); + if (payload.category) { const matchedCat = categories.find( - (c) => c.name.toLowerCase() === payload.category.toLowerCase() + (c) => c.name.toLowerCase() === payload.category.toLowerCase(), ); if (matchedCat) { setValue("categoryId", matchedCat.id); @@ -127,18 +142,17 @@ export const BookModal = ({ setScanCounter(nextCount); localStorage.setItem("dev_scan_counter", nextCount.toString()); - toast.success("Cover parsed with classification filters!", { - id: "azure-scan", - }); + toast.success("Cover parsed with classification filters!", { id: "azure-scan" }); } else { toast.error("Parsing threshold matching failed.", { id: "azure-scan" }); } } catch (error) { console.error(error); const isAxiosError = error && typeof error === "object" && "code" in error; - const errorMsg = isAxiosError && (error as { code: string }).code === "ECONNABORTED" - ? "Request timeout! AI process layer took too long." - : "Error communicating with AI parser layer."; + const errorMsg = + isAxiosError && (error as { code: string }).code === "ECONNABORTED" + ? "Request timeout! AI process layer took too long." + : "Error communicating with AI parser layer."; toast.error(errorMsg, { id: "azure-scan" }); } finally { @@ -147,14 +161,10 @@ export const BookModal = ({ } }; - // UI Text Formatter for Metadata Strings const renderFormattedOverview = (text: string) => { if (!text) return null; - // Splits content cleanly by finding keywords or explicit newlines - const lines = text.split(/\n+/).map(l => l.trim()).filter(Boolean); - - // Fixed: 'const' instead of 'let' to satisfy ESLint prefer-const rule + const lines = text.split(/\n+/).map((l) => l.trim()).filter(Boolean); const details: string[] = []; let summaryText = ""; let insideSummary = false; @@ -172,7 +182,6 @@ export const BookModal = ({ return (
- {/* Core Metadata Elements List */}
{details.map((detail, idx) => { const splitIdx = detail.indexOf(":"); @@ -181,21 +190,17 @@ export const BookModal = ({ const val = detail.substring(splitIdx + 1).trim(); return (
- {/* Fixed: Upgraded arbitrary 'min-w-[140px]' to canonical Tailwind v4 'min-w-35' */} {label}: {val}
); } return ( -

- {detail} -

+

{detail}

); })}
- {/* Separated Summary Block Component */} {summaryText && (
@@ -214,13 +219,9 @@ export const BookModal = ({ return (
-
- {/* LEFT COMPONENT: Primary Form Input Layout */} +
+
- - {/* Header Framework */}

@@ -240,16 +241,13 @@ export const BookModal = ({

- - {/* Intelligent OCR Scanning Engine Header Frame */} {!editingBook && (
- + Scans: {scanCounter}/50
@@ -265,20 +263,34 @@ export const BookModal = ({
+ + {/* 🚀 NEW: MANDATORY ISBN FIELD COMPONENT CONTAINER */} +
+ + + {errors.isbn && ( +

{errors.isbn.message}

+ )} +
+
- {/* Fixed: outline-none replaces custom token constraints */} {errors.title && ( -

- {errors.title.message} -

+

{errors.title.message}

)}
@@ -286,16 +298,13 @@ export const BookModal = ({ - {/* Fixed: outline-none replaces custom token constraints */} {errors.author && ( -

- {errors.author.message} -

+

{errors.author.message}

)}
@@ -303,7 +312,6 @@ export const BookModal = ({ - {/* Fixed: outline-none replaces custom token constraints */} Total Copies - {/* Fixed: outline-none replaces custom token constraints */}
- {/* Fixed: outline-none replaces custom token constraints */} { setSearchTerm(e.target.value); setCurrentPage(1); }} + value={searchInput} + onChange={(e) => setSearchInput(e.target.value)} className="bg-transparent border-0 outline-hidden w-full text-xs font-medium text-[#1A365D] placeholder-[#A0AEC0] p-0 focus:ring-0 focus:outline-hidden" /> - {searchTerm && ( - )} @@ -281,8 +307,8 @@ export const BooksPage = () => { @@ -290,7 +316,64 @@ export const BooksPage = () => {
+ + {isSelectionMode && ( + + )} + +
+ +
- {/* PRIMARY DATA VIEWS LAYOUT CONTAINER */}
{isLoading ? ( @@ -312,15 +394,57 @@ export const BooksPage = () => { - + {isSelectionMode && - {/* LANGUAGE INTERACTIVE SELECT HEADER CELL */} - - - {/* CATEGORY INTERACTIVE SELECT HEADER CELL */} - - + + {bookList.length === 0 ? ( - ) : ( bookList.map((book) => { - const isCurrentSelection = selectedBook?.id === book.id && isDetailOpen; + const isCurrentSelection = selectedBookId === book.id && isDetailOpen; + const isRowChecked = selectedBookIds.includes(book.id); + return ( { - setSelectedBook(book); - setIsDetailOpen(true); + if (isSelectionMode) { + setSelectedBookIds((prev) => + prev.includes(book.id) ? prev.filter((id) => id !== book.id) : [...prev, book.id] + ); + } else { + setSelectedBookId(book.id); + setIsDetailOpen(true); + } }} className={`transition-all duration-150 cursor-pointer border-l-4 ${ - isCurrentSelection - ? 'bg-slate-50/80 border-l-4 border-l-blue-500' - : 'hover:bg-blue-50/40 border-l-4 border-l-transparent' + isRowChecked ? "bg-blue-50/30 border-l-[#2B6CB0]" : isCurrentSelection ? "bg-slate-50/80 border-l-blue-500" : "hover:bg-blue-50/40 border-l-transparent" }`} > - + )} + - - - + ); }) @@ -469,28 +640,50 @@ export const BooksPage = () => {
Book Title & Creator Index} + + + + {activeHeaderDropdown === "name" && ( +
e.stopPropagation()} + className="absolute left-0 top-7 z-50 w-44 bg-white border border-gray-200 rounded-lg shadow-xl py-1.5 text-xs text-[#2D3748] font-medium normal-case tracking-normal font-sans" + > + + + +
+ )} +
+ {activeHeaderDropdown === "language" && ( -
e.stopPropagation()} className="absolute left-4 top-7 z-50 w-40 bg-white border border-gray-200 rounded-lg shadow-xl py-1.5 text-xs text-[#2D3748] font-medium normal-case tracking-normal max-h-60 overflow-y-auto" >
)}
+ + - + {activeHeaderDropdown === "category" && ( -
e.stopPropagation()} className="absolute left-4 top-7 z-50 w-48 bg-white border border-gray-200 rounded-lg shadow-xl py-1.5 text-xs text-[#2D3748] font-medium normal-case tracking-normal max-h-60 overflow-y-auto" >
AvailabilityAvailabilityActions
+ No matching records currently indexed inside this filtered view.
+ {isSelectionMode && ( + + +
{book.title}
-
- By {book.author} -
+
By {book.author}
- - {book.language} - + {book.language} -
- {book.categoryName} -
+
{book.categoryName}
0 ? "bg-emerald-500" : "bg-rose-500"}`} /> - 0 ? "text-emerald-700 font-mono" : "text-rose-700 font-mono"}> + 0 ? "text-emerald-700" : "text-rose-700"}> {book.availableCopies} / {book.totalCopies} Left +
+ + +
+
- {/* PAGINATION SUITE LAYER BLOCK CONTROLS */} + {/* Dynamic Rows Pagination Footer block */} {totalPages > 0 && (
- Page {currentPage} of {totalPages} -
- - + +
+ )}
)}
@@ -498,37 +691,46 @@ export const BooksPage = () => {
- {/* THE INTEGRATED BOOK DETAIL DIALOG POPUP MODAL */} { setIsDetailOpen(false); - setSelectedBook(null); // Keep this safely here ONLY for standard manual modal exits + setSelectedBookId(null); }} onEditTrigger={() => { - // CRITICAL FIX: Close the detail overlay window, but DO NOT nullify selectedBook! setIsDetailOpen(false); setIsFormOpen(true); }} onDeleteTrigger={() => { - if (selectedBook) { - deleteBookMutation.mutate(selectedBook.id); + if (selectedBookId) { + deleteBooksMutation.mutate([selectedBookId]); setIsDetailOpen(false); } }} /> - {/* OVERLAY CONFIGURATION/ADD FORM DIALOG LAYER */} { setIsFormOpen(false); - setSelectedBook(null); // Cleans up after form submission/cancellation + setSelectedBookId(null); }} onSubmit={(vals) => saveBookMutation.mutate(vals)} categories={categories} - editingBook={selectedBook} + editingBook={detailedBook} + /> + + setDeleteTargetBooks(null)} + onConfirm={() => { + if (deleteTargetBooks) { + deleteBooksMutation.mutate(deleteTargetBooks.ids); + } + }} + bookTitle={deleteTargetBooks?.displayTitle || ""} />
); diff --git a/client/src/features/books/schemas/bookSchema.ts b/client/src/features/books/schemas/bookSchema.ts index 8cafcfd..9180b34 100644 --- a/client/src/features/books/schemas/bookSchema.ts +++ b/client/src/features/books/schemas/bookSchema.ts @@ -8,6 +8,13 @@ export const BookFormSchema = z.object({ .min(1, { message: "Minimum catalog collection entry requires 1 copy" }), categoryId: z.string().min(1, { message: "Please map this asset to an organizational category" }), language: z.string().min(1, { message: "language is required" }), + isbn: z + .string() + .min(1, "ISBN number is required") + .regex( + /^(?:ISBN(?:-1[03])?:?\s*)?(?:(?=^[0-9X]{10}$)|(?=^[0-9-]{13}$)|(?=^[0-9X]{13}$))?(?:97[89]-?)?[0-9]{1,5}-?[0-9]+-?[0-9]+-?[0-9X]$/i, + "Please enter a valid ISBN-10 or ISBN-13 format" + ), }); export type BookFormValues = z.infer; \ No newline at end of file diff --git a/client/src/features/categories/components/DeleteCategoryConfirmationModal.tsx b/client/src/features/categories/components/DeleteCategoryConfirmationModal.tsx index 76798e8..b458fd2 100644 --- a/client/src/features/categories/components/DeleteCategoryConfirmationModal.tsx +++ b/client/src/features/categories/components/DeleteCategoryConfirmationModal.tsx @@ -71,7 +71,7 @@ export const DeleteCategoryConfirmationModal: React.FC

- There are currently {category.booksCount} unique titles registered under this category. Dropping this registry will cause all associated books to be shown as unclassified inside the catalogs. + There are currently {category.booksCount} books registered under this category. Dropping this registry will cause all associated books to be shown as unclassified inside the catalogs.

) : ( diff --git a/client/src/features/dashboard/components/AmnestySimulator.tsx b/client/src/features/dashboard/components/AmnestySimulator.tsx index 6ce55bb..f8f1c0b 100644 --- a/client/src/features/dashboard/components/AmnestySimulator.tsx +++ b/client/src/features/dashboard/components/AmnestySimulator.tsx @@ -32,7 +32,7 @@ export const AmnestySimulator = ({ Relief Rate: {discount}% - + Projected Cash Collection: ₹{projectedRecovery}
@@ -57,4 +57,4 @@ export const AmnestySimulator = ({
); -}; \ No newline at end of file +}; diff --git a/client/src/features/dashboard/components/CategoryTreeMap.tsx b/client/src/features/dashboard/components/CategoryTreeMap.tsx index 209d592..1700f25 100644 --- a/client/src/features/dashboard/components/CategoryTreeMap.tsx +++ b/client/src/features/dashboard/components/CategoryTreeMap.tsx @@ -15,20 +15,25 @@ interface CategoryItem { color: string; } -export const CategoryTreeMap = ({ categories }: { categories: CategoryItem[] }) => { +export const CategoryTreeMap = ({ + categories, +}: { + categories: CategoryItem[]; +}) => { const totalValue = categories.reduce((sum, c) => sum + c.value, 0) || 1; return (
- {/* 👋 HIDDEN COMPILER HOOK: Prevents asset pruning */} - {Object.keys(tailwindColorSafelist).length} + + {Object.keys(tailwindColorSafelist).length} + {/* HEADER BLOCK: Premium Swiss Editorial Hierarchy */}
-

- - Categories +

+ + Categories

Top 4 categories @@ -50,9 +55,9 @@ export const CategoryTreeMap = ({ categories }: { categories: CategoryItem[] }) > {/* Overlay highlight */}
- + {/* Dynamic CSS Popover Tooltip */} -
+
{cat.name}: {itemPct}% ({cat.value})
@@ -72,10 +77,13 @@ export const CategoryTreeMap = ({ categories }: { categories: CategoryItem[] }) className={`w-2.5 h-2.5 rounded-full ${cat.color} shrink-0 mt-1 ring-4 ring-white shadow-2xs`} />
- + {cat.name} - + {cat.value.toLocaleString()} Books
@@ -83,9 +91,6 @@ export const CategoryTreeMap = ({ categories }: { categories: CategoryItem[] }) ); })}
- - -

); -}; \ No newline at end of file +}; diff --git a/client/src/features/dashboard/components/CriticalDeficitWidget.tsx b/client/src/features/dashboard/components/CriticalDeficitWidget.tsx index 8790050..d5a055a 100644 --- a/client/src/features/dashboard/components/CriticalDeficitWidget.tsx +++ b/client/src/features/dashboard/components/CriticalDeficitWidget.tsx @@ -12,18 +12,18 @@ export const CriticalDeficitWidget = ({ items }: { items: DeficitItem[] }) => { return (
- {/* HEADER BLOCK: Premium Swiss Editorial Hierarchy */}
-

- +

+ Stock Management

Out of Stock Alerts

- Critical titles with 0 copies remaining on shelves alongside mounting reserve waitlists. + Critical titles with 0 copies remaining on shelves alongside mounting + reserve waitlists.

@@ -48,7 +48,7 @@ export const CriticalDeficitWidget = ({ items }: { items: DeficitItem[] }) => { > {item.name} - + {/* High-Contrast Dynamic Utility Badge — Shows absolute real-world item demand */} 0 Availble Copies @@ -64,12 +64,11 @@ export const CriticalDeficitWidget = ({ items }: { items: DeficitItem[] }) => {
- Need to Restock Copies + Need to Restock Copies
)} -
); -}; \ No newline at end of file +}; diff --git a/client/src/features/dashboard/components/EngagementLeaderboard.tsx b/client/src/features/dashboard/components/EngagementLeaderboard.tsx index 51fa3eb..4cd2a6b 100644 --- a/client/src/features/dashboard/components/EngagementLeaderboard.tsx +++ b/client/src/features/dashboard/components/EngagementLeaderboard.tsx @@ -11,18 +11,18 @@ interface TopUser { export const EngagementLeaderboard = ({ members }: { members: TopUser[] }) => { return (
- {/* HEADER BLOCK: Premium Swiss Editorial Hierarchy */}
-

- - Top Readers +

+ + Top Readers

Elite Reader Engagement

- Top-performing member accounts maintaining high circulation volume and exemplary return compliance metrics. + Top-performing member accounts maintaining high circulation volume and + exemplary return compliance metrics.

@@ -45,7 +45,7 @@ export const EngagementLeaderboard = ({ members }: { members: TopUser[] }) => { {/* Left Side: Avatar Rank and Label Identification */}
{ {/* Right Side: Account Activity Performance Stats */}
-

+

{member.loans} Loans

-

+

{member.onTimeRate}% On-Time

@@ -78,4 +78,4 @@ export const EngagementLeaderboard = ({ members }: { members: TopUser[] }) => {
); -}; \ No newline at end of file +}; diff --git a/client/src/features/dashboard/components/FineVelocityGauge.tsx b/client/src/features/dashboard/components/FineVelocityGauge.tsx index 46fa38a..d69bb43 100644 --- a/client/src/features/dashboard/components/FineVelocityGauge.tsx +++ b/client/src/features/dashboard/components/FineVelocityGauge.tsx @@ -1,5 +1,5 @@ // Editorial Visual Assets -import { Banknote, } from "lucide-react"; +import { Banknote } from "lucide-react"; export const FineVelocityGauge = ({ collected, @@ -13,35 +13,34 @@ export const FineVelocityGauge = ({ return (
- {/* HEADER BLOCK: Standardized Institutional Hierarchy */}
-

- - Fines +

+ + Fines

Recovery Collection Velocity

- The structural clearance index of settled overdue account liabilities against outstanding book penalties. + The structural clearance index of settled overdue account liabilities + against outstanding book penalties.

{/* CORE DISPLAY: High Contrast Numeric Layout & SVG Metric Ring */}
- {/* Left Side: Numeric Value Readouts */}
- + Collected Fines

₹{collected.toLocaleString("en-IN")}

- +

Outstanding Fines:{" "} @@ -74,7 +73,7 @@ export const FineVelocityGauge = ({ d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831" /> - + {/* Central Percentage Value Text */}

@@ -85,9 +84,7 @@ export const FineVelocityGauge = ({
-
-
); -}; \ No newline at end of file +}; diff --git a/client/src/features/dashboard/components/MetricsGrid.tsx b/client/src/features/dashboard/components/MetricsGrid.tsx index da5ca50..e0aceb3 100644 --- a/client/src/features/dashboard/components/MetricsGrid.tsx +++ b/client/src/features/dashboard/components/MetricsGrid.tsx @@ -1,16 +1,15 @@ -import { useState, useEffect, useCallback } from "react"; import type { DashboardSummaryMetrics } from "../../../types/dashboard"; - -// Editorial Visual Assets import { BookOpen, - BookCheck, + Layers, Users, - AlertTriangle, - Coins, - ChevronLeft, - ChevronRight, - Info + AlertCircle, + DollarSign, + TrendingUp, + CheckCircle, + Activity, + ShieldAlert, + Book } from "lucide-react"; interface MetricsBannerProps { @@ -18,160 +17,257 @@ interface MetricsBannerProps { } export const MetricsGrid = ({ data }: MetricsBannerProps) => { - const [currentIndex, setCurrentIndex] = useState(0); - - // Configuration for 5 distinct operational contextual slides - const slides = [ - { - title: "Global Catalog Registry", - metricName: "Total Books", - value: data?.totalBooks || 0, - subtext: `Total architectural copies managed: ${data?.totalCopies || 0}`, - icon: , - // Local or fallback high-resolution cinematic background relative paths - bgUrl: "https://images.unsplash.com/photo-1507842217343-583bb7270b66?q=80&w=1600&auto=format&fit=crop", - tagline: "Comprehensive volume control across distributed library collections.", - }, - { - title: "Live Circulation Inventory", - metricName: "Available Books", - value: data?.availableBooks || 0, - subtext: "Ready for immediate member acquisition and processing", - icon: , - bgUrl: "https://images.unsplash.com/photo-1521587760476-6c12a4b040da?q=80&w=1600&auto=format&fit=crop", - tagline: "Real-time verification indices matching current shelf states.", - }, - { - title: "Active Core Patron Base", - metricName: "Active Members", - value: data?.activeMembers || 0, - subtext: "System accounts showing valid active interaction indicators", - icon: , - bgUrl: "https://images.unsplash.com/photo-1529156069898-49953e39b3ac?q=80&w=1600&auto=format&fit=crop", - tagline: "Tracking regular platform use and digital resource checkouts.", - }, - { - title: "Circulation Critical Exceptions", - metricName: "Books Overdue", - value: data?.overdueCount || 0, - subtext: `Current return latency calculation ratio: ${data?.overduePercentage || 0}%`, - icon: , - bgUrl: "https://images.unsplash.com/photo-1516979187457-637abb4f9353?q=80&w=1600&auto=format&fit=crop", - tagline: "Requires prompt systemic notifications and penalty assignment protocols.", - }, - { - title: "Audit Financial Liability Pipeline", - metricName: "Outstanding Fines", - value: `₹${data?.totalOutstandingFines || 0}`, - subtext: "Fixed structural accrual baseline: ₹10/day per item", - icon: , - bgUrl: "https://images.unsplash.com/photo-1554224155-8d04cb21cd6c?q=80&w=1600&auto=format&fit=crop", - tagline: "Aggregated outstanding system accounts receivable ledger metrics.", - }, - ]; + + return ( +
+ + {/* SECTION HEADER BLOCK */} +
+

+ + Librarian Dashboard +

+

+ Live administrative dashboard displaying physical volume tracking variables, structural metrics, and fiscal pipelines. +

+
- const handleNext = useCallback(() => { - setCurrentIndex((prev) => (prev === slides.length - 1 ? 0 : prev + 1)); - }, [slides.length]); + {/* METRIC CORE MATRIX GRID CONTAINER */} +
+ + {/* CARD 1: CATALOG INDEX */} +
+
+
+ +
+
+ Catalog Index + +
+
+

{data?.totalBooks || 0}

+

Total Books Registered

+
+

+ Accurately tracks regular platform interactions, digital checkouts, and resource vectors. +

+
- const handlePrev = useCallback(() => { - setCurrentIndex((prev) => (prev === 0 ? slides.length - 1 : prev - 1)); - }, [slides.length]); + {/*
+ + +
*/} +
- // Synchronous 5-second automatic rotational interval heartbeat execution loops - useEffect(() => { - const autoSlideTimer = setInterval(() => { - handleNext(); - }, 5000); + {/* CARD 2: CIRCULATION LIVE */} +
+
+
+ +
+
+ Circulation Live + +
+
+

{data?.availableBooks || 0}

+

Available Inventory Copies

+
+

+ Live updates cross-referencing checkout requests against physical core assets. +

+
- return () => clearInterval(autoSlideTimer); - }, [handleNext]); + {/*
+ + +
*/} +
- return ( -
- - {/* Structural Banner Dynamic Background Image Layout Layers */} - {slides.map((slide, index) => ( -
- {/* Netflix-Style Double Gradient Vignette Mask Engine */} -
-
+ {/* CARD 3: MEMBER ANALYTICS */} +
+
+
- {/* Content Overlays Area */} -
- -
- - {slide.icon} - - - {slide.title} - +
+
+ Member Analytics +
- -
-

- {slide.value} -

-

- {slide.metricName} -

+
+

{data?.activeMembers || 0}

+

Active Members Base

- -

- {slide.tagline} +

+ Maps structural platform engagement trends and historical profile validation entries.

- - {slide.subtext && ( -
- - {slide.subtext} -
- )}
+ + {/*
+ + +
*/}
- ))} - {/* Symmetric Left Control Slider Override Button */} - + {/* CARD 4: EXCEPTION METRICS */} +
+
+
+ +
+
+ Exception Metrics + +
+
+

{data?.overdueCount || 0}

+

Books Overdue Queue

+
+

+ Algorithmic parameter locks catching operational return anomalies and late system logs. +

+
- {/* Symmetric Right Control Slider Override Button */} - + {/*
+ + +
*/} +
- {/* Discrete Bottom Linear Micro Progress Navigation Nodes Indicators */} -
- {slides.map((_, index) => ( - + +
*/} +
+ +
+ + {/* DASHBOARD STATUS FOOTER ADORNMENT */} +
+
+ + All relational core indicators synced successfully +
+
+ Latency: 12ms + Encryption: TLS 1.3 + Nodes: 3 Active +
diff --git a/client/src/features/dashboard/components/OverdueTable.tsx b/client/src/features/dashboard/components/OverdueTable.tsx index cc1faf7..ab86c75 100644 --- a/client/src/features/dashboard/components/OverdueTable.tsx +++ b/client/src/features/dashboard/components/OverdueTable.tsx @@ -10,7 +10,7 @@ export const OverdueCounter = ({ // 1. Calculate unique member data map distribution properties safely const memberBookCounts: Record = {}; - + if (records) { records.forEach((r) => { if (r.memberId) { @@ -36,8 +36,8 @@ export const OverdueCounter = ({ return (
-

- +

+ Operations Liability

@@ -47,11 +47,12 @@ export const OverdueCounter = ({
-

+

System Verification Clear

- No unreturned items or overdue log instances registered in the backend registry. + No unreturned items or overdue log instances registered in the + backend registry.

@@ -60,29 +61,28 @@ export const OverdueCounter = ({ return (
- {/* HEADER BLOCK */}
-

- +

+ Operations Liability

Overdue Hold Queue

- Real-time analytical tracking of unreturned assets grouped by patron liability profiles. + Real-time analytical tracking of unreturned assets grouped by patron + liability profiles.

{/* CORE DISPLAY: Two-Column High Contrast Metrics Layout */}
- {/* LEFT COLUMN: Total Book Sum & Total Unique Users Overview */}
{/* Total Vol Counters */}
- + {overdueCount} @@ -97,7 +97,8 @@ export const OverdueCounter = ({

- {uniqueMembersCount} {uniqueMembersCount === 1 ? "Memebr" : "Members"} + {uniqueMembersCount}{" "} + {uniqueMembersCount === 1 ? "Memebr" : "Members"}

Have Overdue @@ -110,7 +111,7 @@ export const OverdueCounter = ({

- + Overdue Breakdown
@@ -120,24 +121,26 @@ export const OverdueCounter = ({ {sortedTiers.map((booksPerUser) => { const matchingUsersCount = distributionTiers[booksPerUser]; const isHighHoarder = booksPerUser >= 3; - + return (
- + {matchingUsersCount} {" "} {matchingUsersCount === 1 ? "member has" : "members have"} - - + + {booksPerUser} {booksPerUser === 1 ? "book" : "books"} each
@@ -145,8 +148,7 @@ export const OverdueCounter = ({ })}
-
); -}; \ No newline at end of file +}; diff --git a/client/src/features/dashboard/components/PeakHoursChart.tsx b/client/src/features/dashboard/components/PeakHoursChart.tsx index 85c0d45..feff1e7 100644 --- a/client/src/features/dashboard/components/PeakHoursChart.tsx +++ b/client/src/features/dashboard/components/PeakHoursChart.tsx @@ -38,7 +38,7 @@ export const PeakHoursChart = ({
{/* Bar Chart Section */} -
+
{/* Background reference tracks */}
@@ -51,7 +51,7 @@ export const PeakHoursChart = ({
{/* High Contrast Tooltip popup indicator */} -
+
{item.count}
@@ -68,7 +68,7 @@ export const PeakHoursChart = ({ /> {/* Day Label with conditional size/weight based on activity */} - {item.day.slice(0, 3)} diff --git a/client/src/features/dashboard/components/RetentionAnalytics.tsx b/client/src/features/dashboard/components/RetentionAnalytics.tsx index 510843d..0232304 100644 --- a/client/src/features/dashboard/components/RetentionAnalytics.tsx +++ b/client/src/features/dashboard/components/RetentionAnalytics.tsx @@ -13,7 +13,6 @@ export const RetentionAnalytics = ({ return (
- {/* HEADER BLOCK: Distinct label hierarchies */}
@@ -24,11 +23,12 @@ export const RetentionAnalytics = ({ ) : ( - Optimal + + Optimal )}
- +

Retention Lifetime

@@ -43,17 +43,17 @@ export const RetentionAnalytics = ({ {avg} - - Mean Turnaround + + Average Due Days
- -
+ +
{maxLimit} Days - - Max Policy Cap + + Maximum Due days
@@ -62,7 +62,7 @@ export const RetentionAnalytics = ({ {/*
Lending Window Allocation - + {percentageUsed.toFixed(0)}% Exhausted
@@ -91,7 +91,6 @@ export const RetentionAnalytics = ({ }

*/} -
); -}; \ No newline at end of file +}; diff --git a/client/src/features/dashboard/components/ReturnForecaster.tsx b/client/src/features/dashboard/components/ReturnForecaster.tsx index 989dd39..930040b 100644 --- a/client/src/features/dashboard/components/ReturnForecaster.tsx +++ b/client/src/features/dashboard/components/ReturnForecaster.tsx @@ -9,21 +9,19 @@ export const ReturnForecaster = ({ const maxForecast = Math.max(...forecast.map((f) => f.count), 1); return (
- -

- 7-Day + 7-Day

- Return Flow Forecaster + Return Flow Forecaster

Expected book return volumes to optimize intake shelf arrangements.

-
+
{/* Horizontal Data Progress Bars Distribution layout */}
@@ -44,7 +42,7 @@ export const ReturnForecaster = ({ />
- + {day.count} books
@@ -52,4 +50,4 @@ export const ReturnForecaster = ({
); -}; \ No newline at end of file +}; diff --git a/client/src/features/fines/components/DeleteFinesModal.tsx b/client/src/features/fines/components/DeleteFinesModal.tsx index 1d384cc..4f4288b 100644 --- a/client/src/features/fines/components/DeleteFinesModal.tsx +++ b/client/src/features/fines/components/DeleteFinesModal.tsx @@ -18,78 +18,77 @@ export const DeleteFinesModal = ({ if (!isOpen) return null; return ( -
-
+
+
+ {/* Header */} +
+

+ Delete Fine Record +

- {/* Header */} -
-

- Delete Fine Record -

- - -
- - {/* Content */} -
-
- +
-

- Confirm Fine Deletion -

- -

- Are you sure you want to permanently remove the fine amount of - - {" "}₹{amount}.00 - - {" "}registered against - - {" "} "{memberName}" - - ? -

+ {/* Content */} +
+
+ +
- {/* Warning Card */} -
- - Permanent Action - +

+ Confirm Fine Deletion +

-

- This operation cannot be undone. The selected fine record will be - permanently removed from the fines ledger, audit history, and - administrative tracking logs. +

+ Are you sure you want to permanently remove the fine amount of + ₹{amount}.00{" "} + registered against + + {" "} + "{memberName}" + + ?

+ + {/* Warning Card */} +
+ + Permanent Action + + +

+ This operation cannot be undone. The selected fine record will be + permanently removed from the fines ledger, audit history, and + administrative tracking logs. +

+
-
- {/* Footer */} -
- + {/* Footer */} +
+ - + +
-
-);} \ No newline at end of file + ); +}; diff --git a/client/src/features/fines/components/FinesNotificationBanner.tsx b/client/src/features/fines/components/FinesNotificationBanner.tsx index 594d397..feb3eea 100644 --- a/client/src/features/fines/components/FinesNotificationBanner.tsx +++ b/client/src/features/fines/components/FinesNotificationBanner.tsx @@ -12,33 +12,33 @@ export const FinesNotificationBanner = ({ if (totalCount === 0) return null; return ( -
+
-
+
-

+

Overdue Fines Detected

-

+

There are currently {totalCount} active overdue items requiring immediate attention.

-
+
- + Total Outstanding Balance ₹{totalUnpaidAmount.toLocaleString()}.00
- +
); -}; +}; \ No newline at end of file diff --git a/client/src/features/fines/pages/FinesPage.tsx b/client/src/features/fines/pages/FinesPage.tsx index e4963fd..ac8bcb5 100644 --- a/client/src/features/fines/pages/FinesPage.tsx +++ b/client/src/features/fines/pages/FinesPage.tsx @@ -1,5 +1,5 @@ -import { useState, useEffect, useRef } from "react"; -import { useLocation, useNavigate } from "react-router-dom"; +import { useState, useEffect, useRef } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { axiosClient } from "../../../api/axiosClient"; import type { FineRecord } from "../../../types/fines"; @@ -33,13 +33,25 @@ interface AxiosErrorResponse { }; } +interface LocationStatePayload { + autoOpenIssueId?: string; + autoOpenSettlement?: boolean; + pendingCondition?: "GOOD" | "DAMAGED" | null; + pendingDescription?: string | null; +} + export const FinePage = () => { const queryClient = useQueryClient(); - const location = useLocation(); - const navigate = useNavigate(); + const location = useLocation(); + const navigate = useNavigate(); + + const routeState = location.state as LocationStatePayload | null; + + // Local interaction overrides tracking modifications manually executed by clerks + const [manualSelectedFine, setManualSelectedFine] = useState(null); + const [manualSelectedFineForSettlement, setManualSelectedFineForSettlement] = useState(null); - const [selectedFine, setSelectedFine] = useState(null); - const [showRestoreModal, setShowRestoreModal] = useState(false); + const [showRestoreModal, setShowRestoreModal] = useState(false); const [activeHeaderDropdown, setActiveHeaderDropdown] = useState<"delay" | null>(null); // Active View Tab Panel Layout Selector ("active" | "history") @@ -53,9 +65,6 @@ export const FinePage = () => { const [currentPage, setCurrentPage] = useState(1); const rowsPerPage = 10; - // Modals Core Management States - const [selectedFineForSettlement, setSelectedFineForSettlement] = useState(null); - // Ref tracking node for catching outside clicks on table header elements const delayDropdownRef = useRef(null); @@ -74,91 +83,78 @@ export const FinePage = () => { return () => document.removeEventListener("mousedown", handleOutsideClick); }, [activeHeaderDropdown]); - // 🟢 NEW MUTATION NODE: Forces dynamic backend recalculation on mount + // Forces dynamic backend recalculation on mount const syncLedgerMutation = useMutation({ mutationFn: async () => { const response = await axiosClient.patch("/fines/recalculate-ledger"); return response.data; }, onSuccess: (res) => { - console.log(`[Sync Engine] ${res.message || "Metrics synchronized successfully."}`, res.data); + console.log( + `[Sync Engine] ${res.message || "Metrics synchronized successfully."}`, + res.data, + ); queryClient.invalidateQueries({ queryKey: ["finesMasterLedgerFeed"] }); }, onError: () => { toast.error("Fine sync ledger recalculation engine structural warning."); - } + }, }); - // 🟢 MOUNT LIFECYCLE ENGINE: Triggers every single time a clerk opens or views this page + // MOUNT LIFECYCLE ENGINE: Triggers every single time a clerk opens or views this page useEffect(() => { - console.log("⚡ Fines Management Desk Mounted. Dispatching master calculation tool..."); + console.log( + "⚡ Fines Management Desk Mounted. Dispatching master calculation tool...", + ); syncLedgerMutation.mutate(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, []); // 1. Fetch Data Stream conditionally depending on active tab view layouts - const { data: finesFeedPayload = [], isLoading: isQueryLoading } = useQuery({ + const { data: finesFeedPayload = [], isLoading: isQueryLoading } = useQuery< + FineRecord[] + >({ queryKey: ["finesMasterLedgerFeed", activeTab], queryFn: async () => { - const endpoint = activeTab === "active" ? "/fines/pending" : "/fines/collected"; + const endpoint = + activeTab === "active" ? "/fines/pending" : "/fines/collected"; const response = await axiosClient.get(endpoint); return response.data?.data || response.data || []; }, }); - // 🟢 COMBINED LOADING EVALUATION: Keeps the pulse screen running until the sync finishes + // COMBINED LOADING EVALUATION: Keeps the pulse screen running until the sync finishes const isLoading = isQueryLoading || syncLedgerMutation.isPending; - // Intercept incoming routing state redirect signatures safely - useEffect(() => { - const routeState = location.state as { autoOpenIssueId?: string; autoOpenSettlement?: boolean } | null; - - if (routeState) { - console.log("======== [DEBUG RECEIVER] ROUTE STATE DETECTED ========"); - console.log("Received routeState:", routeState); - } - - if (routeState?.autoOpenIssueId && finesFeedPayload.length > 0) { - const incomingId = routeState.autoOpenIssueId; - - const matchingFine = finesFeedPayload.find((fine) => { + // 🧠 DERIVE DIRECTLY DURING RENDER PHASE: Zero useEffect state synchronizations or rule violations + const matchedRouteFine = routeState?.autoOpenIssueId && finesFeedPayload.length > 0 + ? finesFeedPayload.find((fine) => { if (!fine) return false; - return ( - fine.issue_id === incomingId || - fine.fine_id === incomingId - ); - }); - - if (matchingFine) { - console.log("✅ SUCCESS: Found a matching fine record object!", matchingFine); - - const timeoutId = setTimeout(() => { - if (routeState.autoOpenSettlement) { - setSelectedFineForSettlement(matchingFine); - setActiveTab("active"); - } else { - setSelectedFine(matchingFine); - setActiveTab("active"); - } - - navigate(location.pathname, { replace: true, state: null }); - }, 0); - - return () => clearTimeout(timeoutId); - } else { - console.error( - `❌ MATCH FAIL: Checked all ${finesFeedPayload.length} ledger entries but found no matching ID for "${incomingId}".` - ); - } + return fine.issue_id === routeState.autoOpenIssueId || fine.fine_id === routeState.autoOpenIssueId; + }) || null + : null; + + // Compute absolute active data targets safely combining local state overrides and render-derived route payloads + const selectedFine = manualSelectedFine || (!routeState?.autoOpenSettlement ? matchedRouteFine : null); + const selectedFineForSettlement = manualSelectedFineForSettlement || (routeState?.autoOpenSettlement ? matchedRouteFine : null); + + // Clear modal selections and dismiss router navigation history context state safely on dismissal + const clearActiveModalsState = () => { + setManualSelectedFine(null); + setManualSelectedFineForSettlement(null); + + if (routeState) { + navigate(location.pathname, { replace: true, state: null }); } - }, [location.state, finesFeedPayload, navigate, location.pathname]); + }; const restoreFineMutation = useMutation({ - mutationFn: async (id: string) => await axiosClient.patch(`/fines/restore/${id}`), + mutationFn: async (id: string) => + await axiosClient.patch(`/fines/restore/${id}`), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["finesMasterLedgerFeed"] }); setShowRestoreModal(false); - setSelectedFine(null); + clearActiveModalsState(); toast.success("Entry restored to active ledger!", { description: @@ -174,28 +170,36 @@ export const FinePage = () => { id, paidDate, paymentMethod, + condition, + damage_description, }: { id: string; paidDate: string; paymentMethod: "CASH" | "CARD" | "UPI"; + condition?: "GOOD" | "DAMAGED" | null; + damage_description?: string | null; }) => { return await axiosClient.patch("/fines/pay", { fine_id: id, paidDate: paidDate, paymentMethod: paymentMethod, + condition: condition || undefined, + damage_description: damage_description || undefined, }); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["finesMasterLedgerFeed"] }); - setSelectedFineForSettlement(null); + clearActiveModalsState(); toast.success("💸 Invoice Ledger Balanced Successfully!", { - description: "Transaction finalized. This record has moved safely to Collected History.", + description: + "Transaction finalized. This record has moved safely to Collected History.", duration: 4000, }); }, onError: (err: AxiosErrorResponse) => { toast.error( - err?.response?.data?.message || "Execution engine rejected settlement input parameters." + err?.response?.data?.message || + "Execution engine rejected settlement input parameters.", ); }, }); @@ -207,7 +211,7 @@ export const FinePage = () => { }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["finesMasterLedgerFeed"] }); - setSelectedFine(null); + clearActiveModalsState(); toast.info("Invoice purged from ledger."); }, }); @@ -216,13 +220,21 @@ export const FinePage = () => { if (!fine) return false; const term = searchQuery.toLowerCase().trim(); - - const nameMatch = String(fine.memberName || "").toLowerCase().includes(term); - const titleMatch = String(fine.bookTitle || "").toLowerCase().includes(term); + + const nameMatch = String(fine.memberName || "") + .toLowerCase() + .includes(term); + const titleMatch = String(fine.bookTitle || "") + .toLowerCase() + .includes(term); const passesSearch = term === "" || nameMatch || titleMatch; let passesDelayRange = true; - if (activeTab === "active" && delayIntervalFilter && typeof fine.delayed_days === 'number') { + if ( + activeTab === "active" && + delayIntervalFilter && + typeof fine.delayed_days === "number" + ) { const days = fine.delayed_days; if (delayIntervalFilter === "7") passesDelayRange = days > 7; if (delayIntervalFilter === "14") passesDelayRange = days > 14; @@ -233,10 +245,11 @@ export const FinePage = () => { }); // 5. Aggregate metrics computation logic blocks - const totalUnpaidInvoicesCount = activeTab === "active" ? finesFeedPayload.length : 0; + const totalUnpaidInvoicesCount = + activeTab === "active" ? finesFeedPayload.length : 0; const aggregateAccruedSumVal = filteredFines.reduce( (sum, current) => sum + (current.fine_amount || 0), - 0 + 0, ); // 6. Inline dynamic pagination offsets @@ -244,7 +257,7 @@ export const FinePage = () => { const totalPagesCount = Math.ceil(totalItemsCount / rowsPerPage) || 1; const paginatedRowsData = filteredFines.slice( (currentPage - 1) * rowsPerPage, - currentPage * rowsPerPage + currentPage * rowsPerPage, ); const handleTabChange = (tab: "active" | "history") => { @@ -268,7 +281,9 @@ export const FinePage = () => {
Fines Management Desk
-

Fines Management Desk

+

+ Fines Management Desk +

Active Plans (₹10/day) | Expired Plans (₹20/day).

@@ -280,7 +295,9 @@ export const FinePage = () => { type="button" onClick={() => handleTabChange("active")} className={`flex items-center gap-1.5 px-3.5 py-4 text-xs font-bold uppercase tracking-wider rounded-lg transition-all cursor-pointer ${ - activeTab === "active" ? "bg-white shadow-xs text-[#1A365D]" : "text-slate-500 hover:text-slate-800" + activeTab === "active" + ? "bg-white shadow-xs text-[#1A365D]" + : "text-slate-500 hover:text-slate-800" }`} > Active Fines @@ -307,16 +324,18 @@ export const FinePage = () => { )} {activeTab === "history" && ( -
+
-
+
Total Audited Balance Collected
-
₹{aggregateAccruedSumVal}.00
+
+ ₹{aggregateAccruedSumVal}.00 +
- - {totalItemsCount} Settled Invoices In Archive Ledger + + {totalItemsCount} Settled Invoices In Archive Ledger
@@ -384,15 +403,16 @@ export const FinePage = () => { onClick={(e) => { e.stopPropagation(); setActiveHeaderDropdown( - activeHeaderDropdown === "delay" ? null : "delay" + activeHeaderDropdown === "delay" ? null : "delay", ); }} className={`inline-flex items-center justify-center gap-1 transition-colors uppercase tracking-widest text-[11px] font-bold hover:text-[#1A365D] ${ - delayIntervalFilter ? "text-[#2B6CB0]" : "text-[#718096]" + delayIntervalFilter + ? "text-[#2B6CB0]" + : "text-[#718096]" }`} > - Delayed Days - {delayIntervalFilter ? ` (${delayIntervalFilter}+)` : ""} + Delayed Days {delayIntervalFilter ? ` (${delayIntervalFilter}+)` : ""} { setCurrentPage(1); }} className={`w-full text-left px-3 py-2 hover:bg-slate-50 transition-colors ${ - !delayIntervalFilter ? "bg-slate-50/80 text-[#2B6CB0] font-semibold" : "" + !delayIntervalFilter + ? "bg-slate-50/80 text-[#2B6CB0] font-semibold" + : "" }`} > All Delays @@ -428,7 +450,9 @@ export const FinePage = () => { setCurrentPage(1); }} className={`w-full text-left px-3 py-2 hover:bg-slate-50 transition-colors ${ - delayIntervalFilter === "7" ? "bg-slate-50/80 text-[#2B6CB0] font-semibold" : "" + delayIntervalFilter === "7" + ? "bg-slate-50/80 text-[#2B6CB0] font-semibold" + : "" }`} > Critical (> 7 Days) @@ -442,7 +466,9 @@ export const FinePage = () => { setCurrentPage(1); }} className={`w-full text-left px-3 py-2 hover:bg-slate-50 transition-colors ${ - delayIntervalFilter === "14" ? "bg-slate-50/80 text-[#2B6CB0] font-semibold" : "" + delayIntervalFilter === "14" + ? "bg-slate-50/80 text-[#2B6CB0] font-semibold" + : "" }`} > Severe (> 14 Days) @@ -456,7 +482,9 @@ export const FinePage = () => { setCurrentPage(1); }} className={`w-full text-left px-3 py-2 hover:bg-slate-50 transition-colors ${ - delayIntervalFilter === "30" ? "bg-slate-50/80 text-[#2B6CB0] font-semibold" : "" + delayIntervalFilter === "30" + ? "bg-slate-50/80 text-[#2B6CB0] font-semibold" + : "" }`} > High Delinquency (> 30 Days) @@ -471,7 +499,10 @@ export const FinePage = () => { {paginatedRowsData.length === 0 ? ( - + Operational Clear View. Zero matching layout targets found. @@ -480,22 +511,29 @@ export const FinePage = () => { { - setSelectedFine(fine); if (activeTab === "history") { - setShowRestoreModal(true); + setManualSelectedFine(fine); + setShowRestoreModal(true); + } else { + setManualSelectedFine(fine); } }} className="transition-all duration-150 cursor-pointer border-l-4 border-l-transparent hover:bg-blue-50/40" > -
{fine.memberName}
-
+
+ {fine.memberName} +
+
{fine.memberEmail}
- + {fine.bookTitle}
@@ -525,15 +563,21 @@ export const FinePage = () => { - {fine.membershipActive ? "Active Plan" : "Plan Expired"} + {fine.membershipActive + ? "Active Plan" + : "Plan Expired"} @@ -547,8 +591,7 @@ export const FinePage = () => { {/* Pagination Command Module */}
- Page {currentPage} / {totalPagesCount} |{" "} - Total {totalItemsCount} Fines + Page {currentPage} / {totalPagesCount} | Total {totalItemsCount} Fines
-
-
+
+
- {/* Member Profile */} + {/* Member Profile Block */}
@@ -133,7 +152,7 @@ export const IssueDetailsModal = ({
- 📖 {record.bookTitle} + `📖 ${record.bookTitle}`
Catalog Author: {record.bookAuthor || "Unknown Reference"} @@ -141,7 +160,7 @@ export const IssueDetailsModal = ({
{/* Timeline Grid */} -
+
@@ -166,8 +185,74 @@ export const IssueDetailsModal = ({
+ {/* ==================== 🚀 NEW DYNAMIC ENTRY DESK: CONDITION EVALUATOR ==================== */} +
+
+ +
+ + + +
+
+ + {/* Conditional Textarea Segment */} + {bookCondition === "DAMAGED" && ( +
+
+ + Damage Reason + + {/* Character Count System Interface */} + + {charactersRemaining} characters left + +
+