From 350ad9e43c26f1a58154d7b023ca9a1b1ef0b669 Mon Sep 17 00:00:00 2001 From: Yogeshwaran S Date: Wed, 17 Jun 2026 17:54:20 +0530 Subject: [PATCH 1/6] fix: Improve the entire Dashboard Layout UI --- .../features/admin/components/AdminLayout.tsx | 183 +++++++----- .../admin/components/ManageLibrarians.tsx | 63 +++- .../features/admin/components/ManageUsers.tsx | 95 +++--- .../admin/components/UserDetailsModal.tsx | 6 +- .../features/admin/components/UserModal.tsx | 130 ++++++--- .../features/books/components/BookModal.tsx | 68 +++-- client/src/features/books/pages/BooksPage.tsx | 250 +++++++++++----- .../dashboard/components/AmnestySimulator.tsx | 4 +- .../dashboard/components/CategoryTreeMap.tsx | 33 ++- .../components/CriticalDeficitWidget.tsx | 15 +- .../components/EngagementLeaderboard.tsx | 18 +- .../components/FineVelocityGauge.tsx | 23 +- .../dashboard/components/MetricsGrid.tsx | 248 +++++++++------- .../dashboard/components/OverdueTable.tsx | 48 +-- .../dashboard/components/PeakHoursChart.tsx | 6 +- .../components/RetentionAnalytics.tsx | 17 +- .../dashboard/components/ReturnForecaster.tsx | 12 +- .../fines/components/DeleteFinesModal.tsx | 127 ++++---- client/src/features/fines/pages/FinesPage.tsx | 177 +++++++---- .../issues/components/IssueDetailsModal.tsx | 29 +- .../issues/components/TransactionModal.tsx | 17 +- .../components/UnpaidFineAlertModal.tsx | 5 +- .../components/ReturnedDetailsModal.tsx | 197 ++++++------- client/src/layouts/DashboardLayout.tsx | 274 ++++++++++++------ client/src/pages/Dashboard.tsx | 42 +-- 25 files changed, 1291 insertions(+), 796 deletions(-) diff --git a/client/src/features/admin/components/AdminLayout.tsx b/client/src/features/admin/components/AdminLayout.tsx index 60f984e..9d5bcdb 100644 --- a/client/src/features/admin/components/AdminLayout.tsx +++ b/client/src/features/admin/components/AdminLayout.tsx @@ -1,4 +1,4 @@ -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"; @@ -11,6 +11,7 @@ import { LogOut, Library, User, + Menu } from "lucide-react"; export const AdminLayout: React.FC = () => { @@ -18,16 +19,39 @@ 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 + // 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); + }, []); + + // Handle immediate viewport layout reset on route change and bind listener useEffect(() => { - if (mainScrollContainerRef.current) { - mainScrollContainerRef.current.scrollTop = 0; + const container = mainScrollContainerRef.current; + + if (container) { + container.scrollTop = 0; + setIsAtAbsoluteTop(true); + container.addEventListener("scroll", handleContainerScroll); } - }, [location.pathname]); + + return () => { + if (container) { + container.removeEventListener("scroll", handleContainerScroll); + } + }; + }, [location.pathname, handleContainerScroll]); const handleSignOut = () => { logout(); @@ -55,55 +79,39 @@ 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 Icon/Expanded Navigation Sidebar Shell — Re-themed background layout */} -
+
+ + {/* Upper Navigation Menu Trigger Action Row */} +
+ + {sidebarExpanded && ( + + Admin Panel + + )} +
+ {/* 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 && ( -
- {/* Minimal Pagination Elements */} + {/* Stabilized Pagination Navigation Footer */} {totalPages > 0 && (
Page {currentPage} of {totalPages} @@ -462,7 +463,7 @@ export const MembersPage = () => { onClick={(e) => { e.stopPropagation(); setCurrentPage((p) => Math.max(p - 1, 1)); }} className="text-gray-600 font-semibold tracking-wider disabled:opacity-20 cursor-pointer hover:text-[#2B6CB0] flex items-center gap-1 transition-colors" > - ← Previous + ← Previous
@@ -481,8 +482,6 @@ export const MembersPage = () => {
{/* ==================== GLOBAL OVERLAY MODALS ==================== */} - - {/* 1. Member Information Details Context Modal */} { isRenewing={renewMutation.isPending} /> - {/* 2. Member Form Modal (Handles Create and Updates) */} { setIsFormOpen(false); setSelectedMember(null); }} diff --git a/server/src/modules/azureAI/aiScanner.service.ts b/server/src/modules/azureAI/aiScanner.service.ts index 5b3fc8e..9933d45 100644 --- a/server/src/modules/azureAI/aiScanner.service.ts +++ b/server/src/modules/azureAI/aiScanner.service.ts @@ -25,7 +25,7 @@ const translationClient = createTextTranslationClient("https://api.cognitive.mic const ai = new GoogleGenAI({ apiKey: geminiApiKey }); export const processBookCoverAI = async (imageBuffer: Buffer) => { - // 🟢 STEP 1: Global Azure Vision OCR Scan + // STEP 1: Global Azure Vision OCR Scan const visionResponse = await visionClient.path("/imageanalysis:analyze").post({ body: imageBuffer, contentType: "application/octet-stream", @@ -46,7 +46,7 @@ export const processBookCoverAI = async (imageBuffer: Buffer) => { }); } - // 🟢 STEP 2: Azure Translation Bulk Normalization + // STEP 2: Azure Translation Bulk Normalization const translationInputs = extractedLines.map(line => ({ text: line, targets: [{ language: "en" }] })); let englishLines: string[] = [...extractedLines]; let azureDetectedLangCode = "en"; @@ -74,7 +74,7 @@ export const processBookCoverAI = async (imageBuffer: Buffer) => { (raw, idx) => `- OCR Extracted: "${raw}" | Translated Target: "${englishLines[idx] || raw}"` ).join("\n"); - // 🟢 STEP 3: Gemini Multimodal Vision & Contextual Verification + // STEP 3: Gemini Multimodal Vision & Contextual Verification let bestTitle = ""; let bestAuthor = ""; let bestLanguage = ""; @@ -148,7 +148,7 @@ export const processBookCoverAI = async (imageBuffer: Buffer) => { title: bestTitle || "Unknown Title", author: bestAuthor || "Unknown Author", language: bestLanguage || "English", - category: detectedCategory || "Non-Fiction", + category: detectedCategory || "Unknown Category", overview: overviewText || "No overview available for this item." }; }; \ No newline at end of file From b8ccd7e249aa0aefb91efbc685e32c138ec90a49 Mon Sep 17 00:00:00 2001 From: Yogeshwaran S Date: Thu, 18 Jun 2026 10:42:35 +0530 Subject: [PATCH 4/6] fix: solve the logical mistakes in dashboard page --- .../features/dashboard/components/RetentionAnalytics.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/features/dashboard/components/RetentionAnalytics.tsx b/client/src/features/dashboard/components/RetentionAnalytics.tsx index 1666a8c..0232304 100644 --- a/client/src/features/dashboard/components/RetentionAnalytics.tsx +++ b/client/src/features/dashboard/components/RetentionAnalytics.tsx @@ -44,16 +44,16 @@ export const RetentionAnalytics = ({ {avg} - Mean Turnaround + Average Due Days
-
+
{maxLimit} Days - Max Policy Cap + Maximum Due days
From 1734a94640d9bc878904c11b2d6dd22cdc7ded2c Mon Sep 17 00:00:00 2001 From: Yogeshwaran S Date: Mon, 22 Jun 2026 13:05:39 +0530 Subject: [PATCH 5/6] feat: add new business logic while return the book is added which is note down the book condition while receiving from member --- client/package-lock.json | 257 +++++++- client/package.json | 4 + .../books/components/BookDetailModal.tsx | 506 ++++++++++---- .../features/books/components/BookModal.tsx | 120 ++-- client/src/features/books/pages/BooksPage.tsx | 622 ++++++++++-------- .../src/features/books/schemas/bookSchema.ts | 7 + .../DeleteCategoryConfirmationModal.tsx | 2 +- client/src/features/fines/pages/FinesPage.tsx | 174 +++-- .../issues/components/IssueDetailsModal.tsx | 240 +++---- .../components/UnpaidFineAlertModal.tsx | 26 +- .../issues/pages/TransactionsPage.tsx | 52 +- .../components/DeleteConfirmationModal.tsx | 33 +- .../members/components/MemberDetailsModal.tsx | 558 +++++++++++----- .../members/components/MemberModal.tsx | 2 - .../features/members/pages/MembersPage.tsx | 481 +++++++++++--- .../components/DeletePlanModal.tsx | 4 +- .../components/ReturnedDetailsModal.tsx | 2 +- .../returnedbooks/pages/ReturnedBooks.tsx | 32 + client/src/layouts/DashboardLayout.tsx | 4 +- client/src/types/books.ts | 30 +- client/src/types/transactions.ts | 29 + server/src/database/associations/index.ts | 28 + server/src/database/models/Book.ts | 11 +- server/src/database/models/Issue.ts | 17 + server/src/database/models/PlanHistory.ts | 77 +++ server/src/modules/books/book.controller.ts | 15 +- server/src/modules/books/book.repository.ts | 242 ++++--- server/src/modules/books/book.routes.ts | 31 +- server/src/modules/books/book.service.ts | 57 +- server/src/modules/books/book.types.ts | 23 +- server/src/modules/books/book.validation.ts | 35 +- server/src/modules/fines/fine.controller.ts | 20 +- server/src/modules/fines/fine.service.ts | 38 +- server/src/modules/fines/fine.validation.ts | 12 +- server/src/modules/issues/issue.controller.ts | 19 +- server/src/modules/issues/issue.repository.ts | 40 +- server/src/modules/issues/issue.service.ts | 107 +-- server/src/modules/issues/issue.types.ts | 12 + server/src/modules/issues/issue.validation.ts | 13 +- .../src/modules/members/member.controller.ts | 69 +- .../src/modules/members/member.repository.ts | 246 ++++--- server/src/modules/members/member.routes.ts | 4 +- server/src/modules/members/member.service.ts | 8 +- server/src/modules/members/member.types.ts | 4 +- .../src/modules/members/member.validation.ts | 53 +- 45 files changed, 3010 insertions(+), 1356 deletions(-) create mode 100644 server/src/database/models/PlanHistory.ts 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/src/features/books/components/BookDetailModal.tsx b/client/src/features/books/components/BookDetailModal.tsx index b621562..dc87cf3 100644 --- a/client/src/features/books/components/BookDetailModal.tsx +++ b/client/src/features/books/components/BookDetailModal.tsx @@ -1,171 +1,403 @@ -import { useState } from "react"; -import type { BookInventoryItem } from "../../../types/books"; +import React, { useState } from "react"; +import { jsPDF } from "jspdf"; +import autoTable from "jspdf-autotable"; +import { + ArrowLeft, + Download, + Calendar, + BookOpen, + History, + AlertTriangle, + CheckCircle, + FileText, + Globe, + Tag, + Hash, + X +} from "lucide-react"; import { DeleteBookModal } from "./DeleteBookModal"; +import type { EditingBookInventoryItem } from "../../../types/books"; interface BookDetailModalProps { isOpen: boolean; onClose: () => void; - book: BookInventoryItem | null; + bookDetails: EditingBookInventoryItem | null; + isLoading: boolean; onEditTrigger: () => void; - onDeleteTrigger: () => void; // Restored the original prop name here + onDeleteTrigger: () => void; } -export const BookDetailModal = ({ +export const BookDetailModal: React.FC = ({ isOpen, onClose, - book, - onEditTrigger, - onDeleteTrigger, // Restored here -}: BookDetailModalProps) => { - // Internal state tracking to switch over to the confirmation display layer + bookDetails, + isLoading, + onDeleteTrigger, +}) => { + const [activeTab, setActiveTab] = useState<"bio" | "logs">("bio"); const [isDeleteOpen, setIsDeleteOpen] = useState(false); - if (!isOpen || !book) return null; - - // Format the ISO database timestamp into a human-readable calendar date - const shelfEntryDate = new Date(book.createdAt).toLocaleDateString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - }); + if (!isOpen) return null; + if (!bookDetails && !isLoading) return null; const handleConfirmDeletion = () => { - setIsDeleteOpen(false); // Close the local delete confirmation modal state - onClose(); // Close the core details framework drawer - onDeleteTrigger(); // Execute the original deletion action from BooksPage.tsx + setIsDeleteOpen(false); + onClose(); + onDeleteTrigger(); }; -return ( - <> - {/* High-contrast background overlay with clean backdrop filter */} -
-
- - {/* Header Framework - Matching Reference Module */} -
-
-

- Book Details -

-

- ID: BOOK- - {book.id ? String(book.id).slice(-4).toUpperCase() : "0000"} -

-
- -
- - {/* Detailed Metadata Body Context Frame */} -
-
- - {/* Top Row Title Stack */} -
-
-

- {book.title} -

-

- by {book.author} -

-
- 0 - ? "bg-emerald-50 text-emerald-700 border border-emerald-200" - : "bg-rose-50 text-rose-700 border border-rose-200" - }`} - > - 0 ? "bg-emerald-500" : "bg-rose-500"}`} /> - {book.availableCopies > 0 ? "Available" : "Out of Stock"} - -
+ // Safe fallback configurations + const displayTitle = bookDetails?.book_name || "Loading Title..."; + const displayAuthor = bookDetails?.book_author || "Please wait"; + const displayId = bookDetails?.book_id ? String(bookDetails.book_id).slice(-4).toUpperCase() : "0000"; + const displayLanguage = bookDetails?.language || "N/A"; + const displayCategory = bookDetails?.category?.category_name || "Unclassified"; + const isbn = bookDetails?.isbn || "N/A"; + const historyLog = bookDetails?.history || []; + console.log("history Log ",historyLog) -
+ const availableCount = bookDetails?.available_copies ?? 0; + const totalCount = bookDetails?.total_copies ?? 0; + const borrowCount = bookDetails?.lending_count ?? 0; - {/* Core Info Properties Grid Layout */} -
-
- - Language Spec - - - {book.language} - -
+ const totalDamagedBooks = historyLog.filter(log => log.condition === "DAMAGED").length; -
- - Category Classification - - - {book.categoryName} - -
+ const shelfEntryDate = bookDetails?.created_at + ? new Date(bookDetails.created_at).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }) + : "Processing..."; -
- - Shelf Registry Date - - - {shelfEntryDate} - -
+ // 🚀 Reusable Date Formatter + const formatDate = (dateString: string | null) => { + if (!dateString) return "—"; + return new Date(dateString).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); + }; + + // ==================== PDF REPORT GENERATOR ==================== + const generateStructuredPDF = () => { + if (!bookDetails) return; + const doc = new jsPDF({ orientation: "portrait", unit: "mm", format: "a4" }); + + // Header Banner Box + doc.setFillColor(26, 54, 93); + doc.rect(0, 0, 210, 40, "F"); + + doc.setTextColor(255, 255, 255); + doc.setFont("helvetica", "bold"); + doc.setFontSize(22); + doc.text("LIBRARY MANAGEMENT SYSTEM", 15, 18); + doc.setFontSize(11); + doc.setFont("helvetica", "normal"); + doc.text("Official Catalog Book Profile Inventory Ledger Report", 15, 26); + + // Core Metadata Info Row Block + doc.setTextColor(45, 55, 72); + doc.setFontSize(14); + doc.setFont("helvetica", "bold"); + doc.text("BOOK RECORD METADATA", 15, 52); + + doc.setFontSize(10); + doc.setFont("helvetica", "normal"); + doc.text([ + `Book Volume Title: ${displayTitle}`, + `Author / Creator: ${displayAuthor}`, + `ISBN Reference Identifier: ${isbn}`, + `Language Classification: ${displayLanguage.toUpperCase()}`, + `Assigned Category: ${displayCategory}`, + `Shelf Registry Date: ${shelfEntryDate}` + ], 15, 60); + + // KPI Summary Widget Box Component Pinned Right + doc.setFillColor(247, 250, 252); + doc.rect(140, 58, 55, 28, "F"); + doc.setTextColor(26, 54, 93); + doc.setFont("helvetica", "bold"); + doc.text("INVENTORY STATISTICS", 143, 64); + doc.setFont("helvetica", "normal"); + doc.setFontSize(9); + doc.text(`Total Holdings: ${totalCount} Copies`, 143, 71); + doc.text(`On-Shelf Copies: ${availableCount} Units`, 143, 77); + doc.text(`Damaged Incidents: ${totalDamagedBooks} items`, 143, 83); + + doc.setDrawColor(226, 232, 240); + doc.line(15, 93, 195, 93); + + doc.setTextColor(26, 54, 93); + doc.setFontSize(12); + doc.setFont("helvetica", "bold"); + doc.text("CIRCULATION TIMELINE & HISTORICAL LOGS", 15, 102); + + // Main History Table + autoTable(doc, { + startY: 107, + margin: { left: 15, right: 15 }, + head: [["Active Borrower Name", "Email Address", "Issued On Date", "Returned On Date", "Item Condition State", "Damage Reason"]], + body: historyLog.map((log) => [ + log.member_name, + log.gmail || "N/A", // 🚀 FIXED: Swapped log.gmail to log.email to match types configuration + log.borrow_date ? formatDate(log.borrow_date) : "—", + log.return_date ? formatDate(log.return_date) : "Active Loan", + log.condition, + log.damage_description + ]), + headStyles: { fillColor: [26, 54, 93], fontStyle: "bold" }, + styles: { fontSize: 8.5, font: "helvetica" } + }); + + const clientFileNameSafeText = displayTitle.replace(/\s+/g, "_"); + doc.save(`${clientFileNameSafeText}_inventory_ledger_report.pdf`); + }; + + return ( + <> +
+
+
+ + {/* ==================== WORKSPACE HEADER CONTROLS BAR ==================== */} +
+
+
- - Lending Metrics (Total / Available / Loans) - -
- - {book.totalCopies} Total - - - {book.availableCopies} On Shelf - - - {book.lendingCount}× Borrowed +
+

{displayTitle}

+ 0 ? "bg-emerald-50 text-emerald-700 border-emerald-200" : "bg-rose-50 text-rose-700 border-rose-200" + }`}> + {availableCount > 0 ? "In Stock Available" : "All Copies Loaned"}
+

+ Catalog Inventory Index Code: BOOK-{displayId} +

- {/* Operations Layout Action Buttons - Matching layout rules and theme */} -
+
+ +
+
+ + {/* ==================== CONTENT BODY MATRIX ROW LAYOUT ==================== */} +
+ + {/* LEFT SIDE SPECIFICATIONS CARD BAR */} +
+
+
+
+ +
+

+ {displayTitle} +

+

by {displayAuthor}

+
+ + {/* Data specifications list details */} +
+
+ ISBN Number + {isbn} +
+
+ Language Spec + {displayLanguage} +
+
+ Category Classification + {displayCategory} +
+
+ Shelf Registry Date + {shelfEntryDate} +
+
+
- +
-
-
-
-
- - {/* Embedded inline confirmation screen */} - setIsDeleteOpen(false)} - onConfirm={handleConfirmDeletion} - bookTitle={book.title} - /> - -);} \ No newline at end of file + {/* RIGHT COLUMN AREA PANEL DESK */} +
+ + {/* Tab Nav Menu Headers */} +
+ + +
+ +
+ {isLoading ? ( +
+ Streaming structural log transactions... +
+ ) : ( + <> + {/* TAB PANEL A: META OVERVIEW DATA SUMMARY */} + {activeTab === "bio" && ( +
+
+

+ Catalog Allocation Profile Status +

+
+ +
+
+
+
+ Total Fleet + {totalCount} Copies +
+
+ Available + {availableCount} On Shelf +
+
+ Total Borrowed + {borrowCount} Times +
+
+
+ Damaged Returns + {totalDamagedBooks} Incidence +
+ +
+
+ + {/* Dangerous Operation Triggers Pin Bottom Row */} + +
+ )} + + {/* TAB PANEL B: CIRCULATION HISTORIC LEDGER DATA TABLE */} + {activeTab === "logs" && ( +
+ + + + + + + + + + {/* 🚀 FIXED: Added missing corresponding closing tag for 'thead' */} + + {historyLog.length === 0 ? ( + + + + ) : ( + historyLog.map((log, idx) => ( + + + + + + + + )) + )} + +
Borrower / MemberIssued DateReturned DateCondition StatusDamage Reason
+ No historic checkout records or active transaction footprints found. +
+
{log.member_name}
+ {/* 🚀 FIXED: Changed log.gmail to log.email to match types file parameters */} +
{log.gmail || "N/A"}
+
+ {log.borrow_date ? formatDate(log.borrow_date) : "—"} + + {log.return_date ? ( + {formatDate(log.return_date)} + ) : ( + + Not Returned yet + + )} + + + {log.condition} + + + + {log.damage_description} + +
+
+ )} + + )} +
+
+ +
{/* 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 2209da2..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,11 +58,12 @@ 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({ @@ -71,6 +72,7 @@ export const BookModal = ({ language: "", totalCopies: 1, categoryId: "", + isbn: "", // 🚀 Reset on creation profile layouts context }); } } @@ -84,6 +86,7 @@ export const BookModal = ({ language: "", totalCopies: 1, categoryId: "", + isbn: "", }); onClose(); }; @@ -103,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" }, @@ -125,6 +126,8 @@ 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( @@ -139,16 +142,13 @@ 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 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." @@ -161,17 +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; @@ -189,7 +182,6 @@ export const BookModal = ({ return (
- {/* Core Metadata Elements List */}
{details.map((detail, idx) => { const splitIdx = detail.indexOf(":"); @@ -197,29 +189,18 @@ export const BookModal = ({ const label = detail.substring(0, splitIdx).trim(); const val = detail.substring(splitIdx + 1).trim(); return ( -
- {/* Fixed: Upgraded arbitrary 'min-w-[140px]' to canonical Tailwind v4 'min-w-35' */} - - {label}: - - - {val} - +
+ {label}: + {val}
); } return ( -

- {detail} -

+

{detail}

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

{editingBook ? "Modify Details" : "Add New Book"}

- {editingBook - ? "Update Existing Record" - : "Create New Inventory Registry"} + {editingBook ? "Update Existing Record" : "Create New Inventory Registry"}

)} @@ -321,17 +307,71 @@ export const BooksPage = () => {
+ + + {isSelectionMode && ( + + )} + +
+
- {/* PRIMARY DATA VIEWS LAYOUT CONTAINER */}
{isLoading ? ( @@ -355,29 +394,62 @@ export const BooksPage = () => { - - - {/* 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" }`} > - + )} + - - - + ); }) @@ -554,43 +640,50 @@ export const BooksPage = () => {
- Book Title & Creator Index + {isSelectionMode && } + + + + {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" && ( @@ -415,25 +487,17 @@ export const BooksPage = () => { )} + {activeHeaderDropdown === "category" && ( @@ -471,81 +535,103 @@ export const BooksPage = () => { )} - Availability - AvailabilityActions
- No matching records currently indexed inside this - filtered view. + + No matching records currently indexed inside this filtered view.
-
-
+ +
+
+
{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 " - : "text-rose-700 " - } - > - {book.availableCopies} / {book.totalCopies}{" "} - Left + 0 ? "bg-emerald-500" : "bg-rose-500"}`} /> + 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} - - -
- - + +
+ )}
)}
@@ -598,38 +691,47 @@ 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 || ""} />
); -}; +}; \ No newline at end of file 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/fines/pages/FinesPage.tsx b/client/src/features/fines/pages/FinesPage.tsx index 3ef2758..ac8bcb5 100644 --- a/client/src/features/fines/pages/FinesPage.tsx +++ b/client/src/features/fines/pages/FinesPage.tsx @@ -33,16 +33,26 @@ 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 [selectedFine, setSelectedFine] = useState(null); + 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 [showRestoreModal, setShowRestoreModal] = useState(false); - const [activeHeaderDropdown, setActiveHeaderDropdown] = useState< - "delay" | null - >(null); + const [activeHeaderDropdown, setActiveHeaderDropdown] = useState<"delay" | null>(null); // Active View Tab Panel Layout Selector ("active" | "history") const [activeTab, setActiveTab] = useState<"active" | "history">("active"); @@ -55,10 +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); @@ -77,7 +83,7 @@ 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"); @@ -95,7 +101,7 @@ export const FinePage = () => { }, }); - // 🟢 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...", @@ -117,55 +123,30 @@ export const FinePage = () => { }, }); - // 🟢 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) => @@ -173,7 +154,7 @@ export const FinePage = () => { onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["finesMasterLedgerFeed"] }); setShowRestoreModal(false); - setSelectedFine(null); + clearActiveModalsState(); toast.success("Entry restored to active ledger!", { description: @@ -189,20 +170,26 @@ 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.", @@ -224,7 +211,7 @@ export const FinePage = () => { }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["finesMasterLedgerFeed"] }); - setSelectedFine(null); + clearActiveModalsState(); toast.info("Invoice purged from ledger."); }, }); @@ -337,23 +324,22 @@ export const FinePage = () => { )} {activeTab === "history" && ( -
-
-
- Total Audited Balance Collected -
-
- ₹{aggregateAccruedSumVal}.00 -
-
-
- - {totalItemsCount} Settled Invoices In - Archive Ledger - -
-
-)} +
+
+
+ Total Audited Balance Collected +
+
+ ₹{aggregateAccruedSumVal}.00 +
+
+
+ + {totalItemsCount} Settled Invoices In Archive Ledger + +
+
+ )} {/* Search Filter Control Grid */}
@@ -426,8 +412,7 @@ export const FinePage = () => { : "text-[#718096]" }`} > - Delayed Days - {delayIntervalFilter ? ` (${delayIntervalFilter}+)` : ""} + Delayed Days {delayIntervalFilter ? ` (${delayIntervalFilter}+)` : ""} { colSpan={5} className="text-center py-16 text-slate-400 font-medium" > - Operational Clear View. Zero matching layout targets - found. + Operational Clear View. Zero matching layout targets found. ) : ( @@ -527,9 +511,11 @@ export const FinePage = () => { { - setSelectedFine(fine); if (activeTab === "history") { + 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" @@ -538,7 +524,7 @@ export const FinePage = () => {
{fine.memberName}
-
+
{fine.memberEmail}
@@ -551,10 +537,7 @@ export const FinePage = () => { {fine.bookTitle}
- Due Date:{" "} - {fine.actualReturnDueDate || - fine.actualReturnDate || - "N/A"} + Due Date: {fine.actualReturnDueDate || fine.actualReturnDate || "N/A"} @@ -564,8 +547,7 @@ export const FinePage = () => { ) : ( - Paid ({fine.paidDate || fine.paid_date || "Settled"} - ) + Paid ({fine.paidDate || fine.paid_date || "Settled"}) )} @@ -609,9 +591,7 @@ export const FinePage = () => { {/* Pagination Command Module */}
- Page {currentPage} / {totalPagesCount}{" "} - | Total{" "} - {totalItemsCount} Fines + Page {currentPage} / {totalPagesCount} | Total {totalItemsCount} Fines
); -}; +}; \ No newline at end of file diff --git a/client/src/features/issues/components/IssueDetailsModal.tsx b/client/src/features/issues/components/IssueDetailsModal.tsx index 0e18b08..313aada 100644 --- a/client/src/features/issues/components/IssueDetailsModal.tsx +++ b/client/src/features/issues/components/IssueDetailsModal.tsx @@ -3,23 +3,24 @@ import { useQuery } from "@tanstack/react-query"; import { axiosClient } from "../../../api/axiosClient"; import type { BookIssueRecord } from "../../../types/transactions"; import { useNavigate } from "react-router-dom"; +import { UnpaidFineAlertModal } from "./UnpaidFineAlertModal"; import { - AlertTriangle, - ArrowRight, X, User, BookOpen, Calendar, Edit2, CheckCircle2, + FileText, } from "lucide-react"; interface IssueDetailsModalProps { isOpen: boolean; onClose: () => void; record: BookIssueRecord | null; - onMarkAsReturned: (issueId: string) => void; + // 🚀 Updated callback signature to pass condition evaluation data down upstream + onMarkAsReturned: (issueId: string, condition: "GOOD" | "DAMAGED", damage_description?: string) => void; onTriggerEdit: () => void; } @@ -32,6 +33,11 @@ export const IssueDetailsModal = ({ }: IssueDetailsModalProps) => { const navigate = useNavigate(); const [showFineBlockModal, setShowFineBlockModal] = useState(false); + + // 🚀 New Local State Vectors for Return processing rules + const [bookCondition, setBookCondition] = useState<"GOOD" | "DAMAGED">("GOOD"); + const [damageDescription, setDamageDescription] = useState(""); + const MAX_CHARS = 255; const { data: memberStats, isLoading: isLoadingStats } = useQuery({ queryKey: ["memberHistoricalReturnsCount", record?.memberId], @@ -53,25 +59,39 @@ export const IssueDetailsModal = ({ : `ISSUE-${record.id}`; const handleReturnClick = () => { + // Basic verification check: validation blocks if damaged variant lacks description reasons + if (bookCondition === "DAMAGED" && !damageDescription.trim()) { + return; + } + const hasUnpaidFine = record.fineAmount && record.fineAmount > 0 && !record.finePaidStatus; if (hasUnpaidFine) { + // 💸 UNPAID FINE DETECTED: Trigger the modal portal setShowFineBlockModal(true); } else { - onMarkAsReturned(record.id); + // 🟢 DIRECT CLEAN RETURN COMMIT: Trigger with context variables attached + onMarkAsReturned( + record.id, + bookCondition, + bookCondition === "DAMAGED" ? damageDescription.trim() : undefined + ); } }; + const charactersRemaining = MAX_CHARS - damageDescription.length; + return ( <> {/* Primary Issue Details Window */} -
-
+
+
+

- Issue Details + Issue Details & Return Processor

ID: {formattedIssueId} @@ -86,9 +106,10 @@ export const IssueDetailsModal = ({

-
-
- {/* Member Profile */} +
+
+ + {/* Member Profile Block */}
@@ -131,7 +152,7 @@ export const IssueDetailsModal = ({
- 📖 {record.bookTitle} + `📖 ${record.bookTitle}`
Catalog Author: {record.bookAuthor || "Unknown Reference"} @@ -139,7 +160,7 @@ export const IssueDetailsModal = ({
{/* Timeline Grid */} -
+
@@ -164,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 + +
+