diff --git a/.husky/pre-push b/.husky/pre-push index 1df1d53f88..073388af8b 100755 --- a/.husky/pre-push +++ b/.husky/pre-push @@ -25,6 +25,12 @@ if [ -z "$changed_files" ]; then exit 0 fi +# Skip checks if any outgoing commit is a merge commit +if git log @{upstream}..HEAD --merges --oneline | grep -q .; then + echo "ℹ️ Outgoing commits include a merge commit. Skipping lint/test checks." + exit 0 +fi + if ! printf '%s\n' "$changed_files" | grep -qvE '^\.husky/'; then echo "ℹ️ Only Husky hook changes detected. Skipping full test and lint checks." exit 0 diff --git a/.stylelintrc b/.stylelintrc index 3e57ddfa6c..6917318a21 100644 --- a/.stylelintrc +++ b/.stylelintrc @@ -6,12 +6,22 @@ "node_modules/**/*.css" ], "rules": { - "no-duplicate-selectors": true, + "no-duplicate-selectors": [true, { + "disallowInList": false + }], "selector-pseudo-class-no-unknown": [ true, { "ignorePseudoClasses": ["global", "local"] } ], "selector-class-pattern": null, "selector-id-pattern": null - } -} + }, + "overrides": [ + { + "files": ["**/*.module.css"], + "rules": { + "no-duplicate-selectors": null + } + } + ] +} \ No newline at end of file diff --git a/conflict_cplog.txt b/conflict_cplog.txt new file mode 100644 index 0000000000..eb61d0d8da Binary files /dev/null and b/conflict_cplog.txt differ diff --git a/conflict_finalday.txt b/conflict_finalday.txt new file mode 100644 index 0000000000..15e5fb8359 Binary files /dev/null and b/conflict_finalday.txt differ diff --git a/conflict_owner.txt b/conflict_owner.txt new file mode 100644 index 0000000000..7e35d18550 Binary files /dev/null and b/conflict_owner.txt differ diff --git a/conflict_popup.txt b/conflict_popup.txt new file mode 100644 index 0000000000..62c70fc0bf Binary files /dev/null and b/conflict_popup.txt differ diff --git a/conflict_spg.txt b/conflict_spg.txt new file mode 100644 index 0000000000..d6cfd76346 Binary files /dev/null and b/conflict_spg.txt differ diff --git a/conflict_usertable.txt b/conflict_usertable.txt new file mode 100644 index 0000000000..11b1a4e84f Binary files /dev/null and b/conflict_usertable.txt differ diff --git a/conflict_weekly.txt b/conflict_weekly.txt new file mode 100644 index 0000000000..b9d4337827 Binary files /dev/null and b/conflict_weekly.txt differ diff --git a/package.json b/package.json index 281255660f..58206d8394 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,9 @@ }, "private": true, "dependencies": { - "@ant-design/charts": "^2.6.4", + "@ant-design/charts": "^2.6.4", "@ant-design/icons": "^6.0.1", + "@babel/plugin-proposal-logical-assignment-operators": "^7.20.7", "@babel/runtime": "^7.27.6", "@babel/runtime-corejs3": "^7.27.6", "@changey/react-leaflet-markercluster": "^4.0.0-rc1", @@ -32,7 +33,7 @@ "@tanstack/react-query": "^5.91.0", "@tinymce/miniature": "^6.0.0", "@tinymce/tinymce-react": "^6.1.0", - "ajv": "^8.18.0", + "ajv": "^8.0.0", "ajv-keywords": "^5.1.0", "ant-design": "^1.0.0", "antd": "^5.29.3", @@ -54,8 +55,8 @@ "date-fns": "^2.14.0", "date-fns-tz": "^2.0.1", "dayjs": "^1.11.13", - "diff": "^8.0.3", - "dompurify": "^3.3.2", + "diff": "^5.0.0", + "dompurify": "^3.2.5", "elliptic": "^6.6.1", "font-awesome": "^4.7.0", "fs-extra": "^11.3.0", @@ -133,7 +134,8 @@ "util": "^0.12.5", "uuid": "^9.0.1", "validator": "^13.15.26", - "webpack": "^5.104.1" + "webpack": "^5.104.1", + "xlsx": "^0.18.5" }, "resolutions": { "react": "18.3.1", @@ -164,7 +166,6 @@ "@babel/core": "^7.28.0", "@babel/eslint-parser": "^7.28.0", "@babel/helper-validator-identifier": "^7.12.11", - "@babel/plugin-proposal-logical-assignment-operators": "^7.20.7", "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", "@babel/plugin-transform-optional-chaining": "^7.27.1", "@babel/plugin-transform-private-property-in-object": "^7.21.11", @@ -180,13 +181,10 @@ "@types/html-to-pdfmake": "^2.4.4", "@types/pdfmake": "^0.2.11", "@types/react-router-dom": "^5.3.3", - "@typescript-eslint/eslint-plugin": "^8.44.1", - "@typescript-eslint/parser": "^8.44.1", "@vitejs/plugin-react": "^4.5.0", "@vitest/coverage-v8": "^3.2.4", "@vitest/ui": "^3.2.4", "babel-jest": "^29.7.0", - "baseline-browser-mapping": "^2.9.17", "cross-env": "^5.2.1", "eslint": "^8.57.1", "eslint-config-prettier": "^5.1.0", @@ -198,12 +196,10 @@ "eslint-plugin-react-hooks": "^4.6.2", "eslint-plugin-testing-library": "^7.11.0", "eslint-plugin-vitest": "^0.5.4", - "husky": "^9.1.7", - "jest-environment-jsdom": "^30.0.5", + "husky": "^7.0.4", "joi-browser": "^13.4.0", "jsdom": "^26.1.0", "lint-staged": "^16.1.5", - "mdn-data": "^2.26.0", "msw": "^2.10.4", "prettier": "^1.19.1", "redux-mock-store": "^1.5.4", @@ -221,8 +217,5 @@ "prettier --write" ], "**/*.{css,scss,sass}": "stylelint" - }, - "optionalDependencies": { - "@rollup/rollup-darwin-arm64": "^4.54.0" } } diff --git a/public/index.css b/public/index.module.css similarity index 99% rename from public/index.css rename to public/index.module.css index 7b86c9d3ee..510ae299be 100644 --- a/public/index.css +++ b/public/index.module.css @@ -409,7 +409,7 @@ body .email-template-list .action-btn.info-btn { /* Fix edit button - orange */ body .email-template-list .action-btn.edit-btn { background-color: #fd7e14 !important; - color: #ffffff !important; + color: #000 !important; border-color: #fd7e14 !important; } diff --git a/src/App.module.css b/src/App.module.css index 74e18fa25c..bdceb7cf6e 100644 --- a/src/App.module.css +++ b/src/App.module.css @@ -24,13 +24,7 @@ button { background-color: #fff; } -html { - background-color: #1a1d23 !important; -} -body { - background-color: #1a1d23 !important; -} /* Dark mode override for #root */ body.dark-mode #root, @@ -301,4 +295,14 @@ body.dark-mode .text-light { /* Override ANY inline styles */ body.dark-mode [style*="background"] { background-color: #1a1d23 !important; +} + +tr.dark-leaderboard-row:hover, +tr.dark-leaderboard-row:hover td, +tr.dark-leaderboard-row:hover th, +tr.dark-leaderboard-row:hover span, +tr.dark-leaderboard-row:hover p, +tr.dark-leaderboard-row:hover a { + background-color: #fff !important; + color: #000 !important; } \ No newline at end of file diff --git a/src/actions/bmdashboard/lessonsAction.js b/src/actions/bmdashboard/lessonsAction.js index 0ad64f97b9..b140463f00 100644 --- a/src/actions/bmdashboard/lessonsAction.js +++ b/src/actions/bmdashboard/lessonsAction.js @@ -45,13 +45,20 @@ export const setLessons = payload => { }; }; +// A valid MongoDB ObjectId is a 24-character hex string. Lesson author/project +// fields sometimes hold display names (e.g. "James", "project 1") instead of +// real references; skip those so we don't fire doomed 404/500/400 requests. +const isObjectId = id => typeof id === 'string' && /^[0-9a-fA-F]{24}$/.test(id); + export const fetchBMLessons = () => { return async dispatch => { try { const response = await axios.get(ENDPOINTS.BM_LESSONS); const lessons = response.data; - const authorIds = [...new Set(lessons.map(lesson => lesson.author))]; - const projectIds = [...new Set(lessons.map(lesson => lesson.relatedProject))]; + const authorIds = [...new Set(lessons.map(lesson => lesson.author).filter(isObjectId))]; + const projectIds = [ + ...new Set(lessons.map(lesson => lesson.relatedProject).filter(isObjectId)), + ]; // Keep the more robust approach from honglin-lesson-list-buttons branch const [authorProfiles, projectDetails] = await Promise.all([ @@ -129,10 +136,11 @@ export const fetchSingleBMLesson = lessonId => { const response = await axios.get(url); const lesson = response.data; - // Fetch user profile and project details concurrently + // Fetch user profile and project details concurrently, but only when the + // stored values are real ObjectIds (not display names like "project 1"). const [projectDetails, userProfile] = await Promise.all([ - dispatch(fetchProjectById(lesson.relatedProject)), - dispatch(getUserProfile(lesson.author)), + isObjectId(lesson.relatedProject) ? dispatch(fetchProjectById(lesson.relatedProject)) : null, + isObjectId(lesson.author) ? dispatch(getUserProfile(lesson.author)) : null, ]); // Update the lesson with author and project details diff --git a/src/actions/warnings.js b/src/actions/warnings.js index 0a1c524945..8deee989d6 100644 --- a/src/actions/warnings.js +++ b/src/actions/warnings.js @@ -9,6 +9,7 @@ import { postNewWarning as postNewWarningAction, deleteWarningDescription as deleteWarningDescriptionAction, updateWarningDescription as updateWarningDescriptionAction, + reorderWarningDescriptions as reorderWarningDescriptionsAction, editWarningDescription as editWarningDescriptionAction, } from '../constants/warning'; @@ -163,3 +164,18 @@ export const updateWarningDescription = warningDescriptionId => { } }; }; + +export const reorderWarningDescriptions = warningDescriptions => { + const url = ENDPOINTS.REORDER_WARNING_DESCRIPTIONS(warningDescriptions); + + return async dispatch => { + try { + const res = await axios.put(url, { warningDescriptions }); + const response = await dispatch(reorderWarningDescriptionsAction(res.data)); + + return response.payload; + } catch (error) { + return { error: error.message }; + } + }; +}; diff --git a/src/components/BMDashboard/Issue/Issue.jsx b/src/components/BMDashboard/Issue/Issue.jsx index 5f4ba32b02..f971ab86c7 100644 --- a/src/components/BMDashboard/Issue/Issue.jsx +++ b/src/components/BMDashboard/Issue/Issue.jsx @@ -5,6 +5,7 @@ import { useHistory, useParams } from 'react-router-dom'; import axios from 'axios'; import { ENDPOINTS } from '../../../utils/URL'; import styles from './Issue.module.css'; +import { useSelector } from 'react-redux'; function Issue() { const ISSUE_FORM_HEADER = 'ISSUE LOG'; @@ -41,6 +42,8 @@ function Issue() { const otherOption = ['Other']; + const darkMode = useSelector(state => state.theme.darkMode); + const [formData, setFormData] = useState({ issueDate: '', dropdown: defaultOption, @@ -222,7 +225,9 @@ function Issue() { }, []); return ( -
+

