diff --git a/client/.env b/client/.env index 45a230c..503fed8 100755 --- a/client/.env +++ b/client/.env @@ -1,2 +1,3 @@ -VITE_API_URL=https://batchflow.onrender.com +# VITE_API_URL=https://batchflow.onrender.com +VITE_API_URL=https://tgaf-batchflow-backend.onrender.com APP_URL=http://localhost:5173 \ No newline at end of file diff --git a/client/.env.dev b/client/.env.dev index 45a230c..3df8329 100644 --- a/client/.env.dev +++ b/client/.env.dev @@ -1,2 +1,2 @@ -VITE_API_URL=https://batchflow.onrender.com +VITE_API_URL=http://localhost:5000 APP_URL=http://localhost:5173 \ No newline at end of file diff --git a/client/index.html b/client/index.html index 946e418..9085f4c 100755 --- a/client/index.html +++ b/client/index.html @@ -3,16 +3,14 @@ - + - + - TGAF material management + NexInventory diff --git a/client/package-lock.json b/client/package-lock.json index cff47db..9d13cc8 100755 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -64,6 +64,7 @@ "@types/jspdf": "^2.0.0", "@types/lodash": "^4.17.16", "@types/moment": "^2.13.0", + "@types/node": "^25.0.10", "@types/react": "^19.0.10", "@types/react-datepicker": "^7.0.0", "@types/react-dom": "^19.0.4", @@ -2516,13 +2517,13 @@ } }, "node_modules/@types/node": { - "version": "22.15.21", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", - "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", - "optional": true, - "peer": true, + "version": "25.0.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.10.tgz", + "integrity": "sha512-zWW5KPngR/yvakJgGOmZ5vTBemDoSqF3AcV/LrO5u5wTWyEAVVh+IT39G4gtyAkh3CtTZs8aX/yRM82OfzHJRg==", + "devOptional": true, + "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/parse-json": { @@ -6351,6 +6352,7 @@ "version": "0.503.0", "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.503.0.tgz", "integrity": "sha512-HGGkdlPWQ0vTF8jJ5TdIqhQXZi6uh3LnNgfZ8MHiuxFfX3RZeA79r2MW2tHAZKlAVfoNE8esm3p+O6VkIvpj6w==", + "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } @@ -8743,11 +8745,11 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "optional": true, - "peer": true + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "devOptional": true, + "license": "MIT" }, "node_modules/unzipper": { "version": "0.10.14", @@ -9077,19 +9079,6 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/client/package.json b/client/package.json index a878575..502454d 100755 --- a/client/package.json +++ b/client/package.json @@ -66,6 +66,7 @@ "@types/jspdf": "^2.0.0", "@types/lodash": "^4.17.16", "@types/moment": "^2.13.0", + "@types/node": "^25.0.10", "@types/react": "^19.0.10", "@types/react-datepicker": "^7.0.0", "@types/react-dom": "^19.0.4", diff --git a/client/public/logo22.png b/client/public/logo22.png new file mode 100644 index 0000000..034b68d Binary files /dev/null and b/client/public/logo22.png differ diff --git a/client/src/assets/batch.png b/client/src/assets/batch.png new file mode 100644 index 0000000..ae1f5ed Binary files /dev/null and b/client/src/assets/batch.png differ diff --git a/client/src/assets/logo.jpg b/client/src/assets/logo.jpg deleted file mode 100644 index e00306e..0000000 Binary files a/client/src/assets/logo.jpg and /dev/null differ diff --git a/client/src/assets/logo1.jpg b/client/src/assets/logo1.jpg deleted file mode 100644 index e1a84f9..0000000 Binary files a/client/src/assets/logo1.jpg and /dev/null differ diff --git a/client/src/assets/logo22.png b/client/src/assets/logo22.png new file mode 100644 index 0000000..034b68d Binary files /dev/null and b/client/src/assets/logo22.png differ diff --git a/client/src/assets/quality.png b/client/src/assets/quality.png new file mode 100644 index 0000000..061f916 Binary files /dev/null and b/client/src/assets/quality.png differ diff --git a/client/src/assets/styles/calender.css b/client/src/assets/styles/calender.css index 079cc80..3769c79 100644 --- a/client/src/assets/styles/calender.css +++ b/client/src/assets/styles/calender.css @@ -1,15 +1,23 @@ -/* Custom Calendar Styling */ +/* Custom Calendar Styling - Dark Mode Support */ + +/* Calendar wrapper for scoped theming */ +.training-calendar-wrapper { + /* Light mode inherits from parent */ +} + +/* Custom Calendar Header */ .custom-calendar .ant-picker-calendar-header { padding: 12px; - background-color: #f9fafb; + background-color: var(--card); border-radius: 8px; margin-bottom: 16px; - border-bottom: 1px solid #e0e7ff; + border-bottom: 1px solid var(--border); } .custom-calendar .ant-picker-panel { border-radius: 8px; overflow: hidden; + background-color: var(--card); } .custom-calendar .ant-picker-cell { @@ -25,17 +33,17 @@ } .custom-calendar .selected-date { - background-color: rgba(59, 130, 246, 0.1); - border: 1px solid #2563eb; + background-color: color-mix(in srgb, var(--primary) 10%, transparent); + border: 1px solid var(--primary); transform: scale(1.02); } .custom-calendar .weekend-date { - background-color: #f9fafb; + background-color: var(--muted); } .custom-calendar .current-date { - background-color: rgba(37, 99, 235, 0.05); + background-color: color-mix(in srgb, var(--primary) 5%, transparent); position: relative; } @@ -50,6 +58,7 @@ display: block; font-size: 11px; white-space: nowrap; + color: var(--foreground); } /* Cell hover animation */ @@ -61,26 +70,47 @@ /* Day drawer styles */ .day-details-drawer .ant-drawer-body { padding: 16px; - background-color: #fafafa; + background-color: var(--background); } .day-details-drawer .ant-drawer-header { - background-color: #f0f7ff; - border-bottom: 1px solid #bfdbfe; + background-color: var(--card); + border-bottom: 1px solid var(--border); +} + +.day-details-drawer .ant-drawer-title { + color: var(--foreground); +} + +.day-details-drawer .ant-drawer-close { + color: var(--foreground); } /* Calendar description modal */ .calendar-description-modal .ant-modal-header { - background-color: #f0f7ff; - border-bottom: 1px solid #bfdbfe; + background-color: var(--card); + border-bottom: 1px solid var(--border); +} + +.calendar-description-modal .ant-modal-title { + color: var(--foreground); } .calendar-description-modal .ant-modal-content { border-radius: 8px; overflow: hidden; + background-color: var(--card); } -/* Training status colors */ +.calendar-description-modal .ant-modal-body { + color: var(--foreground); +} + +.calendar-description-modal .ant-modal-close { + color: var(--muted-foreground); +} + +/* Training status colors - Light mode */ .status-tag-SCHEDULED { color: #1890ff; background: #e6f7ff; @@ -111,13 +141,44 @@ border-color: #d3adf7; } +/* Training status colors - Dark mode */ +.dark .status-tag-SCHEDULED { + color: #69c0ff; + background: rgba(24, 144, 255, 0.2); + border-color: rgba(24, 144, 255, 0.4); +} + +.dark .status-tag-IN_PROGRESS { + color: #ffc53d; + background: rgba(250, 173, 20, 0.2); + border-color: rgba(250, 173, 20, 0.4); +} + +.dark .status-tag-COMPLETED { + color: #95de64; + background: rgba(82, 196, 26, 0.2); + border-color: rgba(82, 196, 26, 0.4); +} + +.dark .status-tag-CANCELLED { + color: #ff7875; + background: rgba(245, 34, 45, 0.2); + border-color: rgba(245, 34, 45, 0.4); +} + +.dark .status-tag-POSTPONED { + color: #b37feb; + background: rgba(114, 46, 209, 0.2); + border-color: rgba(114, 46, 209, 0.4); +} + /* Select dropdown custom styling */ .select-dropdown-custom .ant-select-item-option-selected { - background-color: #f0f7ff; + background-color: color-mix(in srgb, var(--primary) 10%, transparent); } .select-dropdown-custom .ant-select-item-option-active { - background-color: #f0f7ff; + background-color: color-mix(in srgb, var(--primary) 10%, transparent); } /* Animation improvements */ @@ -130,6 +191,7 @@ opacity: 0; transform: translate3d(0, 20px, 0); } + to { opacity: 1; transform: translate3d(0, 0, 0); @@ -142,4 +204,262 @@ .ant-badge:hover .ant-badge-status-dot { transform: scale(1.2); +} + +/* Training calendar wrapper specific styles */ +.training-calendar-wrapper select { + background-color: var(--card) !important; + color: var(--foreground) !important; + border-color: var(--border) !important; +} + +.training-calendar-wrapper select option { + background-color: var(--card); + color: var(--foreground); +} + +.training-calendar-wrapper input { + background-color: var(--card) !important; + color: var(--foreground) !important; + border-color: var(--border) !important; +} + +.training-calendar-wrapper input::placeholder { + color: var(--muted-foreground) !important; +} + +.training-calendar-wrapper textarea { + background-color: var(--card) !important; + color: var(--foreground) !important; + border-color: var(--border) !important; +} + +.training-calendar-wrapper textarea::placeholder { + color: var(--muted-foreground) !important; +} + +/* Dark mode shadow adjustments */ +.dark .training-calendar-wrapper .shadow-lg { + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.2) !important; +} + +.dark .training-calendar-wrapper .shadow-md { + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3), 0 2px 4px -1px rgba(0, 0, 0, 0.2) !important; +} + +.dark .training-calendar-wrapper .shadow-sm { + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.3) !important; +} + +/* Hover states in dark mode */ +.dark .training-calendar-wrapper .hover\:shadow-lg:hover { + box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.4), 0 4px 10px -2px rgba(0, 0, 0, 0.3) !important; +} + +.dark .training-calendar-wrapper .hover\:shadow-md:hover { + box-shadow: 0 6px 12px -1px rgba(0, 0, 0, 0.35), 0 3px 6px -1px rgba(0, 0, 0, 0.25) !important; +} + +/* ===================================================== + AUDIT CALENDAR - FULLCALENDAR DARK MODE SUPPORT + ===================================================== */ + +/* Audit Calendar Wrapper */ +.audit-calendar-wrapper { + background-color: var(--card); +} + +/* FullCalendar Base Styles */ +.audit-calendar-wrapper .fc { + background-color: var(--card); + color: var(--foreground); +} + +/* FullCalendar Table */ +.audit-calendar-wrapper .fc-scrollgrid { + border-color: var(--border) !important; +} + +.audit-calendar-wrapper .fc-scrollgrid td, +.audit-calendar-wrapper .fc-scrollgrid th { + border-color: var(--border) !important; +} + +/* Day Grid Month View */ +.audit-calendar-wrapper .fc-daygrid-day { + background-color: var(--card); +} + +.audit-calendar-wrapper .fc-daygrid-day:hover { + background-color: var(--muted); +} + +.audit-calendar-wrapper .fc-daygrid-day-number { + color: var(--foreground); + font-weight: 500; +} + +.audit-calendar-wrapper .fc-daygrid-day-top { + padding: 4px 8px; +} + +/* Today Highlight */ +.audit-calendar-wrapper .fc-day-today { + background-color: color-mix(in srgb, var(--primary) 10%, transparent) !important; +} + +.audit-calendar-wrapper .fc-day-today .fc-daygrid-day-number { + color: var(--primary); + font-weight: 700; +} + +/* Weekend Days */ +.audit-calendar-wrapper .fc-day-sat, +.audit-calendar-wrapper .fc-day-sun { + background-color: var(--muted); +} + +/* Column Headers (Day Names) */ +.audit-calendar-wrapper .fc-col-header-cell { + background-color: var(--muted); + border-color: var(--border) !important; +} + +.audit-calendar-wrapper .fc-col-header-cell-cushion { + color: var(--foreground); + font-weight: 600; + padding: 12px 8px; +} + +/* Time Grid View (Week/Day) */ +.audit-calendar-wrapper .fc-timegrid-slot { + border-color: var(--border) !important; +} + +.audit-calendar-wrapper .fc-timegrid-slot-label-cushion { + color: var(--muted-foreground); +} + +.audit-calendar-wrapper .fc-timegrid-axis-cushion { + color: var(--muted-foreground); +} + +.audit-calendar-wrapper .fc-timegrid-now-indicator-line { + border-color: var(--primary) !important; +} + +.audit-calendar-wrapper .fc-timegrid-now-indicator-arrow { + border-color: var(--primary) !important; + border-top-color: transparent !important; + border-bottom-color: transparent !important; +} + +/* List View */ +.audit-calendar-wrapper .fc-list { + border-color: var(--border) !important; +} + +.audit-calendar-wrapper .fc-list-sticky .fc-list-day>* { + background-color: var(--muted) !important; +} + +.audit-calendar-wrapper .fc-list-day-cushion { + background-color: var(--muted) !important; + color: var(--foreground); +} + +.audit-calendar-wrapper .fc-list-event { + background-color: var(--card); +} + +.audit-calendar-wrapper .fc-list-event:hover td { + background-color: var(--muted) !important; +} + +.audit-calendar-wrapper .fc-list-event-time, +.audit-calendar-wrapper .fc-list-event-title { + color: var(--foreground); +} + +/* Buttons (if any default toolbar is shown) */ +.audit-calendar-wrapper .fc-button { + background-color: var(--card) !important; + border-color: var(--border) !important; + color: var(--foreground) !important; +} + +.audit-calendar-wrapper .fc-button:hover { + background-color: var(--muted) !important; +} + +.audit-calendar-wrapper .fc-button-active { + background-color: var(--primary) !important; + color: var(--primary-foreground) !important; +} + +/* More Events Popover */ +.audit-calendar-wrapper .fc-more-popover { + background-color: var(--card); + border-color: var(--border); + box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.3); +} + +.audit-calendar-wrapper .fc-more-popover .fc-popover-header { + background-color: var(--muted); + color: var(--foreground); +} + +.audit-calendar-wrapper .fc-more-popover .fc-popover-body { + background-color: var(--card); +} + +/* Other/Past Days */ +.audit-calendar-wrapper .fc-day-other .fc-daygrid-day-number { + color: var(--muted-foreground); +} + +/* Events */ +.audit-calendar-wrapper .fc-event { + border-radius: 4px; + margin-bottom: 2px; +} + +.audit-calendar-wrapper .fc-daygrid-event { + border-radius: 4px; +} + +.audit-calendar-wrapper .fc-daygrid-more-link { + color: var(--primary); + font-weight: 500; +} + +/* Dark mode specific overrides */ +.dark .audit-calendar-wrapper .fc-scrollgrid { + border-color: var(--border) !important; +} + +.dark .audit-calendar-wrapper .fc-scrollgrid td, +.dark .audit-calendar-wrapper .fc-scrollgrid th { + border-color: var(--border) !important; +} + +.dark .audit-calendar-wrapper .fc-daygrid-day { + background-color: var(--card); +} + +.dark .audit-calendar-wrapper .fc-col-header-cell { + background-color: var(--muted); +} + +.dark .audit-calendar-wrapper .fc-list-day-cushion { + background-color: var(--muted) !important; +} + +.dark .audit-calendar-wrapper .fc-more-popover { + background-color: var(--card); + border-color: var(--border); +} + +.dark .audit-calendar-wrapper .fc-more-popover .fc-popover-header { + background-color: var(--muted); } \ No newline at end of file diff --git a/client/src/assets/training.png b/client/src/assets/training.png new file mode 100644 index 0000000..dccd6d4 Binary files /dev/null and b/client/src/assets/training.png differ diff --git a/client/src/components/common/EditableCell.tsx b/client/src/components/common/EditableCell.tsx index b1492a7..c11a2ca 100755 --- a/client/src/components/common/EditableCell.tsx +++ b/client/src/components/common/EditableCell.tsx @@ -55,12 +55,20 @@ const EditableCell: React.FC = ({ value, onSave }) => { onChange={(e) => setCurrentValue(e.target.value)} onBlur={handleBlur} onKeyDown={handleKeyDown} - className="border border-gray-300 rounded px-2 py-1 w-full" + className="rounded px-2 py-1 w-full focus:outline-none focus:ring-2 focus:ring-[var(--primary)]" + style={{ + background: 'var(--card)', + color: 'var(--foreground)', + border: '1px solid var(--border)' + }} /> ) : (
setIsEditing(true)} - className="cursor-pointer hover:bg-gray-100 px-2 py-1" + className="cursor-pointer px-2 py-1 rounded transition-colors" + style={{ color: 'var(--foreground)' }} + onMouseEnter={(e) => e.currentTarget.style.background = 'var(--muted)'} + onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'} > {value || "N/A"}
diff --git a/client/src/components/layout/AppLayout.tsx b/client/src/components/layout/AppLayout.tsx index df67d3b..02f67de 100755 --- a/client/src/components/layout/AppLayout.tsx +++ b/client/src/components/layout/AppLayout.tsx @@ -157,35 +157,34 @@ const AppLayout = () => { }} /> - {/* Sidebar */} - - {/* Fixed position header - static, no animations */} -
+
+ {/* Sidebar */} + + {/* Main Content */} - {/* Content area with padding (below header) */} -
+ {/* Content area - no top padding, use margin instead */} +
{/* Main Content Container */} = ({ const [showSearchPanel, setShowSearchPanel] = useState(false); const [searchQuery, setSearchQuery] = useState(''); const navigate = useNavigate(); - const [theme, setTheme] = useState<'light' | 'dark'>('light'); const brandRef = useRef(null); const { data: user, isLoading } = useQuery({ @@ -97,24 +94,54 @@ const HeaderBar: React.FC = ({ setShowSearchPanel(false); }; - const toggleTheme = () => { - if (theme === 'light') { - document.body.classList.add('dark'); - setTheme('dark'); - localStorage.setItem('theme', 'dark'); - } else { - document.body.classList.remove('dark'); - setTheme('light'); - localStorage.setItem('theme', 'light'); - } - }; + // Theme toggle disabled + // const toggleTheme = () => { + // if (theme === 'light') { + // document.body.classList.add('dark'); + // setTheme('dark'); + // localStorage.setItem('theme', 'dark'); + // } else { + // document.body.classList.remove('dark'); + // setTheme('light'); + // localStorage.setItem('theme', 'light'); + // } + // }; - useEffect(() => { - // Always set theme to light on mount - document.body.classList.remove('dark'); - setTheme('light'); - localStorage.setItem('theme', 'light'); - }, []); + // Theme initialization and system preference listener disabled + // useEffect(() => { + // // Check for saved theme preference, default to light + // const savedTheme = localStorage.getItem('theme'); + // + // if (savedTheme === 'dark') { + // document.body.classList.add('dark'); + // setTheme('dark'); + // } else { + // document.body.classList.remove('dark'); + // setTheme('light'); + // // Set default if not already set + // if (!savedTheme) { + // localStorage.setItem('theme', 'light'); + // } + // } + // + // // Listen for system theme changes if no manual preference is set + // const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + // const handleChange = (e: MediaQueryListEvent) => { + // const currentSavedTheme = localStorage.getItem('theme'); + // if (!currentSavedTheme) { + // if (e.matches) { + // document.body.classList.add('dark'); + // setTheme('dark'); + // } else { + // document.body.classList.remove('dark'); + // setTheme('light'); + // } + // } + // }; + // + // mediaQuery.addEventListener('change', handleChange); + // return () => mediaQuery.removeEventListener('change', handleChange); + // }, []); // GSAP animation for brand name on mount useEffect(() => { @@ -271,47 +298,72 @@ const HeaderBar: React.FC = ({ return (
-
-
- {/* Brand Logo and Name */} -
+ {/* Brand Logo and Name */} + {/* Brand Logo and Name */} +
+ NexInventory Logo +
+ {/* Action Buttons */} +
+ {/* Theme toggle hidden (disabled) */} + {/* - NexInventory Logo -
- {/* Action Buttons */} -
- {/* Theme Toggle Button */} + {theme === 'dark' ? ( + + ) : ( + + )} + */} + {/* Search Button */} +
= ({ justifyContent: 'center', }} > - {theme === 'dark' ? ( - - ) : ( - - )} + - {/* Search Button */} -
- - - - {/* Search Panel */} - - {showSearchPanel && ( - e.stopPropagation()} + {/* Search Panel */} + + {showSearchPanel && ( + e.stopPropagation()} + style={{ + backdropFilter: 'blur(20px)', + background: 'var(--popover)', + border: '1px solid var(--border)', + + }} + > +
-
-
- - setSearchQuery(e.target.value)} - className="w-full pl-8 pr-3 py-2 rounded-lg focus:outline-none focus:ring-2 transition-all text-sm" +
+ + setSearchQuery(e.target.value)} + className="w-full pl-8 pr-3 py-2 rounded-lg focus:outline-none focus:ring-2 transition-all text-sm" + style={{ + background: 'var(--card)', + border: '1px solid var(--border)', + color: 'var(--foreground)', + }} + autoFocus + /> +
+
+
+
+

+ Quick Access +

+ {filteredSuggestions.map((item, index) => ( + { + navigate(item.path); + setShowSearchPanel(false); + setSearchQuery(''); + }} style={{ - background: 'var(--card)', - border: '1px solid var(--border)', + background: 'transparent', color: 'var(--foreground)', }} - autoFocus - /> -
-
-
-
-

- Quick Access -

- {filteredSuggestions.map((item, index) => ( - { - navigate(item.path); - setShowSearchPanel(false); - setSearchQuery(''); - }} - style={{ - background: 'transparent', - color: 'var(--foreground)', - }} - > - {item.icon} -
-

- {item.title} -

-

- {item.category} -

-
- {item.icon} +
+

+ {item.title} +

+

- - ))} -

+ > + {item.category} +

+
+ + + ))}
- - )} - -
- {/* Media Library */} +
+
+ )} +
+
+ {/* Media Library */} + + + + {/* Activity Logs */} +
= ({ justifyContent: 'center', }} > - - - {/* Activity Logs */} -
- + - + /> + + {/* Activity logs dropdown */} + + {showActivityLogs && ( e.stopPropagation()} style={{ - background: 'var(--primary)', + backdropFilter: 'blur(20px)', + background: 'var(--popover)', + border: '1px solid var(--border)', }} - /> - - {/* Activity logs dropdown */} - - {showActivityLogs && ( - e.stopPropagation()} + > +
-
- + + + Recent Activity + +

+ Latest system activities +

+
+
+ {isLoadingLogs ? ( +
+

- - - Recent Activity - -

+

+ ) : activityLogs.length === 0 ? ( +
- Latest system activities -

-
-
- {isLoadingLogs ? ( -
- -

- Loading activities... -

-
- ) : activityLogs.length === 0 ? ( -
- -

- No recent activities -

-

- Activities will appear here when available -

-
- ) : ( -
- {activityLogs.map( - (log: ActivityLogType, index: number) => ( - -
- - {getActivityIcon(log.action)} - -
-
-

- {log.action.replace(/_/g, ' ')} -

-
- - {formatDate(log.createdAt)} -
-
+ +

+ No recent activities +

+

+ Activities will appear here when available +

+
+ ) : ( +
+ {activityLogs.map( + (log: ActivityLogType, index: number) => ( + +
+ + {getActivityIcon(log.action)} + +
+

- {log.details} + {log.action.replace(/_/g, ' ')}

-
-
- - {log.User?.name || 'System'} -
+
+ + {formatDate(log.createdAt)} +
+
+

+ {log.details} +

+
+
+ + {log.User?.name || 'System'}
- - ) - )} -
- )} -
-
+ + ) + )} +
+ )} +
+
+ navigate('/activity-logs')} style={{ - borderColor: 'var(--border)', - background: 'var(--popover)', + background: 'var(--primary)', + color: 'var(--primary-foreground)', }} > - navigate('/activity-logs')} - style={{ - background: 'var(--primary)', - color: 'var(--primary-foreground)', - }} - > - View All Activities - - -
- - )} - -
- {/* User profile */} -
+ View All Activities + + +
+
+ )} + +
+ {/* User profile */} +
+ +
+

