diff --git a/package.json b/package.json index 0fa4ff8ec9..9fa8f9f99f 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "ant-design": "^1.0.0", "antd": "^5.27.6", "assert": "^2.1.0", - "axios": "^1.11.0", + "axios": "^1.13.5", "axios-mock-adapter": "^1.22.0", "bootstrap": "^4.5.3", "braces": "^3.0.3", diff --git a/src/components/BMDashboard/Issues/IssueDashboard.jsx b/src/components/BMDashboard/Issues/IssueDashboard.jsx index 7609e9386d..d9cfaabb22 100644 --- a/src/components/BMDashboard/Issues/IssueDashboard.jsx +++ b/src/components/BMDashboard/Issues/IssueDashboard.jsx @@ -6,9 +6,18 @@ import { FiTrash2, FiCopy, FiEdit, + FiDownload, } from 'react-icons/fi'; import styles from './IssueDashboard.module.css'; -import { Col, Row, Table } from 'reactstrap'; +import { + Col, + Row, + Table, + UncontrolledDropdown, + DropdownToggle, + DropdownMenu, + DropdownItem, +} from 'reactstrap'; import { useDispatch, useSelector } from 'react-redux'; import { copyIssue, @@ -17,6 +26,8 @@ import { renameIssue, } from '~/actions/bmdashboard/issueActions'; import IssueHeader from './IssueHeader'; +import { toast } from 'react-toastify'; +import { jsPDF } from 'jspdf'; export default function IssueDashboard() { const dispatch = useDispatch(); @@ -26,14 +37,18 @@ export default function IssueDashboard() { const [currentPage, setCurrentPage] = useState(1); const [menuOpen, setMenuOpen] = useState(null); const itemsPerPage = 5; - const totalPages = Math.ceil(issues.length / itemsPerPage); + const displayIssues = issues; + const totalPages = Math.ceil(displayIssues.length / itemsPerPage); const [showDeleteModal, setShowDeleteModal] = useState(false); const [showRenameModal, setShowRenameModal] = useState(false); const [showCopyModal, setShowCopyModal] = useState(false); const [selectedIssue, setSelectedIssue] = useState(null); const [renameValue, setRenameValue] = useState(''); - const currentItems = issues.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage); + const currentItems = displayIssues.slice( + (currentPage - 1) * itemsPerPage, + currentPage * itemsPerPage, + ); const toggleMenu = id => setMenuOpen(open => (open === id ? null : id)); @@ -77,6 +92,137 @@ export default function IssueDashboard() { dispatch(fetchAllIssues()); }, [dispatch]); + const buildExportRows = sourceIssues => { + return (sourceIssues || []).map(issue => { + const assignedUser = issue.assignedTo + ? `${issue.assignedTo.firstName || ''} ${issue.assignedTo.lastName || ''}`.trim() + : issue.assignedToName || issue.assignee || 'Unassigned'; + + const formatDate = value => { + if (!value) return '-'; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return `${value}`; + return date.toLocaleDateString(); + }; + + return { + issueName: issue.name || issue.issueName || '-', + status: issue.status || issue.state || issue.issueStatus || '-', + priority: issue.priority || issue.severity || issue.issuePriority || '-', + assignedUser: assignedUser || '-', + createdDate: formatDate( + issue.createdDate || issue.createdAt || issue.openDate || issue.dateCreated, + ), + lastUpdated: formatDate(issue.updatedDate || issue.updatedAt || issue.lastUpdated), + }; + }); + }; + + const exportHeaders = [ + 'Issue Name', + 'Status', + 'Priority', + 'Assigned User', + 'Created Date', + 'Last Updated', + ]; + + const downloadBlob = (blob, filename) => { + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.download = filename; + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + }; + + const handleExportCsv = () => { + const exportRows = buildExportRows(displayIssues); + if (exportRows.length === 0) { + toast.info('No issues available to export.'); + return; + } + const escapeCsv = value => `"${String(value ?? '').replace(/"/g, '""')}"`; + const rows = [ + exportHeaders, + ...exportRows.map(row => [ + row.issueName || '-', + row.status || '-', + row.priority || '-', + row.assignedUser || '-', + row.createdDate || '-', + row.lastUpdated || '-', + ]), + ]; + + const csvContent = rows.map(row => row.map(escapeCsv).join(',')).join('\n'); + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + downloadBlob(blob, `issues-export-${new Date().toISOString().slice(0, 10)}.csv`); + toast.success('Issue export generated (CSV).'); + }; + + const handleExportPdf = () => { + const exportRows = buildExportRows(displayIssues); + if (exportRows.length === 0) { + toast.info('No issues available to export.'); + return; + } + const doc = new jsPDF({ orientation: 'portrait', unit: 'pt', format: 'a4' }); + const pageWidth = doc.internal.pageSize.getWidth(); + const startX = 40; + const startY = 50; + const rowHeight = 18; + const colWidths = [160, 70, 70, 110, 80, 80]; + const truncate = (text, maxWidth) => { + if (doc.getTextWidth(text) <= maxWidth) return text; + let truncated = text; + while (truncated.length > 0 && doc.getTextWidth(`${truncated}…`) > maxWidth) { + truncated = truncated.slice(0, -1); + } + return `${truncated}…`; + }; + + doc.setFontSize(12); + doc.text('Issue Export', startX, startY - 20); + doc.setFontSize(9); + + let x = startX; + exportHeaders.forEach((header, index) => { + const width = colWidths[index]; + doc.text(truncate(header, width - 4), x, startY); + x += width; + }); + + let y = startY + rowHeight; + exportRows.forEach(row => { + if (y > doc.internal.pageSize.getHeight() - 40) { + doc.addPage(); + y = 50; + } + const values = [ + row.issueName || '-', + row.status || '-', + row.priority || '-', + row.assignedUser || '-', + row.createdDate || '-', + row.lastUpdated || '-', + ]; + let colX = startX; + values.forEach((value, index) => { + const width = colWidths[index]; + doc.text(truncate(String(value ?? ''), width - 4), colX, y); + colX += width; + }); + y += rowHeight; + }); + + const filename = `issues-export-${new Date().toISOString().slice(0, 10)}.pdf`; + doc.save(filename); + toast.success('Issue export generated (PDF).'); + }; + function getTimeSince(dateStr) { const date = new Date(dateStr); const now = new Date(); @@ -108,6 +254,28 @@ export default function IssueDashboard() {

Issue Dashboard

+ + + + + Export + + + + Export as CSV + + + Export as PDF + + + +
@@ -239,31 +407,6 @@ export default function IssueDashboard() { - {Array.from({ length: totalPages }, (_, i) => { - const isActive = currentPage === i + 1; - let buttonClass = 'page-link'; - - if (darkMode) { - buttonClass += ' bg-dark text-light border-secondary'; - } - - if (isActive) { - buttonClass += darkMode ? ' bg-secondary' : ' bg-primary'; - } - - return ( -
  • - -
  • - ); - })} -
  • +
  • + {props.children} + + ); + }; + FilterMenuList.displayName = 'IssueChartFilterMenuList'; + + const activeFilterSummary = useMemo(() => { + const issueTypeCount = filters.issueTypes.length || Object.keys(issues || {}).length; + const yearList = filters.years.length ? filters.years : uniqueYears; + const range = + yearList.length > 0 ? `${Math.min(...yearList)}–${Math.max(...yearList)}` : 'No years'; + return `${issueTypeCount} Issue Types | ${range}`; + }, [filters.issueTypes, filters.years, issues, uniqueYears]); return (
    Issue Type: @@ -333,10 +550,14 @@ function IssueChart() { isMulti options={issueTypes} onChange={selected => handleFilterChange(selected, 'issueTypes')} - value={issueTypes.filter(option => filters.issueTypes.includes(option.value))} + value={flatIssueTypeOptions.filter(option => + filters.issueTypes.includes(option.value), + )} styles={selectStyles} aria-label="Filter issues by type" placeholder="Select issue types" + components={{ MenuList: FilterMenuList }} + filterField="issueTypes" />
    @@ -358,6 +579,8 @@ function IssueChart() { styles={selectStyles} aria-label="Filter issues by year" placeholder="Select years" + components={{ MenuList: FilterMenuList }} + filterField="years" />
    @@ -389,7 +612,9 @@ function IssueChart() { className={`${styles.issueChartYearGroup} ${styles.issueTypeGroup} ${ darkMode ? styles.issueChartYearGroupDark : '' }`} + style={{ marginTop: 24 }} > +
    {activeFilterSummary}
    { setData(filteredItems); }, [filteredItems]); @@ -98,7 +105,53 @@ export default function ItemsTable({ } setProjectNameCol({ iconsToDisplay: faSort, sortOrder: 'default' }); } + // Sorting for Bought + if (columnName === 'Bought') { + if (boughtCol.sortOrder === 'default' || boughtCol.sortOrder === 'desc') { + newSortedData.sort((a, b) => (a.stockBought || 0) - (b.stockBought || 0)); + setBoughtCol({ iconsToDisplay: faSortUp, sortOrder: 'asc' }); + } else { + newSortedData.sort((a, b) => (b.stockBought || 0) - (a.stockBought || 0)); + setBoughtCol({ iconsToDisplay: faSortDown, sortOrder: 'desc' }); + } + resetOtherDynamicColumns('Bought'); + } + + // Sorting for Used + if (columnName === 'Used') { + if (usedCol.sortOrder === 'default' || usedCol.sortOrder === 'desc') { + newSortedData.sort((a, b) => (a.stockUsed || 0) - (b.stockUsed || 0)); + setUsedCol({ iconsToDisplay: faSortUp, sortOrder: 'asc' }); + } else { + newSortedData.sort((a, b) => (b.stockUsed || 0) - (a.stockUsed || 0)); + setUsedCol({ iconsToDisplay: faSortDown, sortOrder: 'desc' }); + } + resetOtherDynamicColumns('Used'); + } + + // Sorting for Available + if (columnName === 'Available') { + if (availableCol.sortOrder === 'default' || availableCol.sortOrder === 'desc') { + newSortedData.sort((a, b) => (a.stockAvailable || 0) - (b.stockAvailable || 0)); + setAvailableCol({ iconsToDisplay: faSortUp, sortOrder: 'asc' }); + } else { + newSortedData.sort((a, b) => (b.stockAvailable || 0) - (a.stockAvailable || 0)); + setAvailableCol({ iconsToDisplay: faSortDown, sortOrder: 'desc' }); + } + resetOtherDynamicColumns('Available'); + } + // Sorting for Wasted + if (columnName === 'Wasted') { + if (wastedCol.sortOrder === 'default' || wastedCol.sortOrder === 'desc') { + newSortedData.sort((a, b) => (a.stockWasted || 0) - (b.stockWasted || 0)); + setWastedCol({ iconsToDisplay: faSortUp, sortOrder: 'asc' }); + } else { + newSortedData.sort((a, b) => (b.stockWasted || 0) - (a.stockWasted || 0)); + setWastedCol({ iconsToDisplay: faSortDown, sortOrder: 'desc' }); + } + resetOtherDynamicColumns('Wasted'); + } setData(newSortedData); }; @@ -106,6 +159,13 @@ export default function ItemsTable({ return path.split('.').reduce((acc, part) => (acc ? acc[part] : null), obj); }; + const resetOtherDynamicColumns = active => { + if (active !== 'Bought') setBoughtCol({ iconsToDisplay: faSort, sortOrder: 'default' }); + if (active !== 'Used') setUsedCol({ iconsToDisplay: faSort, sortOrder: 'default' }); + if (active !== 'Available') setAvailableCol({ iconsToDisplay: faSort, sortOrder: 'default' }); + if (active !== 'Wasted') setWastedCol({ iconsToDisplay: faSort, sortOrder: 'default' }); + }; + return ( <> {/* Regular Records Modal for Update and Purchase records */} @@ -141,9 +201,21 @@ export default function ItemsTable({ ) : ( Name )} - {dynamicColumns.map(({ label }) => ( - {label} - ))} + {dynamicColumns.map(({ label }) => { + const stateMap = { + Bought: boughtCol, + Used: usedCol, + Available: availableCol, + Wasted: wastedCol, + }; + + return ( + sortData(label)}> + {label}{' '} + + + ); + })} Usage Record Updates Purchases diff --git a/src/components/BMDashboard/Lesson/LessonForm.jsx b/src/components/BMDashboard/Lesson/LessonForm.jsx index 7d5627f618..4eeb7a51d8 100644 --- a/src/components/BMDashboard/Lesson/LessonForm.jsx +++ b/src/components/BMDashboard/Lesson/LessonForm.jsx @@ -1,5 +1,5 @@ /* eslint-disable testing-library/no-node-access */ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { Form, FormControl, Button } from 'react-bootstrap'; import axios from 'axios'; import { ENDPOINTS } from '~/utils/URL'; @@ -23,6 +23,7 @@ function LessonForm() { const roles = useSelector(state => state.role.roles); // grab all roles from store // const projects = useSelector(state => state.allProjects.projects); // grab all projects from store(not BM projects) const projects = useSelector(state => state.bmProjects); // grab all BM projects from store + const darkMode = useSelector(state => state.theme.darkMode); const [LessonFormtags, setLessonFormTags] = useState([]); // save all tags user inputs const [permanentTags, setPermanentTags] = useState([]); const [tagInput, setTagInput] = useState(''); // track user input in tag input @@ -36,6 +37,12 @@ function LessonForm() { // track filtered tags const [filteredTags, setFilteredTags] = useState([]); const [showDropdown, setShowDropdown] = useState(false); + const [suppressInitialFocus, setSuppressInitialFocus] = useState(true); + const [hasUserFocused, setHasUserFocused] = useState(false); + const lessonTitleRef = useRef(null); + const lessonTextRef = useRef(null); + const formContainerRef = useRef(null); + const allowNextFocusRef = useRef(false); // track user input in the tag input feild @@ -102,6 +109,57 @@ function LessonForm() { dispatch(fetchBMProjects(projectId)); dispatch(getAllRoles()); }, [dispatch, projectId]); + + useEffect(() => { + if (document.activeElement && document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + const blurTargets = () => { + lessonTitleRef.current?.blur(); + lessonTextRef.current?.blur(); + if (document.activeElement && document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + }; + blurTargets(); + const timer = setTimeout(blurTargets, 50); + return () => clearTimeout(timer); + }, []); + useEffect(() => { + const clearInactiveSelection = () => { + const active = document.activeElement; + [lessonTitleRef.current, lessonTextRef.current].forEach(el => { + if (!el || el === active) return; + if (typeof el.setSelectionRange === 'function') { + el.setSelectionRange(0, 0); + } + }); + }; + document.addEventListener('selectionchange', clearInactiveSelection); + document.addEventListener('mouseup', clearInactiveSelection); + document.addEventListener('keyup', clearInactiveSelection); + return () => { + document.removeEventListener('selectionchange', clearInactiveSelection); + document.removeEventListener('mouseup', clearInactiveSelection); + document.removeEventListener('keyup', clearInactiveSelection); + }; + }, []); + const blockInitialFocus = e => { + if (!suppressInitialFocus) return; + if (allowNextFocusRef.current) { + allowNextFocusRef.current = false; + setSuppressInitialFocus(false); + return; + } + const target = e.target; + if (target && typeof target.blur === 'function') { + target.blur(); + } + }; + const allowFocusOnUserAction = () => { + allowNextFocusRef.current = true; + setHasUserFocused(true); + }; // logic if there is a projectId passed in params(on project specific from) to add the project tag automatically // useEffect handles click away from input drop down menu @@ -185,6 +243,82 @@ function LessonForm() { const lessonformtitleinput = e.target.value; setLessonTitleText(lessonformtitleinput); }; + const preventTextSelection = e => { + if (typeof e.preventDefault === 'function') { + e.preventDefault(); + } + const target = e.currentTarget; + if (target && typeof target.setSelectionRange === 'function') { + const end = target.value ? target.value.length : 0; + target.setSelectionRange(end, end); + } + }; + const clearSelectionOnInput = e => { + const target = e.currentTarget; + if (typeof window.getSelection === 'function') { + const selection = window.getSelection(); + if (selection && selection.removeAllRanges) { + selection.removeAllRanges(); + } + } + if (target && typeof target.setSelectionRange === 'function') { + const end = target.value ? target.value.length : 0; + target.setSelectionRange(end, end); + } + }; + const clearOtherSelections = target => { + if (typeof window.getSelection === 'function') { + const selection = window.getSelection(); + if (selection && selection.removeAllRanges) { + selection.removeAllRanges(); + } + } + const refs = [lessonTitleRef.current, lessonTextRef.current]; + refs.forEach(el => { + if (!el || el === target) return; + if (typeof el.setSelectionRange === 'function') { + const end = el.value ? el.value.length : 0; + el.setSelectionRange(end, end); + } + }); + }; + const clearSelectionGlobal = () => { + if (typeof window.getSelection === 'function') { + const selection = window.getSelection(); + if (selection && selection.removeAllRanges) { + selection.removeAllRanges(); + } + } + [lessonTitleRef.current, lessonTextRef.current].forEach(el => { + if (el && typeof el.setSelectionRange === 'function') { + el.setSelectionRange(0, 0); + } + }); + }; + const enforceDarkInputStyle = e => { + if (!darkMode) return; + const target = e.currentTarget; + clearOtherSelections(target); + target.style.setProperty('background-color', '#1C2541', 'important'); + target.style.setProperty('color', '#ffffff', 'important'); + target.style.setProperty('border-color', '#2563eb', 'important'); + target.style.setProperty('box-shadow', '0 0 0 1000px #1C2541 inset', 'important'); + target.style.setProperty('-webkit-box-shadow', '0 0 0 1000px #1C2541 inset', 'important'); + }; + const enforceDarkInputBlurStyle = e => { + if (!darkMode) return; + const target = e.currentTarget; + clearOtherSelections(target); + target.style.setProperty('background-color', '#1C2541', 'important'); + target.style.setProperty('color', '#ffffff', 'important'); + target.style.setProperty('border-color', '#404040', 'important'); + target.style.setProperty('box-shadow', 'none', 'important'); + target.style.setProperty('-webkit-box-shadow', 'none', 'important'); + if (typeof target.setSelectionRange === 'function') { + const end = target.value ? target.value.length : 0; + target.setSelectionRange(end, end); + } + }; // Lesson submit. all the data from user input is in here const LessonFormSubmit = async e => { @@ -218,55 +352,121 @@ function LessonForm() { } }; return ( -
    -
    -
    +
    +
    +
    - Lesson Title + + Lesson Title + * - Write a Lesson + + Write a Lesson + * - Add tag (Press enter to add tag) + + Add tag (Press enter to add tag) +
    { + allowFocusOnUserAction(); if (e.key === 'Enter') { addTag(e); } }} - className={`${styles.formControl}`} + className={`${styles.formControl} ${darkMode ? styles.formControlDark : ''}`} /> {showDropdown && filteredTags.length > 0 && ( -
    +
    {filteredTags.map(tag => (
    {LessonFormtags.map(tag => ( -
    +
    {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({ - + { setPrimaryId(currentTarget.value); + if (fieldErrors.primaryId) { + setFieldErrors(prev => ({ ...prev, primaryId: null })); + } }} - disabled={!primaryData.length} + onBlur={() => handleFieldBlur('primaryId', primaryId)} + disabled={!primaryData.length || isSubmitting} + className={fieldErrors.primaryId ? styles.invalidField : ''} > {primaryData.map(item => ( @@ -128,17 +264,28 @@ function PurchaseForm({ ))} + {fieldErrors.primaryId && ( +
    {fieldErrors.primaryId}
    + )}
    - + { setSecondaryId(currentTarget.value); + if (fieldErrors.secondaryId) { + setFieldErrors(prev => ({ ...prev, secondaryId: null })); + } }} + onBlur={() => handleFieldBlur('secondaryId', secondaryId)} + disabled={isSubmitting} + className={fieldErrors.secondaryId ? styles.invalidField : ''} > {secondaryData.map(item => ( @@ -147,36 +294,65 @@ function PurchaseForm({ ))} + {fieldErrors.secondaryId && ( +
    {fieldErrors.secondaryId}
    + )}
    - +
    setQuantity(e.target.value)} + onChange={e => { + setQuantity(e.target.value); + if (fieldErrors.quantity) { + setFieldErrors(prev => ({ ...prev, quantity: null })); + } + }} + onBlur={() => handleFieldBlur('quantity', quantity)} placeholder={formLabels.quantityPlaceholder} + disabled={isSubmitting} + className={fieldErrors.quantity ? styles.invalidField : ''} /> {unit}
    + {fieldErrors.quantity && ( +
    {fieldErrors.quantity}
    + )}
    - + setPriority(e.target.value)} + onChange={e => { + setPriority(e.target.value); + if (fieldErrors.priority) { + setFieldErrors(prev => ({ ...prev, priority: null })); + } + }} + disabled={isSubmitting} + className={fieldErrors.priority ? styles.invalidField : ''} > + {fieldErrors.priority && ( +
    {fieldErrors.priority}
    + )}
    @@ -188,6 +364,7 @@ function PurchaseForm({ value={brand} onChange={e => setBrand(e.target.value)} placeholder={formLabels.brandPlaceholder} + disabled={isSubmitting} /> @@ -204,20 +381,53 @@ function PurchaseForm({ color="secondary" onClick={handleCancel} style={boxStyle} + disabled={isSubmitting} > Cancel
    + + {/* Success Modal */} + setShowSuccessModal(false)}> + setShowSuccessModal(false)}> + Request Submitted Successfully + + + {submittedData && ( +

    + Your purchase request for {submittedData.materialName} on project{' '} + {submittedData.projectName} has been submitted for approval. +

    + )} +

    What would you like to do next?

    +
    + + + + +
    ); } 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() { )}
    + {detailOpen && ( + <> + {/* Backdrop */} +
    setDetailOpen(false)} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + setDetailOpen(false); + } + }} + /> + + {/* Slide-out Panel */} +
    + + + {detailLoading &&

    Loading details...

    } + + {!detailLoading && selectedToolDetail && !selectedToolDetail.error && ( + <> +

    {selectedToolDetail.toolName}

    +

    Total Checkouts: {selectedToolDetail.totalCheckouts ?? '—'}

    +

    Late Returns: {selectedToolDetail.percentLate}%

    +

    + Average Delay:{' '} + {selectedToolDetail.avgDelayDays != null + ? `${selectedToolDetail.avgDelayDays} days` + : '—'} +

    + + )} + + {!detailLoading && selectedToolDetail?.error &&

    {selectedToolDetail.error}

    } +
    + + )}
    ); } diff --git a/src/components/BMDashboard/RentalChart/ReturnedLateChart.module.css b/src/components/BMDashboard/RentalChart/ReturnedLateChart.module.css index 2917c5fdcd..120e148202 100644 --- a/src/components/BMDashboard/RentalChart/ReturnedLateChart.module.css +++ b/src/components/BMDashboard/RentalChart/ReturnedLateChart.module.css @@ -79,3 +79,37 @@ color: #666; font-style: italic; } + +.returned-late-detail-backdrop { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.25); + z-index: 2147483646; +} + +.returned-late-detail-panel { + position: fixed; + top: 0; + right: 0; + width: 360px; + height: 100vh; + padding: 20px; + z-index: 2147483647; + box-shadow: -4px 0 10px rgba(0, 0, 0, 0.4); + background: #ffffff; + color: #111111; +} + +.dark-panel { + background: #0b2545; + color: #ffffff; +} + +.returned-late-detail-close { + background: none; + border: none; + font-size: 20px; + float: right; + cursor: pointer; + color: inherit; +} diff --git a/src/components/CommunityPortal/Calendar/CommunityCalendar.jsx b/src/components/CommunityPortal/Calendar/CommunityCalendar.jsx index dcff52393c..ebd19e7180 100644 --- a/src/components/CommunityPortal/Calendar/CommunityCalendar.jsx +++ b/src/components/CommunityPortal/Calendar/CommunityCalendar.jsx @@ -22,6 +22,7 @@ export default function CommunityCalendar() { const [showEventModal, setShowEventModal] = useState(false); const [overflowDate, setOverflowDate] = useState(null); const popupRef = useRef(null); + const [calendarView, setCalendarView] = useState('month'); const currentDate = new Date(); const currentMonth = currentDate.getMonth(); @@ -192,6 +193,106 @@ export default function CommunityCalendar() { 'Full Event': 'statusFull', }; + function WeeklyTimeGrid({ events, selectedDate, onEventClick, darkMode }) { + const hours = Array.from({ length: 24 }, (_, i) => i); + + const startOfWeek = useMemo(() => { + const d = new Date(selectedDate); + d.setDate(d.getDate() - d.getDay()); + return d; + }, [selectedDate]); + + const weekDays = useMemo(() => { + return Array.from({ length: 7 }, (_, i) => { + const day = new Date(startOfWeek); + day.setDate(startOfWeek.getDate() + i); + return day; + }); + }, [startOfWeek]); + + return ( +
    +
    +
    + {weekDays.map(date => ( +
    +
    + {date.toLocaleDateString('en-US', { weekday: 'short' })} +
    +
    + {date.getDate()} +
    +
    + ))} +
    + +
    + {hours.map(hour => ( +
    +
    + {hour === 0 + ? '12 AM' + : hour > 12 + ? `${hour - 12} PM` + : hour === 12 + ? '12 PM' + : `${hour} AM`} +
    + + {weekDays.map(date => { + const cellEvents = events.filter(e => { + const eventDate = new Date(e.date); + const [hStr] = e.time.split(':'); + let h = parseInt(hStr, 10); + const isPM = e.time.toLowerCase().includes('pm'); + const isAM = e.time.toLowerCase().includes('am'); + if (isPM && h !== 12) h += 12; + if (isAM && h === 12) h = 0; + + return eventDate.toDateString() === date.toDateString() && h === hour; + }); + + return ( +
    + {cellEvents.map(ev => ( + + ))} +
    + ); + })} +
    + ))} +
    +
    + ); + } + // Render event tiles const tileContent = useCallback( ({ date, view }) => { @@ -211,20 +312,24 @@ export default function CommunityCalendar() { key={e.id} type="button" className={`${styles.eventItem} ${styles[statusKey] || ''}`} - onClick={() => handleEventClick(e)} + onClick={e_obj => { + e_obj.stopPropagation(); + handleEventClick(e); + }} title={e.title} > {e.title} ); })} - {hiddenCount > 0 && ( @@ -345,6 +450,16 @@ export default function CommunityCalendar() {

    Community Calendar

    + handleDateChange(e, 'from')} + /> +
    +
    + + handleDateChange(e, 'to')} + /> +
    + + {/* Project Filter */} +
    + + +
    +
    + + {/* Chart */} +
    {renderChart()}
    +
    + ); +} diff --git a/src/components/MaterialUtilization/MaterialUtilizationChart.module.css b/src/components/MaterialUtilization/MaterialUtilizationChart.module.css new file mode 100644 index 0000000000..4edd95ac3f --- /dev/null +++ b/src/components/MaterialUtilization/MaterialUtilizationChart.module.css @@ -0,0 +1,81 @@ +.container { + width: 100%; + padding: 24px; + background-color: white; + border-radius: 8px; + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +.title { + font-size: 24px; + font-weight: bold; + text-align: center; + margin-bottom: 24px; +} + +.filtersContainer { + display: flex; + flex-direction: column; + gap: 16px; + margin-bottom: 24px; +} + +/* Use media query for larger screens */ +@media (min-width: 768px) { + .filtersContainer { + flex-direction: row; + justify-content: space-between; + } +} + +.filterGroup { + width: 100%; +} + +.filterLabel { + display: block; + font-size: 14px; + font-weight: 500; + margin-bottom: 4px; +} + +.dateInput { + width: 100%; + padding: 8px 12px; + border: 1px solid #d1d5db; + border-radius: 6px; + box-sizing: border-box; /* Important for 100% width */ +} + +.chartContainer { + height: 400px; + margin-top: 24px; +} + +/* Used for Loading, Error, and Empty states */ +.infoMessage { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + color: #6b7280; + font-size: 16px; +} + +.tooltip { + background-color: white; + border: 1px solid #d1d5db; + border-radius: 6px; + padding: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); +} + +.tooltipLabel { + font-weight: bold; + margin-bottom: 8px; +} + +.tooltipItem { + margin-bottom: 4px; + color: #374151; +} diff --git a/src/components/PRAnalyticsDashboard/ReviewsInsight/ActionDoneGraph.jsx b/src/components/PRAnalyticsDashboard/ReviewsInsight/ActionDoneGraph.jsx index 16613f7931..6a6fba7755 100644 --- a/src/components/PRAnalyticsDashboard/ReviewsInsight/ActionDoneGraph.jsx +++ b/src/components/PRAnalyticsDashboard/ReviewsInsight/ActionDoneGraph.jsx @@ -1,9 +1,11 @@ import { Bar } from 'react-chartjs-2'; -import styles from './ReviewsInsight.module.css'; +import sharedStyles from './ReviewsInsight.module.css'; import { useSelector } from 'react-redux'; import { Chart } from 'chart.js'; import ChartDataLabels from 'chartjs-plugin-datalabels'; +Chart.register(ChartDataLabels); + function ActionDoneGraph({ selectedTeams, teamData }) { const darkMode = useSelector(state => state.theme.darkMode); @@ -12,7 +14,7 @@ function ActionDoneGraph({ selectedTeams, teamData }) { } if (!teamData || Object.keys(teamData).length === 0) { - return
    No data available for Action Graph.
    ; + return
    No data available for Action Graph.
    ; } const isAllTeams = selectedTeams.some(team => team.value === 'All'); @@ -47,33 +49,39 @@ function ActionDoneGraph({ selectedTeams, teamData }) { legend: { display: true, labels: { - font: { - size: 12, - }, + font: { size: 12 }, color: darkMode ? '#fff' : '#000', }, }, tooltip: { enabled: true, + callbacks: { + label: function(context) { + const label = context.dataset.label || ''; + const value = Math.round(context.raw); + return `${label}: ${value} PRs reviewed`; + }, + }, }, datalabels: { color: darkMode ? '#fff' : '#000', font: { weight: 'bold', size: 11 }, - formatter: value => { - if (!value) return 0; - return value; - }, + formatter: value => (value ? value : ''), }, }, scales: { x: { title: { display: true, - text: 'Count of PRs', + text: 'Number of PRs Reviewed', color: darkMode ? '#fff' : '#000', }, ticks: { color: darkMode ? '#fff' : '#000', + stepSize: 1, + callback: function(value) { + return Math.floor(value); + }, }, beginAtZero: true, }, @@ -91,11 +99,9 @@ function ActionDoneGraph({ selectedTeams, teamData }) { }; return ( -
    -

    - PR: Action Done -

    -
    +
    +

    PR: Action Done

    +
    diff --git a/src/components/PRAnalyticsDashboard/ReviewsInsight/PRQualityGraph.jsx b/src/components/PRAnalyticsDashboard/ReviewsInsight/PRQualityGraph.jsx index 31d84af489..97d47ffbe0 100644 --- a/src/components/PRAnalyticsDashboard/ReviewsInsight/PRQualityGraph.jsx +++ b/src/components/PRAnalyticsDashboard/ReviewsInsight/PRQualityGraph.jsx @@ -1,5 +1,5 @@ import { Pie } from 'react-chartjs-2'; -import styles from './ReviewsInsight.module.css'; +import sharedStyles from './ReviewsInsight.module.css'; import { useSelector } from 'react-redux'; import { Chart } from 'chart.js'; import ChartDataLabels from 'chartjs-plugin-datalabels'; @@ -14,7 +14,7 @@ function PRQualityGraph({ selectedTeams, qualityData, isDataViewActive }) { } if (!qualityData || Object.keys(qualityData).length === 0) { - return
    No data available for Quality Graph.
    ; + return
    No data available for Quality Graph.
    ; } const isAllTeams = selectedTeams.some(team => team.value === 'All'); @@ -79,15 +79,22 @@ function PRQualityGraph({ selectedTeams, qualityData, isDataViewActive }) { }; return ( -
    -

    +
    +

    PR Quality Distribution

    -
    +
    {teamsToDisplay.map(team => ( -
    -

    +
    +

    {team}

    diff --git a/src/components/PRAnalyticsDashboard/ReviewsInsight/ReviewsInsight.jsx b/src/components/PRAnalyticsDashboard/ReviewsInsight/ReviewsInsight.jsx index 1526667a6a..7322a1f19f 100644 --- a/src/components/PRAnalyticsDashboard/ReviewsInsight/ReviewsInsight.jsx +++ b/src/components/PRAnalyticsDashboard/ReviewsInsight/ReviewsInsight.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { useSelector, useDispatch } from 'react-redux'; import Select from 'react-select'; -import styles from './ReviewsInsight.module.css'; +import sharedStyles from './ReviewsInsight.module.css'; import ActionDoneGraph from './ActionDoneGraph'; import PRQualityGraph from './PRQualityGraph'; import { fetchReviewsInsights } from '../../../actions/prAnalytics/reviewsInsightsAction'; @@ -56,26 +56,21 @@ function ReviewsInsight() { formattedTeamData[team._id] = { actionSummary: { - Approved: actionSummary.find(action => action.actionTaken === 'Approved')?.count || 0, + Approved: actionSummary.find(a => a.actionTaken === 'Approved')?.count || 0, 'Changes Requested': - actionSummary.find(action => action.actionTaken === 'Changes Requested')?.count || 0, - Commented: actionSummary.find(action => action.actionTaken === 'Commented')?.count || 0, + actionSummary.find(a => a.actionTaken === 'Changes Requested')?.count || 0, + Commented: actionSummary.find(a => a.actionTaken === 'Commented')?.count || 0, }, }; formattedQualityData[team._id] = { - NotApproved: - qualityDistribution.find(quality => quality.qualityLevel === 'Not approved')?.count || - 0, - LowQuality: - qualityDistribution.find(quality => quality.qualityLevel === 'Low Quality')?.count || 0, - Sufficient: - qualityDistribution.find(quality => quality.qualityLevel === 'Sufficient')?.count || 0, - Exceptional: - qualityDistribution.find(quality => quality.qualityLevel === 'Exceptional')?.count || 0, - memberCount: team.memberCount, + NotApproved: qualityDistribution.find(q => q.qualityLevel === 'Not approved')?.count || 0, + LowQuality: qualityDistribution.find(q => q.qualityLevel === 'Low Quality')?.count || 0, + Sufficient: qualityDistribution.find(q => q.qualityLevel === 'Sufficient')?.count || 0, + Exceptional: qualityDistribution.find(q => q.qualityLevel === 'Exceptional')?.count || 0, }; }); + setTeamData(formattedTeamData); setQualityData(formattedQualityData); } @@ -96,27 +91,20 @@ function ReviewsInsight() { return (
    -

    PR Reviews Insights

    +

    PR Reviews Insights

    -
    -
    - +
    +
    +
    -
    - +
    + setDataViewActive(v => !v)} aria-label="Toggle data view: Percent vs Number" - className={styles.riSwitchInput} + className={sharedStyles.riSwitchInput} + /> + - - + {dataViewActive ? 'PERCENT' : 'NUMBER'}
    -
    +
    {selectedTeams.length === 0 ? ( -

    +

    No teams selected

    ) : selectedTeams.some(team => team.value === 'All') ? ( -

    +

    Selected Teams: All Teams

    ) : ( -

    +

    Selected Teams: {selectedTeams.map(team => team.label).join(', ')}

    )}
    - {loading && ( -
    - Loading... -
    - )} - {error &&
    {error}
    } + {loading &&
    Loading...
    } + {error &&
    {error}
    } {!loading && !error && ( -
    +
    @@ -12,10 +12,18 @@ function SearchBar() {
    - - + + {searchTerm && ( + + )}
    ); @@ -37,7 +45,7 @@ function ResourceManagement() { }, { id: 2, - user: 'First Last', + user: 'Test Last', timeDuration: '02:20:00', facilities: 'CRM Admin pages', materials: 'Larry San Francisco', @@ -45,7 +53,7 @@ function ResourceManagement() { }, { id: 3, - user: 'First Last', + user: 'Lorem ipsum', timeDuration: '03:00:00', facilities: 'Client Project', materials: 'Bagwell Avenue Ocala', @@ -53,7 +61,7 @@ function ResourceManagement() { }, { id: 4, - user: 'First Last', + user: 'Dolor Sit', timeDuration: '02:45:00', facilities: 'Admin Dashboard', materials: 'Washburn Baton Rouge', @@ -61,7 +69,7 @@ function ResourceManagement() { }, { id: 5, - user: 'First Last', + user: 'Elit Quisque', timeDuration: '03:30:00', facilities: 'App Landing page', materials: 'Nest Lane Olivette', @@ -69,6 +77,20 @@ function ResourceManagement() { }, ]); + const [searchTerm, setSearchTerm] = useState(() => localStorage.getItem('resourceSearch') || ''); + + useEffect(() => { + localStorage.setItem('resourceSearch', searchTerm); + }, [searchTerm]); + + const handleSearchChange = e => { + setSearchTerm(e.target.value); + }; + + const filteredResources = resources.filter(resource => + resource.user.toLowerCase().includes(searchTerm.toLowerCase()), + ); + return (
    - + setSearchTerm('')} + />
    @@ -97,23 +123,28 @@ function ResourceManagement() {

    - {resources.map(resource => ( -
    -
    -
    - -
    -
    {resource.user}
    -
    {resource.timeDuration}
    -
    {resource.facilities}
    -
    {resource.materials}
    -
    - 📅 {formatDateTimeLocal(resource.date)} + {filteredResources.length === 0 ? ( +
    No user found
    + ) : ( + filteredResources.map(resource => ( +
    +
    +
    + +
    +
    {resource.user}
    +
    {resource.timeDuration}
    +
    {resource.facilities}
    +
    {resource.materials}
    +
    + 📅{' '} + {formatDateTimeLocal(resource.date)} +
    +
    -
    -
    - ))} + )) + )}
    diff --git a/src/components/ResourceManagement/ResourceManagement.module.css b/src/components/ResourceManagement/ResourceManagement.module.css index 9798982ae7..2108f348a4 100644 --- a/src/components/ResourceManagement/ResourceManagement.module.css +++ b/src/components/ResourceManagement/ResourceManagement.module.css @@ -99,6 +99,7 @@ .searchBarContainerRight { display: flex; + position: relative; gap: 10px; } @@ -114,17 +115,21 @@ color: var(--table-header-color); } -.searchButton { - background-color: #006FE6; - color: white !important; +.clearButton { + position: absolute; + top: 10px; + right: 8px; + background: transparent; border: none; - padding: 5px 10px; - border-radius: 5px; cursor: pointer; + font-size: 16px; + color: var(--table-header-color); + padding: 0; + line-height: 1; } -.searchButton:hover { - background-color: #0056b3; +.clearButton:hover { + color: var(--text-color); } /* ========================================== @@ -177,6 +182,11 @@ margin: 0; } +.noResultsMessage{ + text-align: center; + margin-top: 20px; +} + /* ========================================== 6) Pagination ========================================== */ diff --git a/src/components/WeeklySummariesReport/FormattedReport.jsx b/src/components/WeeklySummariesReport/FormattedReport.jsx index 86e3827c06..e4ab43b080 100644 --- a/src/components/WeeklySummariesReport/FormattedReport.jsx +++ b/src/components/WeeklySummariesReport/FormattedReport.jsx @@ -352,7 +352,7 @@ function ReportDetails({
    import('./components/ResourceManagement/ResourceManagement')); @@ -556,6 +560,7 @@ export default ( fallback allowedRoles={[UserRole.Owner]} /> + `${APIEndpoint}/userprofile/${userId}/addInfringement`, TOP_CONVERTED: (limit, startDate, endDate) => - `${APIEndpoint}/job-analytics/top-converted?limit=${limit}${startDate && endDate ? `&startDate=${encodeURIComponent(startDate)}&endDate=${encodeURIComponent(endDate)}` : ''}`, + `${APIEndpoint}/job-analytics/top-converted?limit=${limit}${ + startDate && endDate + ? `&startDate=${encodeURIComponent(startDate)}&endDate=${encodeURIComponent(endDate)}` + : '' + }`, LEAST_CONVERTED: (limit, startDate, endDate) => - `${APIEndpoint}/job-analytics/least-converted?limit=${limit}${startDate && endDate ? `&startDate=${encodeURIComponent(startDate)}&endDate=${encodeURIComponent(endDate)}` : ''}`, + `${APIEndpoint}/job-analytics/least-converted?limit=${limit}${ + startDate && endDate + ? `&startDate=${encodeURIComponent(startDate)}&endDate=${encodeURIComponent(endDate)}` + : '' + }`, MODIFY_BLUE_SQUARE: (userId, blueSquareId) => `${APIEndpoint}/userprofile/${userId}/infringements/${blueSquareId}`, - // Blue Square Email Triggers - BLUE_SQUARE_RESEND_INFRINGEMENT_EMAILS: () => `${APIEndpoint}/blueSquare/resend-infringement-emails-only`, - BLUE_SQUARE_RESEND_WEEKLY_SUMMARY_EMAILS: () => `${APIEndpoint}/blueSquare/resend-weekly-summary-emails`, + BLUE_SQUARE_RESEND_INFRINGEMENT_EMAILS: () => + `${APIEndpoint}/blueSquare/resend-infringement-emails-only`, + BLUE_SQUARE_RESEND_WEEKLY_SUMMARY_EMAILS: () => + `${APIEndpoint}/blueSquare/resend-weekly-summary-emails`, USERS_ALLTEAMCODE_CHANGE: `${APIEndpoint}/AllTeamCodeChanges`, REPLACE_TEAM_CODE: `${APIEndpoint}/userProfile/replaceTeamCode`, @@ -131,9 +140,10 @@ export const ENDPOINTS = { `${APIEndpoint}/userProfile/authorizeUser/weeeklySummaries`, TOTAL_ORG_SUMMARY: (startDate, endDate, comparisonStartDate, comparisonEndDate) => `${APIEndpoint}/reports/volunteerstats?startDate=${startDate}&endDate=${endDate}&comparisonStartDate=${comparisonStartDate || - ''}&comparisonEndDate=${comparisonEndDate || ''}`, + ''}&comparisonEndDate=${comparisonEndDate || ''}`, VOLUNTEER_TRENDS: (timeFrame, offset, customStartDate, customEndDate) => - `${APIEndpoint}/reports/volunteertrends?timeFrame=${timeFrame}&offset=${offset}${customStartDate ? `&customStartDate=${customStartDate}` : '' + `${APIEndpoint}/reports/volunteertrends?timeFrame=${timeFrame}&offset=${offset}${ + customStartDate ? `&customStartDate=${customStartDate}` : '' }${customEndDate ? `&customEndDate=${customEndDate}` : ''}`, HOURS_TOTAL_ORG_SUMMARY: (startDate, endDate) => `${APIEndpoint}/reports/overviewsummaries/taskandprojectstats?startDate=${startDate}&endDate=${endDate}`, @@ -183,7 +193,7 @@ export const ENDPOINTS = { return url.slice(0, -1); }, ENHANCED_POPULARITY_ROLES: `${APIEndpoint}/popularity-enhanced/roles-enhanced`, - ENHANCED_POPULARITY_PAIRS: (roles) => + ENHANCED_POPULARITY_PAIRS: roles => `${APIEndpoint}/popularity-enhanced/role-pairs?roles=${encodeURIComponent(roles.join(','))}`, // titles endpoints @@ -359,7 +369,7 @@ export const ENDPOINTS = { BM_TAGS_DELETE: `${APIEndpoint}/bm/tags`, BM_ORGS_WITH_LOCATION: `${APIEndpoint}/bm/orgLocation`, - ORG_DETAILS: (projectId) => `${APIEndpoint}/bm/orgLocation/${projectId}`, + ORG_DETAILS: projectId => `${APIEndpoint}/bm/orgLocation/${projectId}`, BM_PROJECT_MEMBERS: projectId => `${APIEndpoint}/bm/project/${projectId}/users`, PROJECT_GLOBAL_DISTRIBUTION: `${APIEndpoint}/projectglobaldistribution`, @@ -497,18 +507,14 @@ export const ENDPOINTS = { ? `startDate=${encodeURIComponent(startDate)}&endDate=${encodeURIComponent(endDate)}` : null, - roles && roles !== "All" - ? `roles=${encodeURIComponent(roles)}` - : null, + roles && roles !== 'All' ? `roles=${encodeURIComponent(roles)}` : null, - granularity - ? `granularity=${encodeURIComponent(granularity)}` - : null, + granularity ? `granularity=${encodeURIComponent(granularity)}` : null, ].filter(Boolean); - const qs = params.length ? `?${params.join("&")}` : ""; + const qs = params.length ? `?${params.join('&')}` : ''; - return `${APIEndpoint.replace("/api", "")}/job-analytics${qs}`; + return `${APIEndpoint.replace('/api', '')}/job-analytics${qs}`; }, JOB_ANALYTICS_ROLES: `${APIEndpoint.replace('/api', '')}/job-analytics/roles`, @@ -517,6 +523,9 @@ export const ENDPOINTS = { PROMOTION_ELIGIBILITY: `${APIEndpoint}/promotion-eligibility`, PROMOTE_MEMBERS: `${APIEndpoint}/promote-members`, + MATERIAL_UTILIZATION: () => `${APIEndpoint}/materials/utilization`, + DISTINCT_PROJECTS: () => `${APIEndpoint}/materials/distinct-projects`, + DISTINCT_MATERIALS: () => `${APIEndpoint}/materials/distinct-materials`, //pull requests analysis PR_REVIEWS_INSIGHTS: `${APIEndpoint}/analytics/pr-review-insights`, diff --git a/yarn.lock b/yarn.lock index 2d07b6c40a..322eca94e7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4460,13 +4460,13 @@ axios-mock-adapter@^1.22.0: fast-deep-equal "^3.1.3" is-buffer "^2.0.5" -axios@^1.11.0: - version "1.11.0" - resolved "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz" - integrity sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA== +axios@^1.13.5: + version "1.13.5" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.5.tgz#5e464688fa127e11a660a2c49441c009f6567a43" + integrity sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q== dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.4" + follow-redirects "^1.15.11" + form-data "^4.0.5" proxy-from-env "^1.1.0" axobject-query@^4.1.0: @@ -4634,7 +4634,7 @@ base64-js@^1.1.2, base64-js@^1.3.0, base64-js@^1.3.1: resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== -baseline-browser-mapping@^2.8.25, baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.17: +baseline-browser-mapping@^2.9.0, baseline-browser-mapping@^2.9.17: version "2.9.19" resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz#3e508c43c46d961eb4d7d2e5b8d1dd0f9ee4f488" integrity sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg== @@ -6790,9 +6790,9 @@ flru@^1.0.2: resolved "https://registry.npmjs.org/flru/-/flru-1.0.2.tgz" integrity sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog== -follow-redirects@^1.15.6: +follow-redirects@^1.15.11: version "1.15.11" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340" integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ== font-awesome@^4.7.0: @@ -6815,10 +6815,10 @@ foreground-child@^3.1.0: cross-spawn "^7.0.6" signal-exit "^4.0.1" -form-data@^4.0.4: - version "4.0.4" - resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz" - integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow== +form-data@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.5.tgz#b49e48858045ff4cbf6b03e1805cebcad3679053" + integrity sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w== dependencies: asynckit "^0.4.0" combined-stream "^1.0.8" @@ -6973,7 +6973,19 @@ glob@^10.3.10: package-json-from-dist "^1.0.0" path-scurry "^1.11.1" -glob@^7.1.3, glob@^7.1.4: +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.4: version "7.2.3" resolved "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -8959,8 +8971,6 @@ node-int64@^0.4.0: node-releases@^2.0.27: version "2.0.27" - resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz" - integrity sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA== normalize-path@^3.0.0: version "3.0.0" @@ -9323,7 +9333,17 @@ picocolors@1.1.1, picocolors@^1.0.0, picocolors@^1.1.1: resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz" integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== -picomatch@^2.0.4, picomatch@^2.2.3, picomatch@^2.3.1: +picomatch@^2.0.4: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^2.2.3: + version "2.3.1" + resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^2.3.1: version "2.3.1" resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==