+
{tag}
-
diff --git a/src/components/BMDashboard/Lesson/LessonForm.module.css b/src/components/BMDashboard/Lesson/LessonForm.module.css
index aa19468d43..f0b567a3eb 100644
--- a/src/components/BMDashboard/Lesson/LessonForm.module.css
+++ b/src/components/BMDashboard/Lesson/LessonForm.module.css
@@ -9,6 +9,28 @@
}
}
+.formControl {
+ width: 25%;
+ flex: 0 0 25%;
+ padding: 0.375rem 0.75rem;
+ font-size: 1rem;
+ line-height: 1.5;
+ color: #212529;
+ background-color: #ffffff;
+ border: 1px solid #ced4da;
+ border-radius: 0.375rem;
+ outline: none;
+ box-shadow: none;
+}
+
+.formControl:focus,
+.formControl:active,
+.formControl:focus-visible {
+ border-color: #86b7fe;
+ outline: none;
+ box-shadow: none;
+}
+
.masterContainer{
display: flex;
flex-direction: column;
@@ -196,4 +218,381 @@
padding-bottom: 20%;
}
- }
\ No newline at end of file
+ }
+
+
+/* ===================== */
+/* Dark Mode Styles */
+/* ===================== */
+
+.masterContainerDark {
+ background-color: #1B2A41;
+ color: #ffffff;
+}
+
+.formContainerDark {
+ background-color: #1C2541;
+ color: #ffffff;
+}
+
+.suppressInitialFocus :global(:focus) {
+ outline: none !important;
+ box-shadow: none !important;
+ border-color: #404040 !important;
+}
+
+.suppressInitialFocus :global(.form-control:focus),
+.suppressInitialFocus :global(textarea:focus),
+.suppressInitialFocus :global(input:focus),
+.suppressInitialFocus :global(select:focus),
+.suppressInitialFocus :global(button:focus) {
+ outline: none !important;
+ box-shadow: none !important;
+}
+
+.noFocusShadow :global(input.form-control),
+.noFocusShadow :global(textarea.form-control),
+.noFocusShadow :global(input[type='text']),
+.noFocusShadow :global(textarea) {
+ box-shadow: none !important;
+ -webkit-box-shadow: none !important;
+}
+
+.noFocusShadow :global(input.form-control:focus),
+.noFocusShadow :global(textarea.form-control:focus),
+.noFocusShadow :global(input[type='text']:focus),
+.noFocusShadow :global(textarea:focus) {
+ -webkit-box-shadow: 0 0 0 1000px #1C2541 inset !important;
+ box-shadow: 0 0 0 1000px #1C2541 inset !important;
+}
+
+/* Force no shadow unless focused */
+.formContainerDark .lessonTitleInputDark:not(:focus):not(:active):not(:focus-visible),
+.formContainerDark .lessonPlaceholderTextDark:not(:focus):not(:active):not(:focus-visible),
+:global(body.dark-mode) .formContainerDark :global(input.form-control:not(:focus)),
+:global(body.bm-dashboard-dark) .formContainerDark :global(input.form-control:not(:focus)),
+:global(body.dark-mode) .formContainerDark :global(textarea.form-control:not(:focus)),
+:global(body.bm-dashboard-dark) .formContainerDark :global(textarea.form-control:not(:focus)),
+:global(body.dark-mode) .formContainerDark :global(input[type='text']:not(:focus)),
+:global(body.bm-dashboard-dark) .formContainerDark :global(input[type='text']:not(:focus)),
+:global(body.dark-mode) .formContainerDark :global(textarea:not(:focus)),
+:global(body.bm-dashboard-dark) .formContainerDark :global(textarea:not(:focus)) {
+ box-shadow: none !important;
+ -webkit-box-shadow: none !important;
+}
+
+.lessonLabelDark {
+ color: #ffffff;
+}
+
+.lessonPlaceholderTextDark {
+ background-color: #1C2541 !important;
+ color: #ffffff !important;
+ border: 1px solid #404040 !important;
+ color-scheme: dark;
+ -webkit-text-fill-color: #ffffff;
+ box-shadow: none !important;
+}
+
+.lessonTitleInput {
+ max-width: unset !important;
+}
+
+.lessonTitleInputDark {
+ color-scheme: dark;
+ caret-color: #ffffff;
+ appearance: none;
+ -webkit-appearance: none;
+ -webkit-text-fill-color: #ffffff;
+ box-shadow: none !important;
+}
+
+.lessonTitleInputDark:focus,
+.lessonTitleInputDark:active,
+.lessonTitleInputDark:focus-visible {
+ background-color: #1C2541 !important;
+ border-color: #2563eb !important;
+ -webkit-box-shadow: 0 0 0 1000px #1C2541 inset !important;
+ background-clip: padding-box !important;
+ box-shadow: 0 0 0 1000px #1C2541 inset !important;
+ outline: none !important;
+}
+
+.formContainerDark .lessonTitleInputDark,
+.formContainerDark .lessonPlaceholderTextDark {
+ background-color: #1C2541 !important;
+ color: #ffffff !important;
+ border-color: #404040 !important;
+ -webkit-box-shadow: none !important;
+ background-clip: padding-box !important;
+ box-shadow: none !important;
+ outline: none !important;
+}
+
+/* Override Bootstrap .form-control:focus background */
+.lessonTitleInputDark:global(.form-control):focus,
+.lessonTitleInputDark:global(.form-control):active,
+.lessonTitleInputDark:global(.form-control):focus-visible,
+.lessonPlaceholderTextDark:global(.form-control):focus,
+.lessonPlaceholderTextDark:global(.form-control):active,
+.lessonPlaceholderTextDark:global(.form-control):focus-visible {
+ background-color: #1C2541 !important;
+ color: #ffffff !important;
+ border-color: #2563eb !important;
+ -webkit-box-shadow: 0 0 0 1000px #1C2541 inset !important;
+ background-clip: padding-box !important;
+ box-shadow: 0 0 0 1000px #1C2541 inset !important;
+ outline: none !important;
+}
+
+.lessonPlaceholderTextDark:focus,
+.lessonPlaceholderTextDark:active {
+ border-color: #2563eb !important;
+ background-color: #1C2541 !important;
+ -webkit-box-shadow: 0 0 0 1000px #1C2541 inset !important;
+ background-clip: padding-box !important;
+ box-shadow: 0 0 0 1000px #1C2541 inset !important;
+ outline: none !important;
+}
+
+/* Override global dark-mode input backgrounds */
+:global(body.dark-mode) .formContainerDark .lessonTitleInputDark,
+:global(body.bm-dashboard-dark) .formContainerDark .lessonTitleInputDark,
+:global(body.dark-mode) .formContainerDark .lessonPlaceholderTextDark,
+:global(body.bm-dashboard-dark) .formContainerDark .lessonPlaceholderTextDark {
+ background-color: #1C2541 !important;
+ color: #ffffff !important;
+ border-color: #404040 !important;
+ -webkit-box-shadow: none !important;
+ box-shadow: none !important;
+}
+
+.formSelectContainerDark {
+ color: #ffffff;
+}
+
+ .singleFormSelectDark {
+ background-color: #1C2541 !important;
+ color: #ffffff !important;
+ border: 1px solid #444;
+ }
+
+ .singleFormSelectDark:focus,
+ .singleFormSelectDark:active {
+ background-color: #1C2541 !important;
+ color: #ffffff !important;
+ border-color: #2563eb !important;
+ box-shadow: none !important;
+ outline: none !important;
+ }
+
+.dragAndDropStyleDark {
+ border: 2px dashed #555;
+ background-color: #1f1f1f;
+ color: #ffffff;
+}
+
+.dragandDropTextDark {
+ color: #b0b0b0;
+}
+
+.fileSelectedDark {
+ border-color: #ffffff;
+}
+
+.lessonFormButtonCancelDark {
+ background-color: #2a2a2a !important;
+ color: #ffffff !important;
+ border-color: #666 !important;
+}
+
+.lessonFormButtonSubmitDark {
+ background-color: #2563eb !important;
+ color: #ffffff !important;
+ border-color: #2563eb !important;
+}
+
+/* Tags - Dark Mode */
+
+.tagsDivDark {
+ color: #ffffff;
+}
+
+.tagDark {
+ background-color: #2a2a2a;
+ color: #ffffff;
+ border: 1px solid #555;
+}
+
+.removeTagBTNDark {
+ color: #ffffff;
+}
+
+.tagDropdownDark {
+ background-color: #1e1e1e;
+ color: #ffffff !important;
+ border: 1px solid #444;
+}
+
+.tagDropdownDark * {
+ color: #ffffff !important;
+}
+
+.tagOptionDark {
+ color: #ffffff !important;
+ background-color: transparent;
+}
+
+.tagOptionDark:hover,
+.tagOptionDark:focus,
+.tagOptionDark:active {
+ background-color: #2d3b66;
+ color: #ffffff !important;
+ outline: none;
+}
+
+.tagOptionDark:hover *,
+.tagOptionDark:focus *,
+.tagOptionDark:active * {
+ color: #ffffff !important;
+ background-color: transparent !important;
+}
+
+.inputGroupDark {
+ background-color: #1e1e1e;
+}
+
+.formControlDark {
+ background-color: #1C2541 !important;
+ color: #ffffff !important;
+ border: 1px solid #404040 !important;
+ box-shadow: none !important;
+}
+
+.formControlDark:focus,
+.formControlDark:active,
+.formControlDark:focus-visible {
+ background-color: #1C2541 !important;
+ color: #ffffff !important;
+ border-color: #2563eb !important;
+ box-shadow: none !important;
+ outline: none !important;
+}
+
+.deleteTagBtnDark {
+ color: #ff6b6b;
+}
+
+.deleteTagBtnDark:hover {
+ color: #ff3b3b;
+}
+/* Remove default arrow in all browsers */
+.formSelectDark {
+ background-color: #1C2541;
+ color: #e8f0fe;
+ border: 1px solid #3a3f45;
+
+ /* Remove default arrows */
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+
+ /* Add custom arrow spacing */
+ padding-right: 32px;
+
+ background-image: url("data:image/svg+xml;utf8,
");
+ background-repeat: no-repeat;
+ background-position: right 10px center;
+ background-size: 18px;
+}
+/* Fix selected text background in dark mode */
+.lessonPlaceholderTextDark::selection,
+.lessonPlaceholderTextDark::-moz-selection,
+.lessonPlaceholderTextDark.form-control::selection,
+.lessonPlaceholderTextDark.form-control::-moz-selection,
+.lessonTitleInput::selection,
+.lessonTitleInput::-moz-selection,
+.lessonTitleInputDark::selection,
+.lessonTitleInputDark::-moz-selection {
+ background-color: #1C2541 !important;
+ color: #ffffff !important;
+}
+
+/* Hide selection when the field is not focused */
+.lessonTitleInputDark:not(:focus)::selection,
+.lessonTitleInputDark:not(:focus)::-moz-selection,
+.lessonPlaceholderTextDark:not(:focus)::selection,
+.lessonPlaceholderTextDark:not(:focus)::-moz-selection {
+ background-color: #1C2541 !important;
+ color: #1C2541 !important;
+}
+
+/* Absolute selection control under global dark-mode rules */
+:global(body.dark-mode) .formContainerDark :global(input.form-control)::selection,
+:global(body.bm-dashboard-dark) .formContainerDark :global(input.form-control)::selection,
+:global(body.dark-mode) .formContainerDark :global(textarea.form-control)::selection,
+:global(body.bm-dashboard-dark) .formContainerDark :global(textarea.form-control)::selection,
+:global(body.dark-mode) .formContainerDark :global(input[type='text'])::selection,
+:global(body.bm-dashboard-dark) .formContainerDark :global(input[type='text'])::selection,
+:global(body.dark-mode) .formContainerDark :global(textarea)::selection,
+:global(body.bm-dashboard-dark) .formContainerDark :global(textarea)::selection {
+ background-color: #1C2541 !important;
+ color: #ffffff !important;
+}
+
+:global(body.dark-mode) .formContainerDark :global(input.form-control:not(:focus))::selection,
+:global(body.bm-dashboard-dark) .formContainerDark :global(input.form-control:not(:focus))::selection,
+:global(body.dark-mode) .formContainerDark :global(textarea.form-control:not(:focus))::selection,
+:global(body.bm-dashboard-dark) .formContainerDark :global(textarea.form-control:not(:focus))::selection,
+:global(body.dark-mode) .formContainerDark :global(input[type='text']:not(:focus))::selection,
+:global(body.bm-dashboard-dark) .formContainerDark :global(input[type='text']:not(:focus))::selection,
+:global(body.dark-mode) .formContainerDark :global(textarea:not(:focus))::selection,
+:global(body.bm-dashboard-dark) .formContainerDark :global(textarea:not(:focus))::selection {
+ background-color: transparent !important;
+ color: inherit !important;
+}
+
+/* Fix Chrome autofill forcing white background */
+.formControlDark:-webkit-autofill,
+.formControlDark:-webkit-autofill:hover,
+.formControlDark:-webkit-autofill:focus,
+.formControlDark:-webkit-autofill:active {
+ -webkit-box-shadow: 0 0 0 1000px #1C2541 inset !important;
+ -webkit-text-fill-color: #ffffff !important;
+ transition: background-color 9999s ease-in-out 0s;
+}
+
+/* Force dark background for browser-autofill on Lesson Title */
+.lessonTitleInputDark:-webkit-autofill,
+.lessonTitleInputDark:-webkit-autofill:hover,
+.lessonTitleInputDark:-webkit-autofill:focus,
+.lessonTitleInputDark:-webkit-autofill:active {
+ -webkit-box-shadow: 0 0 0 1000px #1C2541 inset !important;
+ -webkit-text-fill-color: #ffffff !important;
+ caret-color: #ffffff !important;
+}
+
+:global(body.dark-mode) .formContainerDark :global(input.form-control:-webkit-autofill),
+:global(body.bm-dashboard-dark) .formContainerDark :global(input.form-control:-webkit-autofill),
+:global(body.dark-mode) .formContainerDark :global(input[type='text']:-webkit-autofill),
+:global(body.bm-dashboard-dark) .formContainerDark :global(input[type='text']:-webkit-autofill) {
+ -webkit-box-shadow: 0 0 0 1000px #1C2541 inset !important;
+ -webkit-text-fill-color: #ffffff !important;
+ caret-color: #ffffff !important;
+}
+
+
+/* FIX: Lesson Title input turns white on focus/selection (Bootstrap override) */
+
+/* FINAL AUTHORITATIVE FIX — Lesson Title NEVER turns white */
+.lessonPlaceholderTextDark,
+.lessonPlaceholderTextDark.form-control,
+.lessonPlaceholderTextDark.form-control:focus,
+.lessonPlaceholderTextDark.form-control:active,
+.lessonPlaceholderTextDark.form-control:focus-visible {
+ background-color: #1C2541 !important;
+ color: #ffffff !important;
+ border: 1px solid #404040 !important;
+ box-shadow: none !important;
+ outline: none !important;
+}
diff --git a/src/components/BMDashboard/PurchaseRequests/PurchaseForm.jsx b/src/components/BMDashboard/PurchaseRequests/PurchaseForm.jsx
index ef3ff628dc..1717455d47 100644
--- a/src/components/BMDashboard/PurchaseRequests/PurchaseForm.jsx
+++ b/src/components/BMDashboard/PurchaseRequests/PurchaseForm.jsx
@@ -3,7 +3,17 @@ import { useHistory } from 'react-router-dom';
import { useDispatch, useSelector } from 'react-redux';
import { toast } from 'react-toastify';
import Joi from 'joi-browser';
-import { Form, FormGroup, Label, Input, Button } from 'reactstrap';
+import {
+ Form,
+ FormGroup,
+ Label,
+ Input,
+ Button,
+ Modal,
+ ModalHeader,
+ ModalBody,
+ ModalFooter,
+} from 'reactstrap';
import { boxStyle } from '~/styles';
import BMError from '../shared/BMError';
import styles from './PurchaseForm.module.css';
@@ -22,14 +32,18 @@ function PurchaseForm({
const primaryData = useSelector(primaryDataSelector);
const secondaryData = useSelector(secondaryDataSelector);
const errors = useSelector(errorSelector);
- const [primaryId, setPrimaryId] = useState('Test');
- const [secondaryId, setSecondaryId] = useState('Test');
+ const [primaryId, setPrimaryId] = useState('');
+ const [secondaryId, setSecondaryId] = useState('');
const [quantity, setQuantity] = useState('');
const [unit, setUnit] = useState('');
const [priority, setPriority] = useState('Low');
const [brand, setBrand] = useState('');
const [validationError, setValidationError] = useState('');
+ const [fieldErrors, setFieldErrors] = useState({});
const [isError, setIsError] = useState(false);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [showSuccessModal, setShowSuccessModal] = useState(false);
+ const [submittedData, setSubmittedData] = useState(null);
// Fetch initial data
useEffect(() => {
@@ -52,48 +66,163 @@ function PurchaseForm({
if (validationError) setValidationError('');
}, [primaryId, secondaryId, quantity, priority, brand]);
+ // Validate individual field
+ const validateField = (fieldName, value) => {
+ const schemas = {
+ primaryId: Joi.string()
+ .required()
+ .label('Project'),
+ secondaryId: Joi.string()
+ .required()
+ .label('Material'),
+ quantity: Joi.number()
+ .greater(0)
+ .required()
+ .label('Quantity'),
+ priority: Joi.string()
+ .valid('Low', 'Medium', 'High')
+ .required()
+ .label('Priority'),
+ };
+
+ if (schemas[fieldName]) {
+ const { error } = schemas[fieldName].validate(value);
+ return error ? error.details[0].message : null;
+ }
+ return null;
+ };
+
+ // Handle field blur for validation
+ const handleFieldBlur = (fieldName, value) => {
+ const error = validateField(fieldName, value);
+ setFieldErrors(prev => ({
+ ...prev,
+ [fieldName]: error,
+ }));
+ };
+
// Form validation logic
const validateForm = () =>
Joi.object({
- primaryId: Joi.string().required(),
- secondaryId: Joi.string().required(),
+ primaryId: Joi.string()
+ .required()
+ .label('Project'),
+ secondaryId: Joi.string()
+ .required()
+ .label('Material'),
quantity: Joi.number()
- .min(1)
- .max(999)
- .integer()
- .required(),
+ .greater(0)
+ .required()
+ .label('Quantity'),
priority: Joi.string()
.valid('Low', 'Medium', 'High')
- .required(),
+ .required()
+ .label('Priority'),
brand: Joi.string().allow(''),
- }).validate({ primaryId, secondaryId, quantity, priority, brand });
+ }).validate({ primaryId, secondaryId, quantity, priority, brand }, { abortEarly: false });
+
+ // Reset form to initial state
+ const resetForm = () => {
+ setPrimaryId('');
+ setSecondaryId('');
+ setQuantity('');
+ setUnit('');
+ setPriority('Low');
+ setBrand('');
+ setValidationError('');
+ setFieldErrors({});
+ };
// Handle form submission
const handleSubmit = async e => {
e.preventDefault();
const { error } = validateForm();
+
if (error) {
- setValidationError(error.details.map(detail => detail.message).join(', '));
+ const errors = {};
+ error.details.forEach(detail => {
+ const fieldName = detail.path[0];
+ errors[fieldName] = detail.message;
+ });
+ setFieldErrors(errors);
+ setValidationError('Please fix the errors above before submitting.');
return;
}
- setValidationError(''); // Clear previous errors
-
- const response = await submitFormAction({ primaryId, secondaryId, quantity, priority, brand });
-
- if (response?.status === 201) {
- toast.success('Success: Your request has been processed.');
- setPrimaryId('');
- setSecondaryId('');
- setQuantity('');
- setUnit('');
- setPriority('Low');
- setBrand('');
- history.push('/bmdashboard/materials');
- } else {
- toast.error(`Error: ${response?.statusText || 'Unknown error'}`);
+
+ setValidationError('');
+ setFieldErrors({});
+ setIsSubmitting(true);
+
+ try {
+ const response = await submitFormAction({
+ primaryId,
+ secondaryId,
+ quantity,
+ priority,
+ brand,
+ });
+
+ if (response?.status === 201) {
+ // Get project and material names for success message
+ const projectName = primaryData.find(p => p._id === primaryId)?.name || 'Unknown Project';
+ const materialName =
+ secondaryData.find(m => m._id === secondaryId)?.name || 'Unknown Material';
+
+ setSubmittedData({ projectName, materialName });
+ toast.success(`Purchase request submitted for ${materialName} on ${projectName}`);
+ setShowSuccessModal(true);
+ } else if (response?.status === 400 && response?.data) {
+ // Backend validation error - show specific error message
+ const backendError = response.data;
+
+ if (backendError.field && backendError.message) {
+ // Map backend field names to frontend field names
+ const fieldMapping = {
+ projectId: 'primaryId',
+ matTypeId: 'secondaryId',
+ quantity: 'quantity',
+ priority: 'priority',
+ requestorId: 'requestorId',
+ };
+
+ const frontendFieldName = fieldMapping[backendError.field] || backendError.field;
+
+ // Set field-specific error
+ setFieldErrors(prev => ({
+ ...prev,
+ [frontendFieldName]: backendError.message,
+ }));
+ toast.error(backendError.message);
+ } else {
+ // Generic validation error
+ toast.error(backendError.message || 'Validation error. Please check your inputs.');
+ }
+ } else {
+ // Other server errors
+ const errorMessage = response?.data?.message || response?.statusText || 'Unknown error';
+ toast.error(
+ `There was an issue submitting your request. ${errorMessage}. Please try again or contact an admin.`,
+ );
+ }
+ } catch (err) {
+ toast.error('Unable to connect. Please check your connection and try again.');
+ } finally {
+ setIsSubmitting(false);
}
};
+ // Handle "Create Another Request" action
+ const handleCreateAnother = () => {
+ setShowSuccessModal(false);
+ resetForm();
+ };
+
+ // Handle "Go to Materials List" action
+ const handleGoToMaterials = () => {
+ setShowSuccessModal(false);
+ history.push('/bmdashboard/materials');
+ };
+
// Handle cancel action
const handleCancel = () => {
history.goBack();
@@ -111,15 +240,22 @@ function PurchaseForm({
);
}
diff --git a/src/components/BMDashboard/PurchaseRequests/PurchaseForm.module.css b/src/components/BMDashboard/PurchaseRequests/PurchaseForm.module.css
index 950898beda..0b928ff655 100644
--- a/src/components/BMDashboard/PurchaseRequests/PurchaseForm.module.css
+++ b/src/components/BMDashboard/PurchaseRequests/PurchaseForm.module.css
@@ -60,6 +60,44 @@
color: red;
}
+/* Required field indicator */
+.requiredIndicator {
+ color: #dc3545;
+ font-weight: bold;
+ margin-left: 0.25rem;
+}
+
+/* Inline field error message */
+.fieldError {
+ color: #dc3545;
+ font-size: 0.875rem;
+ margin-top: 0.25rem;
+ display: block;
+}
+
+/* Invalid field styling */
+.invalidField {
+ border-color: #dc3545 !important;
+ box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25) !important;
+}
+
+/* Loading button state */
+.submitButtonLoading {
+ opacity: 0.7;
+ cursor: not-allowed;
+}
+
+/* Success modal actions */
+.successModalActions {
+ display: flex;
+ gap: 1rem;
+ justify-content: space-between;
+}
+
+.successModalActions button {
+ flex: 1;
+}
+
@media screen and (max-width: 640px) {
.purchaseFlexGroup {
display: block;
@@ -67,6 +105,17 @@
.purchaseQtyGroup {
width: auto;
}
+
+ .successModalActions {
+ flex-direction: column;
+ }
+}
+
+:global(body.dark-mode) .purchaseRequestContainer select,
+:global(body.bm-dashboard-dark) .purchaseRequestContainer select {
+ appearance: none;
+ -webkit-appearance: none;
+ -moz-appearance: none;
}
diff --git a/src/components/BMDashboard/RentalChart/ReturnedLateChart.jsx b/src/components/BMDashboard/RentalChart/ReturnedLateChart.jsx
index 2e686f5a73..ffa399af02 100644
--- a/src/components/BMDashboard/RentalChart/ReturnedLateChart.jsx
+++ b/src/components/BMDashboard/RentalChart/ReturnedLateChart.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useMemo, useRef, useState } from 'react';
+import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
import axios from 'axios';
import { ENDPOINTS } from '~/utils/URL';
import { Bar } from 'react-chartjs-2';
@@ -34,6 +34,9 @@ export default function ReturnedLateChart() {
});
const [chartData, setChartData] = useState({ labels: [], datasets: [] });
const [rawToolsData, setRawToolsData] = useState([]);
+ const [selectedToolDetail, setSelectedToolDetail] = useState(null);
+ const [detailOpen, setDetailOpen] = useState(false);
+ const [detailLoading, setDetailLoading] = useState(false);
const darkMode = useSelector(state => state.theme.darkMode);
useEffect(() => {
@@ -126,12 +129,32 @@ export default function ReturnedLateChart() {
fetchData();
}, [selectedProject, dateRange, selectedTools]);
+ const handleBarClick = useCallback(
+ (event, elements) => {
+ if (!elements || !elements.length) return;
+
+ const index = elements[0].index;
+ const toolName = chartData.labels[index];
+
+ const toolDetail = rawToolsData.find(t => t.toolName === toolName);
+
+ setSelectedToolDetail(toolDetail || null);
+ setDetailOpen(true);
+ },
+ [chartData.labels, rawToolsData],
+ );
+
const options = useMemo(() => {
const textColor = darkMode ? '#fff' : '#333';
const datalabelCOlor = darkMode ? '#fff' : '#111';
return {
responsive: true,
maintainAspectRatio: false,
+ onClick: handleBarClick,
+ interaction: {
+ mode: 'nearest',
+ intersect: true,
+ },
plugins: {
legend: { display: false },
title: {
@@ -182,7 +205,7 @@ export default function ReturnedLateChart() {
},
},
};
- }, [chartData, darkMode]);
+ }, [chartData, darkMode, handleBarClick]);
const handleProjectChange = e => setSelectedProject(e.target.value);
const handleStartDateChange = date =>
@@ -284,6 +307,55 @@ export default function ReturnedLateChart() {