+ {isLoading ? 'Loading...' : user?.name || 'User'} +

+

+ {isLoading + ? '...' + : user?.email?.split('@')[0] || 'guest'} +

+
+ + + -
-

- {isLoading ? 'Loading...' : user?.name || 'User'} -

-

- {isLoading - ? '...' - : user?.email?.split('@')[0] || 'guest'} -

-
+ - - + className="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 border rounded-full" + animate={{ scale: [1, 1.2, 1] }} + transition={{ duration: 2, repeat: Infinity }} + style={{ + background: 'var(--primary)', + borderColor: 'var(--card)', + }} + /> +
+
+ {/* User dropdown menu */} + + {showUserMenu && ( e.stopPropagation()} style={{ - background: - 'linear-gradient(135deg, var(--primary), var(--sidebar-primary))', - boxShadow: '0 2px 8px 0 var(--sidebar-primary)', + backdropFilter: 'blur(20px)', + background: 'var(--popover)', + border: '1px solid var(--border)', }} > - - - - - {/* User dropdown menu */} - - {showUserMenu && ( - e.stopPropagation()} - style={{ - backdropFilter: 'blur(20px)', + borderColor: 'var(--border)', background: 'var(--popover)', - border: '1px solid var(--border)', }} > -
-
- + + + +
+

- - -

-

- {isLoading - ? 'Loading...' - : user?.name || 'Guest User'} -

-

- {isLoading - ? 'Loading...' - : user?.email || 'guest@example.com'} -

-
-
-
-
- {[ - { - icon: User, - label: 'Profile', - path: '/profile', - color: 'var(--primary)', - }, - { - icon: Settings, - label: 'Settings', - path: '/settings', - color: 'var(--sidebar-primary)', - }, - { - icon: Shield, - label: 'Access Control', - path: '/access-control', - color: 'var(--sidebar-accent)', - }, - ].map((item, index) => ( - navigate(item.path)} - style={{ - color: 'var(--foreground)', - background: 'transparent', - }} + {isLoading + ? 'Loading...' + : user?.name || 'Guest User'} +

+

- - - - - {item.label} - - - - - - ))} + {isLoading + ? 'Loading...' + : user?.email || 'guest@example.com'} +

+
-
+
+
+ {[ + { + icon: User, + label: 'Profile', + path: '/profile', + color: 'var(--primary)', + }, + { + icon: Settings, + label: 'Settings', + path: '/settings', + color: 'var(--sidebar-primary)', + }, + { + icon: Shield, + label: 'Access Control', + path: '/access-control', + color: 'var(--sidebar-accent)', + }, + ].map((item, index) => ( navigate(item.path)} style={{ - color: 'var(--destructive)', + color: 'var(--foreground)', background: 'transparent', }} > @@ -904,24 +886,67 @@ const HeaderBar: React.FC = ({ className="flex items-center justify-center w-8 h-8 rounded-lg transition-all shadow-sm" whileHover={{ scale: 1.05 }} style={{ - background: 'var(--destructive)', - color: 'var(--destructive-foreground)', + background: item.color, + color: 'var(--primary-foreground)', }} > - + - Logout + {item.label} + + + -
-
- )} -
-
+ ))} +
+
+ + + + + + Logout + + +
+ + )} +
diff --git a/client/src/components/pages/Audit/AuditCalender.tsx b/client/src/components/pages/Audit/AuditCalender.tsx index 5f3715f..5906697 100644 --- a/client/src/components/pages/Audit/AuditCalender.tsx +++ b/client/src/components/pages/Audit/AuditCalender.tsx @@ -130,11 +130,9 @@ const AuditCalendar: React.FC = () => { let start: Date, end: Date; if (filters.month !== null) { - // Month view - show specific month start = startOfMonth(new Date(filters.year, filters.month)); end = endOfMonth(new Date(filters.year, filters.month)); } else { - // Year view - show entire year start = startOfYear(new Date(filters.year, 0)); end = endOfYear(new Date(filters.year, 11)); } @@ -260,16 +258,16 @@ const AuditCalendar: React.FC = () => { }; return ( -
+
{/* Enhanced Header Section */} -
+
{/* Background Pattern */}
{ initial={{ scale: 0, rotate: -180 }} animate={{ scale: 1, rotate: 0 }} transition={{ duration: 0.6, delay: 0.3 }} - className="bg-white bg-opacity-20 backdrop-blur-sm p-3 rounded-xl mr-4 border border-white/30" + className="bg-white/20 backdrop-blur-sm p-3 rounded-xl mr-4 border border-white/30" > - +
Audit Calendar @@ -305,7 +303,7 @@ const AuditCalendar: React.FC = () => { initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ delay: 0.5 }} - className="text-blue-100 text-lg mt-1 flex items-center" + className="text-primary-foreground/80 text-lg mt-1 flex items-center" > {filters.month !== null @@ -324,15 +322,15 @@ const AuditCalendar: React.FC = () => { - +
{/* Month Filter */} @@ -340,16 +338,16 @@ const AuditCalendar: React.FC = () => { - +
{/* Filter Button */} @@ -359,8 +357,8 @@ const AuditCalendar: React.FC = () => { onClick={() => setShowFilters(!showFilters)} className={`flex items-center gap-2 rounded-xl px-5 py-3 border font-medium shadow-lg transition-all ${ showFilters || (filters.auditType || filters.status || filters.departmentId) - ? 'bg-white text-blue-600 border-transparent shadow-xl' - : 'bg-white/20 backdrop-blur-sm text-white border-white/30 hover:bg-white/30' + ? 'bg-card text-primary border-transparent shadow-xl' + : 'bg-white/20 backdrop-blur-sm text-primary-foreground border-white/30 hover:bg-white/30' }`} > @@ -369,7 +367,7 @@ const AuditCalendar: React.FC = () => { {[filters.auditType, filters.status, filters.departmentId].filter(Boolean).length} @@ -383,8 +381,8 @@ const AuditCalendar: React.FC = () => { onClick={() => setViewMode('calendar')} className={`flex items-center gap-2 px-4 py-3 font-medium transition-all ${ viewMode === 'calendar' - ? 'bg-white text-blue-600 shadow-lg' - : 'text-white' + ? 'bg-card text-primary shadow-lg' + : 'text-primary-foreground' }`} > @@ -395,8 +393,8 @@ const AuditCalendar: React.FC = () => { onClick={() => setViewMode('list')} className={`flex items-center gap-2 px-4 py-3 font-medium transition-all ${ viewMode === 'list' - ? 'bg-white text-blue-600 shadow-lg' - : 'text-white' + ? 'bg-card text-primary shadow-lg' + : 'text-primary-foreground' }`} > @@ -416,16 +414,16 @@ const AuditCalendar: React.FC = () => { animate={{ height: 'auto', opacity: 1, y: 0 }} exit={{ height: 0, opacity: 0, y: -20 }} transition={{ duration: 0.4, ease: "easeInOut" }} - className="bg-gradient-to-r from-gray-50 to-blue-50 border-b border-gray-200 overflow-hidden" + className="bg-muted/50 border-b border-border overflow-hidden" >
- + Advanced Filters @@ -435,7 +433,7 @@ const AuditCalendar: React.FC = () => { whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} onClick={resetFilters} - className="flex items-center gap-2 px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300 transition-all font-medium" + className="flex items-center gap-2 px-4 py-2 bg-muted text-foreground rounded-lg hover:bg-muted/80 transition-all font-medium" > Reset All @@ -450,14 +448,14 @@ const AuditCalendar: React.FC = () => { transition={{ delay: 0.1 }} className="space-y-2" > -
-
- ); -}; + ); + }; const BatchVerification: React.FC = () => { const [searchTerm, setSearchTerm] = useState(''); @@ -634,39 +621,29 @@ const BatchVerification: React.FC = () => { if (error) { return ( -
- +
+
-

+

Error Loading Batches

-

+

Failed to load batches for verification

- refetch()} - className="px-6 py-3 bg-blue-600 text-white rounded-xl hover:bg-blue-700 transition-all font-medium" + className="px-6 py-3 rounded transition-colors font-medium" + style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }} > Try Again - - + +
); } return ( - +
{/* Show details view if selected */} {selectedBatchId ? ( @@ -674,14 +651,11 @@ const BatchVerification: React.FC = () => { {parametersLoading ? (
{[...Array(3)].map((_, i) => ( -
-
+
+
{[...Array(4)].map((_, j) => ( -
+
))}
@@ -690,41 +664,37 @@ const BatchVerification: React.FC = () => { ) : parametersData ? (
{/* Enhanced Batch Info */} - -
+
+
- - - + + Back - -
- + +
+
-

+

{parametersData.batch.batchNumber}

-

+

{parametersData.batch.product.name}

-

+

Total Parameters

-

+

{parametersData.totalParameters}

@@ -733,13 +703,13 @@ const BatchVerification: React.FC = () => {
-
- +
+
- + Production Date -

+

{new Date( parametersData.batch.dateOfProduction ).toLocaleDateString()} @@ -747,70 +717,70 @@ const BatchVerification: React.FC = () => {

-
- +
+
- + Best Before Date -

+

{parametersData.batch.bestBeforeDate ? new Date( - parametersData.batch.bestBeforeDate - ).toLocaleDateString() + parametersData.batch.bestBeforeDate + ).toLocaleDateString() : 'N/A'}

-
- +
+
- + Maker -

+

{parametersData.batch.maker.name}

-
- +
+
- + Sample Analysis Started -

+

{parametersData.batch.sampleAnalysisStarted ? new Date( - parametersData.batch.sampleAnalysisStarted - ).toLocaleDateString() + parametersData.batch.sampleAnalysisStarted + ).toLocaleDateString() : 'N/A'}

-
- +
+
- + Sample Analysis Completed -

+

{parametersData.batch.sampleAnalysisCompleted ? new Date( - parametersData.batch.sampleAnalysisCompleted - ).toLocaleDateString() + parametersData.batch.sampleAnalysisCompleted + ).toLocaleDateString() : 'In Progress'}

-
- +
+
- + Status
@@ -831,22 +801,21 @@ const BatchVerification: React.FC = () => { if (isVerified) { return (
- Export Certificate of Analysis - +
); } return null; })()}
- +
{/* Show verification status if batch is verified */} {(() => { @@ -858,51 +827,25 @@ const BatchVerification: React.FC = () => { if (isVerified) { return ( - +
-
+
{selectedBatch?.status === 'APPROVED' ? ( - + ) : ( - + )}
-

- Batch{' '} - {selectedBatch?.status === 'APPROVED' - ? 'Approved' - : 'Rejected'} +

+ Batch {selectedBatch?.status === 'APPROVED' ? 'Approved' : 'Rejected'}

This batch has been{' '} {selectedBatch?.status?.toLowerCase()} and @@ -916,7 +859,7 @@ const BatchVerification: React.FC = () => {

- +
); } return null; @@ -924,7 +867,7 @@ const BatchVerification: React.FC = () => { {/* Enhanced Parameters by Category */} {Object.entries(parametersData.parametersByCategory).map( - ([category, parameters], categoryIndex) => { + ([category, parameters]) => { const selectedBatch = batches.find( (b) => b.id === selectedBatchId ); @@ -932,41 +875,29 @@ const BatchVerification: React.FC = () => { selectedBatch && isBatchVerified(selectedBatch); return ( - -
+
+
-
- +
+
-

+

{category}

-

+

{(parameters as any[]).length} parameters {isVerified ? ' (verified)' : ' to verify'}

- +
{(parameters as any[]).length} tests - +
@@ -979,7 +910,7 @@ const BatchVerification: React.FC = () => { isDisabled={isVerified} />
- +
); } )} @@ -994,20 +925,17 @@ const BatchVerification: React.FC = () => { if (!isVerified) { return ( - -
+
+
-
- +
+
-

+

Complete Verification

-

+

Save test results and make final decision

@@ -1016,47 +944,41 @@ const BatchVerification: React.FC = () => {
- Save Progress - + - { - const remarks = prompt( - 'Enter rejection remarks:' - ); - if (remarks) - handleCompleteBatch('REJECT', remarks); + const remarks = prompt('Enter rejection remarks:'); + if (remarks) handleCompleteBatch('REJECT', remarks); }} disabled={completeBatchMutation.isPending} - className="px-8 py-4 bg-gradient-to-r from-red-600 to-rose-600 text-white rounded-xl hover:from-red-700 hover:to-rose-700 disabled:opacity-50 transition-all shadow-lg hover:shadow-xl flex items-center gap-3 font-semibold text-lg" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} + className="px-8 py-4 rounded disabled:opacity-50 transition-colors flex items-center gap-3 font-semibold text-lg" + style={{ background: 'var(--destructive)', color: 'var(--destructive-foreground)' }} > Reject Batch - + - handleCompleteBatch('APPROVE')} disabled={completeBatchMutation.isPending} - className="px-8 py-4 bg-gradient-to-r from-green-600 to-emerald-600 text-white rounded-xl hover:from-green-700 hover:to-emerald-700 disabled:opacity-50 transition-all shadow-lg hover:shadow-xl flex items-center gap-3 font-semibold text-lg" - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} + className="px-8 py-4 rounded disabled:opacity-50 transition-colors flex items-center gap-3 font-semibold text-lg" + style={{ background: 'var(--success, #16a34a)', color: '#fff' }} > Approve Batch - +
- +
); } return null; @@ -1066,32 +988,23 @@ const BatchVerification: React.FC = () => {
) : ( /* Main Container */ - +
{/* Header Section */} -
-
- -
- -
-
-
-
-
- -
-
-

- Batch Verification -

-

- Review and verify quality parameters for submitted - batches -

-
+
+
+
+
+
+ +
+
+

+ Batch Verification +

+

+ Review and verify quality parameters for submitted + batches +

@@ -1099,271 +1012,234 @@ const BatchVerification: React.FC = () => {
{/* Search and Filters Section */} -
+
- +
setSearchTerm(e.target.value)} - className="pl-10 pr-4 py-2.5 w-full border border-gray-200 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-all duration-200 shadow-sm text-sm" + className="pl-10 pr-4 py-2.5 w-full rounded focus:outline-none transition-colors shadow-sm text-sm" + style={{ border: '1px solid var(--border)', background: 'var(--card)', color: 'var(--foreground)' }} />
- setIsFilterOpen(!isFilterOpen)} - className="flex items-center gap-2 px-4 py-2.5 border border-gray-200 bg-white rounded-lg hover:bg-gray-50 transition-colors duration-200 shadow-sm text-sm" - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} + className="flex items-center gap-2 px-4 py-2.5 rounded transition-colors text-sm" + style={{ border: '1px solid var(--border)', background: 'var(--card)', color: 'var(--foreground)' }} > - - Filters - - - - + + Filters + + - refetch()} - className="flex items-center gap-2 px-4 py-2.5 border border-gray-200 bg-white rounded-lg hover:bg-gray-50 transition-colors duration-200 shadow-sm text-sm" - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} + className="flex items-center gap-2 px-4 py-2.5 rounded transition-colors text-sm" + style={{ border: '1px solid var(--border)', background: 'var(--card)', color: 'var(--foreground)' }} > - - Refresh - + + Refresh +
{/* Updated Filter Section */} - - {isFilterOpen && ( - -
-
-
- - -
-
+ {isFilterOpen && ( +
+
+
+ + +
+
-
- { - setSearchTerm(''); - setFilterStatus('all'); - }} - className="px-4 py-2 border border-gray-200 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors duration-200 text-sm" - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > -
- - Clear -
-
- setIsFilterOpen(false)} - className="px-4 py-2 bg-blue-600 text-white rounded-lg shadow-md text-sm" - whileHover={{ scale: 1.02 }} - whileTap={{ scale: 0.98 }} - > -
- - Apply -
-
+
+
- - )} - + + +
+
+ )}
{/* Table Section */}
{isLoading ? (
- +
) : filteredBatches.length === 0 ? ( -
-
- +
+
+
-

+

No batches found

-

+

No batches are currently available for verification or match your search criteria.

) : (
- +
- + - + {filteredBatches.map( - (batch: BatchForVerification, index: number) => ( - - + - - - - - + ) )} @@ -1371,10 +1247,10 @@ const BatchVerification: React.FC = () => { )} - + )} - + ); }; diff --git a/client/src/components/pages/Dashboard/auditdashboard.tsx b/client/src/components/pages/Dashboard/auditdashboard.tsx index 30bff4e..3804587 100644 --- a/client/src/components/pages/Dashboard/auditdashboard.tsx +++ b/client/src/components/pages/Dashboard/auditdashboard.tsx @@ -176,26 +176,26 @@ const cardVariants = { } }; -// Status color mapping +// Status color mapping - theme aware const getStatusColor = (status: string) => { const colors = { - 'COMPLETED': 'text-green-600 bg-green-100', - 'IN_PROGRESS': 'text-blue-600 bg-blue-100', - 'PLANNED': 'text-yellow-600 bg-yellow-100', - 'DRAFT': 'text-gray-600 bg-gray-100', - 'CANCELLED': 'text-red-600 bg-red-100' + 'COMPLETED': 'text-green-700 bg-green-100 dark:bg-green-900/40 dark:text-green-300', + 'IN_PROGRESS': 'text-blue-700 bg-blue-100 dark:bg-blue-900/40 dark:text-blue-300', + 'PLANNED': 'text-yellow-700 bg-yellow-100 dark:bg-yellow-900/40 dark:text-yellow-300', + 'DRAFT': 'text-gray-700 bg-gray-100 dark:bg-gray-700/40 dark:text-gray-300', + 'CANCELLED': 'text-red-700 bg-red-100 dark:bg-red-900/40 dark:text-red-300' }; - return colors[status as keyof typeof colors] || 'text-gray-600 bg-gray-100'; + return colors[status as keyof typeof colors] || 'text-gray-700 bg-gray-100 dark:bg-gray-700/40 dark:text-gray-300'; }; const getPriorityColor = (priority: string) => { const colors = { - 'CRITICAL': 'text-red-600 bg-red-100', - 'HIGH': 'text-orange-600 bg-orange-100', - 'MEDIUM': 'text-yellow-600 bg-yellow-100', - 'LOW': 'text-green-600 bg-green-100' + 'CRITICAL': 'text-red-700 bg-red-100 dark:bg-red-900/40 dark:text-red-300', + 'HIGH': 'text-orange-700 bg-orange-100 dark:bg-orange-900/40 dark:text-orange-300', + 'MEDIUM': 'text-yellow-700 bg-yellow-100 dark:bg-yellow-900/40 dark:text-yellow-300', + 'LOW': 'text-green-700 bg-green-100 dark:bg-green-900/40 dark:text-green-300' }; - return colors[priority as keyof typeof colors] || 'text-gray-600 bg-gray-100'; + return colors[priority as keyof typeof colors] || 'text-gray-700 bg-gray-100 dark:bg-gray-700/40 dark:text-gray-300'; }; // Components @@ -205,29 +205,30 @@ const StatCard: React.FC<{ icon: React.ReactNode; change?: string; changeType?: 'increase' | 'decrease'; - color: string; -}> = ({ title, value, icon, change, changeType, color }) => ( + colorClass: string; + iconColorClass: string; +}> = ({ title, value, icon, change, changeType, colorClass, iconColorClass }) => ( -
+
-

{title}

-

{value}

+

{title}

+

{value}

{change && (
{changeType === 'increase' ? : } {change}
)}
-
+
{icon}
@@ -243,9 +244,9 @@ const ChartCard: React.FC<{ -

{title}

+

{title}

{children}
); @@ -258,10 +259,10 @@ const ListCard: React.FC<{
-

{title}

+

{title}

{action}
{children} @@ -320,14 +321,14 @@ const AuditDashboard: React.FC = () => { if (overviewError) { return ( -
+
- -

Error loading dashboard

-

Please try refreshing the page

+ +

Error loading dashboard

+

Please try refreshing the page

@@ -337,25 +338,25 @@ const AuditDashboard: React.FC = () => { } return ( -
+
{/* Header */}
- -

Audit Dashboard

+ +

Audit Dashboard

@@ -363,14 +364,14 @@ const AuditDashboard: React.FC = () => { @@ -396,9 +397,9 @@ const AuditDashboard: React.FC = () => { transition={{ duration: 1, repeat: Infinity, ease: "linear" }} className="inline-block" > - + -

Loading dashboard...

+

Loading dashboard...

) : ( @@ -412,31 +413,35 @@ const AuditDashboard: React.FC = () => { {/* Overview Stats */} {overview && ( -

Overview

+

Overview

} - color="border-blue-500" + colorClass="border-primary" + iconColorClass="text-primary" /> } - color="border-green-500" + colorClass="border-secondary" + iconColorClass="text-secondary" /> } - color="border-purple-500" + colorClass="border-purple-500" + iconColorClass="text-purple-500 dark:text-purple-400" /> } - color="border-red-500" + colorClass="border-destructive" + iconColorClass="text-destructive" />
@@ -453,12 +458,12 @@ const AuditDashboard: React.FC = () => { initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} transition={{ delay: index * 0.1 }} - className="text-center p-4 rounded-lg bg-gray-50" + className="text-center p-4 rounded-lg bg-muted" >
{item.status.replace('_', ' ')}
-

{item.count}

+

{item.count}

))}
@@ -477,10 +482,10 @@ const AuditDashboard: React.FC = () => { initial={{ x: -50, opacity: 0 }} animate={{ x: 0, opacity: 1 }} transition={{ delay: index * 0.1 }} - className="flex items-center justify-between p-3 rounded-lg bg-gray-50" + className="flex items-center justify-between p-3 rounded-lg bg-muted" > - {item.type.replace('_', ' ')} - + {item.type.replace('_', ' ')} + {item.count} @@ -500,7 +505,7 @@ const AuditDashboard: React.FC = () => { View All @@ -514,16 +519,16 @@ const AuditDashboard: React.FC = () => { initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: index * 0.1 }} - className="flex items-center justify-between p-3 rounded-lg border border-gray-200 hover:bg-gray-50 transition-colors group" + className="flex items-center justify-between p-3 rounded-lg border border-border hover:bg-muted/50 transition-colors group" >
-

+

{audit.name}

-

+

{audit.department.name} • {audit.auditor.name}

-

+

{formatDate(audit.createdAt)}

@@ -531,7 +536,7 @@ const AuditDashboard: React.FC = () => { {audit.status.replace('_', ' ')} - + {/* */}
))} @@ -549,7 +554,7 @@ const AuditDashboard: React.FC = () => { View All @@ -563,13 +568,13 @@ const AuditDashboard: React.FC = () => { initial={{ y: 20, opacity: 0 }} animate={{ y: 0, opacity: 1 }} transition={{ delay: index * 0.1 }} - className="p-4 rounded-lg border-l-4 border-red-500 bg-red-50 hover:bg-red-100 transition-colors group" + className="p-4 rounded-lg border-l-4 border-destructive bg-destructive/5 hover:bg-destructive/10 transition-colors group" >
-

{finding.title}

-

{finding.audit.name}

-

+

{finding.title}

+

{finding.audit.name}

+

Assigned to: {finding.assignedTo.name}

@@ -577,12 +582,12 @@ const AuditDashboard: React.FC = () => { {finding.priority} - +
{finding.actions.length > 0 && ( -
-

+

+

{finding.actions.length} action(s) • Due: {formatDate(finding.actions[0].dueDate)}

@@ -604,7 +609,7 @@ const AuditDashboard: React.FC = () => { Manage All @@ -617,19 +622,19 @@ const AuditDashboard: React.FC = () => { initial={{ x: -50, opacity: 0 }} animate={{ x: 0, opacity: 1 }} transition={{ delay: index * 0.1 }} - className="p-4 rounded-lg bg-yellow-50 border border-yellow-200 hover:bg-yellow-100 transition-colors" + className="p-4 rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800 hover:bg-yellow-100 dark:hover:bg-yellow-900/30 transition-colors" >
-

{action.title}

-

{action.audit.name}

-

Finding: {action.finding.title}

+

{action.title}

+

{action.audit.name}

+

Finding: {action.finding.title}

-

+

Overdue by {Math.ceil((new Date().getTime() - new Date(action.dueDate).getTime()) / (1000 * 60 * 60 * 24))} days

-

+

Assigned to: {action.assignedTo.name}

@@ -648,4 +653,4 @@ const AuditDashboard: React.FC = () => { ); }; -export default AuditDashboard; \ No newline at end of file +export default AuditDashboard; diff --git a/client/src/components/pages/Dashboard/batchdashboard.tsx b/client/src/components/pages/Dashboard/batchdashboard.tsx index fc8bd69..1bab0f5 100644 --- a/client/src/components/pages/Dashboard/batchdashboard.tsx +++ b/client/src/components/pages/Dashboard/batchdashboard.tsx @@ -1,31 +1,21 @@ -import React, { useState } from 'react'; +import React from 'react'; import { useQuery } from '@tanstack/react-query'; -import { motion } from 'framer-motion'; import { BarChart3, - PieChart, - TrendingUp, Users, - Layers, + Clipboard, FileBox, CheckCircle2, XCircle, Clock, - ArrowUpCircle, - CalendarRange, - Download, - BarChart4, - CheckCheck, - Droplets, - Factory, - Award + TrendingUp, + PieChart, } from 'lucide-react'; import { - Bar, Line, - Pie, - Doughnut + Doughnut, + Bar, } from 'react-chartjs-2'; import { Chart as ChartJS, @@ -40,7 +30,6 @@ import { Legend, Filler } from 'chart.js'; -import { format } from 'date-fns'; import api, { API_ROUTES } from '../../../utils/api'; // Register ChartJS components @@ -58,15 +47,45 @@ ChartJS.register( ); const Dashboard: React.FC = () => { - const [timeframe, setTimeframe] = useState<'weekly' | 'monthly' | 'quarterly'>('monthly'); - const [selectedMonth, setSelectedMonth] = useState(format(new Date(), 'MM')); - const [selectedYear, setSelectedYear] = useState(format(new Date(), 'yyyy')); + // Helpers to resolve CSS variables to concrete colors for canvas rendering + const getCssVar = (name: string, fallback: string) => { + try { + if (typeof window === 'undefined') return fallback; + const val = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); + return val || fallback; + } catch (e) { + return fallback; + } + }; + const parseColorToRgba = (color: string, alpha: number) => { + if (!color) return `rgba(0,0,0,${alpha})`; + color = color.trim(); + // already rgba + if (color.startsWith('rgba')) return color.replace(/rgba\(([^)]+)\)/, (_, vals) => `rgba(${vals.split(',').slice(0, 3).join(',')},${alpha})`); + // rgb -> rgba + if (color.startsWith('rgb(')) return color.replace('rgb(', 'rgba(').replace(')', `,${alpha})`); + // hex -> rgba + if (color.startsWith('#')) { + const hex = color.replace('#', ''); + const bigint = parseInt(hex.length === 3 ? hex.split('').map(c => c + c).join('') : hex, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + return `rgba(${r},${g},${b},${alpha})`; + } + // fallback: return color as-is (may be a named color) without alpha + return color; + }; + + const successColor = getCssVar('--success', '#16a34a'); + const secondaryColor = getCssVar('--secondary', '#3b82f6'); + const destructiveColor = getCssVar('--destructive', '#ef4444'); // Fetch overview statistics - const { - data: overviewData, - isLoading: overviewLoading, - error: overviewError + const { + data: overviewData, + isLoading: overviewLoading, + error: overviewError } = useQuery({ queryKey: ['dashboardOverview'], queryFn: async () => { @@ -81,15 +100,14 @@ const Dashboard: React.FC = () => { }); // Fetch batch trends - const { - data: trendData, - isLoading: trendLoading, - error: trendError + const { + data: trendData, + isLoading: trendLoading, } = useQuery({ - queryKey: ['batchTrends', timeframe], + queryKey: ['batchTrends', 'monthly'], queryFn: async () => { const res = await api.get(API_ROUTES.DASHBOARD.BATCH_TRENDS, { - params: { period: timeframe }, + params: { period: 'monthly' }, headers: { 'Authorization': `Bearer ${localStorage.getItem('authToken')}` } @@ -100,10 +118,9 @@ const Dashboard: React.FC = () => { }); // Fetch product performance - const { - data: productData, - isLoading: productLoading, - error: productError + const { + data: productData, + isLoading: productLoading, } = useQuery({ queryKey: ['productPerformance'], queryFn: async () => { @@ -117,1215 +134,301 @@ const Dashboard: React.FC = () => { staleTime: 5 * 60 * 1000 }); - // Fetch user activity - const { - data: userData, - isLoading: userLoading, - error: userError - } = useQuery({ - queryKey: ['userActivity'], - queryFn: async () => { - const res = await api.get(API_ROUTES.DASHBOARD.USER_ACTIVITY, { - headers: { - 'Authorization': `Bearer ${localStorage.getItem('authToken')}` - } - }); - return res.data.users; - }, - staleTime: 5 * 60 * 1000 - }); - - // Fetch quality metrics - const { - data: qualityData, - isLoading: qualityLoading, - error: qualityError - } = useQuery({ - queryKey: ['qualityMetrics'], - queryFn: async () => { - const res = await api.get(API_ROUTES.DASHBOARD.QUALITY_METRICS, { - headers: { - 'Authorization': `Bearer ${localStorage.getItem('authToken')}` - } - }); - return res.data.qualityMetrics; - }, - staleTime: 5 * 60 * 1000 - }); - - // Fetch monthly batch summary - const { - data: monthlySummaryData, - isLoading: monthlySummaryLoading, - error: monthlySummaryError - } = useQuery({ - queryKey: ['monthlySummary', selectedMonth, selectedYear], - queryFn: async () => { - const res = await api.get(API_ROUTES.DASHBOARD.MONTHLY_SUMMARY, { - params: { month: selectedMonth, year: selectedYear }, - headers: { - 'Authorization': `Bearer ${localStorage.getItem('authToken')}` - } - }); - return res.data.summary; - }, - staleTime: 5 * 60 * 1000 - }); - - // Fetch standard usage - const { - data: standardData, - isLoading: standardLoading, - error: standardError - } = useQuery({ - queryKey: ['standardUsage'], - queryFn: async () => { - const res = await api.get(API_ROUTES.DASHBOARD.STANDARD_USAGE, { - headers: { - 'Authorization': `Bearer ${localStorage.getItem('authToken')}` - } - }); - return res.data.standards; - }, - staleTime: 5 * 60 * 1000 - }); - - // Prepare chart data based on API responses + // Prepare chart data const batchTrendChartData = React.useMemo(() => { if (!trendData?.trends) return null; - + return { - labels: trendData.trends.map((item: any) => item.date), + labels: trendData.trends.slice(-6).map((item: any) => item.date), datasets: [ { label: 'Approved', - data: trendData.trends.map((item: any) => item.approved), - borderColor: 'rgba(34, 197, 94, 1)', - backgroundColor: 'rgba(34, 197, 94, 0.5)', + data: trendData.trends.slice(-6).map((item: any) => item.approved), + borderColor: successColor, + backgroundColor: parseColorToRgba(successColor, 0.12), tension: 0.3, }, { - label: 'Submitted', - data: trendData.trends.map((item: any) => item.submitted), - borderColor: 'rgba(59, 130, 246, 1)', - backgroundColor: 'rgba(59, 130, 246, 0.5)', - tension: 0.3, - }, - { - label: 'Rejected', - data: trendData.trends.map((item: any) => item.rejected), - borderColor: 'rgba(239, 68, 68, 1)', - backgroundColor: 'rgba(239, 68, 68, 0.5)', - tension: 0.3, - }, - { - label: 'Draft', - data: trendData.trends.map((item: any) => item.draft), - borderColor: 'rgba(161, 161, 170, 1)', - backgroundColor: 'rgba(161, 161, 170, 0.5)', + label: 'Pending', + data: trendData.trends.slice(-6).map((item: any) => item.submitted + item.draft), + borderColor: secondaryColor, + backgroundColor: parseColorToRgba(secondaryColor, 0.12), tension: 0.3, } ] }; }, [trendData]); - // Prepare product performance chart data - const productPerformanceChartData = React.useMemo(() => { - if (!productData || productData.length === 0) return null; - - // Sort products by total batches - const topProducts = [...productData].sort((a, b) => b.totalBatches - a.totalBatches).slice(0, 5); - - return { - labels: topProducts.map(product => product.name), - datasets: [ - { - label: 'Approved', - data: topProducts.map(product => product.approvedBatches), - backgroundColor: 'rgba(34, 197, 94, 0.8)', - }, - { - label: 'Rejected', - data: topProducts.map(product => product.rejectedBatches), - backgroundColor: 'rgba(239, 68, 68, 0.8)', - }, - { - label: 'Pending', - data: topProducts.map(product => product.pendingBatches), - backgroundColor: 'rgba(59, 130, 246, 0.8)', - } - ] - }; - }, [productData]); - - // Prepare quality metrics chart data - const qualityMetricsChartData = React.useMemo(() => { - if (!qualityData || qualityData.length === 0) return null; - - // Take top 5 products by batch count - const topProducts = [...qualityData].sort((a, b) => b.totalBatches - a.totalBatches).slice(0, 5); - - return { - labels: topProducts.map(product => product.productName), - datasets: [ - { - label: 'Moisture (%)', - data: topProducts.map(product => product.avgMoisture), - backgroundColor: 'rgba(147, 51, 234, 0.7)', - borderColor: 'rgba(147, 51, 234, 1)', - borderWidth: 1, - borderRadius: 5, - }, - { - label: 'Total Ash (%)', - data: topProducts.map(product => product.avgTotalAsh), - backgroundColor: 'rgba(249, 115, 22, 0.7)', - borderColor: 'rgba(249, 115, 22, 1)', - borderWidth: 1, - borderRadius: 5, - } - ] - }; - }, [qualityData]); - - // Prepare water activity chart (separate from other metrics due to scale difference) - const waterActivityChartData = React.useMemo(() => { - if (!qualityData || qualityData.length === 0) return null; - - // Take top 5 products by batch count - const topProducts = [...qualityData].sort((a, b) => b.totalBatches - a.totalBatches).slice(0, 5); - - return { - labels: topProducts.map(product => product.productName), - datasets: [ - { - label: 'Water Activity', - data: topProducts.map(product => product.avgWaterActivity), - backgroundColor: 'rgba(6, 182, 212, 0.7)', - borderColor: 'rgba(6, 182, 212, 1)', - borderWidth: 1, - borderRadius: 5, - } - ] - }; - }, [qualityData]); - - - - // Prepare status distribution doughnut chart const statusDistributionChartData = React.useMemo(() => { if (!overviewData) return null; - + return { - labels: ['Approved', 'Pending', 'Rejected', 'Draft'], + labels: ['Approved', 'Pending', 'Rejected'], datasets: [ { data: [ overviewData.batches.approved, overviewData.batches.pending, - overviewData.batches.rejected, - overviewData.batches.total - (overviewData.batches.approved + overviewData.batches.pending + overviewData.batches.rejected) + overviewData.batches.rejected ], backgroundColor: [ - 'rgba(34, 197, 94, 0.8)', - 'rgba(59, 130, 246, 0.8)', - 'rgba(239, 68, 68, 0.8)', - 'rgba(161, 161, 170, 0.8)' - ], - borderColor: [ - 'rgba(34, 197, 94, 1)', - 'rgba(59, 130, 246, 1)', - 'rgba(239, 68, 68, 1)', - 'rgba(161, 161, 170, 1)' + parseColorToRgba(successColor, 0.85), + parseColorToRgba(secondaryColor, 0.85), + parseColorToRgba(destructiveColor, 0.85), ], + borderColor: [successColor, secondaryColor, destructiveColor], borderWidth: 1, } ] }; }, [overviewData]); - - // Standard usage chart - const standardUsageChartData = React.useMemo(() => { - if (!standardData || standardData.length === 0) return null; - - // Take top 7 standards by usage - const topStandards = [...standardData].sort((a, b) => b.usageCount - a.usageCount).slice(0, 7); - + + const productPerformanceChartData = React.useMemo(() => { + if (!productData || productData.length === 0) return null; + + const topProducts = [...productData].sort((a, b) => b.totalBatches - a.totalBatches).slice(0, 5); + return { - labels: topStandards.map(std => std.name), + labels: topProducts.map(product => product.name.length > 15 ? product.name.substring(0, 15) + '...' : product.name), datasets: [ { - data: topStandards.map(std => std.usageCount), - backgroundColor: [ - 'rgba(59, 130, 246, 0.7)', - 'rgba(147, 51, 234, 0.7)', - 'rgba(249, 115, 22, 0.7)', - 'rgba(16, 185, 129, 0.7)', - 'rgba(239, 68, 68, 0.7)', - 'rgba(245, 158, 11, 0.7)', - 'rgba(6, 182, 212, 0.7)' - ], - borderWidth: 0, - hoverOffset: 10 + label: 'Approved', + data: topProducts.map(product => product.approvedBatches), + backgroundColor: 'rgba(23, 142, 200, 0.8)', + borderColor: 'rgba(23, 142, 200, 1)', + borderWidth: 1, + borderRadius: 4, } ] }; - }, [standardData]); - - // Page animation variants - const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.1 - } - } - }; - - const itemVariants = { - hidden: { y: 20, opacity: 0 }, - visible: { - y: 0, - opacity: 1, - transition: { - type: "spring", - damping: 15 - } - } - }; - - // Generate month options for dropdown - const monthOptions = Array.from({ length: 12 }, (_, i) => ({ - value: String(i + 1).padStart(2, '0'), - label: format(new Date(2000, i, 1), 'MMMM') - })); - - // Generate year options for dropdown - const currentYear = new Date().getFullYear(); - const yearOptions = Array.from({ length: 5 }, (_, i) => ({ - value: String(currentYear - i), - label: String(currentYear - i) - })); + }, [productData]); - return ( - - {/* Dashboard Header */} - -
-

Batch Dashboard

-

Overview of batch processing performance and metrics

+ if (overviewLoading) { + return ( +
+
+
+

Loading dashboard...

- -
- {/* Time frame selector */} -
- - - -
- - {/* Month and Year selector */} -
- - - - - -
+
+ ); + } + + if (overviewError) { + return ( +
+
+ +

Failed to load dashboard data

- - - {/* Top Stats Cards */} -
- {/* Total Batches */} - -
-
- -
-
-

TOTAL BATCHES

-
- - {overviewLoading ? "..." : overviewData?.batches?.total || 0} - - - - {trendData?.trends && trendData.trends.length > 1 - ? `${Math.round(((trendData.trends[trendData.trends.length-1].approved + - trendData.trends[trendData.trends.length-1].submitted + - trendData.trends[trendData.trends.length-1].rejected + - trendData.trends[trendData.trends.length-1].draft) / - (trendData.trends[trendData.trends.length-2].approved + - trendData.trends[trendData.trends.length-2].submitted + - trendData.trends[trendData.trends.length-2].rejected + - trendData.trends[trendData.trends.length-2].draft || 1) - 1) * 100)}%` - : "0%" - } - -
-
-
-
-
-
- Approved: {overviewData?.batches?.approved || 0} -
-
-
- Pending: {overviewData?.batches?.pending || 0} -
-
-
- Rejected: {overviewData?.batches?.rejected || 0} -
-
-
- - {/* Products */} - -
-
- -
-
-

PRODUCTS

-
- - {overviewLoading ? "..." : overviewData?.products || 0} - - {productData && ( - - {productData.length} active - - )} -
-
-
-
-
-
-
-
-
+
+ ); + } + + const stats = [ + { + title: 'Total Batches', + value: overviewData?.batches?.total || 0, + icon: BarChart3, + iconColorVar: 'var(--primary)', + }, + { + title: 'Approved', + value: overviewData?.batches?.approved || 0, + icon: CheckCircle2, + iconColorVar: 'var(--success, #16a34a)', + }, + { + title: 'Pending', + value: overviewData?.batches?.pending || 0, + icon: Clock, + iconColorVar: 'var(--secondary)', + }, + { + title: 'Products', + value: overviewData?.products || 0, + icon: FileBox, + iconColorVar: 'var(--primary)', + }, + { + title: 'Users', + value: overviewData?.users || 0, + icon: Users, + iconColorVar: 'var(--secondary)', + }, + { + title: 'Standards', + value: overviewData?.standards || 0, + icon: Clipboard, + iconColorVar: 'var(--primary)', + }, + ]; - {/* Standards */} - -
-
- -
-
-

STANDARDS

-
- - {overviewLoading ? "..." : overviewData?.standards || 0} - - {standardData && ( - - {standardData.filter((s: { status: string; }) => s.status === 'ACTIVE').length} active - - )} -
-
-
-
-
- - Top: {standardData && standardData.length > 0 ? standardData[0].name : 'N/A'} -
- {standardData && standardData.length > 0 ? `${standardData[0].usageCount} uses` : ''} -
-
+ return ( +
+
+ {/* Header */} +
+

Dashboard

+

Batch processing overview

+
- {/* Users */} - -
-
- -
-
-

USERS

-
- - {overviewLoading ? "..." : overviewData?.users || 0} - - {userData && ( - - {userData.filter((u: { totalActivities: number; }) => u.totalActivities > 0).length} active - - )} + {/* Stats Grid */} +
+ {stats.map((stat, index) => ( +
+
+
+

{stat.title}

+

+ {stat.value.toLocaleString()} +

+
+
+ +
-
-
-
- - Most active: {userData && userData.length > 0 ? userData[0].name : 'N/A'} -
- {userData && userData.length > 0 ? `${userData[0].totalActivities} actions` : ''} -
- -
+ ))} +
-
- {/* Batch Trends Chart */} - -
-
-

- - Batch Processing Trends -

-
- {timeframe === 'weekly' ? 'Last 7 days' : - timeframe === 'monthly' ? 'Last 30 days' : 'Last 3 months'} -
+ {/* Charts Section */} +
+ {/* Batch Trends */} +
+
+ +

Batch Trends

-
-
{trendLoading ? (
-
-
- ) : trendError ? ( -
- Failed to load trend data +
- ) : !batchTrendChartData ? ( -
- No trend data available -
- ) : ( + ) : batchTrendChartData ? (
-
+ ) : ( +
+ No trend data available +
)}
- - {/* Status Distribution */} - -
-
-

- - Batch Status Distribution -

+ {/* Status Distribution */} +
+
+ +

Status Distribution

-
-
- {overviewLoading ? ( -
-
-
- ) : overviewError ? ( -
- Failed to load overview data -
- ) : !statusDistributionChartData ? ( -
- No status data available -
- ) : ( + {statusDistributionChartData ? (
-
- + - {overviewData && ( -
-
{overviewData.batches.total}
-
Total Batches
-
- )} -
-
- )} -
- -
- -
- {/* Product Performance */} - -
-
-

- - Product Performance -

- -
-
-
- {productLoading ? ( -
-
-
- ) : productError ? ( -
- Failed to load product data -
- ) : !productPerformanceChartData ? ( -
- No product data available -
- ) : ( -
- ({ - ...ds, - barThickness: 20 - })) - }} - options={{ - responsive: true, - maintainAspectRatio: false, - scales: { - x: { - stacked: true, - grid: { - display: false - } - }, - y: { - stacked: true, - beginAtZero: true, - grid: { - color: 'rgba(0, 0, 0, 0.05)', - } - } - }, - plugins: { - legend: { - position: 'top' as const, - labels: { - boxWidth: 12, - usePointStyle: true, - pointStyle: 'circle' - } - }, - tooltip: { - callbacks: { - footer: (tooltipItems) => { - // Add approval rate to the tooltip - if (tooltipItems.length > 0) { - - const label = tooltipItems[0].label; - const matchingProduct = productData?.find((p: { name: string; }) => p.name === label); - if (matchingProduct) { - return `Approval Rate: ${matchingProduct.approvalRate}%`; - } - } - return ''; - } - } - } - } - }} - /> -
- )} -
-
- - {/* Quality Metrics */} - -
-
-

- - Quality Metrics -

-
-
-
- Moisture -
-
-
- Total Ash
-
-
-
- {qualityLoading ? ( -
-
-
- ) : qualityError ? ( -
- Failed to load quality data -
- ) : !qualityMetricsChartData ? ( -
- No quality metrics available -
) : ( -
- ({ - ...ds, - barThickness: 20 - })) - }} - options={{ - responsive: true, - maintainAspectRatio: false, - scales: { - x: { - grid: { - display: false - } - }, - y: { - beginAtZero: true, - grid: { - color: 'rgba(0, 0, 0, 0.05)', - }, - title: { - display: true, - text: 'Percentage (%)' - } - } - }, - plugins: { - legend: { - position: 'top' as const, - align: 'end' as const, - labels: { - boxWidth: 12, - usePointStyle: true, - pointStyle: 'circle' - } - }, - tooltip: { - callbacks: { - label: function(context) { - return context.dataset.label + ': ' + context.parsed.y + '%'; - } - } - } - } - }} - /> +
+ No status data available
)}
- -
+
-
- {/* Water Activity Chart */} - -
-
-

- - Water Activity -

-
+ {/* Product Performance */} +
+
+ +

Top Products Performance

-
- {qualityLoading ? ( -
-
-
- ) : qualityError ? ( -
- Failed to load quality data -
- ) : !waterActivityChartData ? ( -
- No water activity data available -
- ) : ( -
- ({ - ...ds, - barThickness: 16 - })) - }} - options={{ - responsive: true, - maintainAspectRatio: false, - indexAxis: 'y' as const, - scales: { - x: { - beginAtZero: true, - grid: { - color: 'rgba(0, 0, 0, 0.05)', - }, - title: { - display: true, - text: 'Water Activity (Aw)' - } - }, - y: { - grid: { - display: false - } - } + {productLoading ? ( +
+
+
+ ) : productPerformanceChartData ? ( +
+ -
- )} -
- - - {/* Standard Usage */} - -
-
-

- - Standard Usage -

-
-
-
- {standardLoading ? ( -
-
-
- ) : standardError ? ( -
- Failed to load standard data -
- ) : !standardUsageChartData ? ( -
- No standard data available -
- ) : ( -
- -
- )} -
-
- - {/* Monthly Summary */} - -
-
-

- - Monthly Summary -

-
- {monthlySummaryData?.month || format(new Date(), 'MMMM yyyy')} -
-
-
-
- {monthlySummaryLoading ? ( -
-
-
- ) : monthlySummaryError ? ( -
- Failed to load monthly data -
- ) : !monthlySummaryData ? ( -
- No monthly data available -
- ) : ( -
-
-
-
Total Batches
-
{monthlySummaryData.totalBatches}
-
-
-
Approval Time
-
{monthlySummaryData.timeToApproval}h
-
average
-
-
- -
-
-
- - Approved -
-
- {monthlySummaryData.approved} - - ({Math.round((monthlySummaryData.approved / monthlySummaryData.totalBatches || 0) * 100)}%) - -
-
- -
-
- - Pending -
-
- {monthlySummaryData.pending} - - ({Math.round((monthlySummaryData.pending / monthlySummaryData.totalBatches || 0) * 100)}%) - -
-
- -
-
- - Rejected -
-
- {monthlySummaryData.rejected} - - ({Math.round((monthlySummaryData.rejected / monthlySummaryData.totalBatches || 0) * 100)}%) - -
-
-
- -
-
Top Products
-
- {monthlySummaryData.productDistribution && - (Object.entries(monthlySummaryData.productDistribution) as [string, number][]) - .sort((a, b) => b[1] - a[1]) - .slice(0, 2) - .map(([name, count], idx) => ( -
- {name} - {count} -
- )) - } -
-
-
- )} -
-
-
- - {/* User Activity */} - -
-
-

- - User Activity -

- -
-
-
- {userLoading ? ( -
-
-
- ) : userError ? ( -
- Failed to load user data -
- ) : !userData || userData.length === 0 ? ( -
- No user activity data available + x: { grid: { display: false } } + }, + plugins: { + legend: { display: false } + } + }} + />
) : ( -
-
Batch Number Product Production Date Maker Verification Status Parameters Actions
+ (batch: BatchForVerification) => ( +
{batch.batchNumber} +
-
+
{batch.product.name}
-
+
{batch.product.code}
+ {formatDate(batch.dateOfProduction)} + {batch.maker.name} +
- + {batch.totalParameters} parameters
- setSelectedBatchId(batch.id)} - className={`px-3 py-2 rounded-lg transition-colors flex items-center gap-1 ml-auto ${ - isBatchVerified(batch) - ? 'text-gray-600 hover:text-gray-800 bg-gray-50 hover:bg-gray-100' - : 'text-blue-600 hover:text-blue-800 bg-blue-50 hover:bg-blue-100' - }`} - whileHover={{ scale: 1.05 }} - whileTap={{ scale: 0.95 }} - title={ - isBatchVerified(batch) - ? 'View Details' - : 'Start Verification' + style={isBatchVerified(batch) + ? { color: 'var(--muted-foreground)', background: 'var(--muted)' } + : undefined } + className={`px-3 py-2 rounded transition-colors flex items-center gap-1 ml-auto ${!isBatchVerified(batch) ? 'verify-glow-btn' : ''}`} + title={isBatchVerified(batch) ? 'View Details' : 'Start Verification'} > {isBatchVerified(batch) ? ( <> - + View ) : ( <> - - + + Verify )} - +
- - - - - - - - - - - {userData.slice(0, 5).map((user: { id: React.Key | null | undefined; name: string | number | bigint | boolean | React.ReactElement> | Iterable | Promise> | Iterable | null | undefined> | null | undefined; email: string | number | bigint | boolean | React.ReactElement> | Iterable | React.ReactPortal | Promise> | Iterable | null | undefined> | null | undefined; role: string | number | bigint | boolean | React.ReactElement> | Iterable | React.ReactPortal | Promise> | Iterable | null | undefined> | null | undefined; batchesCreated: string | number | bigint | boolean | React.ReactElement> | Iterable | React.ReactPortal | Promise> | Iterable | null | undefined> | null | undefined; batchesReviewed: string | number | bigint | boolean | React.ReactElement> | Iterable | React.ReactPortal | Promise> | Iterable | null | undefined> | null | undefined; totalActivities: string | number | bigint | boolean | React.ReactElement> | Iterable | Promise> | Iterable | null | undefined> | null | undefined; }) => ( - - - - - - - - ))} - -
- User - - Role - - Batches Created - - Batches Reviewed - - Total Activities -
-
-
- {(user.name?.toString().charAt(0).toUpperCase() ?? '')} -
-
-
{user.name}
-
{user.email}
-
-
-
- - {user.role} - - - {user.batchesCreated} - - {user.batchesReviewed} - -
{user.totalActivities}
-
-
-
-
+
+ No product data available
)}
- - - {/* Footer Stats */} - -
-
-
- -
-
- {overviewLoading ? "..." : - overviewData?.batches?.total ? - Math.round((overviewData.batches.approved / overviewData.batches.total) * 100) : 0}% -
-
Approval Rate
-
- -
-
- -
-
- {monthlySummaryData?.timeToApproval || "N/A"} -
-
Avg. Processing Time (hours)
-
- -
-
- -
-
- {productData?.length ? - productData.reduce((sum: any, p: { totalBatches: any; }) => sum + p.totalBatches, 0) / productData.length : 0} -
-
Avg. Batches per Product
-
- -
-
- -
-
- {qualityData?.length || 0} + + {/* Summary Section */} +
+

Quick Summary

+
+
+ Approval Rate: + + {overviewData?.batches?.total ? + Math.round((overviewData.batches.approved / overviewData.batches.total) * 100) : 0}% + +
+
+ Active Products: + {overviewData?.products || 0} +
+
+ Total Standards: + {overviewData?.standards || 0}
-
Products Monitored
- - +
+
); }; -export default Dashboard; \ No newline at end of file +export default Dashboard; diff --git a/client/src/components/pages/Dashboard/rawDashboard.tsx b/client/src/components/pages/Dashboard/rawDashboard.tsx index e06e728..e6292d3 100644 --- a/client/src/components/pages/Dashboard/rawDashboard.tsx +++ b/client/src/components/pages/Dashboard/rawDashboard.tsx @@ -4,18 +4,9 @@ import type React from 'react'; import { useEffect, useState } from 'react'; import { motion } from 'framer-motion'; import { - Package, - Truck, - Search, - Settings, AlertTriangle, - Recycle, - TrendingUp, Loader2, - Warehouse, Factory, - ChevronDown, - ChevronUp, } from 'lucide-react'; import api, { API_ROUTES } from '../../../utils/api'; import { Bar, Doughnut } from 'react-chartjs-2'; @@ -60,7 +51,6 @@ interface WasteStock { const RawDashboard: React.FC = () => { const [totalStock, setTotalStock] = useState(0); - const [totalStockDetails, setTotalStockDetails] = useState([]); const [pendingPOs, setPendingPOs] = useState(0); const [pendingPODetails, setPendingPODetails] = useState([]); const [stockUnderCleaning, setStockUnderCleaning] = useState(0); @@ -74,10 +64,7 @@ const RawDashboard: React.FC = () => { total: 0, }); const [loading, setLoading] = useState(true); - const [totalVendors, setTotalVendors] = useState(0); - const [totalPurchaseOrders, setTotalPurchaseOrders] = useState(0); const [productWiseWaste, setProductWiseWaste] = useState([]); - //const [productWiseWaste, setProductWiseWaste] = useState({ afterCleaning: [], afterProcessing: [] }) const [stockDistribution, setStockDistribution] = useState([]); const [productWiseConversion, setProductWiseConversion] = useState([]); @@ -94,8 +81,6 @@ const RawDashboard: React.FC = () => { api.get(API_ROUTES.RAW.GET_STOCK_IN_PROCESSING, { headers }), api.get(API_ROUTES.RAW.GET_LOW_STOCK_ALERTS, { headers }), api.get(API_ROUTES.RAW.GET_WASTE_STOCK, { headers }), - api.get(API_ROUTES.RAW.GET_TOTAL_VENDORS, { headers }), - api.get(API_ROUTES.RAW.GET_TOTAL_PURCHASE_ORDERS, { headers }), api.get(API_ROUTES.RAW.GET_PRODUCT_WISE_WASTE, { headers }), api.get(API_ROUTES.RAW.GET_STOCK_DISTRIBUTION, { headers }), api.get(API_ROUTES.RAW.GET_PRODUCT_WISE_CONVERSION, { headers }), @@ -108,15 +93,12 @@ const RawDashboard: React.FC = () => { processingRes, lowStockRes, wasteRes, - vendorsRes, - poRes, productWiseWasteRes, stockDistributionRes, productWiseConversionRes, ] = responses; setTotalStock(totalStockRes.data.totalRawMaterialStock || 0); - setTotalStockDetails(totalStockRes.data.details || []); setPendingPOs(pendingPOsRes.data.pendingPOs || 0); setPendingPODetails(pendingPOsRes.data.details || []); setStockUnderCleaning(cleaningRes.data.stockUnderCleaning || 0); @@ -131,8 +113,6 @@ const RawDashboard: React.FC = () => { total: 0, } ); - setTotalVendors(vendorsRes.data.totalVendors || 0); - setTotalPurchaseOrders(poRes.data.totalPurchaseOrders || 0); // ...existing code... setProductWiseWaste( productWiseWasteRes.data.productWiseWasteStock || [] @@ -198,67 +178,6 @@ const RawDashboard: React.FC = () => { const normalizedProductList = Object.values(normalizedProductWiseWaste); - // Normalize Total Raw Material Stock details - const normalizedTotalStockDetails = totalStockDetails.reduce((acc: any, item: any) => { - const normalizedName = item.rawMaterial?.name?.toUpperCase() || 'UNKNOWN'; - const key = `${normalizedName}-${item.warehouse?.name || 'UNKNOWN'}`; - - if (!acc[key]) { - acc[key] = { - rawMaterial: { - name: normalizedName, - skuCode: item.rawMaterial?.skuCode || '', - }, - warehouse: item.warehouse, - currentQuantity: (item.currentQuantity || 0), - }; - } else { - acc[key].currentQuantity += (item.currentQuantity || 0); - } - return acc; - }, {}); - - const normalizedTotalStockDetailsList = Object.values(normalizedTotalStockDetails); - - // Create product-wise aggregation for pie chart - const productQuantityMap = normalizedTotalStockDetailsList.reduce((acc: Record, item: any) => { - const productName = item.rawMaterial?.name || 'UNKNOWN'; - if (!acc[productName]) { - acc[productName] = 0; - } - acc[productName] += item.currentQuantity; - return acc; - }, {}); - - const productLabels = Object.keys(productQuantityMap); - const productQuantities = Object.values(productQuantityMap); - - const productPieData = { - labels: productLabels, - datasets: [ - { - label: 'Quantity by Product', - data: productQuantities, - backgroundColor: [ - '#3B82F6', - '#10B981', - '#F59E0B', - '#EF4444', - '#8B5CF6', - '#06B6D4', - '#84CC16', - '#F97316', - '#EC4899', - '#14B8A6', - '#F43F5E', - '#6366F1', - ], - borderWidth: 2, - borderColor: '#ffffff', - }, - ], - }; - const warehouseLabels = stockDistribution.map( (w) => w.warehouse?.name || 'N/A' ); @@ -331,237 +250,134 @@ const RawDashboard: React.FC = () => { } return ( -
- {/* Enhanced Header */} - -
-
-
-
- -
- -
-

- Raw Material Dashboard -

-

- Real-time inventory monitoring and analytics -

-
-
-
-
- - Live Data - -
+
+ {/* Compact Header */} +
+
+
+ +
+
+

Raw Material Dashboard

+

Real-time inventory overview

- +
-
- {/* Enhanced Stats Grid - Better 4-column layout */} +
+ {/* Key Metrics - Compact Table Style */} -
- } - label="Total Raw Material Stock" - value={totalStock} - unit="kg/litre" - color="blue" - details={normalizedTotalStockDetailsList} - detailsFormatter={(details) => - details.map( - (s: any) => - `${s.rawMaterial?.name || ''} (${s.rawMaterial?.skuCode || ''}): ${s.currentQuantity} in ${s.warehouse?.name || ''}` - ) - } - large={true} - /> -
- - {/* Product-wise Quantity Pie Chart */} - -
-
- -
-
-

- Product-wise Stock Distribution -

-

By product quantity

-
-
-
- -
-
- - } - label="POs Pending Delivery" - value={pendingPOs} - unit="orders" - color="orange" - details={pendingPODetails} - detailsFormatter={(details) => - details.map( - (po) => - `PO#${po.id} (${po.vendor?.name || ''}): ${po.items?.length || 0} items` - ) - } - /> - } - label="Waste Stock" - value={wasteStock.total} - unit="kg/litre" - color="red" - tooltip={`After Cleaning: ${wasteStock.afterCleaning.total}, After Processing: ${wasteStock.afterProcessing.total}`} - /> - } - label="Under Cleaning" - value={stockUnderCleaning} - unit="kg/litre" - color="purple" - details={cleaningDetails} - detailsFormatter={(details) => - details.map( - (c) => - `${c.rawMaterial?.name || ''}: ${c.quantity} (${c.status})` - ) - } - /> - } - label="In Processing" - value={stockInProcessing} - unit="kg/litre" - color="green" - details={processingDetails} - detailsFormatter={(details) => - details.map( - (p) => - `${p.inputRawMaterial?.name || ''}: ${p.quantityInput} (${p.status})` - ) - } - /> - } - label="Total Vendors" - value={totalVendors} - unit="vendors" - color="indigo" - /> - } - label="Purchase Orders" - value={totalPurchaseOrders} - unit="orders" - color="teal" - /> + + + {/* Row 1 - Main Stock */} + + + + + + {/* Row 2 - Pending POs */} + + + + + + + {/* Row 3 - Under Cleaning */} + + + + + + + {/* Row 4 - In Processing */} + + + + + + + {/* Row 5 - Waste Stock */} + + + + + + +
Total Raw Material +
+ {totalStock.toLocaleString()} + kg +
+
POs Pending +
+ {pendingPOs} + orders +
+
{pendingPODetails.length} details
Under Cleaning +
+ {stockUnderCleaning.toLocaleString()} + kg +
+
{cleaningDetails.length} items
In Processing +
+ {stockInProcessing.toLocaleString()} + kg +
+
{processingDetails.length} items
Waste Stock +
+ {wasteStock.total.toLocaleString()} + kg +
+
+ Cleaning: {wasteStock.afterCleaning.total} | Processing: {wasteStock.afterProcessing.total} +
- {/* Charts Section - Better Layout */} -
- {/* Product-wise Waste Chart - Takes 2 columns */} + {/* Charts Section - Compact */} +
+ {/* Product-wise Waste Table */} -
-
- -
-
-

- Product-wise Waste Table -

-

- Summary of raw material, cleaning, waste, and processing -

-
+
+

Product-wise Summary

- +
- - - {normalizedProductList.map((p: any) => ( - + + {normalizedProductList.slice(0, 4).map((p: any) => ( + ))} {[ - { label: 'Raw material', key: 'rawMaterial' }, - { label: 'Cleaning', key: 'cleaning' }, - { - label: 'Waste after cleaning', - key: 'wasteAfterCleaning', - }, + { label: 'Raw', key: 'rawMaterial' }, { label: 'Cleaned', key: 'cleaned' }, - { label: 'Processing', key: 'processing' }, - { - label: 'Waste after processing', - key: 'wasteAfterProcessing', - }, { label: 'Processed', key: 'processed' }, ].map((row) => ( - - - {normalizedProductList.map((p: any) => ( - + + {normalizedProductList.slice(0, 4).map((p: any) => ( + ))} @@ -572,26 +388,18 @@ const RawDashboard: React.FC = () => { - {/* Stock Distribution Pie Chart - Takes 1 column */} + {/* Stock Distribution Pie */} -
-
- -
-
-

- Stock Distribution -

-

By warehouse

-
-
-
+
Stock Distribution
+
{ plugins: { legend: { position: 'bottom', - labels: { - usePointStyle: true, - padding: 15, - font: { size: 11 }, - }, + labels: { font: { size: 10 }, padding: 10 }, }, }, cutout: '60%', @@ -614,403 +418,60 @@ const RawDashboard: React.FC = () => {
- {/* Conversion Ratio Chart - Full Width */} + {/* Conversion Bar Chart */} -
-
- -
-
-

- Product-wise Conversion Efficiency -

-

- Conversion percentage by product SKU -

-
-
-
+
Conversion Efficiency (%)
+
value + '%', - }, - }, + x: { grid: { display: false }, ticks: { font: { size: 10 } } }, + y: { beginAtZero: true, max: 100, ticks: { callback: (v) => v + '%', font: { size: 10 } } }, }, }} />
- {/* Alerts and Details Section */} -
- {/* Low Stock Alerts */} - -
-
-
-
- -
-
-

- Low Stock Alerts -

-

- Items below minimum reorder level -

-
-
- {lowStockAlerts.length > 0 && ( - - {lowStockAlerts.length} alerts - - )} -
-
-
- {lowStockAlerts.length === 0 ? ( -
-
- -
-

- All stock levels are healthy! -

-

- No items below minimum reorder levels -

-
- ) : ( -
- {lowStockAlerts.map((alert, index) => ( - -
-
-

- {alert.name} -

-

- {alert.skuCode} -

-
-
-

Available

-

- {alert.available} -

-
-
-
- - Min Level: {alert.minReorderLevel} - - - {alert.minReorderLevel - alert.available} units short - -
-
- ))} -
- )} -
-
- - {/* Waste Stock Details */} + {/* Low Stock Alerts */} + {lowStockAlerts.length > 0 && ( -
-
-
- -
-
-

- Waste Stock Breakdown -

-

- Detailed waste analysis -

-
-
+
+ + Low Stock Alerts ({lowStockAlerts.length})
-
-
-
- -

After Cleaning

-

- {wasteStock.afterCleaning.total} -

-
-
- -

After Processing

-

- {wasteStock.afterProcessing.total} -

+
+ {lowStockAlerts.slice(0, 6).map((alert) => ( +
+
{alert.name}
+
{alert.available} avail / {alert.minReorderLevel} min
-
- -
- {wasteStock.afterCleaning.details.length > 0 && ( -
-

-
- Cleaning Waste Details -

-
- {wasteStock.afterCleaning.details - .slice(0, 3) - .map((w, idx) => ( -
- - {w.warehouse?.name || 'N/A'} - - - {w.quantity} - -
- ))} -
-
- )} - - {wasteStock.afterProcessing.details.length > 0 && ( -
-

-
- Processing Waste Details -

-
- {wasteStock.afterProcessing.details - .slice(0, 3) - .map((w, idx) => ( -
- - {w.warehouse?.name || 'N/A'} - - - {w.quantity} - -
- ))} -
-
- )} -
-
- -
-
-
- ); -}; - -interface StatCardProps { - icon: React.ReactNode; - label: string; - value: number; - unit?: string; - color: 'blue' | 'orange' | 'purple' | 'green' | 'red' | 'indigo' | 'teal'; - tooltip?: string; - details?: any[]; - detailsFormatter?: (details: any[]) => string[]; - large?: boolean; -} - -const StatCard: React.FC = ({ - icon, - label, - value, - unit, - color, - tooltip, - details, - detailsFormatter, - large = false, -}) => { - const [showDetails, setShowDetails] = useState(false); - - const colorClasses = { - blue: { - bg: 'bg-blue-50', - icon: 'text-blue-600', - value: 'text-blue-600', - border: 'border-blue-200', - gradient: 'from-blue-500 to-blue-600', - }, - orange: { - bg: 'bg-orange-50', - icon: 'text-orange-600', - value: 'text-orange-600', - border: 'border-orange-200', - gradient: 'from-orange-500 to-orange-600', - }, - purple: { - bg: 'bg-purple-50', - icon: 'text-purple-600', - value: 'text-purple-600', - border: 'border-purple-200', - gradient: 'from-purple-500 to-purple-600', - }, - green: { - bg: 'bg-green-50', - icon: 'text-green-600', - value: 'text-green-600', - border: 'border-green-200', - gradient: 'from-green-500 to-green-600', - }, - red: { - bg: 'bg-red-50', - icon: 'text-red-600', - value: 'text-red-600', - border: 'border-red-200', - gradient: 'from-red-500 to-red-600', - }, - indigo: { - bg: 'bg-indigo-50', - icon: 'text-indigo-600', - value: 'text-indigo-600', - border: 'border-indigo-200', - gradient: 'from-indigo-500 to-indigo-600', - }, - teal: { - bg: 'bg-teal-50', - icon: 'text-teal-600', - value: 'text-teal-600', - border: 'border-teal-200', - gradient: 'from-teal-500 to-teal-600', - }, - }; - - const classes = colorClasses[color]; - - return ( - -
details && setShowDetails(!showDetails)} - > -
-
-
{icon}
-
- {details && details.length > 0 && ( - - )} -
- -
-

- {label} -

-
- - {value.toLocaleString()} - - {unit && ( - - {unit} - - )} -
-
- - {showDetails && details && detailsFormatter && ( - -
- {detailsFormatter(details) - .slice(0, 3) - .map((detail, idx) => ( -
- {detail} -
- ))} - {detailsFormatter(details).length > 3 && ( -
- +{detailsFormatter(details).length - 3} more items -
- )} + ))}
)}
-
+
); }; diff --git a/client/src/components/pages/Dashboard/trainingdashboard.tsx b/client/src/components/pages/Dashboard/trainingdashboard.tsx index 01f88ff..a30bfda 100644 --- a/client/src/components/pages/Dashboard/trainingdashboard.tsx +++ b/client/src/components/pages/Dashboard/trainingdashboard.tsx @@ -195,8 +195,40 @@ type ParticipantEngagement = { const TrainingDashboard: React.FC = () => { const [selectedYear, setSelectedYear] = useState(new Date().getFullYear()); - - + // Helpers to resolve CSS variables and produce rgba strings for charts + const getCssVar = (name: string, fallback: string) => { + try { + if (typeof window === 'undefined') return fallback; + const val = getComputedStyle(document.documentElement).getPropertyValue(name).trim(); + return val || fallback; + } catch (e) { + return fallback; + } + }; + + const parseColorToRgba = (color: string, alpha: number) => { + if (!color) return `rgba(0,0,0,${alpha})`; + color = color.trim(); + if (color.startsWith('rgba')) return color.replace(/rgba\(([^)]+)\)/, (_, vals) => `rgba(${vals.split(',').slice(0, 3).join(',')},${alpha})`); + if (color.startsWith('rgb(')) return color.replace('rgb(', 'rgba(').replace(')', `,${alpha})`); + if (color.startsWith('#')) { + const hex = color.replace('#', ''); + const normalized = hex.length === 3 ? hex.split('').map(c => c + c).join('') : hex; + const bigint = parseInt(normalized, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + return `rgba(${r},${g},${b},${alpha})`; + } + return color; + }; + + const primaryColor = getCssVar('--primary', '#6366F1'); + const secondaryColor = getCssVar('--secondary', '#3b82f6'); + const successColor = getCssVar('--success', '#10b981'); + const mutedColor = getCssVar('--muted', 'rgba(0,0,0,0.6)'); + + // Fetch main dashboard statistics const { data: dashboardData, @@ -214,7 +246,7 @@ const TrainingDashboard: React.FC = () => { }, staleTime: 5 * 60 * 1000 // 5 minutes }); - + // Fetch feedback statistics const { data: feedbackData, @@ -225,14 +257,14 @@ const TrainingDashboard: React.FC = () => { queryFn: async () => { const res = await api.get(API_ROUTES.TRAINING.GET_TRAINING_FEEDBACK_STATS, { headers: { - 'Authorization': `Bearer ${localStorage.getItem('authToken')}` + 'Authorization': `Bearer ${localStorage.getItem('authToken')}` } }); return res.data.data; }, staleTime: 5 * 60 * 1000 }); - + // Fetch trainer statistics const { data: trainerData, @@ -243,14 +275,14 @@ const TrainingDashboard: React.FC = () => { queryFn: async () => { const res = await api.get(API_ROUTES.TRAINING.GET_TRAINING_TRAINER_STATS, { headers: { - 'Authorization': `Bearer ${localStorage.getItem('authToken')}` + 'Authorization': `Bearer ${localStorage.getItem('authToken')}` } }); return res.data.data; }, staleTime: 5 * 60 * 1000 }); - + // Fetch monthly training statistics const { data: monthlyData, @@ -269,7 +301,7 @@ const TrainingDashboard: React.FC = () => { }, staleTime: 5 * 60 * 1000 }); - + // Fetch attendance statistics const { data: attendanceData, @@ -287,7 +319,7 @@ const TrainingDashboard: React.FC = () => { }, staleTime: 5 * 60 * 1000 }); - + // Fetch participant engagement statistics const { data: engagementData, @@ -305,44 +337,44 @@ const TrainingDashboard: React.FC = () => { }, staleTime: 5 * 60 * 1000 }); - + // Prepare monthly training data for charts const monthlyTrainingChartData = useMemo(() => { if (!monthlyData?.months) return null; - + return { labels: monthlyData.months.map(m => m.monthName), datasets: [ { label: 'Total Trainings', data: monthlyData.months.map(m => m.trainingsCount), - borderColor: 'rgba(99, 102, 241, 1)', - backgroundColor: 'rgba(99, 102, 241, 0.2)', + borderColor: parseColorToRgba(primaryColor, 1), + backgroundColor: parseColorToRgba(primaryColor, 0.2), fill: true, tension: 0.4 }, { label: 'Completed Trainings', data: monthlyData.months.map(m => m.completedTrainings), - borderColor: 'rgba(16, 185, 129, 1)', - backgroundColor: 'rgba(16, 185, 129, 0.2)', + borderColor: parseColorToRgba(successColor, 1), + backgroundColor: parseColorToRgba(successColor, 0.2), fill: true, tension: 0.4 } ] }; }, [monthlyData]); - - - + + + // Training status distribution const statusDistributionData = useMemo(() => { if (!dashboardData?.summary) return null; - + return { labels: [ - 'Scheduled', - 'In Progress', + 'Scheduled', + 'In Progress', 'Completed' ], datasets: [ @@ -353,43 +385,43 @@ const TrainingDashboard: React.FC = () => { dashboardData.summary.completedTrainings ], backgroundColor: [ - 'rgba(59, 130, 246, 0.8)', // Blue - 'rgba(245, 158, 11, 0.8)', // Amber - 'rgba(16, 185, 129, 0.8)' // Green + parseColorToRgba(secondaryColor, 0.85), + parseColorToRgba(primaryColor, 0.85), + parseColorToRgba(successColor, 0.85) ], borderWidth: 0 } ] }; }, [dashboardData]); - - - + + + // Monthly participation chart data const participationTrendData = useMemo(() => { if (!engagementData?.monthlyParticipation) return null; - + return { labels: engagementData.monthlyParticipation.map(item => item.label), datasets: [ { label: 'Participants', data: engagementData.monthlyParticipation.map(item => item.participantsCount), - borderColor: 'rgba(99, 102, 241, 1)', - backgroundColor: 'rgba(99, 102, 241, 0.2)', + borderColor: parseColorToRgba(primaryColor, 1), + backgroundColor: parseColorToRgba(primaryColor, 0.2), fill: true, tension: 0.4 } ] }; }, [engagementData]); - - - + + + // Detailed feedback ratings data const detailedFeedbackData = useMemo(() => { if (!feedbackData?.overallAverages) return null; - + return { labels: ['Content', 'Trainer', 'Materials', 'Venue', 'Overall'], datasets: [ @@ -403,11 +435,11 @@ const TrainingDashboard: React.FC = () => { feedbackData.overallAverages.overall ], backgroundColor: [ - 'rgba(99, 102, 241, 0.7)', - 'rgba(16, 185, 129, 0.7)', - 'rgba(245, 158, 11, 0.7)', - 'rgba(59, 130, 246, 0.7)', - 'rgba(139, 92, 246, 0.7)' + parseColorToRgba(primaryColor, 0.7), + parseColorToRgba(successColor, 0.7), + parseColorToRgba(secondaryColor, 0.7), + parseColorToRgba(secondaryColor, 0.7), + parseColorToRgba(primaryColor, 0.7) ], borderWidth: 0, borderRadius: 4, @@ -416,13 +448,13 @@ const TrainingDashboard: React.FC = () => { ] }; }, [feedbackData]); - + // Generate year options for dropdown const yearOptions = useMemo(() => { const currentYear = new Date().getFullYear(); return Array.from({ length: 5 }, (_, i) => currentYear - i); }, []); - + // Animation variants const containerVariants = { hidden: { opacity: 0 }, @@ -433,7 +465,7 @@ const TrainingDashboard: React.FC = () => { } } }; - + const itemVariants = { hidden: { y: 20, opacity: 0 }, visible: { @@ -445,9 +477,9 @@ const TrainingDashboard: React.FC = () => { } } }; - + return ( - { {/* Dashboard Header */}
-

Training Dashboard

-

Comprehensive overview of training programs and metrics

+

Training Dashboard

+

Comprehensive overview of training programs and metrics

- +
-
+
-
- + {/* Top Stats Cards */}
{/* Total Trainings */}
-
- +
+
-

TOTAL TRAININGS

+

TOTAL TRAININGS

- + {dashboardLoading ? "..." : dashboardData?.summary.totalTrainings || 0} - + - {dashboardData?.summary.currentMonthTrainings + {dashboardData?.summary.currentMonthTrainings ? `${dashboardData.summary.currentMonthTrainings} this month` : "0 this month" } @@ -509,7 +547,7 @@ const TrainingDashboard: React.FC = () => {
-
+
Scheduled: {dashboardData?.summary.scheduledTrainings || 0} @@ -524,70 +562,81 @@ const TrainingDashboard: React.FC = () => {
- + {/* Total Participants */}
-
- +
+
-

PARTICIPANTS

+

PARTICIPANTS

- + {dashboardLoading ? "..." : dashboardData?.summary.totalParticipants || 0} {engagementData && ( - + {engagementData.topParticipants.length} active )}
-
-
-
+
+
p.engagementScore > 70).length / + .filter(p => p.engagementScore > 70).length / engagementData.topParticipants.length) * 100)) - : 0}%` + : 0}%` }} >
- + {/* Satisfaction Rate */}
-
- +
+
-

SATISFACTION RATE

+

SATISFACTION RATE

- + {dashboardLoading ? "..." : `${dashboardData?.summary.averageRating.toFixed(1)}/5.0` || "0/5.0"} {feedbackData?.ratingDistribution && ( - + {feedbackData.ratingDistribution.excellent + feedbackData.ratingDistribution.good} positive )}
-
+
{feedbackData?.ratingDistribution && ( <>
@@ -602,44 +651,50 @@ const TrainingDashboard: React.FC = () => { )}
- + {/* Attendance Rate */}
-
- +
+
-

ATTENDANCE RATE

+

ATTENDANCE RATE

- - {attendanceLoading ? "..." : - attendanceData?.statusDistribution ? - `${Math.round((attendanceData.statusDistribution - .find(s => s.status === 'PRESENT')?.percentage || 0))}%` : - "0%" + + {attendanceLoading ? "..." : + attendanceData?.statusDistribution ? + `${Math.round((attendanceData.statusDistribution + .find(s => s.status === 'PRESENT')?.percentage || 0))}%` : + "0%" } - {attendanceData?.totalAttendance && ( - - {attendanceData.totalAttendance} records + {(attendanceData?.totalAttendance ?? 0) > 0 && ( + + {attendanceData?.totalAttendance} + {' '} + records )}
-
+
{attendanceData?.statusDistribution && ( attendanceData.statusDistribution.slice(0, 2).map((status, idx) => (
-
+
{status.status}: {status.percentage}%
)) @@ -647,20 +702,21 @@ const TrainingDashboard: React.FC = () => {
- +
{/* Monthly Training Trend */} -
+
-

- +

+ Monthly Training Trend

- + {selectedYear}

@@ -675,7 +731,7 @@ const TrainingDashboard: React.FC = () => { Failed to load monthly data
) : !monthlyTrainingChartData ? ( -
+
No monthly data available
) : ( @@ -689,7 +745,7 @@ const TrainingDashboard: React.FC = () => { y: { beginAtZero: true, grid: { - color: 'rgba(0, 0, 0, 0.05)', + color: parseColorToRgba(mutedColor, 0.05), }, ticks: { precision: 0 @@ -722,16 +778,17 @@ const TrainingDashboard: React.FC = () => { )}
- + {/* Training Status Distribution */} -
+
-

- +

+ Status Distribution

@@ -746,7 +803,7 @@ const TrainingDashboard: React.FC = () => { Failed to load status data
) : !statusDistributionData ? ( -
+
No status data available
) : ( @@ -772,8 +829,8 @@ const TrainingDashboard: React.FC = () => { /> {dashboardData?.summary && (
-
{dashboardData.summary.totalTrainings}
-
Total
+
{dashboardData.summary.totalTrainings}
+
Total
)}
@@ -782,17 +839,18 @@ const TrainingDashboard: React.FC = () => {
- +
{/* Upcoming Trainings */} -
+
-

- +

+ Upcoming Trainings

@@ -808,21 +866,20 @@ const TrainingDashboard: React.FC = () => { Failed to load upcoming trainings

) : !dashboardData?.upcomingTrainings || dashboardData.upcomingTrainings.length === 0 ? ( -
- +
+

No upcoming trainings scheduled

) : ( -
+
{dashboardData.upcomingTrainings.map((training) => ( -
+
-
@@ -833,10 +890,10 @@ const TrainingDashboard: React.FC = () => {
- +
-

{training.title}

-
+

{training.title}

+
{training.trainerName} @@ -851,25 +908,25 @@ const TrainingDashboard: React.FC = () => {
- {training.trainingType} - - {training.daysUntilStart > 0 ? - `${training.daysUntilStart} day${training.daysUntilStart !== 1 ? 's' : ''} remaining` : + + {training.daysUntilStart > 0 ? + `${training.daysUntilStart} day${training.daysUntilStart !== 1 ? 's' : ''} remaining` : 'Today' }
- -
@@ -879,16 +936,17 @@ const TrainingDashboard: React.FC = () => { )}
- + {/* Trainer Performance */} -
+
-

- +

+ Trainer Performance

@@ -904,55 +962,54 @@ const TrainingDashboard: React.FC = () => { Failed to load trainer data

) : !trainerData?.trainers || trainerData.trainers.length === 0 ? ( -
+
No trainer data available
) : (
-
- Metric - - {p.productName} +
Metric + {p.productName.substring(0, 10)}
- {row.label} - +
{row.label} {p[row.key]}
+
- - - - - + {trainerData.trainers.slice(0, 6).map((trainer) => ( - + +
+ Trainer + Trainings + Rating + Completion
{trainer.name.charAt(0).toUpperCase()}
-
{trainer.name}
-
{trainer.email}
+
{trainer.name}
+
{trainer.email}
-
{trainer.trainingsCount}
-
{trainer.completedTrainings} completed
+
{trainer.trainingsCount}
+
{trainer.completedTrainings} completed
- = 4.5 ? 'bg-green-100 text-green-800' : + = 4.5 ? 'bg-green-100 text-green-800' : trainer.ratings.overall >= 3.5 ? 'bg-blue-100 text-blue-800' : - trainer.ratings.overall >= 2.5 ? 'bg-amber-100 text-amber-800' : - 'bg-red-100 text-red-800' - }`} + trainer.ratings.overall >= 2.5 ? 'bg-amber-100 text-amber-800' : + 'bg-red-100 text-red-800' + }`} > {trainer.ratings.overall.toFixed(1)} @@ -960,15 +1017,14 @@ const TrainingDashboard: React.FC = () => {
-
{trainer.completionRate}%
-
-
= 80 ? 'bg-green-500' : +
{trainer.completionRate}%
+
+
= 80 ? 'bg-green-500' : trainer.completionRate >= 60 ? 'bg-blue-500' : - trainer.completionRate >= 40 ? 'bg-amber-500' : - 'bg-red-500' - }`} + trainer.completionRate >= 40 ? 'bg-amber-500' : + 'bg-red-500' + }`} style={{ width: `${trainer.completionRate}%` }} >
@@ -982,17 +1038,18 @@ const TrainingDashboard: React.FC = () => {
- +
{/* Feedback Ratings Breakdown */} -
+
-

- +

+ Feedback Ratings Breakdown

@@ -1007,7 +1064,7 @@ const TrainingDashboard: React.FC = () => { Failed to load feedback data
) : !detailedFeedbackData ? ( -
+
No feedback data available
) : ( @@ -1028,7 +1085,7 @@ const TrainingDashboard: React.FC = () => { beginAtZero: true, max: 5, grid: { - color: 'rgba(0, 0, 0, 0.05)', + color: parseColorToRgba(mutedColor, 0.05), }, ticks: { stepSize: 1 @@ -1051,16 +1108,17 @@ const TrainingDashboard: React.FC = () => { )}
- + {/* Participant Engagement */} -
+
-

- +

+ Participant Engagement

@@ -1075,73 +1133,70 @@ const TrainingDashboard: React.FC = () => { Failed to load engagement data
) : !engagementData?.topParticipants || engagementData.topParticipants.length === 0 ? ( -
+
No engagement data available
) : (
{engagementData.topParticipants.slice(0, 5).map((participant, idx) => ( -
-
+ idx === 2 ? 'bg-green-600' : + 'bg-gray-600' + }`}> {participant.name.charAt(0).toUpperCase()}
-
{participant.name}
-
{participant.organization}
+
{participant.name}
+
{participant.organization}
-
= 80 ? 'bg-green-100 text-green-800' : +
= 80 ? 'bg-green-100 text-green-800' : participant.engagementScore >= 60 ? 'bg-blue-100 text-blue-800' : - participant.engagementScore >= 40 ? 'bg-amber-100 text-amber-800' : - 'bg-red-100 text-red-800' - }`} + participant.engagementScore >= 40 ? 'bg-amber-100 text-amber-800' : + 'bg-red-100 text-red-800' + }`} > {participant.engagementScore}%
-
+
Attendance: {participant.attendanceRate}%
-
-
= 80 ? 'bg-green-500' : +
+
= 80 ? 'bg-green-500' : participant.attendanceRate >= 60 ? 'bg-blue-500' : - participant.attendanceRate >= 40 ? 'bg-amber-500' : - 'bg-red-500' - }`} + participant.attendanceRate >= 40 ? 'bg-amber-500' : + 'bg-red-500' + }`} style={{ width: `${participant.attendanceRate}%` }} >
-
+
Feedback: {participant.feedbackRate}%
-
-
= 80 ? 'bg-green-500' : +
+
= 80 ? 'bg-green-500' : participant.feedbackRate >= 60 ? 'bg-blue-500' : - participant.feedbackRate >= 40 ? 'bg-amber-500' : - 'bg-red-500' - }`} + participant.feedbackRate >= 40 ? 'bg-amber-500' : + 'bg-red-500' + }`} style={{ width: `${participant.feedbackRate}%` }} >
@@ -1154,20 +1209,21 @@ const TrainingDashboard: React.FC = () => {
- +
{/* Monthly Participation Trend */} -
+
-

- +

+ Monthly Participation

- Last 6 months + Last 6 months

@@ -1180,7 +1236,7 @@ const TrainingDashboard: React.FC = () => { Failed to load monthly participation data
) : !participationTrendData ? ( -
+
No participation trend data available
) : ( @@ -1194,7 +1250,7 @@ const TrainingDashboard: React.FC = () => { y: { beginAtZero: true, grid: { - color: 'rgba(0, 0, 0, 0.05)', + color: parseColorToRgba(mutedColor, 0.05), }, ticks: { precision: 0 @@ -1217,16 +1273,17 @@ const TrainingDashboard: React.FC = () => { )}
- + {/* Attendance Rates */} -
+
-

- +

+ Training Attendance Rates

@@ -1241,7 +1298,7 @@ const TrainingDashboard: React.FC = () => { Failed to load attendance data
) : !attendanceData?.trainingAttendanceRates || attendanceData.trainingAttendanceRates.length === 0 ? ( -
+
No attendance rate data available
) : ( @@ -1249,32 +1306,30 @@ const TrainingDashboard: React.FC = () => { {attendanceData.trainingAttendanceRates.slice(0, 6).map((training) => (
-
+
{training.title}
-
= 80 ? 'text-green-600' : +
= 80 ? 'text-green-600' : training.attendanceRate >= 60 ? 'text-blue-600' : - training.attendanceRate >= 40 ? 'text-amber-600' : - 'text-red-600' - }`} + training.attendanceRate >= 40 ? 'text-amber-600' : + 'text-red-600' + }`} > {training.attendanceRate}%
-
+
= 80 ? 'bg-green-500' : + className={`h-2 rounded-full ${training.attendanceRate >= 80 ? 'bg-green-500' : training.attendanceRate >= 60 ? 'bg-blue-500' : - training.attendanceRate >= 40 ? 'bg-amber-500' : - 'bg-red-500' - }`} + training.attendanceRate >= 40 ? 'bg-amber-500' : + 'bg-red-500' + }`} style={{ width: `${training.attendanceRate}%` }} >
-
+
{format(new Date(training.endDate), 'MMM d, yyyy')} @@ -1289,7 +1344,7 @@ const TrainingDashboard: React.FC = () => {
- + {/* Training Quality Score */} { >
-
+
- {dashboardLoading ? "..." : - dashboardData?.summary ? - `${Math.round((dashboardData.summary.completedTrainings / dashboardData.summary.totalTrainings) * 100) || 0}%` : - "0%" + {dashboardLoading ? "..." : + dashboardData?.summary ? + `${Math.round((dashboardData.summary.completedTrainings / dashboardData.summary.totalTrainings) * 100) || 0}%` : + "0%" }
-
Completion Rate
+
Completion Rate
- +
-
+
- {feedbackLoading ? "..." : - feedbackData?.overallAverages ? - feedbackData.overallAverages.overall.toFixed(1) : - "0.0" + {feedbackLoading ? "..." : + feedbackData?.overallAverages ? + feedbackData.overallAverages.overall.toFixed(1) : + "0.0" }
-
Overall Rating
+
Overall Rating
- +
-
+
- {attendanceLoading ? "..." : - attendanceData?.statusDistribution ? - `${attendanceData.statusDistribution.find(s => s.status === 'PRESENT')?.percentage || 0}%` : - "0%" + {attendanceLoading ? "..." : + attendanceData?.statusDistribution ? + `${attendanceData.statusDistribution.find(s => s.status === 'PRESENT')?.percentage || 0}%` : + "0%" }
-
Attendance Rate
+
Attendance Rate
- +
-
+
- {engagementData?.topParticipants ? - new Set(engagementData.topParticipants.map(p => p.organization)).size : + {engagementData?.topParticipants ? + new Set(engagementData.topParticipants.map(p => p.organization)).size : 0 }
-
Organizations
+
Organizations
- +
-
+
- {trainerLoading ? "..." : - trainerData?.trainers ? - trainerData.trainers.length : - 0 + {trainerLoading ? "..." : + trainerData?.trainers ? + trainerData.trainers.length : + 0 }
-
Active Trainers
+
Active Trainers
- + {/* CTA Section */}
-

Need to schedule a new training?

-

Create a new training session for your team or organization.

+

Need to schedule a new training?

+

Create a new training session for your team or organization.

- + +
+
+ + {/* Stats Bar - Compact */} +
+ +
+
+

Total Orders

+

{totalOrders}

+
+ +
+
+ - {/* Header */} -
-
-
- -
-
-

- Purchase Orders -

-

- View and manage all purchase orders with status update options -

-
-
- -
+
+
+

Received

+

{receivedOrders}

+
- {/* Unified Stats + Table */} -
- - {/* Stats Row */} - + + + +
+
+

Cancelled

+

{cancelledOrders}

+
+ +
+
+ + + {/* Full-Width Table */} + +
+
+ + + + + + + + + + + + + + + + {orders.length === 0 ? ( - - - - - - - - - - + - - - {orders.map((order, index) => ( - + ) : ( + orders.flatMap((order, orderIndex) => + order.items.map((item, itemIndex) => ( - - + + + ) : null} + - - + + + ) : null} + - - - - {/* Expanded items row */} - - {expandedOrderIds.includes(order.id) && ( - - - - )} - - - ))} - -
+
+ + PO Number +
+
+
+ + Vendor +
+
+
+ + SKU Code +
+
+
+ + Product Name +
+
+
+ + Order Date +
+
+
+ + Expected Date +
+
+
+ + Quantity +
+
RateStatusActions
-
-
-

- Total Orders -

-

- {totalOrders} -

-
- - - All records - -
-
- -
-

- Received -

-

- {receivedOrders} -

-
- - - Received - -
-
-
-

- Cancelled -

-

- {cancelledOrders} -

-
- - - Cancelled - -
-
-
-
-
- - PO Number -
-
-
- - Vendor -
-
-
- - Order Date -
-
-
- - Expected Date -
-
-
- - Days to Deliver -
-
-
- - Items Ordered -
-
+ No purchase orders found +
- toggleExpand(order.id)} - whileHover={{ scale: 1.1 }} - whileTap={{ scale: 0.95 }} - aria-label={ - expandedOrderIds.includes(order.id) - ? 'Collapse' - : 'Expand' - } - > - {expandedOrderIds.includes(order.id) ? ( - - ) : ( - - )} - - - {order.poNumber} + {itemIndex === 0 ? ( + <> + + {order.poNumber} + + {order.vendor?.name || '-'} + + {item.rawMaterial?.skuCode || '-'} - {order.vendor?.name || '-'} + + {item.rawMaterial?.name || 'Unknown'} - {formatDate(order.orderDate)} + {itemIndex === 0 ? ( + <> + + {formatDate(order.orderDate)} + + {formatDate(order.expectedDate)} + + {item.quantityOrdered} - {formatDate(order.expectedDate)} + + ₦{item.rate.toLocaleString()} - {calculateDaysToDeliver(order.orderDate, order.expectedDate)} + + + {item.status === 'Received' ? : } + {item.status} + - {order.items.length} + +
+ handleStartReceive(item, order)} + disabled={item.status === 'Received'} + className={`px-3 py-1 text-xs font-medium rounded transition ${item.status === 'Received' + ? 'bg-muted text-muted-foreground cursor-not-allowed' + : 'bg-primary text-primary-foreground hover:bg-primary/90' + }`} + > + {item.status === 'Received' ? 'Received' : 'Receive'} + +
- -
-

- - Order Items ({order.items.length}) -

-
-
- - - - - - - - - - - - - - {order.items.map((item, itemIndex) => { - const raw = - rawMaterialCache[item.rawMaterialId]; - return ( - - - - - - - - - - ); - })} - -
- SKU Code - - Product Name - - Unit - - Quantity - - Rate - - Status - - Update -
- {raw ? ( - - {raw.skuCode} - - ) : ( - - Loading... - - )} - - {raw ? ( - {raw.name} - ) : ( - - Loading... - - )} - - {raw ? ( - raw.unitOfMeasurement || - raw.unit || ( - - N/A - - ) - ) : ( - - Loading... - - )} - - {item.quantityOrdered} - - {item.rate} - - - {statusIcons[item.status] || null} - {item.status} - - -
- - {item.status === 'Received' && ( -
- After receiving, you can't - update status -
- )} -
-
-
-
-
-
- -
+ )) + ) + )} +
+
+ + + {/* Modals */} + { + setShowGRNBagWeightModal(false); + setCurrentItemForGRN(null); + }} + onSubmit={handleBagWeightsSubmit} + invoiceQtyBags={currentItemForGRN?.item.quantityOrdered || 0} + itemName={currentItemForGRN?.item.rawMaterial?.name || 'Unknown'} + /> + { + setShowGRNSummaryModal(false); + setShowGRNBagWeightModal(true); // Go back to bag weight entry + }} + onConfirm={handleGRNConfirm} + grnData={grnData} + /> setShowReceiveModal(false)} + onClose={() => { + setShowReceiveModal(false); + setGrnData(null); + grnDataRef.current = null; + setCurrentItemForGRN(null); + }} onConfirm={handleReceiveConfirm} defaultQuantity={receiveDefaultQty} /> diff --git a/client/src/components/pages/Order/TransactionalLog.tsx b/client/src/components/pages/Order/TransactionalLog.tsx index 8bc06e2..2eef27a 100644 --- a/client/src/components/pages/Order/TransactionalLog.tsx +++ b/client/src/components/pages/Order/TransactionalLog.tsx @@ -1,416 +1,343 @@ import React, { useEffect, useState } from "react"; import { - Search, - ChevronDown, - ChevronUp, - Info, - User, - Database, - Clock, - FilterIcon as FilterList, - RefreshCw, - Activity, - FileText, - Layers, + Search, + Info, + User, + Database, + FilterIcon as FilterList, + RefreshCw, + Activity, + FileText, + Layers, } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; import dayjs from "dayjs"; import api, { API_ROUTES } from "../../../utils/api"; interface TransactionLog { - id: string; - type: string; - entity: string; - entityId: string; - userId: string; - description: string; - createdAt: string; - user?: { name: string; email: string }; + id: string; + type: string; + entity: string; + entityId: string; + userId: string; + description: string; + createdAt: string; + user?: { name: string; email: string }; } const getSummary = (log: TransactionLog) => { - if (!log.description) return "No description available"; - const lines = log.description.split('\n').filter(line => line.trim()); - if (lines.length === 0) return "No description available"; - const firstLine = lines[0].trim(); - if (firstLine.includes('{') || firstLine.includes('[')) { - try { - const parsed = JSON.parse(log.description); - if (parsed.action) return parsed.action; - if (parsed.operation) return parsed.operation; - if (parsed.type) return `${parsed.type} operation`; - if (parsed.name) return `Operation on ${parsed.name}`; - } catch (e) {} - } - const actionMatch = firstLine.match(/^(Created|Updated|Deleted|Modified|Added|Removed)\s+(.+?)(?:\s*:|\s*$)/i); - if (actionMatch) { - return `${actionMatch[1]} ${actionMatch[2]}`; - } - if (firstLine.length > 60) { - return firstLine.substring(0, 57) + '...'; - } - return firstLine || `${log.type} operation on ${log.entity}`; + if (!log.description) return "No description available"; + const lines = log.description.split('\n').filter(line => line.trim()); + if (lines.length === 0) return "No description available"; + const firstLine = lines[0].trim(); + if (firstLine.length > 60) { + return firstLine.substring(0, 57) + '...'; + } + return firstLine || `${log.type} operation on ${log.entity}`; }; const getDetails = (log: TransactionLog) => { - if (!log.description) return ""; - const detailsIndex = log.description.toLowerCase().indexOf('details:'); - if (detailsIndex !== -1) { - return log.description.substring(detailsIndex + 8).trim(); - } - const lines = log.description.split('\n'); - if (lines.length > 1) { - return lines.slice(1).join('\n').trim(); - } - if (log.description.length < 100) return ""; - return log.description; -}; - -const formatDetailsContent = (details: string) => { - if (!details) return ""; - try { - const parsed = JSON.parse(details); - return JSON.stringify(parsed, null, 2); - } catch (e) { - return details; - } + if (!log.description) return ""; + const lines = log.description.split('\n'); + if (lines.length > 1) { + return lines.slice(1).join('\n').trim(); + } + return ""; }; const getTypeColor = (type: string) => { - switch (type.toUpperCase()) { - case 'CREATE': return { bg: 'bg-emerald-100', text: 'text-emerald-800', icon: 'text-emerald-600' }; - case 'UPDATE': return { bg: 'bg-amber-100', text: 'text-amber-800', icon: 'text-amber-600' }; - case 'DELETE': return { bg: 'bg-red-100', text: 'text-red-800', icon: 'text-red-600' }; - case 'READ': return { bg: 'bg-blue-100', text: 'text-blue-800', icon: 'text-blue-600' }; - default: return { bg: 'bg-gray-100', text: 'text-gray-800', icon: 'text-gray-600' }; - } + switch (type.toUpperCase()) { + case 'CREATE': return { badge: 'bg-emerald-100 text-emerald-700', icon: 'text-emerald-600' }; + case 'UPDATE': return { badge: 'bg-amber-100 text-amber-700', icon: 'text-amber-600' }; + case 'DELETE': return { badge: 'bg-red-100 text-red-700', icon: 'text-red-600' }; + case 'READ': return { badge: 'bg-blue-100 text-blue-700', icon: 'text-blue-600' }; + default: return { badge: 'bg-gray-100 text-gray-700', icon: 'text-gray-600' }; + } }; const getEntityIcon = (entity: string) => { - switch (entity.toLowerCase()) { - case 'purchaseorder': return ; - case 'rawmaterialproduct': return ; - case 'user': return ; - default: return ; - } + switch (entity.toLowerCase()) { + case 'purchaseorder': return ; + case 'rawmaterialproduct': return ; + case 'user': return ; + default: return ; + } }; const TransactionalLog: React.FC = () => { - const [logs, setLogs] = useState([]); - const [search, setSearch] = useState(""); - const [fromDate, setFromDate] = useState(""); - const [toDate, setToDate] = useState(""); - const [loading, setLoading] = useState(false); - const [expanded, setExpanded] = useState(null); - const [showFilters, setShowFilters] = useState(false); + const [logs, setLogs] = useState([]); + const [search, setSearch] = useState(""); + const [fromDate, setFromDate] = useState(""); + const [toDate, setToDate] = useState(""); + const [loading, setLoading] = useState(false); + const [showFilters, setShowFilters] = useState(false); + const [expandedId, setExpandedId] = useState(null); - const fetchLogs = async () => { - setLoading(true); - try { - const params: any = {}; - if (search) params.search = search; - if (fromDate) params.from = dayjs(fromDate).startOf("day").toISOString(); - if (toDate) params.to = dayjs(toDate).endOf("day").toISOString(); - const authToken = localStorage.getItem('authToken'); - const res = await api.get(API_ROUTES.RAW.GET_ALL_TRANSACTION_LOGS, { - params, - headers: { Authorization: `Bearer ${authToken}` }, - }); - setLogs(res.data); - } catch (err) { - setLogs([]); - } - setLoading(false); - }; - - useEffect(() => { - fetchLogs(); - // eslint-disable-next-line - }, []); + const fetchLogs = async () => { + setLoading(true); + try { + const params: any = {}; + if (search) params.search = search; + if (fromDate) params.from = dayjs(fromDate).startOf("day").toISOString(); + if (toDate) params.to = dayjs(toDate).endOf("day").toISOString(); + const authToken = localStorage.getItem('authToken'); + const res = await api.get(API_ROUTES.RAW.GET_ALL_TRANSACTION_LOGS, { + params, + headers: { Authorization: `Bearer ${authToken}` }, + }); + setLogs(res.data); + } catch (err) { + console.error('Error fetching logs:', err); + setLogs([]); + } + setLoading(false); + }; - const handleSearch = () => { - fetchLogs(); - }; + useEffect(() => { + fetchLogs(); + }, []); - const handleReset = () => { - setSearch(""); - setFromDate(""); - setToDate(""); - fetchLogs(); - }; + const handleSearch = () => { + fetchLogs(); + }; - const LoadingSkeleton = () => ( -
- {[...Array(5)].map((_, i) => ( -
-
-
-
-
-
-
-
-
-
-
-
-
-
- ))} -
- ); + const handleReset = () => { + setSearch(""); + setFromDate(""); + setToDate(""); + setLogs([]); + }; - return ( -
-
- {/* Header */} - -
-
-
- -
-
-
- Transaction Logs -
-
- Monitor and track all system activities -
-
-
-
- - -
-
- - {/* Search and Filters */} -
-
-
-
-
- setSearch(e.target.value)} - onKeyDown={e => e.key === "Enter" && handleSearch()} - /> - - - -
-
-
- - -
-
- - - {showFilters && ( - -
-
- - setFromDate(e.target.value)} - /> -
-
- - setToDate(e.target.value)} - /> -
-
-
- )} -
-
-
-
- - {/* Logs List */} -
- {loading ? ( - - ) : logs.length === 0 ? ( + return ( +
+ {/* Header */} -
- -
-
- No transaction logs found -
-
- Try adjusting your search criteria or check back later -
-
- ) : ( - - {logs.map((log, index) => { - const typeColor = getTypeColor(log.type); - const hasDetails = getDetails(log).length > 0; - const isExpanded = expanded === log.id; - - return ( - -
-
-
-
-
-
-
- {getEntityIcon(log.entity)} -
-
-
-
- - {log.type} - - {log.entity} -
-
-
- - {dayjs(log.createdAt).format("MMM DD, YYYY HH:mm")} -
-
- - {log.user?.name || log.userId} -
-
-
-
- {hasDetails && ( - - )} -
+
+
+
+ +
+
+

Transaction Logs

+

Monitor system activities and changes

+
+
+
+ + + + setShowFilters(!showFilters)} + className={`p-2 bg-card border border-border/50 rounded-lg hover:bg-muted transition ${showFilters ? 'bg-primary/10' : ''}`} + title={showFilters ? "Hide Filters" : "Show Filters"} + > + + +
+
-
-
-
-
-
- {getSummary(log)} -
- {log.user?.email && ( -
- by {log.user.email} -
- )} -
-
-
+ {/* Search and Filters */} +
+
+
+ + setSearch(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleSearch()} + />
+ + Search + + + Reset + +
- - {isExpanded && ( + + {showFilters && ( -
-
-
-
- Transaction Details +
+
+ + setFromDate(e.target.value)} + />
-
- - - ID: {log.entityId} - +
+ + setToDate(e.target.value)} + />
-
-
-
-                                      {formatDetailsContent(getDetails(log))}
-                                    
-
-
- )} - -
-
- - ); - })} - - )} + )} + +
+
+ + {/* Table */} + +
+ + + + + + + + + + + + + + {loading ? ( + + + + ) : logs.length === 0 ? ( + + + + ) : ( + logs.map((log, index) => { + const typeColor = getTypeColor(log.type); + const hasDetails = getDetails(log).length > 0; + + return ( + + + + + + + + + + + + {/* Expanded details row */} + + {expandedId === log.id && hasDetails && ( + + + + )} + + + ); + }) + )} + +
TypeEntityDescriptionUserDate & TimeEntity IDDetails
+ Loading... +
+ No transaction logs found +
+ + {getEntityIcon(log.entity)} + {log.type} + + + {log.entity} + + {getSummary(log)} + + {log.user?.name || log.user?.email || log.userId} + + {dayjs(log.createdAt).format("MMM DD, YYYY HH:mm")} + + {log.entityId.substring(0, 8)}... + + {hasDetails && ( + setExpandedId(expandedId === log.id ? null : log.id)} + className="text-primary hover:text-primary/80 transition" + > + + + )} + + +
Transaction Details:
+
+                                                                    {getDetails(log)}
+                                                                
+
+
+
+
-
-
- ); + ); }; -export default TransactionalLog; \ No newline at end of file +export default TransactionalLog; diff --git a/client/src/components/pages/QualityReport/RMQualityReport.tsx b/client/src/components/pages/QualityReport/RMQualityReport.tsx index 822c1d9..b534c97 100644 --- a/client/src/components/pages/QualityReport/RMQualityReport.tsx +++ b/client/src/components/pages/QualityReport/RMQualityReport.tsx @@ -4,15 +4,13 @@ import { Plus, Save, Download, - Edit2, - Trash2, ChevronRight, Search, RefreshCw, FileText, Clock, Package, - Calendar, + Mail, Building, Hash, Beaker, @@ -23,8 +21,6 @@ import { RotateCw, ChevronDown, Filter, - Ruler, - Settings, Target, Award, } from 'lucide-react'; @@ -38,6 +34,8 @@ import { } from '../../../utils/api'; import { RMQualityReport as RMQualityReportType } from '../../../Types/qualityTypes'; import api, { API_ROUTES } from '../../../utils/api'; +import { mailFilteredRMQualityReports } from '../../../utils/api'; +import { exportFilteredRMQualityReports } from '../../../utils/api'; import { format } from 'date-fns'; // Enhanced animations @@ -63,7 +61,7 @@ const itemVariants = { // Fixed parameters for Chilli const CHILLI_PARAMETERS = [ - { parameter: 'Moisture', standard: 'max 8%' }, + { parameter: 'Moisture', standard: 'max 10%' }, { parameter: 'ASTA Color', standard: 'min 40' }, { parameter: 'Acid Insoluble Ash', standard: 'max 1.5%' }, { parameter: 'Total Ash', standard: 'max 8%' }, @@ -78,6 +76,48 @@ const formatDate = (dateString: string) => { }; const RMQualityReport: React.FC = () => { + const [isExportingFiltered, setIsExportingFiltered] = useState(false); + // Handler for exporting filtered reports + const handleExportFiltered = async () => { + if (filteredReports.length === 0) { + setError('No filtered reports available to export'); + return; + } + try { + setIsExportingFiltered(true); + setError(null); + const filtersToSend = { + supplier: appliedFilters.supplier, + grn: appliedFilters.grn, + fromDate: appliedFilters.fromDate, + toDate: appliedFilters.toDate, + }; + const response = await exportFilteredRMQualityReports(filtersToSend); + if (response && response.data) { + const blob = new Blob([response.data], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute( + 'download', + `Filtered_RM_Quality_Reports_${new Date().toISOString().split('T')[0]}.xlsx` + ); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + toast.success('Filtered Excel export started'); + } else { + setError('Failed to export filtered reports'); + } + } catch (error) { + setError('Failed to export filtered reports'); + } finally { + setIsExportingFiltered(false); + } + }; const [reports, setReports] = useState([]); const [loading, setLoading] = useState(false); const [showForm, setShowForm] = useState(false); @@ -87,9 +127,69 @@ const RMQualityReport: React.FC = () => { const [isFormValid, setIsFormValid] = useState(false); const [isSaving, setIsSaving] = useState(false); const [isExportingAll, setIsExportingAll] = useState(false); + const [isMailingAll, setIsMailingAll] = useState(false); + const [isMailingFiltered, setIsMailingFiltered] = useState(false); + const [selectedReportIds, setSelectedReportIds] = useState([]); + const [isDeletingMultiple, setIsDeletingMultiple] = useState(false); + + // Handler for mailing filtered reports + const handleMailFiltered = async () => { + if (filteredReports.length === 0) { + setError('No filtered reports available to mail'); + return; + } + try { + setIsMailingFiltered(true); + setError(null); + const filtersToSend = { + supplier: appliedFilters.supplier, + grn: appliedFilters.grn, + fromDate: appliedFilters.fromDate, + toDate: appliedFilters.toDate, + }; + const response = await mailFilteredRMQualityReports(filtersToSend); + if (response.data.success) { + toast.success(response.data.message || 'Filtered reports mailed successfully'); + } else { + setError(response.data.error || 'Failed to mail filtered reports'); + } + } catch (error) { + setError('Failed to mail filtered reports'); + } finally { + setIsMailingFiltered(false); + } + }; const [error, setError] = useState(null); const [isFilterOpen, setIsFilterOpen] = useState(false); + // Filters + const [filters, setFilters] = useState({ + supplier: '', + grn: '', + fromDate: '', + toDate: '', + }); + const [appliedFilters, setAppliedFilters] = useState({ + supplier: '', + grn: '', + fromDate: '', + toDate: '', + }); + + const applyFilters = () => { + setAppliedFilters(filters); + }; + + const clearFilters = () => { + const empty = { supplier: '', grn: '', fromDate: '', toDate: '' }; + setFilters(empty); + setAppliedFilters(empty); + }; + + const handleFilterChange = (name: string, value: string) => { + setFilters((prev) => ({ ...prev, [name]: value })); + }; + const [receivedRawMaterials, setReceivedRawMaterials] = useState([]); const [receivedVendors, setReceivedVendors] = useState([]); @@ -261,6 +361,52 @@ const RMQualityReport: React.FC = () => { } }; + const handleToggleSelect = (id: string) => { + setSelectedReportIds(prev => + prev.includes(id) ? prev.filter(reportId => reportId !== id) : [...prev, id] + ); + }; + + const handleToggleSelectAll = () => { + if (selectedReportIds.length === filteredReports.length) { + setSelectedReportIds([]); + } else { + setSelectedReportIds(filteredReports.map(r => r.id)); + } + }; + + const handleDeleteMultiple = async () => { + if (selectedReportIds.length === 0) { + toast.error('No reports selected'); + return; + } + + if (window.confirm(`Are you sure you want to delete ${selectedReportIds.length} selected report(s)?`)) { + try { + setIsDeletingMultiple(true); + const deletePromises = selectedReportIds.map(id => deleteRMQualityReport(id)); + const results = await Promise.all(deletePromises); + + const successCount = results.filter(r => r.success).length; + const failCount = results.length - successCount; + + if (successCount > 0) { + toast.success(`${successCount} report(s) deleted successfully`); + } + if (failCount > 0) { + toast.error(`Failed to delete ${failCount} report(s)`); + } + + setSelectedReportIds([]); + fetchReports(); + } catch (error) { + toast.error('Failed to delete reports'); + } finally { + setIsDeletingMultiple(false); + } + } + }; + const handleExport = async ( id: string, format: 'excel' | 'pdf' = 'excel' @@ -303,44 +449,72 @@ const RMQualityReport: React.FC = () => { } }; - const handleExportAll = async () => { - if (reports.length === 0) { - setError('No reports available to export'); - return; - } - - try { - setIsExportingAll(true); - const response = await api.get( - `${API_ROUTES.RAW.EXPORT_ALL_QUALITY_REPORTS}`, - { - headers: { Authorization: `Bearer ${authToken}` }, - responseType: 'blob', - } - ); - - // Create blob and trigger download - const blob = new Blob([response.data], { - type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // <-- Excel MIME type - }); - const url = window.URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.setAttribute( - 'download', - `RM_Quality_Reports_${new Date().toISOString().split('T')[0]}.xlsx` // <-- Excel extension - ); - document.body.appendChild(link); - link.click(); - link.remove(); - window.URL.revokeObjectURL(url); - } catch (error) { - console.error('Export failed:', error); - setError('Failed to export reports'); - } finally { - setIsExportingAll(false); - } - }; + const handleExportAll = async () => { + if (reports.length === 0) { + setError('No reports available to export'); + return; + } + + try { + setIsExportingAll(true); + const response = await api.get( + `${API_ROUTES.RAW.EXPORT_ALL_QUALITY_REPORTS}`, + { + headers: { Authorization: `Bearer ${authToken}` }, + responseType: 'blob', + } + ); + + // Create blob and trigger download + const blob = new Blob([response.data], { + type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', // <-- Excel MIME type + }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute( + 'download', + `RM_Quality_Reports_${new Date().toISOString().split('T')[0]}.xlsx` // <-- Excel extension + ); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + } catch (error) { + console.error('Export failed:', error); + setError('Failed to export reports'); + } finally { + setIsExportingAll(false); + } + }; + + const handleMailAll = async () => { + if (reports.length === 0) { + setError('No reports available to mail'); + return; + } + + try { + setIsMailingAll(true); + const response = await api.get( + `${API_ROUTES.RAW.MAIL_ALL_QUALITY_REPORTS}`, + { + headers: { Authorization: `Bearer ${authToken}` }, + } + ); + + if (response.data.success) { + toast.success(response.data.message || 'Reports mailed successfully'); + } else { + setError(response.data.error || 'Failed to mail reports'); + } + } catch (error) { + console.error('Mail failed:', error); + setError('Failed to mail reports'); + } finally { + setIsMailingAll(false); + } + }; const resetForm = () => { setFormData({ @@ -353,13 +527,26 @@ const RMQualityReport: React.FC = () => { setError(null); }; - const filteredReports = reports.filter( - (report) => - report.rawMaterialName.toLowerCase().includes(searchTerm.toLowerCase()) || - report.variety.toLowerCase().includes(searchTerm.toLowerCase()) || - report.supplier.toLowerCase().includes(searchTerm.toLowerCase()) || - report.grn.toLowerCase().includes(searchTerm.toLowerCase()) - ); + const filteredReports = reports.filter((report) => { + // Search match + const q = searchTerm.trim().toLowerCase(); + const matchesSearch = + !q || + [report.rawMaterialName, report.variety, report.supplier, report.grn].some((f) => + String(f || '').toLowerCase().includes(q) + ); + + // Applied filters + const { supplier, grn, fromDate, toDate } = appliedFilters; + if (supplier && supplier !== report.supplier) return false; + if (grn && !report.grn.toLowerCase().includes(grn.toLowerCase())) return false; + + const reportDate = report.dateOfReport ? new Date(report.dateOfReport) : null; + if (fromDate && reportDate && new Date(fromDate) > reportDate) return false; + if (toDate && reportDate && new Date(toDate) < reportDate) return false; + + return matchesSearch; + }); const basicInfoComplete = formData.rawMaterialName && @@ -565,94 +752,92 @@ const RMQualityReport: React.FC = () => { {/* Right Content: Quality Parameters (75%) */} -
-
-

- +
+ {/* Header */} +
+

+ Quality Parameters - {parametersComplete && ( - - - Complete - - )}

+ + {parametersComplete && ( + + + Complete + + )}
-
-
+ {/* Body */} + {formData.rawMaterialName ? ( +
{CHILLI_PARAMETERS.map((param, index) => ( -
-
-
- -
- {param.parameter} -
+
+ {/* Parameter */} +
+
+ Parameter
-
- -
- {param.standard} -
+
+ {param.parameter}
-
- - - handleResultChange(index, e.target.value) - } - className="w-full border border-input rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/40 focus:border-primary transition-all bg-background text-sm" - placeholder="Enter result" - required - /> +
+ + {/* Standard */} +
+
+ Standard +
+
+ {param.standard}
-
- {results[index]?.trim() !== '' ? ( - - - Filled - + + {/* Result */} +
+ + handleResultChange(index, e.target.value) + } + className="w-full text-sm px-2 py-1 border border-input bg-background focus:outline-none focus:ring-1 focus:ring-primary/40" + placeholder="Result *" + required + /> +
+ + {/* Status */} +
+ {results[index]?.trim() ? ( + ) : ( - - - Pending - + )}
))}
-
+ ) : ( +
+
+

+ Select a product to view quality parameters. +

+
+
+ )}
+
{/* Footer actions */} @@ -700,11 +885,10 @@ const RMQualityReport: React.FC = () => { type="button" onClick={handleSubmit} disabled={isSaving || !isFormValid} - className={`px-5 py-2 rounded-lg text-sm font-semibold inline-flex items-center gap-2 transition ${ - isSaving || !isFormValid - ? 'bg-muted text-muted-foreground cursor-not-allowed' - : 'bg-primary text-primary-foreground hover:bg-primary/90' - }`} + className={`px-5 py-2 rounded-lg text-sm font-semibold inline-flex items-center gap-2 transition ${isSaving || !isFormValid + ? 'bg-muted text-muted-foreground cursor-not-allowed' + : 'bg-primary text-primary-foreground hover:bg-primary/90' + }`} > {isSaving ? ( <> @@ -766,16 +950,90 @@ const RMQualityReport: React.FC = () => {

+ {/* Bulk Delete Button - shown when items are selected */} + {selectedReportIds.length > 0 && ( + + {isDeletingMultiple ? ( + <> + + + + Deleting... + + ) : ( + <> + + Delete ({selectedReportIds.length}) + + )} + + )} + {/* Export Filtered */} + + {isExportingFiltered ? ( + <> + + + + Exporting... + + ) : ( + <> + + Export Filtered + + )} + + {/* Export All */} {isExportingAll ? ( <> @@ -802,6 +1060,79 @@ const RMQualityReport: React.FC = () => { )} + {/* Mail All */} + + {isMailingAll ? ( + <> + + + + Mailing... + + ) : ( + <> + + Mail All + + )} + + {/* Mail Filtered */} + + {isMailingFiltered ? ( + <> + + + + Mailing... + + ) : ( + <> + + Mail Filtered + + )} + { {/* Search and Filters Section */}
-
-
- + {!isFilterOpen && ( +
+
+ +
+ setSearchTerm(e.target.value)} + className="pl-10 pr-4 py-2.5 w-full border border-input rounded-xl focus:ring-2 focus:ring-ring focus:border-ring outline-none transition-all duration-200 text-sm bg-background" + />
- setSearchTerm(e.target.value)} - className="pl-10 pr-4 py-2.5 w-full border border-input rounded-xl focus:ring-2 focus:ring-ring focus:border-ring outline-none transition-all duration-200 text-sm bg-background" - /> -
+ )} -
+
setIsFilterOpen(!isFilterOpen)} - className="flex items-center gap-2 px-4 py-2.5 border border-input bg-background rounded-lg hover:bg-accent transition-colors duration-200 text-sm" + className={`flex items-center gap-2 px-4 py-2.5 rounded-lg transition-colors duration-200 text-sm ${isFilterOpen ? 'bg-accent/10 border border-primary/20' : 'bg-background border border-input hover:bg-accent'}`} whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }} > Filter + {Object.values(appliedFilters).some((v) => v) && ( + + Applied + + )} { Refresh
+ + {isFilterOpen && ( +
+
+ + +
+ +
+ + handleFilterChange('grn', e.target.value)} + className="w-full border border-input rounded px-2 py-1 bg-background text-xs focus:ring-1 focus:ring-primary/30 h-8" + placeholder="Contains GRN" + /> +
+ +
+ + handleFilterChange('fromDate', e.target.value)} + className="w-full border border-input rounded px-2 py-1 bg-background text-xs focus:ring-1 focus:ring-primary/30 h-8" + /> +
+ +
+ + handleFilterChange('toDate', e.target.value)} + className="w-full border border-input rounded px-2 py-1 bg-background text-xs focus:ring-1 focus:ring-primary/30 h-8" + /> +
+ +
+ + +
+
+ )}
@@ -918,154 +1319,101 @@ const RMQualityReport: React.FC = () => { ) : ( <>
- - - - - - - - - -
-
- - Raw Material -
-
-
- - Variety -
-
-
- - Supplier -
-
-
- - GRN -
-
-
- - Date -
-
-
- - Parameters -
-
-
- - Actions -
+ + + + + + + + + + + - + + {filteredReports.map((report, index) => ( - + + - - - - - - ))} @@ -1073,6 +1421,7 @@ const RMQualityReport: React.FC = () => {
+ 0 && selectedReportIds.length === filteredReports.length} + onChange={handleToggleSelectAll} + className="w-4 h-4 cursor-pointer accent-primary" + title="Select all" + /> Raw MaterialVarietySupplierGRNDateParamsActions
+ + handleToggleSelect(report.id)} + className="w-4 h-4 cursor-pointer accent-primary" + onClick={(e) => e.stopPropagation()} + /> + {report.rawMaterialName} - - {report.variety} - + + + {report.variety} -
- - {report.supplier} -
+ +
+ {report.supplier} -
- - {report.grn} -
+ +
+ {report.grn} -
- - {formatDate(report.dateOfReport)} -
+ +
+ {formatDate(report.dateOfReport)} - - - {report.parameters.length} params - + + + {report.parameters.length} -
- handleEdit(report)} - className="group relative flex items-center justify-center w-8 h-8 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-all border border-primary/20 cursor-pointer" - title="Edit Report" - > - - - handleExport(report.id, 'excel')} - className="group relative flex items-center justify-center w-8 h-8 bg-primary/10 text-primary rounded-lg hover:bg-primary/20 transition-all border border-primary/20 cursor-pointer" - title="Export as Excel" - > - - - handleDelete(report.id)} - className="group relative flex items-center justify-center w-8 h-8 bg-destructive/10 text-destructive rounded-lg hover:bg-destructive/20 transition-all border border-destructive/20 cursor-pointer" - title="Delete Report" - > - - -
+ + {/* āœ… Proper actions */} +
+
+ + ā‹® + + +
+ + + + + +
+
+ )} diff --git a/client/src/components/pages/Stock/Stock.tsx b/client/src/components/pages/Stock/Stock.tsx index 0a47e20..5ff335e 100644 --- a/client/src/components/pages/Stock/Stock.tsx +++ b/client/src/components/pages/Stock/Stock.tsx @@ -1,58 +1,9 @@ import React, { useEffect, useState } from 'react'; import api, { API_ROUTES } from '../../../utils/api'; -import { Spin, Alert, Card, Empty, Button } from 'antd'; -import { motion } from 'framer-motion'; -import { - Boxes, - Warehouse, - ZoomIn, - ZoomOut, - RotateCcw, - MapPin, - BarChart3, - Maximize2, - Grid3X3, -} from 'lucide-react'; +import { Spin, Alert, Card, Empty } from 'antd'; +import { Boxes, Warehouse } from 'lucide-react'; import ProductPurchaseOrdersView from './Timeline'; -// Helper to interpolate color between multiple colors based on value (0-1) -function interpolateColor(factor: number) { - const LOW_COLOR = '#f8faff'; // very light blue-white - const MID_COLOR = '#bfdbfe'; // light blue - const HIGH_COLOR = '#1e40af'; // deep blue - - if (factor <= 0.5) { - // Interpolate between low and mid - const normalizedFactor = factor * 2; - return interpolateColorBetween(LOW_COLOR, MID_COLOR, normalizedFactor); - } else { - // Interpolate between mid and high - const normalizedFactor = (factor - 0.5) * 2; - return interpolateColorBetween(MID_COLOR, HIGH_COLOR, normalizedFactor); - } -} - -function interpolateColorBetween( - color1: string, - color2: string, - factor: number -) { - let c1 = color1.substring(1); - let c2 = color2.substring(1); - let rgb1 = [ - parseInt(c1.substring(0, 2), 16), - parseInt(c1.substring(2, 4), 16), - parseInt(c1.substring(4, 6), 16), - ]; - let rgb2 = [ - parseInt(c2.substring(0, 2), 16), - parseInt(c2.substring(2, 4), 16), - parseInt(c2.substring(4, 6), 16), - ]; - let rgb = rgb1.map((v, i) => Math.round(v + (rgb2[i] - v) * factor)); - return `rgb(${rgb[0]},${rgb[1]},${rgb[2]})`; -} - type StockItem = { rawMaterial: { id: string; @@ -71,8 +22,6 @@ const Stock: React.FC = () => { const [stock, setStock] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [zoomLevel, setZoomLevel] = useState(100); - const [viewMode, setViewMode] = useState<'compact' | 'detailed'>('detailed'); const [modalOpen, setModalOpen] = useState(false); const [selectedProduct, setSelectedProduct] = useState<{ id: string; @@ -105,16 +54,12 @@ const Stock: React.FC = () => { }); }, []); - const handleZoomIn = () => setZoomLevel((prev) => Math.min(prev + 20, 200)); - const handleZoomOut = () => setZoomLevel((prev) => Math.max(prev - 20, 60)); - const handleResetZoom = () => setZoomLevel(100); - if (loading) { return ( -
+
-

+

Loading stock distribution...

@@ -124,7 +69,7 @@ const Stock: React.FC = () => { if (error) { return ( -
+
{ if (stock.length === 0) { return ( -
+
{ }); }); - // Find min and max quantity for color scaling - const allQuantities = stock - .map((item) => item.currentQuantity) - .filter((q) => q > 0); - const minQ = Math.min(...allQuantities, 0); - const maxQ = Math.max(...allQuantities, 1); - - // Calculate dynamic cell dimensions based on content - const headerHeight = viewMode === 'detailed' ? '80px' : '60px'; - const cellHeight = viewMode === 'detailed' ? '70px' : '50px'; - const productColumnWidth = '200px'; - - // Calculate warehouse column width dynamically - - // Animation variants - const containerVariants = { - hidden: { opacity: 0, y: 20 }, - visible: { - opacity: 1, - y: 0, - transition: { - duration: 0.6, - staggerChildren: 0.05, - }, - }, - }; - - const cellVariants = { - hidden: { opacity: 0, scale: 0.9 }, - visible: { - opacity: 1, - scale: 1, - transition: { duration: 0.3 }, - }, - hover: { - scale: 1.02, - transition: { duration: 0.2 }, - }, - }; - - const scaleStyle = { - transform: `scale(${zoomLevel / 100})`, - transformOrigin: 'top left', - transition: 'transform 0.3s ease-in-out', - }; - return ( <> - {modalOpen && selectedProduct ? ( + {modalOpen && selectedProduct ? ( setModalOpen(false)} rawMaterialId={selectedProduct.id} rawMaterialName={selectedProduct.name} /> ) : ( -
- {/* Header Section */} - -
+
+ {/* Header Section */} +
-
- +
+
-

- Stock Distribution Heatmap +

+ Stock Distribution

-

- {products.length} products across {warehouses.length}{' '} - warehouses +

+ {products.length} products across {warehouses.length} warehouses

- - {/* Controls */} -
- - -
-
-
- - {/* Heatmap Section */} -
-
-
- - {/* Header Row */} - -
- - Product -
-
- - {warehouses.map((wh) => ( - -
- - - {wh} - + {/* Table Section */} +
+ + + + + {warehouses.map((wh) => ( + + + + {products.map((prod, index) => ( + + {warehouses.map((wh) => { const item = matrix[prod][wh]; - const q = item?.currentQuantity ?? 0; - // Normalize quantity for color - const norm = - maxQ === minQ ? 0 : (q - minQ) / (maxQ - minQ); - const bg = q === 0 ? '#f3f4f6' : interpolateColor(norm); - const textColor = norm > 0.6 ? '#ffffff' : '#1f2937'; - + let rawQ: unknown = item?.currentQuantity ?? 0; + let q: number; + // Fix: If rawQ is a string (from backend bug), try to parse as float, else fallback to 0 + if (typeof rawQ === 'string') { + // Remove commas and try to parse + const parsed = parseFloat(rawQ.replace(/,/g, '')); + q = isNaN(parsed) ? 0 : parsed; + } else if (typeof rawQ === 'number' && isFinite(rawQ)) { + q = rawQ; + } else { + q = 0; + } + // Get unit of measurement, but ignore it if it looks like a number + let unit = item?.rawMaterial.unitOfMeasurement || 'units'; + if (!isNaN(parseFloat(unit))) { + unit = 'kG'; // fallback if unit is actually a number + } return ( - -
- {q > 0 ? ( - <> - - {q.toLocaleString()} - - {viewMode === 'detailed' && item && ( - - {item.rawMaterial.unitOfMeasurement || - 'unit'} - - )} - - ) : ( -
- — - {viewMode === 'detailed' && ( - No Stock - )} -
- )} -
- - {/* Hover overlay */} -
+ ); })} - + ))} - - + +
+
+ + Product
- {viewMode === 'detailed' && ( -
- - {stock.find((item) => item.warehouse.name === wh) - ?.warehouse?.location || 'N/A'} +
+
+ + {wh}
- )} - - ))} - - {/* Data Rows */} - {products.map((prod) => ( - - {/* Product Name Cell */} - + ))} +
{ - // Find the first item for this product to get its id - const item = stock.find( - (s) => s.rawMaterial.name === prod - ); + const item = stock.find((s) => s.rawMaterial.name === prod); if (item) { setSelectedProduct({ id: item.rawMaterial.id, @@ -356,72 +178,47 @@ const Stock: React.FC = () => { } }} > -
-
- - {prod} - -
- - - {/* Data Cells */} + {prod} +
+ {q > 0 ? ( + + {q.toLocaleString(undefined, { maximumFractionDigits: 2 })} {unit} + + ) : ( + + — + + )} +
-
)} ); diff --git a/client/src/components/pages/Stock/Timeline.tsx b/client/src/components/pages/Stock/Timeline.tsx index 5bc8170..7bdeef5 100644 --- a/client/src/components/pages/Stock/Timeline.tsx +++ b/client/src/components/pages/Stock/Timeline.tsx @@ -15,7 +15,7 @@ import { AlertCircle, } from "lucide-react" import api, { API_ROUTES } from "../../../utils/api" -import { Modal } from "antd"; + type PurchaseOrder = { id: string @@ -139,101 +139,101 @@ const ProductPurchaseOrdersView: React.FC = ({ onClose, rawMaterialId, ra } const TimelineComponent = React.memo(({ events }: { events: TimelineEvent[] }) => { - const handleMouseEnter = useCallback((index: number) => { - setHoveredEvent(index) - }, []) + const handleMouseEnter = useCallback((index: number) => { + setHoveredEvent(index) + }, []) - const handleMouseLeave = useCallback(() => { - setHoveredEvent(null) - }, []) + const handleMouseLeave = useCallback(() => { + setHoveredEvent(null) + }, []) - if (!events.length) { - return ( -
- -

No Timeline Events

-

No events found for this purchase order.

-
- ) - } + if (!events.length) { + return ( +
+ +

No Timeline Events

+

No events found for this purchase order.

+
+ ) + } - return ( -
-
-
-
- {events.map((event, index) => { - const config = getEventConfig(event.type) - return ( -
- handleMouseEnter(index)} - onMouseLeave={handleMouseLeave} - role="button" - tabIndex={0} - aria-label={`${config.label} - ${new Date(event.date).toLocaleDateString()}`} - onKeyDown={(e) => { - if (e.key === "Enter" || e.key === " ") { - handleMouseEnter(index) - } - }} - > - -
{config.icon}
-
- -
{config.label}
-
{new Date(event.date).toLocaleDateString()}
-
- - {hoveredEvent === index && ( - -
{config.label}
-
{new Date(event.date).toLocaleString()}
-
{event.details}
-
- + return ( +
+
+
+
+ {events.map((event, index) => { + const config = getEventConfig(event.type) + return ( +
+ handleMouseEnter(index)} + onMouseLeave={handleMouseLeave} + role="button" + tabIndex={0} + aria-label={`${config.label} - ${new Date(event.date).toLocaleDateString()}`} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + handleMouseEnter(index) + } + }} + > + +
{config.icon}
+
+ +
{config.label}
+
{new Date(event.date).toLocaleDateString()}
+
+ + {hoveredEvent === index && ( + +
{config.label}
+
{new Date(event.date).toLocaleString()}
+
{event.details}
+
+ + )} + + {index < events.length - 1 && ( +
)} - - {index < events.length - 1 && ( -
- )} -
- ) - })} +
+ ) + })} +
-
- ) -}) + ) + }) TimelineComponent.displayName = "TimelineComponent" @@ -271,25 +271,35 @@ const ProductPurchaseOrdersView: React.FC = ({ onClose, rawMaterialId, ra ) return ( - - - {selectedOrder - ? `Timeline: ${selectedOrder.orderNumber}` - : `Purchase Orders for ${rawMaterialName}`} - -
- } - bodyStyle={{ padding: 0, background: "#f9fafb" }} - destroyOnClose - centered +
-
+
+ {/* Close button and header */} + +
+ +

+ Purchase Orders for {rawMaterialName} +

+
+ {/* Header */} = ({ onClose, rawMaterialId, ra
{order.status} @@ -449,7 +458,7 @@ const ProductPurchaseOrdersView: React.FC = ({ onClose, rawMaterialId, ra )}
- +
) } diff --git a/client/src/components/pages/standard/standard.tsx b/client/src/components/pages/standard/standard.tsx index a4ebf2f..42a2d33 100755 --- a/client/src/components/pages/standard/standard.tsx +++ b/client/src/components/pages/standard/standard.tsx @@ -7,7 +7,6 @@ import StandardCategory from '../../ui/standard/stanadardCategory/StandardCatego import AddStandardCategory from '../../ui/standard/stanadardCategory/AddCategory'; import StandardParameterList from '../../ui/standard/standardParameters/standardParamlist'; import AddStandardParameter from '../../ui/standard/standardParameters/AddStandardParameter'; -import { motion } from 'framer-motion'; import { Folder, Package, Tag } from 'lucide-react'; interface TabConfig { @@ -16,8 +15,6 @@ interface TabConfig { addComponent: React.ReactNode; icon: React.ReactNode; description: string; - gradient: string; - shadow: string; } // Minimalistic, bold, slightly larger Tabs with border @@ -30,26 +27,26 @@ const SimpleTabs: React.FC<{ activeTab: number; onTabChange: (index: number) => void; }> = ({ tabs, activeTab, onTabChange }) => ( -
+
{tabs.map((tab, idx) => ( @@ -99,8 +96,6 @@ export default function Standard() { /> ), icon: tabIcons.category, - gradient: '', - shadow: '', }, { title: 'Parameters', @@ -113,8 +108,6 @@ export default function Standard() { /> ), icon: tabIcons.parameters, - gradient: '', - shadow: '', }, { title: 'Units', @@ -124,88 +117,78 @@ export default function Standard() { ), icon: tabIcons.unit, - gradient: '', - shadow: '', }, ]; - const tabContent = showAddComponent ? ( - -
-
-
- {tabs[activeTab].icon} -
-
-

- Add {tabs[activeTab].title.slice(0, -1)} -

-

- Create a new {tabs[activeTab].title.toLowerCase().slice(0, -1)}{' '} - entry -

-
-
- -
-
- - {tabs[activeTab].addComponent} - + // Render add-component or tab content inline to avoid unused variables + // (keeps logic local and avoids stale references) + + + + return ( +
+ {/* Header */} +
+

Standards

+

Standard management overview

- - ) : ( -
-
- {tabs[activeTab].content} + {/* Tabs */} + ({ + title: tab.title, + content: showAddComponent ? tab.addComponent : tab.content, + icon: tab.icon, + }))} + activeTab={activeTab} + onTabChange={(index) => { + setActiveTab(index); + setShowAddComponent(false); + }} + /> +
+
+ {showAddComponent ? ( +
+
+
+
+ {tabs[activeTab].icon} +
+
+

+ Add {tabs[activeTab].title.slice(0, -1)} +

+

+ Create a new {tabs[activeTab].title.toLowerCase().slice(0, -1)} entry +

+
+
+ +
+
{tabs[activeTab].addComponent}
+
+ ) : ( +
+
+ {tabs[activeTab].content} +
+ +
+ )} +
- - -
); - - const tabsData = tabs.map((tab) => ({ - title: tab.title, - content: tabContent, - icon: tab.icon, - })); - - return ( -
- { - setActiveTab(index); - setShowAddComponent(false); - }} - /> -
- {' '} - {/* <-- justify-start to align left */} -
{tabContent}
-
-
- ); } diff --git a/client/src/components/pages/training/Calender.tsx b/client/src/components/pages/training/Calender.tsx index 4d0c17d..76adee3 100644 --- a/client/src/components/pages/training/Calender.tsx +++ b/client/src/components/pages/training/Calender.tsx @@ -111,6 +111,46 @@ interface StatisticsResponse { upcomingSessions?: any[]; } +// Status color mapping - theme aware +const getStatusClasses = (status: string) => { + switch (status) { + case 'SCHEDULED': + return 'bg-blue-100 text-blue-800 dark:bg-blue-900/40 dark:text-blue-300'; + case 'IN_PROGRESS': + return 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/40 dark:text-yellow-300'; + case 'COMPLETED': + return 'bg-green-100 text-green-800 dark:bg-green-900/40 dark:text-green-300'; + case 'CANCELLED': + return 'bg-red-100 text-red-800 dark:bg-red-900/40 dark:text-red-300'; + case 'POSTPONED': + return 'bg-purple-100 text-purple-800 dark:bg-purple-900/40 dark:text-purple-300'; + default: + return 'bg-muted text-muted-foreground'; + } +}; + +// Training type color mapping - theme aware +const getTrainingTypeClasses = (type: string) => { + switch (type) { + case 'TECHNICAL': + return 'bg-indigo-100 text-indigo-800 dark:bg-indigo-900/40 dark:text-indigo-300'; + case 'SAFETY': + return 'bg-orange-100 text-orange-800 dark:bg-orange-900/40 dark:text-orange-300'; + case 'COMPLIANCE': + return 'bg-teal-100 text-teal-800 dark:bg-teal-900/40 dark:text-teal-300'; + case 'ONBOARDING': + return 'bg-pink-100 text-pink-800 dark:bg-pink-900/40 dark:text-pink-300'; + case 'WORKSHOP': + return 'bg-cyan-100 text-cyan-800 dark:bg-cyan-900/40 dark:text-cyan-300'; + case 'SEMINAR': + return 'bg-lime-100 text-lime-800 dark:bg-lime-900/40 dark:text-lime-300'; + case 'PROFESSIONAL_DEVELOPMENT': + return 'bg-violet-100 text-violet-800 dark:bg-violet-900/40 dark:text-violet-300'; + default: + return 'bg-muted text-muted-foreground'; + } +}; + // Calendar component const TrainingCalendar: React.FC = () => { // State management @@ -268,32 +308,6 @@ const TrainingCalendar: React.FC = () => { ); }; - // Status color mapping - const getStatusColor = (status: string) => { - switch (status) { - case 'SCHEDULED': return 'bg-blue-100 text-blue-800'; - case 'IN_PROGRESS': return 'bg-yellow-100 text-yellow-800'; - case 'COMPLETED': return 'bg-green-100 text-green-800'; - case 'CANCELLED': return 'bg-red-100 text-red-800'; - case 'POSTPONED': return 'bg-purple-100 text-purple-800'; - default: return 'bg-gray-100 text-gray-800'; - } - }; - - // Training type color mapping - const getTrainingTypeColor = (type: string) => { - switch (type) { - case 'TECHNICAL': return 'bg-indigo-100 text-indigo-800'; - case 'SAFETY': return 'bg-orange-100 text-orange-800'; - case 'COMPLIANCE': return 'bg-teal-100 text-teal-800'; - case 'ONBOARDING': return 'bg-pink-100 text-pink-800'; - case 'WORKSHOP': return 'bg-cyan-100 text-cyan-800'; - case 'SEMINAR': return 'bg-lime-100 text-lime-800'; - case 'PROFESSIONAL_DEVELOPMENT': return 'bg-violet-100 text-violet-800'; - default: return 'bg-gray-100 text-gray-800'; - } - }; - // Animation variants const pageVariants = { initial: { opacity: 0, y: 20 }, @@ -314,16 +328,16 @@ const TrainingCalendar: React.FC = () => { (viewMode === 'stats' && isLoadingStats) ) { return ( -
+
-

Loading calendar...

+

Loading calendar...

); } @@ -335,16 +349,16 @@ const TrainingCalendar: React.FC = () => { (viewMode === 'stats' && isErrorStats) ) { return ( -
-
-

Error loading calendar data

+
+
+

Error loading calendar data

@@ -354,7 +368,7 @@ const TrainingCalendar: React.FC = () => { } return ( -
+
{
-

Training Calendar

+

Training Calendar

- - - {/* Month selector */} -
- -
- - - -
-
- - {/* Year selector */} -
- -
- - - -
-
- - - - -
+ + + {/* Month selector */} +
+ +
+ + + +
+
+ + {/* Year selector */} +
+ +
+ + + +
+
+ + + + +
{ animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2 }} > -
+
{format(currentDate, 'MMMM yyyy')}
@@ -479,22 +501,22 @@ const TrainingCalendar: React.FC = () => { placeholder="Search trainings..." value={filters.search} onChange={(e) => handleFilterChange('search', e.target.value)} - className="pl-10 pr-4 py-2 border border-blue-200 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-400 bg-white" + className="pl-10 pr-4 py-2 border border-border rounded-full focus:outline-none focus:ring-2 focus:ring-primary/50 bg-card text-foreground placeholder:text-muted-foreground" /> - +
@@ -510,13 +532,13 @@ const TrainingCalendar: React.FC = () => { transition={{ duration: 0.3 }} className="overflow-hidden mt-4" > -
+
- + handleFilterChange('trainingType', e.target.value)} - className="w-full p-2 border border-gray-300 rounded-md focus:ring-blue-500 focus:border-blue-500" + className="w-full p-2 border border-border rounded-md focus:ring-primary focus:border-primary bg-card text-foreground" > @@ -546,11 +568,11 @@ const TrainingCalendar: React.FC = () => {
- +