{ISSUE_FORM_HEADER}

diff --git a/src/components/BMDashboard/Issue/Issue.module.css b/src/components/BMDashboard/Issue/Issue.module.css index 7ca40d3f0e..333e406281 100644 --- a/src/components/BMDashboard/Issue/Issue.module.css +++ b/src/components/BMDashboard/Issue/Issue.module.css @@ -1,7 +1,13 @@ /* stylelint-disable */ .issueFormContainer { margin: 0 auto; - max-width: 625px; + width: 100%; + padding: 5px; + background-color: white; +} + +.darkModeIssueFormContainer{ + color: white; } .redText { diff --git a/src/components/BMDashboard/Issues/IssueDashboard.jsx b/src/components/BMDashboard/Issues/IssueDashboard.jsx index 81833a3229..b0ceb33bb8 100644 --- a/src/components/BMDashboard/Issues/IssueDashboard.jsx +++ b/src/components/BMDashboard/Issues/IssueDashboard.jsx @@ -31,7 +31,7 @@ import { jsPDF } from 'jspdf'; export default function IssueDashboard() { const dispatch = useDispatch(); - const issues = useSelector(state => state.bmissuechart?.issues || []); + const issues = useSelector(state => state.bmIssues || []); const darkMode = useSelector(state => state.theme.darkMode); const [currentPage, setCurrentPage] = useState(1); @@ -170,7 +170,6 @@ export default function IssueDashboard() { 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; @@ -244,7 +243,7 @@ export default function IssueDashboard() { return (
@@ -252,7 +251,9 @@ export default function IssueDashboard() {
-

Issue Dashboard

+

+ Issue Dashboard +

@@ -282,7 +283,7 @@ export default function IssueDashboard() { - + @@ -303,15 +304,17 @@ export default function IssueDashboard() { return ( - - + + - - + +
Issue Name Issue Name Open since Category Person dealing
{issue.name}{openSince} + {issue.name} + {openSince} {category} {assignedTo}{cost}{assignedTo}{cost} -
+
-
@@ -458,28 +473,26 @@ export default function IssueDashboard() { {/* Delete Modal */} {showDeleteModal && ( -
-
-
+
+
+
Confirm Delete
-

- Are you sure you want to delete {selectedIssue.name}? +

+ Are you sure you want to delete {selectedIssue?.name}?

-
@@ -490,28 +503,26 @@ export default function IssueDashboard() { {/* Copy Modal */} {showCopyModal && ( -
-
-
+
+
+
Confirm Copy
-

- Are you sure you want to copy {selectedIssue.name}? +

+ Are you sure you want to copy {selectedIssue?.name}?

-
diff --git a/src/components/BMDashboard/Issues/IssueDashboard.module.css b/src/components/BMDashboard/Issues/IssueDashboard.module.css index 52e73f5f4b..5319e68b96 100644 --- a/src/components/BMDashboard/Issues/IssueDashboard.module.css +++ b/src/components/BMDashboard/Issues/IssueDashboard.module.css @@ -1,21 +1,67 @@ -/* stylelint-disable */ +/* stylelint-disable */ + +/* ========================================================================== + TABLE & CONTAINER STRUCTURAL LAYOUT + ========================================================================== */ + +.issuesTableResponsive { + overflow: visible !important; /* Ensure action dropdowns aren't clipped */ + position: relative; + width: 100%; +} + +.issuesTableResponsive :global(td), +.issuesTableResponsive :global(th) { + text-align: left !important; +} + +.textEnd { + text-align: right; +} + .issuesTableResponsive :global(.text-end) { font-weight: 500; color: #6c757d; border-bottom-width: 1px; } -.issuesTableResponsive:global(.bg-oxford-blue) :global(.text-end), -.issuesTableResponsive:global(.bg-oxford-blue) :global(th) { - color: #fff !important; +/* Global wrappers layout guard rails */ +:global(.card) { + overflow: hidden; +} + +:global(.container-fluid) { + max-width: 100%; } -/* Improved positioning for the action menu */ +/* ========================================================================== + DROPDOWNS & ACTIONS MENUS + ========================================================================== */ + +.issueDashboardDropdown { + position: static; /* Let cell control alignment context */ +} + +:global(td.text-end) { + position: relative; /* Anchor point for custom inline menu mapping */ + white-space: nowrap; + width: 1%; +} + +/* Legacy positioning override helper for specific row dropdown structures */ +:global(td.text-end) .issueDashboardDropdown :global(.issue-dashboard-dropdown-menu) { + position: absolute; + right: 3rem !important; + top: 65px !important; + left: auto !important; +} + +/* The custom inline action floating menu container */ .actionMenu { position: absolute; - top: 0; /* Position at the top of relative parent */ - right: 100%; /* Position to the left of the button */ - transform: translateY(-25%); /* Adjust vertical position */ + top: 0; + right: 100%; /* Spawns neatly to the left of your ellipsis button */ + transform: translateY(-25%); min-width: 10rem; z-index: 1050; padding: 0.5rem 0; @@ -23,7 +69,7 @@ border-radius: 0.25rem; box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 15%); white-space: nowrap; - background-color: white; + background-color: #ffffff; } .actionMenuDark { @@ -32,17 +78,18 @@ box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 50%); } +/* Action items inside menu container */ .issueDashboardDropdownItem { display: flex; align-items: center; padding: 0.5rem 1rem; - color: #1C1E1C; + color: #1c1e1c; background-color: transparent; border: none; width: 100%; text-align: left; cursor: pointer; - transition: background-color 0.2s; + transition: background-color 0.2s, color 0.2s; } .issueDashboardDropdownItem:hover { @@ -50,37 +97,57 @@ } .issueDashboardDropdownItemDark { - color: #fff; + color: #ffffff !important; } .issueDashboardDropdownItemDark:hover { - background-color: #2f4157; + background-color: #2f4157 !important; +} + +/* Specific text color targets for danger items across variants */ +:global(.text-danger).issueDashboardDropdownItemDark { + color: #ff6b6b !important; } +:global(.text-danger).issueDashboardDropdownItemDark:hover { + background-color: #2f4157 !important; + color: #ff8787 !important; +} + +/* Header Export Feature Dropdowns (Dark mode overrides) */ .exportDropdownMenuDark { - background-color: #1c2541; - border: 1px solid #3a506b; + background-color: #1c2541 !important; + border: 1px solid #3a506b !important; } .exportDropdownItemDark { - color: #fff !important; + color: #ffffff !important; } .exportDropdownItemDark:hover, .exportDropdownItemDark:focus { background-color: #2d3b66 !important; - color: #fff !important; + color: #ffffff !important; } -.issueDashboardDropdownItemDark.textDanger { - color: #ff6b6b; +/* Icon button styles linking the anchor items */ +.issueDashboardDropdown :global(.btn-link) { + color: #6c757d; + text-decoration: none; } -.issueDashboardDropdownItemDark.textDanger:hover { - background-color: #2f4157; - color: #ff8787; +.issueDashboardDropdownDark :global(.btn-link) { + color: #ffffff; } +.issueDashboardDropdownDark :global(.btn-link:hover) { + color: #ffffff; +} + +/* ========================================================================== + UI UTILITY BADGES & PAGINATION ELEMENTS + ========================================================================== */ + .badge :global(.bg-info) { background-color: #cfe2ff !important; color: #084298 !important; @@ -89,7 +156,7 @@ .badge :global(.bg-info-dark) { background-color: #3a506b !important; - color: #fff !important; + color: #ffffff !important; font-weight: 500; } @@ -105,100 +172,57 @@ border-color: #e9ecef; } -.issueDashboardDropdown :global(.btn-link) { - color: #6c757d; - text-decoration: none; -} - -.issueDashboardDropdownDark :global(.btn-link) { - color: #fff; -} - -.issueDashboardDropdownDark :global(.btn-link:hover) { - color: #fff; -} - -/* Fixed table responsiveness */ -.issuesTableResponsive { - overflow: visible !important; /* Prevent horizontal scrollbar */ /* Ensure dropdowns aren't clipped */ - position: relative; - width: 100%; - table-layout: fixed; -} - -.issuesTableResponsive :global(td), -.issuesTableResponsive :global(th) { - text-align: left !important; -} - -.textEnd { - text-align: right; -} - -/* Improved dropdown positioning */ -.issueDashboardDropdown { - position: static; /* Make it relative to the table cell instead */ -} - -:global(td.text-end) { - position: relative; /* Create positioning context for dropdown */ - white-space: nowrap; - width: 1%; -} - -/* Ensure card doesn't create scrollbars */ -:global(.card) { - overflow: hidden; -} +/* ========================================================================== + MODAL WINDOW DIALOG ARCHITECTURE + ========================================================================== */ -/* Make sure content doesn't force horizontal scroll */ -:global(.container-fluid) { - max-width: 100%; - overflow-x: hidden; -} - -/* Fix for the dropdown menu to appear inside table */ -:global(td.text-end) .issueDashboardDropdown :global(.issue-dashboard-dropdown-menu) { - position: absolute; - right: 3rem !important; - top: 65px !important; - left: auto !important; -} - -.issuesModalBackdrop { +/* Shared base layout variables for light and dark layouts */ +.issuesModalBackdrop, +.issuesModalBackdropDark { position: fixed !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; - background-color: rgb(0 0 0 / 30%) !important; z-index: 1050 !important; display: flex !important; align-items: center !important; justify-content: center !important; } +/* Conditional backdrop color treatment overlays */ +.issuesModalBackdrop { + background-color: rgb(0 0 0 / 30%) !important; +} + +.issuesModalBackdropDark { + background-color: rgb(0 0 0 / 60%) !important; +} -.issuesModalBackdrop :global(.modal-dialog) { +/* Target structural settings across layout components */ +.issuesModalBackdrop :global(.modal-dialog), +.issuesModalBackdropDark :global(.modal-dialog) { border-radius: 8px; max-width: 400px; width: 90%; + margin: 0 auto; } .issuesModalBackdrop :global(.modal-content) { - background-color: white; + background-color: #ffffff; } +/* Dark mode variations for interactive element layouts inside modals */ .issuesModalBackdropDark :global(.modal-content) { - background-color: #1c2541; - color: #fff; + background-color: #1c2541 !important; + color: #ffffff !important; border: 1px solid #3a506b; } .issuesModalBackdropDark :global(.modal-content input) { - background-color: #1b2a41; - border-color: #3a506b; - color: #fff; + background-color: #1b2a41 !important; + border-color: #3a506b !important; + color: #ffffff !important; } .issuesModalBackdropDark :global(.modal-content input::placeholder) { @@ -206,35 +230,40 @@ } .issuesModalBackdropDark :global(.modal-content input:focus) { - background-color: #1b2a41; - border-color: #4a5f7a; - color: #fff; -} - -.issuesModalBackdropDark :global(.modal-content h5) { - color: #fff; -} - -.issuesModalBackdropDark :global(.modal-content p) { - color: #fff; + background-color: #1b2a41 !important; + border-color: #4a5f7a !important; + color: #ffffff !important; + box-shadow: 0 0 0 0.25rem rgb(58 80 107 / 50%) !important; } +.issuesModalBackdropDark :global(.modal-content h5), +.issuesModalBackdropDark :global(.modal-content p), .issuesModalBackdropDark :global(.modal-content strong) { - color: #fff; + color: #ffffff !important; } +/* ========================================================================== + GLOBAL BOOTSTRAP DATATABLE THEME TARGET OVERRIDES + ========================================================================== */ + :global(.table-dark thead tr:hover) { background-color: #1b2a41; } :global(.table-dark thead tr th) { - color: #fff; + color: #ffffff; } :global(.table-dark tbody tr) { - color: #fff; + color: #ffffff; } :global(.table-dark tbody tr:hover) { background-color: #2f4157; } + +/* Global utility classes handling explicit canvas colors for header mapping selectors */ +.issuesTableResponsive:global(.bg-oxford-blue) :global(.text-end), +.issuesTableResponsive:global(.bg-oxford-blue) :global(th) { + color: #ffffff !important; +} \ No newline at end of file diff --git a/src/components/BMDashboard/Issues/IssueHeader.jsx b/src/components/BMDashboard/Issues/IssueHeader.jsx index afe49eba3d..22232a3142 100644 --- a/src/components/BMDashboard/Issues/IssueHeader.jsx +++ b/src/components/BMDashboard/Issues/IssueHeader.jsx @@ -1,4 +1,4 @@ -import { Search, MoreHorizontal, ChevronDown } from 'lucide-react'; +import { Search, ChevronDown } from 'lucide-react'; import styles from './IssueHeader.module.css'; import { connect, useDispatch, useSelector } from 'react-redux'; import { getHeaderData } from '~/actions/authActions'; @@ -44,12 +44,8 @@ export function IssueHeader(props) {

Issues

- -
- - +
+ diff --git a/src/components/BMDashboard/Issues/openIssueCharts.jsx b/src/components/BMDashboard/Issues/openIssueCharts.jsx index c6a3274ec4..d4072daed5 100644 --- a/src/components/BMDashboard/Issues/openIssueCharts.jsx +++ b/src/components/BMDashboard/Issues/openIssueCharts.jsx @@ -1,11 +1,10 @@ -import { useEffect, useState, useRef } from 'react'; +import { useEffect, useState, useRef, useMemo } from 'react'; import DatePicker from 'react-datepicker'; import 'react-datepicker/dist/react-datepicker.css'; import { useDispatch, useSelector } from 'react-redux'; import { BarChart, Bar, - Cell, XAxis, YAxis, CartesianGrid, @@ -90,39 +89,11 @@ const createSelectStyles = (isDark, textColor) => ({ }), }); -// Deterministic, collision-resistant color per projectId -const getStableProjectColor = projectId => { - if (!projectId) return '#94a3b8'; - - let hash = 0; - for (let i = 0; i < projectId.length; i++) { - hash = projectId.charCodeAt(i) + ((hash << 5) - hash); - } - - const hue = Math.abs(hash) % 360; - const saturation = 65; - const lightness = 50; - - return `hsl(${hue}, ${saturation}%, ${lightness}%)`; -}; - -const getProjectColorMap = issues => { - const map = {}; - (issues || []).forEach(issue => { - (issue.projects || []).forEach(p => { - if (p?.projectId && !map[p.projectId]) { - map[p.projectId] = getStableProjectColor(p.projectId); - } - }); - }); - return map; -}; - /* --------------------------- component --------------------------- */ function IssueCharts() { const dispatch = useDispatch(); - const darkMode = useSelector(state => state.theme.darkMode); + const darkMode = useSelector(state => state.theme?.darkMode); const { issues, loading, error, selectedProjects } = useSelector(state => state.bmissuechart); const projects = useSelector(state => state.bmProjects); @@ -155,6 +126,86 @@ function IssueCharts() { padding: '20px', }; + // Normalize issues for chart + // Number issues per project to avoid conflicts when multiple projects are selected + // Prefix with project name when multiple projects are selected to distinguish them + const normalizedIssues = useMemo(() => { + if (!issues || issues.length === 0) return []; + + // Check if multiple projects are selected + const uniqueProjectIds = new Set((issues || []).map(item => item.projectId).filter(Boolean)); + const multipleProjects = uniqueProjectIds.size > 1; + + // Group issues by projectId + const issuesByProject = new Map(); + (issues || []).forEach(item => { + const projectId = item.projectId || 'unknown'; + if (!issuesByProject.has(projectId)) { + issuesByProject.set(projectId, []); + } + issuesByProject.get(projectId).push(item); + }); + + // Create per-project numbering maps + const projectNumberMaps = new Map(); + + issuesByProject.forEach((projectIssues, projectId) => { + const issueIdToNumber = new Map(); + let counter = 1; + + // Sort issues by issueId within each project for consistent ordering + const sortedById = [...projectIssues].sort((a, b) => { + const idA = a.issueId || ''; + const idB = b.issueId || ''; + return idA.localeCompare(idB); + }); + + // Assign numbers to issues without names within this project + sortedById.forEach(item => { + if (!item.issueName && item.issueId && !issueIdToNumber.has(item.issueId)) { + issueIdToNumber.set(item.issueId, counter++); + } + }); + + projectNumberMaps.set(projectId, issueIdToNumber); + }); + + // Map back to original order (sorted by duration) but use per-project numbers + return (issues || []).map(item => { + const projectId = item.projectId || 'unknown'; + const projectNumberMap = projectNumberMaps.get(projectId); + + // Generate the base issue name + let baseIssueName = item.issueName; + let isGeneratedName = false; + + if (!baseIssueName) { + // If no name, generate Issue #X based on project numbering + if (item.issueId && projectNumberMap?.has(item.issueId)) { + baseIssueName = `Issue #${projectNumberMap.get(item.issueId)}`; + isGeneratedName = true; + } else { + baseIssueName = 'Untitled Issue'; + isGeneratedName = true; + } + } + + // Only prefix with project name if: + // 1. Multiple projects are selected AND + // 2. The issue name was generated (Issue #X), not a real name + // This keeps the display clean for named issues while distinguishing unnamed issues + const finalIssueName = + multipleProjects && isGeneratedName && item.projectName + ? `${item.projectName} - ${baseIssueName}` + : baseIssueName; + + return { + issueName: finalIssueName, + durationOpen: item.durationOpen ?? 0, + }; + }); + }, [issues]); + useEffect(() => { dispatch(fetchBMProjects()); }, [dispatch]); @@ -176,57 +227,6 @@ function IssueCharts() { dispatch(fetchLongestOpenIssues(dateRange, selectedProjects)); }, [dispatch, startDate, endDate, selectedProjects]); - const normalizedIssues = (issues || []).map(issue => { - if (Array.isArray(issue.projects) && issue.projects.length > 0) { - return issue; - } - // fallback when backend does not send projects - return { - ...issue, - projects: [ - { - projectId: 'unknown', - projectName: 'Unknown Project', - durationOpen: issue.durationOpen, - }, - ], - }; - }); - - // Step 1: Normalize missing issue names - const safeIssues = normalizedIssues.map(issue => { - const name = issue.issueName; - return { - ...issue, - issueName: - typeof name === 'string' && name.trim() && name !== 'undefined' - ? name.trim() - : 'Unknown Issue', - }; - }); - - // Step 2: Use safeIssues for chartData - const chartData = safeIssues.flatMap(issue => - (issue.projects || []).map(project => ({ - issueName: issue.issueName, - projectId: project.projectId, - durationOpen: project.durationOpen, - })), - ); - - // Step 3: Stable project color map and legend - const projectColorMap = getProjectColorMap(safeIssues); - - const projectLegend = Object.entries(projectColorMap).map(([projectId, color]) => { - const project = safeIssues.flatMap(i => i.projects || []).find(p => p.projectId === projectId); - - return { - projectId, - projectName: project?.projectName || 'Unknown Project', - color, - }; - }); - useEffect(() => { function handleResize() { if (chartContainerRef.current) { @@ -260,7 +260,66 @@ function IssueCharts() { /* ------------ decide what to show inside chart container ------------ */ - // (chartContent block removed; chart rendering is below in render) + let chartContent; + + if (error) { + chartContent =
Error: {error}
; + } else if (loading) { + chartContent =
Loading chart data...
; + } else if (!normalizedIssues || normalizedIssues.length === 0) { + chartContent = ( +
+
+

No Open Issues Found

+

There are currently no open issues matching your selected criteria.

+

Try adjusting your date range or project filters to see more results.

+
+
+ ); + } else { + chartContent = ( + + + + + + `${value} months`} + labelFormatter={label => `Issue : ${label}`} + cursor={{ fill: hoverBg, opacity: 0.8 }} + /> + + `${v} months`} + style={{ fill: textColor }} + /> + + + + ); + } /* --------------------------- render --------------------------- */ @@ -373,79 +432,7 @@ function IssueCharts() {
- {/* Step 6: Project Legend above the chart */} -
- {projectLegend.map(p => ( -
- - {p.projectName} -
- ))} -
- {!issues || issues.length === 0 ? ( -
-
-

No Open Issues Found

-

There are currently no open issues matching your selected criteria.

-

Try adjusting your date range or project filters to see more results.

-
-
- ) : ( - - - - - - `${value} months`} - labelFormatter={label => `Issue: ${label}`} - /> - - {chartData.map((entry, index) => ( - - ))} - - - - )} + {chartContent}
); diff --git a/src/components/BMDashboard/LessonList/LessonCard.jsx b/src/components/BMDashboard/LessonList/LessonCard.jsx index 67cb4e4d4d..3e11412dee 100644 --- a/src/components/BMDashboard/LessonList/LessonCard.jsx +++ b/src/components/BMDashboard/LessonList/LessonCard.jsx @@ -13,13 +13,14 @@ function LessonCard({ filteredLessons, onEditLessonSummary, onDeliteLessonCard, const darkMode = useSelector(state => state.theme.darkMode); const maxSummaryLength = 1500; const [expandedCards, setExpandedCards] = useState([]); - const auth = useSelector(state => state.auth); - const currentUserId = auth.user.userid; const [editableLessonId, setEditableLessonId] = useState(null); const [editableLessonSummary, setEditableLessonSummary] = useState(''); const [validationError, setValidationError] = useState(''); const [showDeletePopup, setShowDeletePopup] = useState(false); const [lessonToDeleteId, setLessonToDeleteId] = useState(null); + + const auth = useSelector(state => state.auth); + const currentUserId = auth.user.userid; const lessons = useSelector(state => state.lessons.lessons); const getLikeStatus = lessonId => { @@ -52,15 +53,11 @@ function LessonCard({ filteredLessons, onEditLessonSummary, onDeliteLessonCard, }; const toggleCardExpansion = lessonId => { - setExpandedCards(prevExpandedCards => { - if (prevExpandedCards.includes(lessonId)) { - // Collapse the clicked card - return prevExpandedCards.filter(id => id !== lessonId); - } - // Expand the clicked card - return [...prevExpandedCards, lessonId]; - }); + setExpandedCards(prev => + prev.includes(lessonId) ? prev.filter(id => id !== lessonId) : [...prev, lessonId], + ); }; + const expandAll = () => { setExpandedCards(filteredLessons.map(lesson => lesson._id)); }; @@ -68,6 +65,7 @@ function LessonCard({ filteredLessons, onEditLessonSummary, onDeliteLessonCard, const collapseAll = () => { setExpandedCards([]); }; + const handleDeletePopup = lessonId => { setShowDeletePopup(!showDeletePopup); setLessonToDeleteId(lessonId); @@ -79,18 +77,16 @@ function LessonCard({ filteredLessons, onEditLessonSummary, onDeliteLessonCard, const lessonCards = (filteredLessons || []).map(lesson => { const { isLiked, totalLikes } = getLikeStatus(lesson._id); + return ( - + toggleCardExpansion(lesson._id)} style={{ cursor: 'pointer' }} - className={`${styles.lessonCardHeader}`} + className={styles.lessonCardHeader} > -