diff --git a/src/actions/intermediateTasks.js b/src/actions/intermediateTasks.js index c4b90b14bb..9d9adccde2 100644 --- a/src/actions/intermediateTasks.js +++ b/src/actions/intermediateTasks.js @@ -18,12 +18,13 @@ export const MARK_INTERMEDIATE_TASK_DONE = 'MARK_INTERMEDIATE_TASK_DONE'; * Fetch intermediate tasks for a parent task */ export const fetchIntermediateTasks = taskId => { - return async () => { + return async dispatch => { try { const response = await httpService.get(ENDPOINTS.INTERMEDIATE_TASKS_BY_PARENT(taskId)); return response.data; - } catch { - return []; + } catch (error) { + toast.error('Failed to fetch sub-tasks'); + throw error; } }; }; diff --git a/src/actions/studentTasks.js b/src/actions/studentTasks.js index 44788318bb..99679ea819 100644 --- a/src/actions/studentTasks.js +++ b/src/actions/studentTasks.js @@ -126,12 +126,27 @@ const fetchTasksFromPrimaryEndpoint = async () => { }; /** - * Handle API error and fall back to demo data - * @param {Error} apiError - The caught API error + * Handle API error and try fallback options + * @param {Error} apiError - The API error * @param {Function} dispatch - Redux dispatch function - * @returns {Array} Mock tasks for demo purposes + * @returns {Promise} Array of tasks (from fallback or mock data) */ -const handleApiError = (apiError, dispatch) => { +const handleApiError = async (apiError, dispatch) => { + console.error('Error response:', apiError.response?.data); + console.error('Error status:', apiError.response?.status); + console.error('Error config:', apiError.config); + + // Try alternative endpoint if the first one fails + if (apiError.response?.status === 404) { + try { + const altResponse = await httpService.post(`${ENDPOINTS.APIEndpoint()}/student-tasks`); + return altResponse.data.tasks || []; + } catch (altError) { + // Alternative endpoint failed + } + } + + toast.info('Using demo data. Student tasks API is not yet available.'); return mockTasks; }; @@ -143,19 +158,20 @@ export const fetchStudentTasks = () => { dispatch(setStudentTasksStart()); try { - let tasks = []; - try { - tasks = await fetchTasksFromPrimaryEndpoint(); - } catch (apiError) { - tasks = handleApiError(apiError, dispatch); + const state = getState(); + const userId = state.auth.user.userid; + + if (!userId) { + dispatch(setStudentTasksError('User not authenticated')); + return; } - // Fall back to mock data if API returned nothing - if (!tasks || tasks.length === 0) { - toast.info('Using demo data. Student tasks API is not yet available.'); - dispatch(setStudentTasks(mockTasks)); - } else { + try { + const tasks = await fetchTasksFromPrimaryEndpoint(); dispatch(setStudentTasks(tasks)); + } catch (apiError) { + const fallbackTasks = await handleApiError(apiError, dispatch); + dispatch(setStudentTasks(fallbackTasks)); } } catch (err) { dispatch(setStudentTasksError(err.message || 'Failed to fetch student tasks')); @@ -246,38 +262,3 @@ export const markStudentTaskAsDone = (taskId) => { } }; }; - -/** - * Log hours against a student task. - * @param {string} taskId - The task ID - * @param {number} hours - Hours to add (positive number) - */ -export const logStudentTaskHours = (taskId, hours) => { - return async (dispatch, getState) => { - try { - const state = getState(); - const userId = state.auth.user.userid; - - const response = await httpService.post(ENDPOINTS.STUDENT_TASK_LOG_HOURS(taskId), { - hours, - requestor: { requestorId: userId }, - }); - - const { loggedHours, suggestedTotalHours, status, canMarkDone } = response.data; - - dispatch({ - type: types.LOG_STUDENT_TASK_HOURS, - taskId, - loggedHours, - suggestedTotalHours, - status, - canMarkDone, - }); - - toast.success(`${hours} hour(s) logged successfully!`); - } catch (error) { - const msg = error.response?.data?.error || 'Failed to log hours. Please try again.'; - toast.error(msg); - } - }; -}; diff --git a/src/components/BMDashboard/ConsumableList/ConsumableListView.jsx b/src/components/BMDashboard/ConsumableList/ConsumableListView.jsx index 230a12ccb4..438b256240 100644 --- a/src/components/BMDashboard/ConsumableList/ConsumableListView.jsx +++ b/src/components/BMDashboard/ConsumableList/ConsumableListView.jsx @@ -3,6 +3,8 @@ import { useDispatch, useSelector } from 'react-redux'; import { fetchAllConsumables } from '../../../actions/bmdashboard/consumableActions'; import ItemListView from '../ItemList/ItemListView'; import UpdateConsumableModal from '../UpdateConsumables/UpdateConsumableModal'; +import { Link } from 'react-router-dom'; +import styles from '../InventoryTypesList/TypesList.module.css'; function ConsumableListView() { const dispatch = useDispatch(); @@ -50,13 +52,19 @@ function ConsumableListView() { ]; return ( - + <> + + All Inventory Types + + + + ); } diff --git a/src/components/BMDashboard/Equipment/List/EquipmentList.jsx b/src/components/BMDashboard/Equipment/List/EquipmentList.jsx index fd4f7fbd32..a591890eea 100644 --- a/src/components/BMDashboard/Equipment/List/EquipmentList.jsx +++ b/src/components/BMDashboard/Equipment/List/EquipmentList.jsx @@ -3,6 +3,8 @@ import useTheme from '../../../../hooks/useTheme'; import EquipmentsTable from './EquipmentsTable'; import EquipmentsInputs from './EquipmentsInputs'; import styles from './Equipments.module.css'; +import { Link } from 'react-router-dom'; +import stylesList from '../../InventoryTypesList/TypesList.module.css'; function EquipmentList() { const [equipment, setEquipment] = useState({ label: 'All Equipments', value: '0' }); @@ -12,25 +14,30 @@ function EquipmentList() { useTheme(); return ( -
-
-
-
EQUIPMENTS
- - + <> + + All Inventory Types + +
+
+
+
EQUIPMENTS
+ + +
-
+ ); } diff --git a/src/components/BMDashboard/InventoryTypesList/InventoryTypesList.jsx b/src/components/BMDashboard/InventoryTypesList/InventoryTypesList.jsx index bc1bc415e2..56ab32c39b 100644 --- a/src/components/BMDashboard/InventoryTypesList/InventoryTypesList.jsx +++ b/src/components/BMDashboard/InventoryTypesList/InventoryTypesList.jsx @@ -1,31 +1,25 @@ import { useState, useEffect } from 'react'; import { connect } from 'react-redux'; -import { useHistory } from 'react-router-dom'; - +import { Link } from 'react-router-dom'; import { fetchInvTypeByType } from '~/actions/bmdashboard/invTypeActions'; import { fetchInvUnits } from '~/actions/bmdashboard/invUnitActions'; -import { Accordion, Card, Button } from 'react-bootstrap'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; - +import { Accordion, Card } from 'react-bootstrap'; import BMError from '../shared/BMError'; -import TypesTable from './TypesTable'; import UnitsTable from './invUnitsTable'; import AccordionToggle from './AccordionToggle'; import styles from './TypesList.module.css'; +const categories = [ + { label: 'Materials', route: '/bmdashboard/materials' }, + { label: 'Consumables', route: '/bmdashboard/consumables' }, + { label: 'Equipments', route: '/bmdashboard/equipment' }, + { label: 'Reusables', route: '/bmdashboard/reusables' }, + { label: 'Tools', route: '/bmdashboard/tools' }, +]; + export function InventoryTypesList(props) { const { invUnits, errors, dispatch } = props; - const history = useHistory(); - - const categories = ['Materials', 'Consumables', 'Equipments', 'Reusables', 'Tools']; - const [isError, setIsError] = useState(false); - const [currentTime, setCurrentTime] = useState(new Date()); - - const handleBack = () => { - history.goBack(); - }; useEffect(() => { dispatch(fetchInvTypeByType('Materials')); @@ -50,52 +44,38 @@ export function InventoryTypesList(props) { } return ( -
-

All Inventory Types

- -
- Time: - setCurrentTime(date)} - dateFormat="MM-dd-yyyy hh:mm:ss" - id="timestamp" - showTimeInput - /> +
+ {/* Page Header */} +
+

All Inventory Types

+

Select a category to view and manage inventory

- - {categories?.map((category, index) => { - return ( - - - {category} - - - - - - - - ); - })} - - - - Unit of Measurement - - - - - - - - + {/* Category Cards Grid */} +
+ {categories.map(({ label, route }) => ( + + {label} +
+ + ))} +
-
- + {/* Unit of Measurement */} +
+

Unit of Measurement

+ + + + View all units + + + + + + + +
); diff --git a/src/components/BMDashboard/InventoryTypesList/TypesList.module.css b/src/components/BMDashboard/InventoryTypesList/TypesList.module.css index a2ea65628f..d407b6fdad 100644 --- a/src/components/BMDashboard/InventoryTypesList/TypesList.module.css +++ b/src/components/BMDashboard/InventoryTypesList/TypesList.module.css @@ -1,10 +1,4 @@ -/* stylelint-disable no-descending-specificity */ -.typesListContainer { - width: 100%; - max-width: 1080px; - margin: 1rem auto; - padding: 0 1rem; - +:root { --color-h50: #e8f4f9; --color-h100: #78bdda; --color-h500: #0d5675; @@ -13,6 +7,13 @@ --font-clickable: roboto; } +.typesListContainer { + width: 100%; + max-width: 1080px; + margin: 1rem auto; + padding: 0 1rem; +} + /* Dark mode support */ :global(.dark-mode) .typesListContainer, :global(.bm-dashboard-dark) .typesListContainer { @@ -259,18 +260,88 @@ font-size: 10px; } -/* Dark mode table styling */ -:global(.dark-mode) .typesListContainer table, -:global(.bm-dashboard-dark) .typesListContainer table { - color: #fff; +/* ---- Page Header ---- */ +.pageHeader { + margin-bottom: 28px; + padding-bottom: 16px; + border-bottom: 2px solid var(--color-h50); } -:global(.dark-mode) .typesListContainer table tbody tr, -:global(.bm-dashboard-dark) .typesListContainer table tbody tr { - background-color: #2a2a2a; +.pageTitle { + font-size: 2rem; + font-weight: 700; + color: var(--color-h500); + margin: 0 0 6px; +} + +.pageSubtitle { + font-size: 14px; + color: #6c757d; + margin: 0; +} + +/* ---- Category Cards Grid ---- */ +.categoryGrid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 16px; + margin-bottom: 36px; +} + +.categoryCard { + display: flex; + align-items: center; + justify-content: space-between; + padding: 20px 24px; + background-color: var(--color-n0); + border: 1.5px solid var(--color-h50); + border-radius: 12px; + text-decoration: none !important; + color: inherit !important; + transition: all 0.2s ease; + box-shadow: 0 1px 4px rgb(0 0 0 / 6%); +} + +.categoryCardLabel { + font-size: 16px; + font-weight: 700; + color: var(--color-h500); +} + +.categoryCardArrow { + font-size: 22px; + color: var(--color-h100); + font-weight: 300; + flex-shrink: 0; +} + +/* ---- Unit Section ---- */ +.unitSection { + margin-top: 8px; +} + +.unitSectionTitle { + font-size: 1.1rem; + font-weight: 600; + color: var(--color-h500); + margin-bottom: 12px; +} + +.unitCard { + border: 1.5px solid var(--color-h50) !important; + border-radius: 10px !important; + overflow: hidden; +} + +/* Main container dark mode */ +:global(.bm-dashboard-dark) .typesListContainer { color: #fff; } +:global(.bm-dashboard-dark) .typesListContainer h1 { + color: #fff !important; +} + :global(.dark-mode) .typesListContainer table tbody tr:hover, :global(.bm-dashboard-dark) .typesListContainer table tbody tr:hover { background-color: #3a3a3a; @@ -327,12 +398,10 @@ color: #fff !important; } -/* Dark mode react-datepicker styling */ -:global(.dark-mode) .timestampContainer :global(.react-datepicker-wrapper) input, -:global(.bm-dashboard-dark) .timestampContainer :global(.react-datepicker-wrapper) input { - background-color: #3a3a3a !important; +:global(.bm-dashboard-dark) #timestamp { + background-color: #2e5061 !important; color: #fff !important; - border-color: rgb(255 255 255 / 20%) !important; + border-color: #3a506b !important; } :global(.dark-mode) :global(.react-datepicker), @@ -558,6 +627,12 @@ box-shadow: 0 0 0 0.2rem rgb(106 241 234 / 25%) !important; } +:global(.bm-dashboard-dark) .typesListContainer :global(.react-datepicker-time__input) input { + background-color: #1c2541 !important; + color: #fff !important; + border-color: #3a506b !important; +} + /* Autocomplete styling */ :global(.bm-dashboard-dark) .typesListContainer input:-webkit-autofill, :global(.bm-dashboard-dark) .typesListContainer input:-webkit-autofill:hover, @@ -618,8 +693,111 @@ color: #fff !important; } -:global(.bm-dashboard-dark) .typesListContainer :global(.react-datepicker-time__input) input { +/* ---- New page element dark mode (before hover rules) ---- */ +:global(.bm-dashboard-dark) .pageTitle { + color: #fff !important; +} + +:global(.bm-dashboard-dark) .pageSubtitle { + color: #b5bac5 !important; +} + +:global(.bm-dashboard-dark) .pageHeader { + border-color: #3a506b !important; +} + +:global(.bm-dashboard-dark) .categoryCard { background-color: #1c2541 !important; + border-color: #3a506b !important; + color: #fff !important; +} + +:global(.bm-dashboard-dark) .categoryCardLabel { color: #fff !important; +} + +:global(.bm-dashboard-dark) .categoryCardArrow { + color: #6af1ea !important; +} + +:global(.bm-dashboard-dark) .unitSectionTitle { + color: #fff !important; +} + +:global(.bm-dashboard-dark) .unitCard { border-color: #3a506b !important; } + +/* ---- Hover rules AFTER dark mode overrides ---- */ +.categoryCard:hover { + border-color: var(--color-h100); + box-shadow: 0 4px 12px rgb(13 86 117 / 12%); + transform: translateY(-2px); + text-decoration: none !important; +} + +:global(.bm-dashboard-dark) .categoryCard:hover { + border-color: #5a7a9b !important; + box-shadow: 0 4px 12px rgb(0 0 0 / 30%) !important; +} + +/* ---- Back link ---- */ +.backLink { + display: inline-flex; + align-items: center; + gap: 6px; + margin-bottom: 20px; + padding: 8px 16px; + background-color: #e8f4f9; + color: #0d5675 !important; + text-decoration: none !important; + font-size: 14px; + font-weight: 600; + border-radius: 25px; + border: 1.5px solid #78bdda; + transition: all 0.2s ease; +} + +.backLink::before { + content: ''; + display: inline-block; + width: 8px; + height: 8px; + border-left: 2px solid #0d5675; + border-bottom: 2px solid #0d5675; + transform: rotate(45deg); + transition: border-color 0.2s ease; + margin-right: 2px; +} + +.backLink:hover { + background-color: #0d5675; + color: #fff !important; + border-color: #0d5675; + text-decoration: none !important; + transform: translateX(-2px); +} + +.backLink:hover::before { + border-color: #fff; +} + +:global(.bm-dashboard-dark) .backLink { + background-color: #1c2541 !important; + color: #6af1ea !important; + border-color: #3a506b !important; +} + +:global(.bm-dashboard-dark) .backLink::before { + border-color: #6af1ea !important; +} + +:global(.bm-dashboard-dark) .backLink:hover { + background-color: #2e5061 !important; + color: #fff !important; + border-color: #5a7a9b !important; +} + +:global(.bm-dashboard-dark) .backLink:hover::before { + border-color: #fff !important; +} diff --git a/src/components/BMDashboard/LessonsLearnt/LessonsLearntChart.jsx b/src/components/BMDashboard/LessonsLearnt/LessonsLearntChart.jsx index c478cb220e..5f6259e751 100644 --- a/src/components/BMDashboard/LessonsLearnt/LessonsLearntChart.jsx +++ b/src/components/BMDashboard/LessonsLearnt/LessonsLearntChart.jsx @@ -1,11 +1,10 @@ -import { useState, useEffect, useMemo } from 'react'; -import { useSelector } from 'react-redux'; +/* eslint-disable */ /* prettier-ignore */ +import { useState, useEffect } from 'react'; import { Bar } from 'react-chartjs-2'; import { Chart as ChartJS, BarElement, CategoryScale, LinearScale, Tooltip, Title } from 'chart.js'; import axios from 'axios'; import Select from 'react-select'; import DatePicker from 'react-datepicker'; -import PropTypes from 'prop-types'; import { ENDPOINTS } from '../../../utils/URL'; import styles from './LessonsLearntChart.module.css'; @@ -13,13 +12,6 @@ import 'react-datepicker/dist/react-datepicker.css'; ChartJS.register(BarElement, CategoryScale, LinearScale, Tooltip, Title); -const toYMD = d => - d instanceof Date && !Number.isNaN(d.getTime()) - ? `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String( - d.getDate(), - ).padStart(2, '0')}` - : ''; - const useLessonsData = (selectedProjects, startDate, endDate) => { const [allProjects, setAllProjects] = useState([]); const [lessonsData, setLessonsData] = useState([]); @@ -27,47 +19,38 @@ const useLessonsData = (selectedProjects, startDate, endDate) => { const [error, setError] = useState(null); useEffect(() => { - let cancelled = false; const fetchProjects = async () => { try { const response = await axios.get(ENDPOINTS.BM_PROJECTS); - if (!cancelled) { - setAllProjects(Array.isArray(response.data) ? response.data : []); - } - } catch { - if (!cancelled) setAllProjects([]); + const projects = Array.isArray(response.data) ? response.data : []; + setAllProjects(projects); + } catch (err) { + console.error('Error fetching projects:', err); + setAllProjects([]); } }; fetchProjects(); - return () => { - cancelled = true; - }; }, []); useEffect(() => { - let cancelled = false; const fetchLessons = async () => { setIsLoading(true); setError(null); try { const params = {}; - // Backend supports single projectId or 'ALL'. - // For one selection send that ID; for 0 or 2+ send ALL and filter client-side. - const hasSelection = Array.isArray(selectedProjects) && selectedProjects.length > 0; - const singleProject = hasSelection && selectedProjects.length === 1; - if (singleProject) { - params.projectId = selectedProjects[0].value; - } else if (hasSelection) { - params.projectId = 'ALL'; + if (selectedProjects && selectedProjects.length > 0) { + params.projectId = selectedProjects.length === 1 ? selectedProjects[0].value : 'ALL'; } - const formattedStart = toYMD(startDate); - const formattedEnd = toYMD(endDate); - if (formattedStart) params.startDate = formattedStart; - if (formattedEnd) params.endDate = formattedEnd; + if (startDate) { + params.startDate = startDate.toISOString(); + } + if (endDate) { + params.endDate = endDate.toISOString(); + } - const response = await axios.get(ENDPOINTS.BM_LESSONS_LEARNT, { params }); + const response = await axios.get(`${ENDPOINTS.BM_LESSONS}-learnt`, { params }); const responseData = response.data?.data || response.data || []; const lessonsArray = Array.isArray(responseData) ? responseData : []; @@ -77,45 +60,30 @@ const useLessonsData = (selectedProjects, startDate, endDate) => { projectId: item.projectId, lessonsCount: item.lessonsCount || 0, percentage: - Number.parseFloat((item.changePercentage || '0%').replace('%', '').replace('+', '')) || - 0, + parseFloat((item.changePercentage || '0%').replace('%', '').replace('+', '')) || 0, changePercentage: item.changePercentage || '0%', })); - // When multiple projects selected, filter to only the chosen ones - const needsClientFilter = hasSelection && !singleProject; - let filtered = transformedData; - if (needsClientFilter) { - const selectedIds = new Set(selectedProjects.map(p => p.value)); - filtered = transformedData.filter(d => selectedIds.has(d.projectId)); - } - - if (!cancelled) setLessonsData(filtered); + setLessonsData(transformedData); } catch (err) { - if (!cancelled) { - setError(err.message || 'Failed to fetch lessons data'); - setLessonsData([]); - } + console.error('Error fetching lessons learnt:', err); + setError(err.message || 'Failed to fetch lessons data'); + setLessonsData([]); } finally { - if (!cancelled) setIsLoading(false); + setIsLoading(false); } }; fetchLessons(); - return () => { - cancelled = true; - }; }, [selectedProjects, startDate, endDate]); return { allProjects, lessonsData, isLoading, error }; }; -const BAR_COLOR_LIGHT = '#10b981'; -const BAR_COLOR_DARK = '#34d399'; - -function LessonsLearntChart({ darkMode: propDarkMode }) { - const reduxDarkMode = useSelector(state => state.theme.darkMode); - const darkMode = propDarkMode === undefined ? reduxDarkMode : propDarkMode; +function ChartTitle({ title }) { + return

{title}

; +} +const LessonsLearntChart = () => { const [selectedProjects, setSelectedProjects] = useState([]); const [startDate, setStartDate] = useState(null); const [endDate, setEndDate] = useState(null); @@ -126,191 +94,111 @@ function LessonsLearntChart({ darkMode: propDarkMode }) { endDate, ); - const today = useMemo(() => { - const d = new Date(); - d.setHours(23, 59, 59, 999); - return d; - }, []); - - const projectOptions = useMemo( - () => - Array.isArray(allProjects) - ? allProjects - .filter(proj => proj && (proj._id || proj.id)) - .map(proj => ({ - value: proj._id || proj.id, - label: proj.name || 'Unnamed Project', - })) - : [], - [allProjects], - ); - - const barColor = darkMode ? BAR_COLOR_DARK : BAR_COLOR_LIGHT; - - const chartData = useMemo(() => { - const safeLabels = Array.isArray(lessonsData) - ? lessonsData.map(d => d?.projectName || 'Unknown') - : []; - const safeData = Array.isArray(lessonsData) ? lessonsData.map(d => d?.lessonsCount || 0) : []; - - return { - labels: safeLabels, - datasets: [ - { - label: 'Lessons Count', - data: safeData, - backgroundColor: barColor, - }, - ], - }; - }, [lessonsData, barColor]); - - const chartOptions = useMemo(() => { - const tickColor = darkMode ? '#cbd5e1' : '#374151'; - const gridColor = darkMode ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)'; - - return { - responsive: true, - maintainAspectRatio: false, - plugins: { - title: { display: false }, - tooltip: { - callbacks: { - afterLabel: context => { - const { dataIndex } = context; - if (Array.isArray(lessonsData) && lessonsData[dataIndex]) { - return `MoM Change: ${lessonsData[dataIndex].changePercentage || '0%'}`; - } - return ''; - }, + const handleProjectChange = selected => { + setSelectedProjects(selected || []); + }; + + const projectOptions = Array.isArray(allProjects) + ? allProjects + .filter(proj => proj && (proj._id || proj.id)) + .map(proj => ({ + value: proj._id || proj.id, + label: proj.name || 'Unnamed Project', + })) + : []; + + const safeLabels = Array.isArray(lessonsData) + ? lessonsData.map(d => d?.projectName || 'Unknown') + : []; + + const safeData = Array.isArray(lessonsData) ? lessonsData.map(d => d?.lessonsCount || 0) : []; + + const chartData = { + labels: safeLabels, + datasets: [ + { + label: 'Lessons Count', + data: safeData, + backgroundColor: '#10b981', + }, + ], + }; + + const chartOptions = { + responsive: true, + maintainAspectRatio: false, + plugins: { + title: { + display: false, + }, + tooltip: { + callbacks: { + afterLabel: context => { + const dataIndex = context.dataIndex; + if (Array.isArray(lessonsData) && lessonsData[dataIndex]) { + return `Change: ${lessonsData[dataIndex].changePercentage || '0%'}`; + } + return ''; }, }, }, - scales: { - y: { - beginAtZero: true, - ticks: { stepSize: 1, color: tickColor }, - grid: { color: gridColor }, - }, - x: { - ticks: { color: tickColor }, - grid: { color: gridColor }, + }, + scales: { + y: { + beginAtZero: true, + ticks: { + stepSize: 1, }, }, - }; - }, [lessonsData, darkMode]); - - const selectStyles = useMemo( - () => ({ - control: base => ({ - ...base, - backgroundColor: darkMode ? '#334155' : '#fff', - borderColor: darkMode ? '#475569' : '#d1d5db', - minHeight: '32px', - fontSize: '13px', - color: darkMode ? '#f1f5f9' : '#000', - }), - multiValue: base => ({ - ...base, - backgroundColor: darkMode ? '#475569' : '#e2e8f0', - }), - multiValueLabel: base => ({ - ...base, - color: darkMode ? '#f1f5f9' : '#000', - fontSize: '11px', - }), - multiValueRemove: base => ({ - ...base, - color: darkMode ? '#94a3b8' : '#6b7280', - ':hover': { backgroundColor: darkMode ? '#64748b' : '#cbd5e1', color: '#fff' }, - }), - menu: base => ({ - ...base, - backgroundColor: darkMode ? '#1e293b' : '#fff', - }), - option: (base, state) => { - const focusedBg = darkMode ? '#475569' : '#e2e8f0'; - const defaultBg = darkMode ? '#1e293b' : '#fff'; - return { - ...base, - backgroundColor: state.isFocused ? focusedBg : defaultBg, - color: darkMode ? '#f1f5f9' : '#000', - fontSize: '12px', - padding: '6px 10px', - }; - }, - input: base => ({ - ...base, - color: darkMode ? '#f1f5f9' : '#000', - }), - placeholder: base => ({ - ...base, - color: darkMode ? '#94a3b8' : '#9ca3af', - }), - }), - [darkMode], - ); + }, + }; return ( -
-

Lessons Learnt

+
+
- + setShowOnlyLowStock(!showOnlyLowStock)} + />{' '} + Show only low-stock materials + + + setShowOnlyLowStock(!showOnlyLowStock)} - />{' '} - Show only low-stock materials - - - - setSearchTerm(e.target.value)} - /> - {searchTerm && ( - - - - )} - - - + type="text" + placeholder="Search Material, PID, Unit..." + value={searchTerm} + onChange={e => setSearchTerm(e.target.value)} + /> + {searchTerm && ( + + + + )} + + + + ); } diff --git a/src/components/BMDashboard/ReusableList/ReusableListView.jsx b/src/components/BMDashboard/ReusableList/ReusableListView.jsx index 01ee88ff08..37fea5b56e 100644 --- a/src/components/BMDashboard/ReusableList/ReusableListView.jsx +++ b/src/components/BMDashboard/ReusableList/ReusableListView.jsx @@ -3,6 +3,8 @@ import { useDispatch, useSelector } from 'react-redux'; import { fetchAllReusables } from '~/actions/bmdashboard/reusableActions'; import ItemListView from '../ItemList/ItemListView'; import UpdateReusableModal from '../UpdateReusables/UpdateReusableModal'; +import { Link } from 'react-router-dom'; +import styles from '../InventoryTypesList/TypesList.module.css'; function ReusableListView() { const dispatch = useDispatch(); @@ -44,13 +46,18 @@ function ReusableListView() { ]; return ( - + <> + + All Inventory Types + + + ); } diff --git a/src/components/BMDashboard/Team/CreateNewTeam/CreateNewTeam.jsx b/src/components/BMDashboard/Team/CreateNewTeam/CreateNewTeam.jsx index 9f851bfa76..46256652d7 100644 --- a/src/components/BMDashboard/Team/CreateNewTeam/CreateNewTeam.jsx +++ b/src/components/BMDashboard/Team/CreateNewTeam/CreateNewTeam.jsx @@ -1,13 +1,11 @@ import { useState, useEffect } from 'react'; -import { useHistory } from 'react-router-dom'; import { Form, FormGroup, Label, Input, Button, Badge } from 'reactstrap'; import { useDispatch, useSelector } from 'react-redux'; import Joi from 'joi'; -import { toast } from 'react-toastify'; import { boxStyle } from '../../../../styles'; import styles from './CreateNewTeam.module.css'; import { getUserProfileBasicInfo } from '../../../../actions/userManagement'; -import { postNewTeam, addTeamMember } from '../../../../actions/allTeamsAction'; +import { toast } from 'react-toastify'; const initialFormState = { teamName: '', @@ -20,7 +18,6 @@ export default function CreateNewTeam() { const [formData, setFormData] = useState(initialFormState); const [errors, setErrors] = useState({}); const dispatch = useDispatch(); - const history = useHistory(); const userProfilesBasicInfo = useSelector( state => state.allUserProfilesBasicInfo?.userProfilesBasicInfo, ); @@ -40,6 +37,8 @@ export default function CreateNewTeam() { additionalInformation: false, }); + const user = useSelector(state => state.auth.user); + const dummyTasks = ['Task 1', 'Task 2', 'Task 3', 'Task 4', 'Task 5']; const [loadingMembers, setLoadingMembers] = useState(false); @@ -124,28 +123,28 @@ export default function CreateNewTeam() { return; } - try { - const res = await dispatch(postNewTeam(formData.teamName, true)); - if (res?.status !== 200) { - const errMsg = res?.data?.error || res?.message || 'Failed to create team.'; - toast.error(errMsg); - return; - } - - const newTeamId = res.data._id; - - // Assign each selected member to the newly created team - await Promise.all(assignedMembers.map(userId => dispatch(addTeamMember(newTeamId, userId)))); - - toast.success(`Team "${formData.teamName}" created successfully!`); - history.push('/bmdashboard'); - } catch (err) { - const errMsg = - err?.response?.data?.error || - err?.message || - 'An unexpected error occurred. Please try again.'; - toast.error(errMsg); - } + const updatedFormData = { + ...formData, + teamMembers: assignedMembers, + tasks: assignedTasks, + }; + + // eslint-disable-next-line no-console + console.log('Form Submitted:', updatedFormData); + + toast.success('Team created successfully!'); + + setSelectedMember(''); + setAssignedMembers([]); + setSelectedTask(''); + setAssignedTasks([]); + setFormData(initialFormState); + setErrors({}); + setTouchedFields({ + teamName: false, + assignedMembers: false, + additionalInformation: false, + }); }; const handleCancelClick = () => { @@ -274,10 +273,10 @@ export default function CreateNewTeam() { {Array.isArray(members) && members.length > 0 ? ( <> - {members.map((member, index) => ( + {members.map((user, index) => ( // eslint-disable-next-line react/no-array-index-key - ))} @@ -312,23 +311,19 @@ export default function CreateNewTeam() { )}
- {assignedMembers.map((memberId, index) => { - const foundMember = members?.find(m => m.id === memberId); - const displayName = foundMember - ? `${foundMember.firstName} ${foundMember.lastName}` - : memberId; + {assignedMembers.map((member, index) => { return ( // eslint-disable-next-line react/no-array-index-key - {displayName} + {member} handleRemoveMember(memberId)} + onClick={() => handleRemoveMember(member)} onKeyDown={e => - (e.key === 'Enter' || e.key === ' ') && handleRemoveMember(memberId) + (e.key === 'Enter' || e.key === ' ') && handleRemoveMember(member) } - aria-label={`Remove member ${displayName}`} + aria-label={`Remove member ${member}`} > X diff --git a/src/components/BMDashboard/Tools/ToolsList.jsx b/src/components/BMDashboard/Tools/ToolsList.jsx index 66d1889946..bda4e26e5a 100644 --- a/src/components/BMDashboard/Tools/ToolsList.jsx +++ b/src/components/BMDashboard/Tools/ToolsList.jsx @@ -3,6 +3,8 @@ import { useDispatch, useSelector } from 'react-redux'; import { fetchTools } from '../../../actions/bmdashboard/toolActions'; import ToolItemListView from '../ToolItemList/ToolItemListView'; import UpdateToolModal from '../UpdateTools/UpdateToolModal'; +import { Link } from 'react-router-dom'; +import styles from '../InventoryTypesList/TypesList.module.css'; function ToolsList() { const dispatch = useDispatch(); @@ -47,13 +49,18 @@ function ToolsList() { ]; return ( - + <> + + All Inventory Types + + + ); } diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx index 550f8337c6..b03d9926a5 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx @@ -27,7 +27,6 @@ import IssueCharts from '../Issues/openIssueCharts'; import LossTrackingLineChart from './Financials/LossTrackingLineCharts/LossTrackingLineChart'; import SupplierPerformanceGraph from './SupplierPerformanceGraph.jsx'; import MostFrequentKeywords from './MostFrequentKeywords/MostFrequentKeywords'; -import LessonsLearntChart from '../LessonsLearnt/LessonsLearntChart'; import DistributionLaborHours from './DistributionLaborHours/DistributionLaborHours'; const projectStatusButtons = [ @@ -329,12 +328,6 @@ function WeeklyProjectSummary() { >
, -
- -
, ], }, { diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.module.css b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.module.css index 67399b10a3..3f3c497543 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.module.css +++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.module.css @@ -42,7 +42,7 @@ max-width: 1280px; padding: 12px 16px; background: var(--section-bg); - box-shadow: 0 2px 4px rgb(0 0 0 / 10%); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); border-radius: 8px; gap: 12px; } @@ -93,19 +93,9 @@ font-size: 14px; background: var(--section-bg); color: var(--text-color); - transition: all 0.3s ease; - appearance: none; - background-image: url("data:image/svg+xml;utf8,"); - background-repeat: no-repeat; - background-position: right 8px center; - background-size: 16px; -} - -.darkMode .weeklySummaryHeaderControls select { - background-image: url("data:image/svg+xml;utf8,"); - background-color: var(--card-bg) !important; - color: var(--text-color) !important; - border-color: var(--card-shadow) !important; + box-sizing: border-box; + line-height: 1.25; + vertical-align: middle; } .weeklySummaryHeaderControls select:focus { @@ -155,11 +145,6 @@ background-color: var(--button-hover) !important; } -.weeklySummaryShareBtn:focus { - outline: none; - box-shadow: 0 0 0 2px rgb(59 130 246 / 50%); -} - .weeklySummaryShareBtn:disabled { opacity: 0.7; cursor: not-allowed; @@ -225,47 +210,7 @@ .darkMode .weeklyProjectSummaryCard { background: var(--card-bg); - box-shadow: 0 2px 5px rgb(255 255 255 / 10%); -} - -.weeklyProjectSummaryCard :global(.container) { - background: #fff !important; - width: 100% !important; - height: 100% !important; - padding: 0 !important; - border: none !important; - box-shadow: none !important; -} - -.darkMode .weeklyProjectSummaryCard :global(.container) { - background: #1e293b !important; -} - -.weeklyProjectSummaryCard :global(.recharts-wrapper) { - width: 100% !important; - height: 100% !important; -} - -.weeklyProjectSummaryCard :global(.recharts-surface) { - overflow: visible !important; -} - -.weeklyProjectSummaryCard :global(.chartContainer) { - flex: 1; - min-height: 280px; - position: relative; -} - -.weeklyProjectSummaryCard :global(.recharts-bar-rectangle) { - shape-rendering: geometricprecision !important; -} - -.weeklyProjectSummaryCard :global(.recharts-cartesian-axis-tick-value) { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif !important; -} - -.weeklyProjectSummaryCard :global(.recharts-cartesian-grid-horizontal line) { - stroke-dasharray: 3 3 !important; + box-shadow: 0 2px 5px rgba(255, 255, 255, 0.1); } .financialSmall { @@ -282,8 +227,8 @@ padding: 0; background: white; border-radius: 12px; - box-shadow: 0 2px 8px rgb(0 0 0 / 10%); - overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + overflow: visible; margin-bottom: 20px; } @@ -356,121 +301,48 @@ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; border-top: 1px solid var(--card-shadow); - animation: fade-in 0.3s ease-in-out; + animation: fadeIn 0.3s ease-in-out; background: var(--section-bg); width: 100%; max-width: 100%; box-sizing: border-box; } -.darkMode .weeklyProjectSummaryDashboardCategoryContent { - background-color: var(--section-bg); - border-top: 1px solid rgb(255 255 255 / 20%); -} - +/* Adjust grid columns based on section size */ .weeklyProjectSummaryDashboardSection.half .weeklyProjectSummaryDashboardCategoryContent { - display: grid; grid-template-columns: repeat(2, 1fr); - gap: 15px; } .weeklyProjectSummaryDashboardSection.small .weeklyProjectSummaryDashboardCategoryContent { - display: grid; grid-template-columns: 1fr; - gap: 15px; -} - -/* ---------------- LESSONS LEARNED SECTION ---------------- */ -.lessonsLearnedGrid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 24px; - width: 100%; - align-items: stretch; - min-height: 600px; -} - -.lessonsCard { - min-height: 600px; - height: 100%; - width: 100%; - overflow: hidden; - display: flex; - flex-direction: column; - border-radius: 12px; - box-shadow: 0 4px 6px var(--card-shadow); - transition: all 0.3s ease; -} - -/* ---------------- TOOLS TRACKING LAYOUT ---------------- */ -.toolsTrackingLayout { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 20px; - width: 100%; -} - -.toolsDonutWrap { - grid-column: span 1; - min-height: 300px; -} - -/* ---------------- FINANCIALS GRID ---------------- */ -.financialsGrid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 15px; - width: 100%; -} - -.financialsGrid .financialBig { - grid-column: span 4; - min-height: 400px; -} - -/* ---------------- LABOR TIME GRID ---------------- */ -.laborTimeGrid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 20px; - width: 100%; } -/* ---------------- FINANCIALS TRACKING GRID ---------------- */ -.financialsTrackingGrid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 15px; - width: 100%; +/* Stack cards vertically on tablet and mobile screens */ +@media (max-width: 1024px) { + .weeklyProjectSummaryDashboardSection.half .weeklyProjectSummaryDashboardCategoryContent { + grid-template-columns: 1fr; + } } -/* ---------------- LIGHT MODE VARIABLES ---------------- */ +/* ---------------- DARK MODE VARIABLES ---------------- */ :root { - --bg-color: #fff; - --text-color: #000; - --text-secondary: #666; - --card-bg: #fff; - --card-shadow: rgb(0 0 0 / 10%); - --section-bg: #fff; - --section-title-bg: #f8fafc; - --section-title-hover: #f1f5f9; + --bg-color: #ffffff; + --text-color: #000000; + --card-bg: #fafafa; + --card-shadow: rgba(0, 0, 0, 0.1); + --section-bg: white; + --section-title-bg: white; + --section-title-hover: #e0e0e0; --button-bg: black; --button-hover: #333; - --border-color: #e5e7eb; - --pos-color: #15803d; - --neg-color: #b91c1c; - --neutral-change: rgb(0 0 0 / 65%); - --card-elev: rgb(0 0 0 / 15%); - --card-elev-hover: rgb(0 0 0 / 18%); } /* ---------------- DARK MODE STYLES ---------------- */ .darkMode { --bg-color: #1b2a41; - --text-color: #fff; - --text-secondary: #94a3b8; + --text-color: #ffffff; --card-bg: #2b3e59; - --card-shadow: rgb(255 255 255 / 10%); + --card-shadow: rgba(255, 255, 255, 0.1); --section-bg: #253342; --section-title-bg: #2d4059; --section-title-hover: #3a506b; @@ -478,11 +350,6 @@ --button-hover: #f5b13a; --border-color: #4a5a77; --focus-border-color: #e8a71c; - --pos-color: #22c55e; - --neg-color: #ef4444; - --neutral-change: rgb(255 255 255 / 75%); - --card-elev: rgb(0 0 0 / 45%); - --card-elev-hover: rgb(0 0 0 / 60%); } /* MERGED: keep background + FORCE all title text visible in dark mode */ @@ -495,70 +362,25 @@ background: #3a506b; } -.darkMode .weeklySummaryHeaderContainer { - background: var(--section-bg); - color: var(--text-color); -} - -.darkMode .weeklySummaryShareBtn { - background-color: var(--button-bg) !important; -} - -.darkMode .weeklySummaryShareBtn:hover { - background-color: var(--button-hover) !important; -} - -/* ---------------- STATUS CARD (base) ---------------- */ -.statusCard { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - border-radius: 25px; - width: 100%; - max-width: 284px; - height: 190px; - text-align: center; - box-shadow: 0 4px 8px rgb(0 0 0 / 15%); - padding: 20px; - border: 1px solid rgb(0 0 0 / 10%); - position: relative; - transition: all 0.3s ease; -} - -.weeklyCardTitle { - color: #000; - font-size: clamp(14px, 1.4vw, 18px); - font-weight: 600; - transition: color 0.3s ease; -} - -.weeklyStatusValue { - color: #000; - font-size: 40px; - font-weight: 600; - transition: color 0.3s ease; +.darkMode .weeklyProjectSummaryDashboardCategoryTitle span { + color: #ffffff; } -.weekly-status-change { - transition: color 0.3s ease; - font-size: 14px; - font-weight: 500; +.darkMode .weeklyProjectSummaryDashboardCategoryTitle * { + color: #ffffff; } -.darkMode .statusCard { - background: var(--card-bg) !important; - color: var(--text-color) !important; - border-color: rgb(255 255 255 / 10%) !important; +.darkMode .weeklyProjectSummaryDashboardCategoryContent { + background-color: var(--section-bg); + border-top: 1px solid rgba(255, 255, 255, 0.2); } -.darkMode .weeklyCardTitle, -.darkMode .weeklyStatusValue, -.darkMode .weekly-status-change { - color: var(--text-color) !important; +.darkMode .weeklySummaryHeaderContainer { + background: var(--section-bg); + color: var(--text-color); } -/* Tooltip styles */ +/* ---------------- TOOLTIP SCROLL FIX ---------------- */ .quantityOfMaterialsUsedChartTooltip { max-height: 80vh !important; overflow-y: auto !important; @@ -566,7 +388,7 @@ /* Firefox */ scrollbar-width: thin; - scrollbar-color: rgb(0 0 0 / 30%) rgb(0 0 0 / 10%); + scrollbar-color: rgba(0, 0, 0, 0.3) rgba(0, 0, 0, 0.1); } /* Chrome/Safari scrollbar */ @@ -575,123 +397,39 @@ } .quantityOfMaterialsUsedChartTooltip::-webkit-scrollbar-track { - background: rgb(0 0 0 / 10%); + background: rgba(0, 0, 0, 0.1); border-radius: 4px; } .quantityOfMaterialsUsedChartTooltip::-webkit-scrollbar-thumb { - background: rgb(0 0 0 / 30%); + background: rgba(0, 0, 0, 0.3); border-radius: 4px; } /* Additional adjustments for dark mode if needed */ .darkMode .quantityOfMaterialsUsedChartTooltip::-webkit-scrollbar-track { - background: rgb(255 255 255 / 10%); + background: rgba(255, 255, 255, 0.1); } .darkMode .quantityOfMaterialsUsedChartTooltip::-webkit-scrollbar-thumb { - background: rgb(255 255 255 / 30%); -} - -/* Animation */ -@keyframes fade-in { - from { - opacity: 0; - transform: translateY(-10px); - } - - to { - opacity: 1; - transform: translateY(0); - } -} - -/* ---------------- RESPONSIVE GRID LAYOUT ---------------- */ -/* MERGED: keep ONLY one projectStatusGrid */ -.projectStatusGrid { - display: grid; - grid-template-columns: repeat(5, 1fr); - gap: 20px; - justify-content: center; - align-items: center; - width: 100%; - max-width: 1600px; - margin: auto; + background: rgba(255, 255, 255, 0.3); } -/* .projectStatusGrid { - display: grid; - grid-template-columns: repeat(6, 1fr); - grid-template-rows: repeat(2, auto); - gap: 10px; -} */ -/* ---------------- OVAL STATUS BUTTON ---------------- */ -.weeklyStatusButton { - width: 130px; - height: 65px; - border-radius: 50px / 32px; - display: flex; - align-items: center; - justify-content: center; - box-shadow: 0 2px 5px rgb(0 0 0 / 15%); - margin: 12px 0; +.darkMode .quantityOfMaterialsUsedChartTooltip { + scrollbar-color: rgba(255, 255, 255, 0.3) rgba(255, 255, 255, 0.1); } -/* ---------------- RESPONSIVE BREAKPOINTS ---------------- */ - -/* Large Screens */ -@media (width >= 1400px) { - .lessonsLearnedGrid { - min-height: 650px; - } - - .lessonsCard { - min-height: 650px; - } -} - -@media (width <= 1399px) and (width >= 1200px) { - .lessonsLearnedGrid { - min-height: 600px; - } - - .lessonsCard { - min-height: 600px; - } -} - -@media (width <= 1199px) and (width >= 992px) { - .lessonsLearnedGrid { - min-height: 550px; - gap: 20px; - } - - .lessonsCard { - min-height: 550px; - } -} - -@media (width <= 991px) and (width >= 769px) { - .lessonsLearnedGrid { - min-height: 500px; - gap: 16px; - } - - .lessonsCard { - min-height: 500px; - } -} - -/* Medium Screens */ -@media (width <= 1024px) { +/* ---------------- RESPONSIVE DESIGN ---------------- */ +@media (max-width: 1024px) { .weeklySummaryHeaderContainer { flex-direction: column; - align-items: center; + align-items: stretch; + width: 95%; } .weeklySummaryHeaderControls { flex-wrap: wrap; - justify-content: center; + justify-content: flex-start; width: 100%; } @@ -700,12 +438,10 @@ } } -/* Small Screens */ -@media (width <= 768px) { +@media (max-width: 768px) { .weeklySummaryHeaderContainer { flex-direction: column; - align-items: center; - width: 95%; + align-items: stretch; } .weeklySummaryHeaderControls { @@ -716,11 +452,15 @@ .weeklySummaryHeaderControls select { width: 100%; + max-width: none; + } + + .weeklySummaryHeaderControls :global(.form-control) { + max-width: none; } .weeklySummaryShareBtn { width: 100%; - margin-top: 5px; } .weeklyProjectSummaryDashboardGrid { @@ -733,153 +473,95 @@ .weeklyProjectSummaryDashboardSection.small { grid-column: span 1; } +} - .projectStatusGrid { - grid-template-columns: 1fr; - } - - .weeklyProjectSummaryDashboardSection.half .weeklyProjectSummaryDashboardCategoryContent { - grid-template-columns: 1fr; - } - - .weeklyProjectSummaryCard :global(.chartContainer) { - min-height: 240px; - } - - .normalCard { - min-height: 280px; - } +/* ---------------- STATUS CARD ---------------- */ +.statusCard { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border-radius: 25px; + width: 100%; + max-width: 284px; + height: 190px; + text-align: center; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15); + padding: 20px; + border: 1px solid rgba(0, 0, 0, 0.1); + position: relative; +} - .lessonsLearnedGrid { - grid-template-columns: 1fr; - gap: 16px; - min-height: auto; - } - - .lessonsCard { - min-height: 500px; - } - - .toolsTrackingLayout { - grid-template-columns: 1fr; - } - - .toolsDonutWrap { - grid-column: span 1; - } - - .financialsGrid { - grid-template-columns: 1fr; - } - - .financialsGrid .financialBig { - grid-column: span 1; - } - - .laborTimeGrid { - grid-template-columns: 1fr; - } - - .financialsTrackingGrid { - grid-template-columns: 1fr; - } +/* ---------------- RESPONSIVE GRID LAYOUT ---------------- */ +/* MERGED: keep ONLY one projectStatusGrid */ +.projectStatusGrid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 20px; + justify-content: center; + align-items: center; + width: 100%; + max-width: 1600px; + margin: auto; } +/* .projectStatusGrid { + display: grid; + grid-template-columns: repeat(6, 1fr); + grid-template-rows: repeat(2, auto); + gap: 10px; +} */ -/* Extra Small Screens */ -@media (width <= 480px) { - .lessonsCard { - min-height: 450px; - } - - .weeklyProjectSummaryCard :global(.chartContainer) { - min-height: 220px; - } - - .normalCard { - min-height: 260px; - } - - .weeklySummaryHeaderTitle { - font-size: large; - flex-direction: column; - gap: 5px; - } +/* ---------------- OVAL STATUS BUTTON ---------------- */ +.weeklyStatusButton { + width: 130px; + height: 65px; + border-radius: 50px / 32px; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.15); + margin: 12px 0; } -/* Status Grid Responsive */ -@media (width >= 1600px) { +.weeklyCardTitle { + color: #000; + font-size: clamp(14px, 1.4vw, 18px); + font-weight: 600; +} + +.weeklyStatusValue { + color: #000; + font-size: 40px; + font-weight: 600; +} + +/* ---------------- RESPONSIVE BREAKPOINTS ---------------- */ +@media (min-width: 1600px) { .projectStatusGrid { grid-template-columns: repeat(6, 1fr); } } -@media (width <= 1400px) { +@media (max-width: 1400px) { .projectStatusGrid { grid-template-columns: repeat(4, 1fr); } } -@media (width <= 1024px) { +@media (max-width: 1024px) { .projectStatusGrid { grid-template-columns: repeat(3, 1fr); } } -@media (width <= 768px) { +@media (max-width: 768px) { .projectStatusGrid { grid-template-columns: repeat(2, 1fr); } } -@media (width <= 576px) { +@media (max-width: 576px) { .projectStatusGrid { grid-template-columns: repeat(1, 1fr); } } - -/* Dark mode fixes */ -.darkMode .weeklyProjectSummaryContainer { - background-color: var(--bg-color); - color: var(--text-color); -} - -.darkMode .weeklyProjectSummaryDashboardContainer { - background-color: var(--bg-color); -} - -.darkMode .weeklyProjectSummaryDashboardSection { - background: var(--section-bg); -} - -.darkMode * { - color: inherit; -} - -.darkMode .lessonsCard { - box-shadow: 0 4px 6px rgb(0 0 0 / 30%); -} - -/* Print styles */ -@media print { - .weeklyProjectSummaryCard { - break-inside: avoid; - } - - .weeklyProjectSummaryCard :global(.container) { - box-shadow: none; - border: 1px solid #ddd; - } - - .lessonsLearnedGrid, - .toolsTrackingLayout, - .financialsGrid, - .laborTimeGrid, - .financialsTrackingGrid { - break-inside: avoid; - } - - .weeklySummaryShareBtn, - .weeklySummaryHeaderControls select { - display: none; - } -} diff --git a/src/components/CommunityPortal/Activities/ActivityList.jsx b/src/components/CommunityPortal/Activities/ActivityList.jsx index 4e7f3137b5..902d4e168b 100644 --- a/src/components/CommunityPortal/Activities/ActivityList.jsx +++ b/src/components/CommunityPortal/Activities/ActivityList.jsx @@ -3,8 +3,6 @@ import { useState, useEffect, useMemo } from 'react'; import { useSelector, useStore } from 'react-redux'; import { Modal, ModalHeader, ModalBody, ModalFooter, Button } from 'reactstrap'; import styles from './ActivityList.module.css'; -// import { useHistory } from 'react-router-dom'; -import { fuzzySearch } from '../../../utils/fuzzySearch'; import { mockActivities } from './mockActivities'; function ActivityList() { @@ -108,15 +106,15 @@ function ActivityList() { .filter(activity => showPastEvents || activity._dateObj >= startOfToday) .filter(activity => { return ( - (!filter.type || fuzzySearch(activity.type, filter.type, 0.5)) && + (!filter.type || activity.type === filter.type) && (!filter.date || activity.date === filter.date) && - (!filter.location || fuzzySearch(activity.location, filter.location, 0.5)) + (!filter.location || + activity.location.toLowerCase().startsWith(filter.location.toLowerCase())) ); }) .sort((a, b) => { const dateA = new Date(a.date); const dateB = new Date(b.date); - return sortOrder === 'earliest' ? dateA - dateB : dateB - dateA; }); diff --git a/src/components/CommunityPortal/CPDashboard.module.css b/src/components/CommunityPortal/CPDashboard.module.css index 9d496ffea0..5d8185c026 100644 --- a/src/components/CommunityPortal/CPDashboard.module.css +++ b/src/components/CommunityPortal/CPDashboard.module.css @@ -25,8 +25,8 @@ .dashboardHeader { display: flex; align-items: center; - justify-content: flex-start; - gap: 20px; + justify-content: flex-start; + gap: 20px; margin-bottom: 30px; padding: 16px 24px; background: linear-gradient(120deg, #fff, #f8f9fa); @@ -40,7 +40,7 @@ font-size: 2.5rem; font-weight: bold; color: #2c3e50; - white-space: nowrap; + white-space: nowrap; flex-shrink: 0; } @@ -55,6 +55,7 @@ gap: 15px; } +/* pill input */ .dashboardSearchInput { width: 100%; border-radius: 999px; @@ -63,37 +64,41 @@ box-sizing: border-box; } +/* clear X button */ .dashboardClearBtn { + position: absolute; + right: 35px; + top: 45%; + transform: translateY(-50%); border: none; background: transparent; cursor: pointer; font-size: 0.9rem; color: #1b3c55; - display: flex; - align-items: center; - justify-content: center; - padding: 0; } +/* search icon button inside the bar */ .dashboardSearchIconBtn { + position: absolute; + right: 1%; + top: 48%; + transform: translateY(-50%); width: 34px; height: 34px; border-radius: 50%; cursor: pointer; background: transparent; - border: none; display: flex; align-items: center; justify-content: center; color: #1b3c55; font-size: 0.9rem; - padding: 0; } .cp_dashboard_header { display: flex; align-items: center; - justify-content: space-between; + justify-content: space-between; gap: 24px; padding: 16px 24px; margin-bottom: 30px; @@ -129,15 +134,12 @@ margin-top: 8px; } -.dashboardSearchContainer input, -.dashboardSearchContainer textarea { +.dashboardSearchContainer input { outline: none; } .dashboardSearchContainer input:focus, -.dashboardSearchContainer input:focus-visible, -.dashboardSearchContainer textarea:focus, -.dashboardSearchContainer textarea:focus-visible { +.dashboardSearchContainer input:focus-visible { outline: none; box-shadow: none; } @@ -152,15 +154,15 @@ box-shadow: none; } -.dashboardSearchContainer:focus-within:not(:focus-visible) { - outline: none; -} - .darkHeader .dashboardSearchContainer { border-color: #fff; background: #1b2a41; } +.dashboardSearchContainer:focus-within:not(:focus-visible) { + outline: none; +} + .dashboardSearchTextarea { width: 100%; resize: none; @@ -251,35 +253,24 @@ border-right: 1px solid #1b2a41; } -.filterSection { - width: 100%; -} - .filterSection h4 { font-size: 1.8rem; color: #2c3e50; font-weight: 600; } -.filterSectionHeader { - margin-bottom: 40px; -} - .filterSectionDivider { display: flex; flex-direction: column; gap: 24px; } -.filterItem { - margin-bottom: 0; +.filterSectionHeader{ + margin-bottom: 40px; } -.filterItem label { - display: block; - font-weight: 600; - color: #34495e; - margin-bottom: 8px; +.filterItem { + margin-bottom: 0; } .filterOptionsVertical { @@ -290,6 +281,13 @@ margin-top: 10px; } +.filterItem label { + display: block; + font-weight: 600; + color: #34495e; + margin-bottom: 8px; +} + .filterItem input:not([type='checkbox'], [type='radio'], [type='date']), .filterItem select { padding: 12px 15px; @@ -303,13 +301,6 @@ transition: all 0.3s ease; } -.filterItem input:focus, -.filterItem select:focus { - border-color: #2c3e50; - box-shadow: 0 0 5px rgb(44 62 80 / 40%); - outline: none; -} - .radioRow { display: flex; flex-direction: column; @@ -318,48 +309,60 @@ padding: 0; } -.radioColumn { - display: flex; - flex-direction: column; - gap: 8px; +.filterItem input:focus, +.filterItem select:focus { + border-color: #2c3e50; + box-shadow: 0 0 5px rgb(44 62 80 / 40%); + outline: none; } -.radioGroup { - display: flex; - align-items: center; - gap: 10px; +.filterItem input[type='radio'], +.filterItem input[type='checkbox'] { + width: 20px; + height: 20px; + cursor: pointer; margin: 0; + padding: 0; + accent-color: #1b3c55; + flex-shrink: 0; + vertical-align: middle; + display: inline-block; } -.radioLabel, -.checkboxLabel { - display: flex; - align-items: center; - gap: 6px; - cursor: pointer; - font-weight: 400; - margin-bottom: 0; - color: #34495e; +.darkSidebar .filterItem label { + color: #ecf0f1; } -.filterItem input[type='radio'], -.filterItem input[type='checkbox'], -.radioInput, -.checkboxInput { +.radioInput { width: 20px; height: 20px; cursor: pointer; - margin: 0; - padding: 0; accent-color: #1b3c55; flex-shrink: 0; + margin: 0; +} + +.radioLabel { + margin-bottom: 0; vertical-align: middle; + cursor: pointer; + font-size: 1rem; + font-weight: 400; + color: #34495e; + display: inline; } -.darkSidebar .filterItem label, -.darkSidebar .radioLabel, -.darkSidebar .checkboxLabel { - color: #ecf0f1; +.radioGroup { + display: flex; + align-items: center; + gap: 10px; + margin: 0; +} + +.radioGroup label.radioLabel { + display: inline; + font-weight: 400; + margin-bottom: 0; } .darkSidebar .radioInput, @@ -367,6 +370,10 @@ accent-color: #4da3ff; } +.darkSidebar .radioLabel { + color: #ecf0f1; +} + .darkSidebar .inputGroup input { color: #fff; } @@ -378,7 +385,7 @@ border: 1px solid #4a6572; } -.darkSidebar input[type='date'] { +.darkSidebar input[type="date"] { color-scheme: dark; } @@ -412,6 +419,7 @@ margin-bottom: 1.5rem; } +/* Search Input Container */ .inputGroup { display: flex; align-items: center; @@ -459,8 +467,6 @@ align-items: center; justify-content: space-between; margin-bottom: 30px; - gap: 16px; - flex-wrap: wrap; } .sectionTitle { @@ -491,7 +497,7 @@ margin-bottom: 30px; } -.clearDateFilterBtn { +.clearDateFilterBtn{ margin-top: 10px; } @@ -500,17 +506,14 @@ margin-bottom: 1.5rem; } -.eventCardLink { +.eventCardImgContainer img { width: 100%; - display: flex; - text-decoration: none; - color: inherit; + height: auto; } .eventCard { width: 100%; height: 100%; - min-height: 100%; box-shadow: 0 4px 12px rgb(0 0 0 / 10%); display: flex; flex-direction: column; @@ -536,12 +539,6 @@ overflow: hidden; background-color: #f0f0f0; position: relative; - flex-shrink: 0; -} - -.eventCardImgContainer img { - width: 100%; - height: auto; } .eventCardImg { @@ -615,7 +612,6 @@ padding: 40px; color: #888; font-size: 1.2rem; - width: 100%; } .darkMain .noEvents { @@ -624,8 +620,7 @@ .dashboardActions { text-align: center; - margin-top: 20px; - margin-bottom: 20px; + margin-top: 30px; } .dashboardActions button { @@ -650,11 +645,6 @@ display: flex; align-items: center; gap: 12px; - flex-wrap: wrap; -} - -.paginationInfo { - font-weight: 500; } .paginationBtn { @@ -663,31 +653,22 @@ color: #fff !important; } -.paginationBtn:disabled { - opacity: 0.65; -} - -.paginationBtn:hover:not(:disabled) { - background-color: #5a6268 !important; - border-color: #545b62 !important; -} - .filterActions { display: flex; - flex-direction: row; + flex-direction: row; gap: 10px; margin-top: 20px; padding-top: 15px; - width: 100%; + width: 100%; } .applyBtn { - flex: 1; + flex: 1; background-color: #27ae60 !important; border: none; color: white; - padding: 10px 5px; - font-size: 0.9rem; + padding: 10px 5px; + font-size: 0.9rem; font-weight: 600; border-radius: 8px; cursor: pointer; @@ -700,11 +681,11 @@ } .clearBtn { - flex: 1; + flex: 1; background-color: #95a5a6 !important; border: none; color: white; - padding: 10px 5px; + padding: 10px 5px; font-size: 0.9rem; font-weight: 600; border-radius: 8px; @@ -729,26 +710,13 @@ .cp_dashboard_search_container { width: 100%; } +} - .centeredRow { - padding: 15px; - } - - .dashboardMain { - width: 100%; - padding: 20px 10px; - } - - .dashboardSidebar { - margin-bottom: 20px; - } - - .eventsHeader { - flex-direction: column; - align-items: flex-start; - } +.paginationBtn:disabled { + opacity: 0.65; +} - .filterActions { - flex-direction: column; - } -} \ No newline at end of file +.paginationBtn:hover:not(:disabled) { + background-color: #5a6268 !important; + border-color: #545b62 !important; +} diff --git a/src/components/CommunityPortal/Calendar/CommunityCalendar.jsx b/src/components/CommunityPortal/Calendar/CommunityCalendar.jsx index 179793cbdc..6289785f47 100644 --- a/src/components/CommunityPortal/Calendar/CommunityCalendar.jsx +++ b/src/components/CommunityPortal/Calendar/CommunityCalendar.jsx @@ -489,21 +489,6 @@ export default function CommunityCalendar() { [darkMode], ); - const getTypeIcon = type => { - switch (type) { - case 'Workshop': - return ; - case 'Webinar': - return ; - case 'Meeting': - return ; - case 'Social Gathering': - return ; - default: - return null; - } - }; - return (
{/* Inline styles to ensure selected date number is visible in dark mode - force dark background */} @@ -791,31 +776,26 @@ export default function CommunityCalendar() {
- {[ - [FaTag, 'Type', selectedEvent.type], - [FaMapMarkerAlt, 'Location', selectedEvent.location], - [FaCalendarAlt, 'Date', selectedEvent.date.toLocaleDateString()], - [FaClock, 'Time', selectedEvent.time], - ].map(([Icon, label, value]) => ( -
- - - {label}: - - - - {label === 'Type' ? getTypeIcon(selectedEvent.type) : null} {value} - -
- ))} +
+ Type: + {selectedEvent.type} +
+
+ Location: + {selectedEvent.location} +
+
+ Date: + {selectedEvent.date.toLocaleDateString()} +
+
+ Time: + {selectedEvent.time} +
- - - Description: - - + Description:

{selectedEvent.description}

diff --git a/src/components/CommunityPortal/Calendar/CommunityCalendar.module.css b/src/components/CommunityPortal/Calendar/CommunityCalendar.module.css index c09cf8f8bb..16738f5451 100644 --- a/src/components/CommunityPortal/Calendar/CommunityCalendar.module.css +++ b/src/components/CommunityPortal/Calendar/CommunityCalendar.module.css @@ -2,8 +2,7 @@ .calendarContainer { display: flex; flex-direction: row; - width: 100%; - min-width: 0; + min-width: 100%; align-items: stretch; gap: 20px; } @@ -23,7 +22,6 @@ box-sizing: border-box; display: flex; flex-direction: column; - min-width: 0; } .calendarActivitySectionDarkMode, @@ -70,6 +68,8 @@ font-size: 1.2rem; border: 2px solid #ddd; border-radius: 8px; + + /* overflow: hidden; */ background-color: #fff; padding: 2rem; } @@ -1377,45 +1377,26 @@ display: block; } -.detailIcon { - margin-right: 8px; - vertical-align: middle; - color: #718096; - font-size: 1rem; -} - -.eventModalDark .detailIcon { - color: #cbd5e0; -} - -.weekGridRoot { - display: flex; - flex-direction: column; - width: 100%; - min-width: 0; -} - /* Week Grid Styles */ -.weekGridRoot .weekGridContainer { +.weekGridContainer { display: flex; flex-direction: column; background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; height: 700px; - max-width: 100%; overflow: hidden; margin-bottom: 20px; } -.weekGridRoot .weekGridBody { +.weekGridBody { flex: 1; overflow-y: scroll; background: #fff; } @media (width <= 1024px) { - .calendarContainer { + .calendarContainer{ flex-direction: column; } @@ -1423,10 +1404,6 @@ padding: 1rem; } - .weekGridRoot .weekGridContainer { - height: auto; - min-height: 500px; - } } .dateLabel { @@ -1435,12 +1412,11 @@ color: #1e293b; } -.weekGridRoot .hourRow { +.hourRow { display: grid; - grid-template-columns: 80px repeat(7, minmax(0, 1fr)); + grid-template-columns: 80px repeat(7, 1fr); min-height: 80px; border-bottom: 1px solid #f1f5f9; - width: 100%; } .timeLabel { @@ -1453,16 +1429,11 @@ border-right: 1px solid #e2e8f0; } -.weekGridRoot .gridCell { - min-width: 0; /* CRITICAL */ - max-width: 100%; /* CRITICAL */ - overflow: hidden; /* prevents bleed */ +.gridCell { border-left: 1px solid #f1f5f9; position: relative; padding: 4px; - box-sizing: border-box; - display: flex; - flex-direction: column; + transition: background 0.2s; } .gridCell:hover { @@ -1470,11 +1441,6 @@ } .gridEvent { - width: 100%; - max-width: 100%; - min-width: 0; - overflow: hidden; /* CRITICAL */ - box-sizing: border-box; background: #dcfce7; border-left: 4px solid #22c55e; border-radius: 6px; @@ -1564,7 +1530,6 @@ display: flex; align-items: center; gap: 4px; - min-width: 0; /* CRITICAL */ overflow: hidden; } @@ -1576,8 +1541,6 @@ } .eventTitleText { - flex: 1; - min-width: 0; /* CRITICAL */ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -1589,4 +1552,3 @@ gap: 4px; font-weight: 600; } - diff --git a/src/components/CommunityPortal/Reports/Participation/DropOffTracking.jsx b/src/components/CommunityPortal/Reports/Participation/DropOffTracking.jsx index fe61dc78d2..6ee0d1cb83 100644 --- a/src/components/CommunityPortal/Reports/Participation/DropOffTracking.jsx +++ b/src/components/CommunityPortal/Reports/Participation/DropOffTracking.jsx @@ -1,7 +1,6 @@ import { useState } from 'react'; import { useSelector } from 'react-redux'; import styles from './Participation.module.css'; -import { ArrowUpDown, ArrowUp, ArrowDown } from 'lucide-react'; import mockEvents from './mockData'; function DropOffTracking() { @@ -11,8 +10,6 @@ function DropOffTracking() { const [isModalOpen, setIsModalOpen] = useState(false); const [activeEvent, setActiveEvent] = useState(null); const [selectedUsers, setSelectedUsers] = useState([]); - const [sortColumn, setSortColumn] = useState(null); - const [sortDirection, setSortDirection] = useState('asc'); const darkMode = useSelector(state => state.theme.darkMode); @@ -65,38 +62,6 @@ function DropOffTracking() { setSelectedUsers([]); }; - const handleSort = column => { - if (sortColumn === column) { - setSortDirection(prev => (prev === 'asc' ? 'desc' : 'asc')); - } else { - setSortColumn(column); - setSortDirection('asc'); - } - }; - - const parseRate = val => Number.parseFloat(val); - - const sortedEvents = [...filteredEvents].sort((a, b) => { - if (!sortColumn) return 0; - let aVal = a[sortColumn]; - let bVal = b[sortColumn]; - if (sortColumn === 'noShowRate' || sortColumn === 'dropOffRate') { - aVal = parseRate(aVal); - bVal = parseRate(bVal); - } else { - aVal = aVal?.toLowerCase() ?? ''; - bVal = bVal?.toLowerCase() ?? ''; - } - if (aVal < bVal) return sortDirection === 'asc' ? -1 : 1; - if (aVal > bVal) return sortDirection === 'asc' ? 1 : -1; - return 0; - }); - - const sortIndicator = column => { - if (sortColumn !== column) return ; - return sortDirection === 'asc' ? : ; - }; - return (
- handleSort('eventName')} className={styles.sortableHeader}> - - Event name {sortIndicator('eventName')} + Event name + + + No-show rate + + ℹ️ - handleSort('noShowRate')} className={styles.sortableHeader}> - - No-show rate {sortIndicator('noShowRate')} + + + Drop-off rate + + ℹ️ - handleSort('dropOffRate')} className={styles.sortableHeader}> - - Drop-off rate {sortIndicator('dropOffRate')} + + + Get list + + ℹ️ - Get list - {sortedEvents.map(event => ( + {filteredEvents.map(event => ( {event.eventName} diff --git a/src/components/CommunityPortal/Reports/Participation/Participation.module.css b/src/components/CommunityPortal/Reports/Participation/Participation.module.css index 0e147193ef..8074f5bbf9 100644 --- a/src/components/CommunityPortal/Reports/Participation/Participation.module.css +++ b/src/components/CommunityPortal/Reports/Participation/Participation.module.css @@ -11,7 +11,6 @@ .participationLandingPage { max-width: 90%; margin: 0 auto; - background: white; } .participationLandingPageDark { @@ -331,26 +330,6 @@ background: #f4f4f4; } -.sortableHeader { - cursor: pointer; - user-select: none; - white-space: nowrap; -} - -.sortableHeaderContent { - display: inline-flex; - align-items: center; - gap: 4px; -} - -.sortableHeader:hover { - background: #e8e8e8; -} - -.trackingTableDark .sortableHeader:hover { - background: #253055; -} - .trackingTableDark thead th { background: #1c2541; color: #fff; @@ -496,12 +475,6 @@ border-right: none; } -.insightsDark .insightsTab { - background-color: #3A506B; - color: #fff; - border-right: 1px solid #555; -} - .insightsTab:hover:not(.activeTab) { background-color: #e4e4e4; } @@ -575,8 +548,8 @@ } .insightsPercentageDark { - color: #fff; - } + color: #fff; +} .insightsDark .insightsPercentageDark { color: red !important; @@ -609,6 +582,69 @@ opacity: 1; } +/* ---------- Export PDF modal ---------- */ +.modalOverlay { + position: fixed; + inset: 0; + background: rgb(0 0 0 / 45%); + display: flex; + justify-content: center; + align-items: center; + z-index: 999; +} + +.modal { + width: 420px; + max-width: calc(100vw - 32px); + background: #fff; + border-radius: 12px; + padding: 16px; +} + +.modalDark { + background: #1C2541; +} + +.modalHeader { + display: flex; + align-items: center; + justify-content: space-between; +} + +.modalTitle { + margin: 0; +} + +.modalClose { + border: none; + background: transparent; + font-size: 22px; + cursor: pointer; +} + +.modalBody { + margin-top: 12px; +} + +.modalMeta { + font-size: 16px; + opacity: 0.85; + display: grid; + gap: 6px; + margin-bottom: 12px; +} + +.modalError { + margin-bottom: 12px; + font-size: 13px; + color: #b00020; +} + +.modalActions { + display: flex; + gap: 10px; +} + .exportOptionsButtons, .exportOptionsButtonsDark { flex: 1; @@ -643,11 +679,25 @@ cursor: not-allowed; } + + /* ---------- PDF/Print helpers ---------- */ -.pageBreakBefore { break-before: always; } -.pageBreakAfter { break-after: always; } +.pageBreakBefore { break-before: page; break-before: always; } +.pageBreakAfter { break-after: page; break-after: always; } .avoidBreak { break-inside: avoid; } +.pageBreakBefore { + break-before: page; +} + +.pageBreakAfter { + break-after: page; +} + +.avoidBreak { + break-inside: avoid; +} + :global(html[data-exporting="true"]) .caseCards:global(.expanded), :global(html[data-exporting="true"]) .caseList:global(.expanded), :global(html[data-exporting="true"]) .trackingListContainer { @@ -677,6 +727,7 @@ display: none; } +/* Print styles using global selectors */ @media print { /* Hide everything except the participation component */ :global(body > *:not(#root)) { @@ -745,6 +796,7 @@ } :global(.case-card-global) { + break-inside: avoid-page !important; break-inside: avoid !important; height: auto !important; } @@ -754,6 +806,7 @@ } :global(.case-list-item-global) { + break-inside: avoid-page !important; break-inside: avoid !important; } @@ -797,7 +850,7 @@ .printOnly { display: block !important; } - + :global(#root) .case-card-global { background: white !important; color: #333 !important; @@ -1046,6 +1099,23 @@ border: 1px solid #3A506B; } +.insightsDark .insightsFilters select { + background-color: #3A506B; + color: #fff; + border: 1px solid #3A506B; +} + +.insightsDark .insightsTab { + background-color: #3A506B; + color: #fff; + border-right: 1px solid #555; +} + +.insightsDark .insightsTab.activeTab { + background-color: #007bff; + color: #ffffff; +} + /* ---------- Info Tooltip Icons ---------- */ .infoIcon { margin-left: 6px; @@ -1060,9 +1130,9 @@ } .trackingTableDark .infoIcon { - color: #ccc; + color: #cccccc; } .trackingTableDark .infoIcon:hover { - color: #fff; -} + color: #ffffff; +} \ No newline at end of file diff --git a/src/components/EductionPortal/StudentDashboard/StudentDashboard.jsx b/src/components/EductionPortal/StudentDashboard/StudentDashboard.jsx index 554251aae3..ee306bfdb7 100644 --- a/src/components/EductionPortal/StudentDashboard/StudentDashboard.jsx +++ b/src/components/EductionPortal/StudentDashboard/StudentDashboard.jsx @@ -9,7 +9,6 @@ import NavigationBar from './NavigationBar'; import SummaryCards from './SummaryCards'; import { fetchStudentTasks, markStudentTaskAsDone } from '~/actions/studentTasks'; import { fetchIntermediateTasks, markIntermediateTaskAsDone } from '~/actions/intermediateTasks'; -import HoursLogPanel from '../StudentTasks/HoursLogPanel'; const StudentDashboard = () => { const [viewMode, setViewMode] = useState('card'); // 'card' or 'list' @@ -21,7 +20,6 @@ const StudentDashboard = () => { }); const [intermediateTasks, setIntermediateTasks] = useState({}); const [expandedTasks, setExpandedTasks] = useState({}); - const [activeLogTask, setActiveLogTask] = useState(null); const dispatch = useDispatch(); const authUser = useSelector(state => state.auth.user); @@ -46,8 +44,8 @@ const StudentDashboard = () => { if (subTasks && subTasks.length > 0) { intermediateTasksData[task.id] = subTasks; } - } catch { - // Non-critical: skip if intermediate tasks unavailable for this task + } catch (error) { + console.error(`Error fetching intermediate tasks for task ${task.id}:`, error); } } @@ -98,11 +96,6 @@ const StudentDashboard = () => { return `${wholeHours}h ${minutes}min`; }; - // Handle log time - const handleLogTime = task => { - setActiveLogTask(task); - }; - // Handle mark as done const handleMarkAsDone = async taskId => { dispatch(markStudentTaskAsDone(taskId)); @@ -227,7 +220,6 @@ const StudentDashboard = () => { { { )}
- - {/* Hours Log Panel */} - {activeLogTask && ( - setActiveLogTask(null)} - /> - )}
); }; diff --git a/src/components/EductionPortal/StudentDashboard/TaskCard.jsx b/src/components/EductionPortal/StudentDashboard/TaskCard.jsx index fa0a81f637..2036c240d3 100644 --- a/src/components/EductionPortal/StudentDashboard/TaskCard.jsx +++ b/src/components/EductionPortal/StudentDashboard/TaskCard.jsx @@ -3,12 +3,10 @@ import styles from './TaskCard.module.css'; import { useTaskLogic } from './useTaskLogic'; import MarkAsDoneButton from './MarkAsDoneButton'; import IntermediateTasksList from './IntermediateTasksList'; -import { taskItemPropTypes, taskItemDefaultProps } from './taskPropTypes'; const TaskCard = ({ task, onMarkAsDone, - onLogTime, intermediateTasks = [], isExpanded = false, onToggleIntermediateTasks, @@ -79,7 +77,7 @@ const TaskCard = ({ {/* Action Buttons */}
- -
- - {/* Real-time progress indicator */} -
-
- - {logged} / {total > 0 ? total : '—'} hrs - - {total > 0 && {progressPercent}%} -
- {total > 0 && ( - = 100 ? styles.progressComplete : '' - }`} - value={progressPercent} - max={100} - aria-label="Task progress" - /> - )} - {progressPercent >= 100 && ( -

✓ Eligible to mark as done

- )} -
- - {/* Hours input */} -
-
- - setInputHours(Math.max(0.5, Number(e.target.value)))} - className={`${styles.hoursInput} ${darkMode ? styles.hoursInputDark : ''}`} - aria-label="Hours to log" - /> - -
- -
- -
- ); -}; - -HoursLogPanel.propTypes = { - task: PropTypes.shape({ - id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - _id: PropTypes.string, - logged_hours: PropTypes.number, - suggested_total_hours: PropTypes.number, - }).isRequired, - darkMode: PropTypes.bool, - onClose: PropTypes.func.isRequired, -}; - -HoursLogPanel.defaultProps = { - darkMode: false, -}; - -export default HoursLogPanel; diff --git a/src/components/EductionPortal/StudentTasks/HoursLogPanel.module.css b/src/components/EductionPortal/StudentTasks/HoursLogPanel.module.css deleted file mode 100644 index 875c76d56d..0000000000 --- a/src/components/EductionPortal/StudentTasks/HoursLogPanel.module.css +++ /dev/null @@ -1,255 +0,0 @@ -/* Overlay backdrop */ -.overlay { - position: fixed; - inset: 0; - background: rgb(0 0 0 / 50%); - display: flex; - align-items: center; - justify-content: center; - z-index: 1050; -} - -/* Reset native and size it as a centered modal */ -dialog.panel { - position: relative; - margin: 0; - inset: auto; - padding: 0; - max-width: 420px; - width: 100%; -} - -.panel { - background: #fff; - border: 1px solid #e5e7eb; - border-radius: 10px; - padding: 1.25rem 1.5rem; - box-shadow: 0 8px 32px rgb(0 0 0 / 20%); - width: 100%; -} - -.panelDark { - background: #1e2533; - border-color: #374151; - color: #f3f4f6; -} - -/* ── Header ──────────────────────────────── */ -.header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 0.9rem; -} - -.title { - font-size: 1rem; - font-weight: 600; - margin: 0; - color: #111827; -} - -.panelDark .title { - color: #f3f4f6; -} - -.closeBtn { - background: none; - border: none; - font-size: 1.4rem; - line-height: 1; - cursor: pointer; - color: #6b7280; - padding: 0 0.2rem; - transition: color 0.15s; -} - -.closeBtn:hover { - color: #111827; -} - -.panelDark .closeBtn { - color: #9ca3af; -} - -.panelDark .closeBtn:hover { - color: #f3f4f6; -} - -/* ── Progress ─────────────────────────────── */ -.progressSection { - margin-bottom: 1rem; -} - -.progressLabel { - display: flex; - justify-content: space-between; - font-size: 0.875rem; - color: #374151; - margin-bottom: 0.4rem; - font-weight: 500; -} - -.panelDark .progressLabel { - color: #d1d5db; -} - -.progressBar { - width: 100%; - height: 8px; - border-radius: 4px; - overflow: hidden; - - /* Reset native element styles */ - appearance: none; - border: none; - background: #e5e7eb; -} - -.progressBar::-webkit-progress-bar { - background: #e5e7eb; - border-radius: 4px; -} - -/* Dark mode progress bar track */ -.panelDark .progressBar { - background: #374151; -} - -.panelDark .progressBar::-webkit-progress-bar { - background: #374151; -} - -.progressBar::-webkit-progress-value { - background: #2563eb; - border-radius: 4px; - transition: width 0.35s ease; -} - -.progressBar::-moz-progress-bar { - background: #2563eb; - border-radius: 4px; - transition: width 0.35s ease; -} - -.progressComplete::-webkit-progress-value { - background: #16a34a; -} - -.progressComplete::-moz-progress-bar { - background: #16a34a; -} - -.eligibleMsg { - font-size: 0.8rem; - color: #16a34a; - margin: 0.4rem 0 0; - font-weight: 500; -} - -/* ── Form ─────────────────────────────────── */ -.form { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.inputRow { - display: flex; - align-items: center; - gap: 0.5rem; -} - -.stepBtn { - width: 2rem; - height: 2rem; - border: 1px solid #d1d5db; - border-radius: 50%; - background: #f9fafb; - font-size: 1.1rem; - line-height: 1; - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - color: #374151; - transition: background 0.15s, border-color 0.15s; - flex-shrink: 0; -} - -.stepBtn:hover { - background: #e5e7eb; - border-color: #9ca3af; -} - -.panelDark .stepBtn { - background: #374151; - border-color: #4b5563; - color: #f3f4f6; -} - -.panelDark .stepBtn:hover { - background: #4b5563; -} - -.hoursInput { - flex: 1; - text-align: center; - border-radius: 6px; - padding: 0.35rem 0.5rem; - font-size: 1rem; - font-weight: 600; - color: #111827; - background: #f9fafb; - outline: none; - min-width: 0; - border: 1px solid #d1d5db; -} - -.hoursInput:focus { - border-color: #2563eb; - box-shadow: 0 0 0 2px rgb(37 99 235 / 15%); -} - -.hoursInputDark { - background: #374151; - border-color: #4b5563; - color: #f3f4f6; -} - -.hoursInputDark:focus { - border-color: #3b82f6; -} - -/* hide number spinner arrows */ -.hoursInput::-webkit-outer-spin-button, -.hoursInput::-webkit-inner-spin-button { - appearance: none; - margin: 0; -} - -.hoursInput[type='number'] { - appearance: textfield; -} - -.submitBtn { - width: 100%; - padding: 0.5rem 1rem; - background: #2563eb; - color: #fff; - border: none; - border-radius: 6px; - font-size: 0.9rem; - font-weight: 600; - cursor: pointer; - transition: background 0.15s; -} - -.submitBtn:disabled { - background: #93c5fd; - cursor: not-allowed; -} - -.submitBtn:hover:not(:disabled) { - background: #1d4ed8; -} diff --git a/src/components/EductionPortal/StudentTasks/StudentTasks.jsx b/src/components/EductionPortal/StudentTasks/StudentTasks.jsx index d56ad0e5aa..c04df9332e 100644 --- a/src/components/EductionPortal/StudentTasks/StudentTasks.jsx +++ b/src/components/EductionPortal/StudentTasks/StudentTasks.jsx @@ -1,51 +1,16 @@ import React, { useState, useEffect, useMemo } from 'react'; -import { useSelector, useDispatch } from 'react-redux'; +import { useSelector } from 'react-redux'; import Sidebar from './StudentSidebar'; import TaskCard from './TaskCard'; import RubricModal from './RubricModal'; -import { fetchStudentTasks } from '../../../actions/studentTasks'; import styles from './StudentTasks.module.css'; const FILTER_OPTIONS = ['All', 'Incomplete', 'Submitted', 'Graded']; const GROUP_OPTIONS = ['subject', 'colorLevel', 'activityGroup', 'strategy']; const MS_PER_DAY = 1000 * 60 * 60 * 24; -// Map API status values to display labels used by TaskCard / filters -const STATUS_DISPLAY_MAP = { - assigned: 'Incomplete', - in_progress: 'Incomplete', - completed: 'Submitted', - graded: 'Graded', -}; - -// Normalise a Redux task (flat format from actions) into the shape TaskCard expects -const normalizeTask = task => ({ - ...task, - title: task.subtitle || task.course_name || 'Untitled Task', - subject: task.subject?.name || task.course_name || 'Unknown Subject', - colorLevel: task.color_level || 'Unknown', - activityGroup: task.activity_group || 'Unassigned', - strategy: task.strategy || 'General', - description: task.subtitle || '', - status: STATUS_DISPLAY_MAP[task.status] || 'Incomplete', - progress: - task.suggested_total_hours > 0 - ? Math.round((task.logged_hours / task.suggested_total_hours) * 100) - : 0, - dueDate: task.dueAt ? new Date(task.dueAt).toISOString().split('T')[0] : 'N/A', - logged_hours: task.logged_hours || 0, - suggested_total_hours: task.suggested_total_hours || 0, -}); - const StudentTasks = () => { - const dispatch = useDispatch(); const darkMode = useSelector(state => state.theme?.darkMode); - const reduxTasks = useSelector(state => state.studentTasks?.taskItems || []); - const loading = useSelector(state => state.studentTasks?.fetching); - - useEffect(() => { - dispatch(fetchStudentTasks()); - }, [dispatch]); useEffect(() => { const htmlEl = document.documentElement; @@ -67,7 +32,50 @@ const StudentTasks = () => { }; }, [darkMode]); - const tasks = useMemo(() => reduxTasks.map(normalizeTask), [reduxTasks]); + const [tasks] = useState([ + { + id: 1, + title: 'Activity 1: Technology, Art, Trades, Health', + subject: 'Technology / Art / Health', + colorLevel: 'Red', + activityGroup: 'Creative Projects', + strategy: 'Teaching Strategy', + description: + 'Choose a person listed in the Technology Subject Page and a related trade. Visit at least 1 location and observe 5 professionals in action. Create a blog post or video.', + status: 'Incomplete', + progress: 25, + dueDate: '2025-10-15', + rubric: ['Clarity', 'Creativity', 'Effort'], + }, + { + id: 2, + title: 'Activity 2: Math, Science, Innovation', + subject: 'Math / Science', + colorLevel: 'Blue', + activityGroup: 'Research Assignments', + strategy: 'Life Strategy', + description: + 'Research patterns of climate bands and write a pamphlet showing your findings. Distribute pamphlets to students.', + status: 'Submitted', + progress: 50, + dueDate: '2025-10-20', + rubric: ['Accuracy', 'Presentation', 'Depth of Research'], + }, + { + id: 3, + title: 'Activity 3: Social Sciences, English, Values', + subject: 'Social Sciences / English', + colorLevel: 'Violet', + activityGroup: 'Written Work', + strategy: 'Teaching Strategy', + description: + 'Write a 10–15 page research paper on cultural development. Include fictional story and empathy-driven examples.', + status: 'Graded', + progress: 80, + dueDate: '2025-10-25', + rubric: ['Critical Thinking', 'Empathy', 'Writing Style'], + }, + ]); const [selectedTask, setSelectedTask] = useState(null); const [filter, setFilter] = useState('All'); @@ -166,12 +174,7 @@ const StudentTasks = () => { ))}
- {loading &&

Loading tasks…

} -
- {!loading && groupKeys.length === 0 && ( -

No tasks found.

- )} {groupKeys.map(key => (
-
- {showHoursPanel && ( - setShowHoursPanel(false)} /> - )} {showRubric && setShowRubric(false)} />} ); @@ -92,27 +71,17 @@ const TaskCard = ({ task, onOpenRubric }) => { TaskCard.propTypes = { task: PropTypes.shape({ - id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + id: PropTypes.number.isRequired, title: PropTypes.string.isRequired, subject: PropTypes.string, colorLevel: PropTypes.string, activityGroup: PropTypes.string, strategy: PropTypes.string, description: PropTypes.string.isRequired, - status: PropTypes.oneOf([ - 'Incomplete', - 'Submitted', - 'Graded', - 'assigned', - 'in_progress', - 'completed', - 'graded', - ]).isRequired, + status: PropTypes.oneOf(['Incomplete', 'Submitted', 'Graded']).isRequired, progress: PropTypes.number.isRequired, dueDate: PropTypes.string.isRequired, rubric: PropTypes.arrayOf(PropTypes.string), - logged_hours: PropTypes.number, - suggested_total_hours: PropTypes.number, }).isRequired, onOpenRubric: PropTypes.func, }; diff --git a/src/components/EventPopularity/EventPopularity.jsx b/src/components/EventPopularity/EventPopularity.jsx index 954a0c5004..004e0a1453 100644 --- a/src/components/EventPopularity/EventPopularity.jsx +++ b/src/components/EventPopularity/EventPopularity.jsx @@ -10,8 +10,6 @@ import { Tooltip, Legend, } from 'recharts'; -import styles from './EventPopularity.module.css'; -import { useSelector } from 'react-redux'; // Sample data const eventTypeData = [ @@ -64,90 +62,254 @@ const participationCards = [ ]; export default function EventDashboard() { - const darkMode = useSelector(state => state.theme?.darkMode); - return ( -
-

Event Attendance Trend

- -
+
+

+ Event Attendance Trend +

+
{/* Event Registration Trend (Type) */} -
-

Event Registration Trend (Type)

- -
-
+
+

+ Event Registration Trend (Type) +

+
+
Event Name Registered Members
- {eventTypeData.map(event => ( -
- {event.name} - -
+
+ + {event.name} + +
- - {event.registered} + + {event.registered} +
))}
-
-
-

325

-

Total Registered Members

-
- -
-

Event Type 1

-

Most Popular Event Type

-
- -
-

Event Type 6

-

Least Popular Event Type

-
+
+ {[ + { title: '325', subtitle: 'Total Registered Members', isPrimary: true }, + { title: 'Event Type 1', subtitle: 'Most Popular Event Type' }, + { title: 'Event Type 6', subtitle: 'Least Popular Event Type' }, + ].map(card => ( +
+

{card.title}

+

+ {card.subtitle} +

+
+ ))}
{/* Event Registration Trend (Time) */} -
-

Event Registration Trend (Time)

- - - - - - - - - - - - +
+

+ Event Registration Trend (Time) +

+
+ + + + + + + + + + + +
-
+
{participationCards.map(card => ( -
-

{card.title}

-

{card.subtitle}

- - {!!card.participants &&
+{card.participants}
} - +
+
+

+ {card.title} +

+ +
+

+ {card.subtitle} +

+ {card.participants && ( +
+ 👥 +{card.participants} +
+ )}

{card.trend} Monthly

@@ -156,6 +318,16 @@ export default function EventDashboard() {
+
); } diff --git a/src/components/EventPopularity/EventPopularity.module.css b/src/components/EventPopularity/EventPopularity.module.css deleted file mode 100644 index 317c6a82a3..0000000000 --- a/src/components/EventPopularity/EventPopularity.module.css +++ /dev/null @@ -1,130 +0,0 @@ -/* Component-scoped variables and dark-mode overrides */ -.eventpopularity { - width: 100%; - max-width: 1200px; - margin: 0 auto; - padding: 20px; - font-family: Arial, sans-serif; - - /* default (light) tokens scoped to this component */ - --ep-text-color: #111827; - --ep-muted-text: #666666; - --ep-card-bg: #ffffff; - --ep-card-alt-bg: #f5f5f5; - --ep-primary: #4A90E2; - --ep-primary-2: #82B7FF; - --ep-card-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - --ep-grid-stroke: #e0e0e0; - --ep-chart-tick: #222222; -} - -/* dark variant applied via module class */ -.dark { - --ep-text-color: #e5e7eb; - --ep-muted-text: #b5bac5; - --ep-card-bg: #0f1724; - --ep-card-alt-bg: #111827; - --ep-primary: #4A90E2; - --ep-primary-2: #82B7FF; - --ep-card-shadow: none; - --ep-grid-stroke: #2f4157; - --ep-chart-tick: #ffffff; -} - -.epheader{ - font-size: 24px; - font-weight: bold; - margin-bottom: 20px; - text-align: center; - color: var(--ep-text-color); -} - -.epgrid{ - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; -} - -.epCard { - background: var(--ep-card-bg); - border-radius: 8px; - box-shadow: var(--ep-card-shadow); - padding: 20px; - color: var(--ep-text-color); -} - -.epRowLabel { - display: flex; - justify-content: space-between; - font-size: 14px; - color: var(--ep-muted-text); - margin-bottom: 10px; -} - -.epEventName { - width: 100px; - margin-right: 10px; - font-size: 14px; - color: var(--ep-muted-text); -} - -.epProgressBar { - flex-grow: 1; - height: 8px; - background: var(--ep-grid-stroke); - border-radius: 4px; - overflow: hidden; -} - -.epProgressFill { - height: 100%; - background: var(--ep-primary); -} - -.epStatsGrid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 10px; -} - -.epStatCard { - background: var(--ep-card-alt-bg); - border-radius: 4px; - padding: 10px; - text-align: center; - color: var(--ep-text-color); -} - -.epStatSubtitle { - font-size: 12px; - color: var(--ep-muted-text); -} - -.epChartCard { - background: var(--ep-card-bg); - border-radius: 8px; - box-shadow: var(--ep-card-shadow); - padding: 20px; - color: var(--ep-text-color); -} - -.epParticipationGrid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 10px; -} - -.epParticipationCard { - background: var(--ep-card-alt-bg); - border-radius: 4px; - padding: 10px; - color: var(--ep-text-color); -} - -.trendPositive { - color: #22c55e; -} - -.trendNegative { - color: #ef4444; -} \ No newline at end of file diff --git a/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx b/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx index 8f342c7f02..0606e36cce 100644 --- a/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx +++ b/src/components/ExperienceDonutChart/ExperienceDonutChart.jsx @@ -1,7 +1,7 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useSelector } from 'react-redux'; import axios from 'axios'; // Added axios import to fix network request errors -import { PieChart, Pie, Cell, ResponsiveContainer, Tooltip } from 'recharts'; +import { PieChart, Pie, Cell, ResponsiveContainer, Sector } from 'recharts'; import styles from './ExperienceDonutChart.module.css'; const SEGMENT_COLORS = [ @@ -16,11 +16,14 @@ const SEGMENT_COLORS = [ const EXPERIENCE_LABELS = ['0-1 years', '1-3 years', '3-5 years', '5+ years']; -// ✅ Crypto-based RNG (safer than Math.random) -function secureRandomInt(min, max) { - const array = new Uint32Array(1); - crypto.getRandomValues(array); - return min + (array[0] % (max - min + 1)); +function getContrastColor(hexColor) { + const hex = hexColor.replace('#', ''); + const bigint = parseInt(hex, 16); + const r = (bigint >> 16) & 255; + const g = (bigint >> 8) & 255; + const b = bigint & 255; + const brightness = (r * 299 + g * 587 + b * 114) / 1000; + return brightness > 150 ? '#111827' : '#ffffff'; } function Spinner() { @@ -32,6 +35,13 @@ function Spinner() { ); } +const TODAY = new Date().toISOString().split('T')[0]; + +const PREFERS_REDUCED_MOTION = + typeof window !== 'undefined' + ? window.matchMedia('(prefers-reduced-motion: reduce)').matches + : false; + export default function ExperienceDonutChart() { const [startDate, setStartDate] = useState(''); const [endDate, setEndDate] = useState(''); @@ -44,7 +54,6 @@ export default function ExperienceDonutChart() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [activeIndex, setActiveIndex] = useState(null); const darkMode = useSelector(state => state.theme.darkMode); const hasFilters = useMemo( @@ -60,7 +69,6 @@ export default function ExperienceDonutChart() { const fetchData = async () => { setLoading(true); setError(null); - setActiveIndex(null); try { const token = localStorage.getItem('token'); @@ -88,7 +96,7 @@ export default function ExperienceDonutChart() { if (!data || data.length === 0) { setChartData(null); - setLoading(false); + setTotal(0); return; } @@ -120,18 +128,128 @@ export default function ExperienceDonutChart() { // eslint-disable-next-line react-hooks/exhaustive-deps }, [appliedFilters]); + const visibleChartData = useMemo(() => chartData?.filter(d => d.value > 0) ?? [], [chartData]); + + // Hide counts until the sweep animation finishes so they don't bleed through + const [animationDone, setAnimationDone] = useState(false); + useEffect(() => { + setAnimationDone(PREFERS_REDUCED_MOTION); + }, [chartData]); + + const [hoveredIndex, setHoveredIndex] = useState(null); + + // Renders the hovered segment with a slightly larger outer radius + const renderActiveShape = props => { + const { cx, cy, innerRadius, outerRadius, startAngle, endAngle, fill } = props; + return ( + + ); + }; + + // Draws the count at the visual center of each segment — only after animation completes + const renderInsideCount = ({ cx, cy, midAngle, innerRadius, outerRadius, value, index }) => { + if (!value || !animationDone) return null; + const isHovered = index === hoveredIndex; + const RADIAN = Math.PI / 180; + // Push centroid outward slightly when hovered to stay centered in the expanded segment + const expandedOuter = isHovered ? outerRadius + 10 : outerRadius; + const radius = (innerRadius - (isHovered ? 3 : 0) + expandedOuter) * 0.5; + const x = cx + radius * Math.cos(-midAngle * RADIAN); + const y = cy + radius * Math.sin(-midAngle * RADIAN); + return ( + + {value.toLocaleString()} + + ); + }; + + // Draws name on top line, percentage below — outside the segment, only after animation completes + const renderOutsideLabel = ({ cx, cy, midAngle, outerRadius, name, percent, index }) => { + if (!animationDone) return null; + const isHovered = index === hoveredIndex; + const RADIAN = Math.PI / 180; + const expandedOuter = isHovered ? outerRadius + 10 : outerRadius; + const lineStart = expandedOuter + 8; + const lineEnd = expandedOuter + 50; + const sx = cx + lineStart * Math.cos(-midAngle * RADIAN); + const sy = cy + lineStart * Math.sin(-midAngle * RADIAN); + const ex = cx + lineEnd * Math.cos(-midAngle * RADIAN); + const ey = cy + lineEnd * Math.sin(-midAngle * RADIAN); + const isRight = ex > cx; + const elbowX = ex + (isRight ? 18 : -18); + const textX = elbowX + (isRight ? 4 : -4); + const textAnchor = isRight ? 'start' : 'end'; + const pct = `${(percent * 100).toFixed(1)}%`; + const labelColor = darkMode ? '#f8fafc' : '#0f172a'; + const lineColor = darkMode ? '#94a3b8' : '#64748b'; + const nameFontSize = isHovered ? '1.05rem' : '0.95rem'; + const pctFontSize = isHovered ? '0.95rem' : '0.85rem'; + const strokeWidth = isHovered ? 2.5 : 1.5; + + return ( + + + + + {name} + + + {pct} + + + + ); + }; + const onRolesChange = e => { setSelectedRoles(Array.from(e.target.selectedOptions, o => o.value)); }; const applyFilters = () => { + if (startDate && startDate > TODAY) { + setError('Start date cannot be in the future.'); + return; + } + if (endDate && endDate > TODAY) { + setError('End date cannot be in the future.'); + return; + } if (startDate && endDate && new Date(startDate) > new Date(endDate)) { - setError(null); - setChartData(null); - setTotal(0); - setLoading(false); + setError('Start date must be before end date.'); return; } + setError(null); setAppliedFilters({ startDate, endDate, roles: selectedRoles }); }; @@ -139,51 +257,10 @@ export default function ExperienceDonutChart() { setStartDate(''); setEndDate(''); setSelectedRoles([]); + setError(null); setAppliedFilters({ startDate: '', endDate: '', roles: [] }); }; - const DetailsPanel = () => { - if (!chartData || total === 0) return null; - - return ( -
- {chartData.map((d, idx) => { - const pct = total > 0 ? ((d.value / total) * 100).toFixed(1) : 0; - return ( -
setActiveIndex(idx)} - onMouseLeave={() => setActiveIndex(null)} - > - - {d.name} - {d.value.toLocaleString()} - {pct}% -
- ); - })} -
- ); - }; - - const CustomTooltip = ({ active, payload }) => { - if (!active || !payload?.length) return null; - const d = payload[0]?.payload; - const pct = total > 0 ? ((d.value / total) * 100).toFixed(1) : 0; - - return ( -
- {/* Corrected tooltip to use name and value from payload for visibility */} - {d.name} -
- Count: {d.value} -
- {pct}% of applicants -
- ); - }; - return (
setStartDate(e.target.value)} />
@@ -218,6 +296,7 @@ export default function ExperienceDonutChart() { type="date" className={styles['filter-input']} value={endDate} + max={TODAY} onChange={e => setEndDate(e.target.value)} />
@@ -261,32 +340,51 @@ export default function ExperienceDonutChart() { {loading && } {!loading && !error && chartData && total > 0 && ( - <> -
- - - setActiveIndex(i)} - onMouseLeave={() => setActiveIndex(null)} - > - {chartData.map((d, i) => ( - - ))} - - } /> +
+ + + setAnimationDone(true)} + activeIndex={hoveredIndex} + activeShape={renderActiveShape} + onMouseEnter={(_, index) => setHoveredIndex(index)} + onMouseLeave={() => setHoveredIndex(null)} + > + {visibleChartData.map(d => ( + + ))} + + {/* Inside counts rendered as a second label pass — animation disabled to prevent double-sweep */} + + {visibleChartData.map(d => ( + + ))} + + {animationDone && ( {total.toLocaleString()} - - -
- - - + )} +
+
+
)} {!loading && !error && (!chartData || total === 0) &&

No Data Available 😢

} diff --git a/src/components/ExperienceDonutChart/ExperienceDonutChart.module.css b/src/components/ExperienceDonutChart/ExperienceDonutChart.module.css index 7cc65e3a1d..8394c77c58 100644 --- a/src/components/ExperienceDonutChart/ExperienceDonutChart.module.css +++ b/src/components/ExperienceDonutChart/ExperienceDonutChart.module.css @@ -10,7 +10,7 @@ .experience-chart-container { position: relative; width: 100%; - max-width: 960px; + max-width: 1100px; margin: 2rem auto; padding: 1.75rem; background: #fff; @@ -200,23 +200,27 @@ .chart-area { width: 100%; min-width: 0; - display: flex; /* ← was commented out; enable flex centering */ + display: flex; flex-direction: column; - align-items: center; /* center horizontally */ - justify-content: center; /* center vertically within area */ + align-items: center; + justify-content: center; text-align: center; - padding-right: 100px !important; } .chart-canvas { display: flex; align-items: center; - justify-content: center; /* center the inner wrapper */ + justify-content: center; width: 100%; - max-width: 520px; - aspect-ratio: 1 / 1; /* keeps donut perfectly square */ - min-height: 240px; /* fallback for older browsers */ + max-width: 680px; + aspect-ratio: 1 / 1; + min-height: 400px; margin: 0 auto; + overflow: visible; +} + +.recharts-wrapper { + overflow: visible !important; } .recharts-wrapper, @@ -234,144 +238,6 @@ margin: 0 auto !important; } -/* ===================== Details (below donut) ===================== */ -.chart-details { - max-width: 960px; /* match .experience-chart-container */ - width: 100%; - margin: 1rem auto 0; - padding-inline: 0.5rem; - display: grid; - gap: 0.8rem; - - /* single strategy: fluid columns that never force overflow */ - grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); - place-items: stretch stretch; - overflow-x: hidden; /* safety net */ -} - -/* 2) Let cards fill the grid cell on desktop (remove narrow cap) */ -.detail-item { - display: grid; - grid-template-columns: 12px 1fr auto auto; /* dot | name | count | pct */ - column-gap: 8px; - align-items: center; - width: 100%; - max-width: none; /* fill grid column, don't cap */ - padding: 0.6rem 0.75rem; - - /* text-align: left; */ - - /* prevent overflow on long role names / unbroken strings */ - overflow-wrap: anywhere; - text-align: center; - justify-content: center; -} - -.detail-item:hover, -.detail-item.active { - background: #111827; - border-color: #0b1220; - box-shadow: 0 8px 18px rgb(2 6 23 / 12%); - transform: translateY(-1px); -} - -.detail-item:hover span, -.detail-item.active span { - color: #fff !important; -} - -/* Map children into the grid */ -.detail-dot { - grid-column: 1; - width: 10px; - height: 10px; - border-radius: 999px; - box-shadow: 0 0 0 2px rgb(255 255 255 / 70%); -} - -.detail-name { - grid-column: 2; - font-weight: 800; - color: #0f172a; - font-size: 0.9rem; -} - -.detail-sep { - display: none; -} /* we don’t need the bullet in a grid layout */ -.detail-count { - grid-column: 3; - font-weight: 800; - color: #0f172a; -} - -.detail-sub { - display: none; -} /* optional: hide to keep cards compact; show if you prefer */ -.detail-pct { - grid-column: 4; - margin-left: 0; - font-weight: 800; - color: #2563eb; -} - -/* .detail-item:hover, -.detail-item.active { - background: #111827; - border-color: #0b1220; - box-shadow: 0 8px 18px rgba(2,6,23,.12); - transform: translateY(-1px); -} */ - -/* .detail-item:hover span, -.detail-item.active span { color: #ffffff !important; } */ -.detail-item:hover .detail-pct, -.detail-item.active .detail-pct { - color: #93c5fd !important; -} /* keep contrast on hover */ - -@media (width <= 640px) { - .chart-details { - grid-template-columns: 1fr; - } - - .detail-item { - display: flex; /* simpler on mobile */ - flex-wrap: wrap; - justify-content: center; - text-align: center; - gap: 0.45rem 0.6rem; - } - - .detail-dot { - order: 1; - } - - .detail-name { - order: 2; - width: 100%; - font-size: 1rem; - } - - .detail-sep { - display: none; - } - - .detail-count { - order: 3; - } - - .detail-sub { - order: 4; - display: inline; - color: #475569; - } - - .detail-pct { - order: 5; - } -} - /* ===================== States ===================== */ .spinner-container { display: flex; @@ -403,200 +269,25 @@ margin: 0; } -.error-container { - padding: 2rem; - text-align: center; -} - -.error-message { - color: #dc2626; - font-weight: 700; - font-size: 0.95rem; - background: #fef2f2; - border: 1px solid #fecaca; - border-radius: 10px; - padding: 1.1rem 1.25rem; - display: inline-block; -} - -.no-data-container { - padding: 3rem 2rem; - text-align: center; -} - -.no-data-message { - font-size: 1.15rem; - font-weight: 800; - color: #6b7280; - margin: 0 0 0.4rem; -} - -.no-data-subtitle { - font-size: 0.95rem; - color: #9ca3af; - margin: 0; - font-style: italic; -} - -/* ===================== Tooltip ===================== */ - -/* Adjusted tooltip for Light Mode: Light background with dark text for contrast */ -.custom-tooltip { - background: rgb(255 255 255 / 96%) !important; - border: 1px solid #e2e8f0 !important; - border-radius: 10px !important; - padding: 0.9rem 1rem !important; - box-shadow: 0 12px 28px rgb(2 6 23 / 15%) !important; - backdrop-filter: blur(10px); - color: #0f172a !important; -} - -.tooltip-content { - color: #fff !important; - margin: 0 !important; - font-size: 0.875rem !important; - line-height: 1.5 !important; -} - -/* ===================== Slices ===================== */ -.pie-cell { - transition: opacity 0.15s ease, filter 0.15s ease, transform 0.15s ease; - cursor: pointer; -} - -.pie-cell:hover { - opacity: 0.9; - filter: brightness(1.05); -} - -/* .detail-item { text-align: center; justify-content: center; } */ - -/* ===================== Responsive tweaks ===================== */ -@media (width <= 640px) { - .filter-group, - .filter-actions { - max-width: 320px; - min-width: 220px; - } - - .chart-canvas { - max-width: 360px; - min-height: 220px; - } - - .chart-details { - grid-template-columns: 1fr; - } /* single column for clarity */ -} - -/* ===================== Print & Accessibility ===================== */ +/* ===================== Print & prefers-color-scheme ===================== */ @media print { - .experience-chart-container { + .experience-donut-chart-dark-mode .experience-chart-container { + background: #fff; + color: #0f172a; box-shadow: none; border: 1px solid #cbd5e1; } - - .filter-section { - display: none; - } -} - -@media (prefers-contrast: more) { - .experience-chart-container { - border: 2px solid #000; - } - - .filter-input, - .filter-select { - border: 2px solid #000; - } } -@media (prefers-reduced-motion: reduce) { - .spinner { - animation: none; - } - - .pie-cell, - .btn, - .filter-input, - .filter-select { - transition: none; - } -} - -@media (width <= 640px) { - .chart-details { - grid-template-columns: 1fr; - } - - .detail-item { - display: flex; - flex-wrap: wrap; - justify-content: center; - text-align: center; - gap: 0.45rem 0.6rem; - } - - .detail-name { - width: 100%; - font-size: 1rem; - } -} - -/* --- Ensure detail cards always show a visible background --- */ -.chart-details .detail-item { - background-color: #f1f5f9; /* stronger than #f8fafc, more visible */ - border: 1px solid #e2e8f0; - border-radius: 10px; - box-shadow: 0 2px 8px rgb(2 6 23 / 6%); - background-clip: padding-box; - transition: background-color 0.15s ease, border-color 0.15s ease, box-shadow 0.2s ease, - transform 0.15s ease; -} - -/* Keep dark hover/active but ensure contrast is clear */ -.chart-details .detail-item:hover, -.chart-details .detail-item.active { - background-color: #111827; - border-color: #0b1220; - box-shadow: 0 8px 18px rgb(2 6 23 / 12%); -} - -/* If your app has global card or utility classes that might strip bg, - this raises specificity a bit further without !important. */ -.experience-chart-container .chart-details .detail-item { - background-color: #f1f5f9; -} - -/* Optional: high-contrast users still get borders */ -@media (prefers-contrast: more) { - .chart-details .detail-item { - border-color: #0f172a; +/* Keep prefers-color-scheme consistent with your chosen dark */ +@media (prefers-color-scheme: dark) { + .experience-donut-chart-dark-mode .experience-chart-container { + background: #1c2441; } } -/* --- Make chart-driven active state win over base card styles --- */ -.chart-details .detail-item.active { - background-color: #111827 !important; /* win against base bg */ - border-color: #0b1220 !important; - box-shadow: 0 8px 18px rgb(2 6 23 / 12%); - color: #fff; -} - -.chart-details .detail-item.active span { - color: #fff !important; /* keep text readable */ -} - -/* Optional: smooth the state change */ - -/* .chart-details .detail-item { - transition: background-color .15s ease, border-color .15s ease, box-shadow .2s ease, transform .15s ease; -} */ - /* ===================== DARK MODE (scoped to wrapper) ===================== */ -/* Ensure every surface flips dark */ .experience-donut-chart.experience-donut-chart-dark-mode, .experience-donut-chart-dark-mode, .experience-donut-chart-dark-mode .experience-chart-container, @@ -610,7 +301,6 @@ margin-top: -35px; } -/* Top gradient bar */ .experience-donut-chart-dark-mode .experience-chart-container::before { background: linear-gradient( 90deg, @@ -622,16 +312,15 @@ ); } -/* Title & header */ .experience-donut-chart-dark-mode .chart-title { - color: #fff !important; /* “Applicants by Experience” */ + color: #fff !important; } .experience-donut-chart-dark-mode .chart-header { color: #f1f5f9; } -/* ===================== Filters ===================== */ +/* ===================== Filters (dark) ===================== */ .experience-donut-chart-dark-mode .filter-section { background: #232d53; border-color: #3b4a75; @@ -660,12 +349,11 @@ box-shadow: 0 0 0 4px rgb(96 165 250 / 25%); } -/* Date input icons */ .experience-donut-chart-dark-mode .filter-input[type='date']::-webkit-calendar-picker-indicator { filter: invert(1) opacity(0.9); } -/* ===================== Buttons ===================== */ +/* ===================== Buttons (dark) ===================== */ .experience-donut-chart-dark-mode .btn { color: #e5e7eb; } @@ -689,70 +377,18 @@ box-shadow: 0 6px 20px rgb(0 0 0 / 35%); } -/* ===================== Chart canvas & Recharts ===================== */ +/* ===================== Chart canvas (dark) ===================== */ .experience-donut-chart-dark-mode .chart-canvas { background: transparent; } -/* Default Recharts labels/ticks/legend */ .experience-donut-chart-dark-mode .recharts-text, -.experience-donut-chart-dark-mode .recharts-label, -.experience-donut-chart-dark-mode .recharts-legend-item-text { +.experience-donut-chart-dark-mode .recharts-label { fill: #e5e7eb !important; color: #e5e7eb !important; } -/* Grid lines if any */ -.experience-donut-chart-dark-mode .recharts-cartesian-grid-horizontal line, -.experience-donut-chart-dark-mode .recharts-cartesian-grid-vertical line { - stroke: #3b4a75 !important; -} - -/* Strong contrast on hover/active (details + svg labels) */ -.experience-donut-chart-dark-mode .chart-details .detail-item:hover span, -.experience-donut-chart-dark-mode .chart-details .detail-item.active span { - color: #fff !important; -} - -.experience-donut-chart-dark-mode .recharts-sector:hover text, -.experience-donut-chart-dark-mode .recharts-pie-sector:hover text { - fill: #fff !important; -} - -/* ===================== Details (cards under chart) ===================== */ -.experience-donut-chart-dark-mode .chart-details { - color: #e5e7eb; -} - -.experience-donut-chart-dark-mode .chart-details .detail-item { - background-color: #2a355f; - border: 1px solid #3b4a75; - color: #e2e8f0; - box-shadow: 0 2px 8px rgb(0 0 0 / 35%); -} - -.experience-donut-chart-dark-mode .detail-name, -.experience-donut-chart-dark-mode .detail-count { - color: #e5e7eb; -} - -.experience-donut-chart-dark-mode .detail-item .detail-pct { - color: #93c5fd; -} - -.experience-donut-chart-dark-mode .detail-dot { - box-shadow: 0 0 0 2px rgb(28 36 65 / 70%); -} - -/* Hover/active state harmonized with palette */ -.experience-donut-chart-dark-mode .chart-details .detail-item:hover, -.experience-donut-chart-dark-mode .chart-details .detail-item.active { - background-color: #232d53; - border-color: #1c2441; - box-shadow: 0 8px 18px rgb(0 0 0 / 45%); -} - -/* ===================== States (loading, error, empty) ===================== */ +/* ===================== States (dark) ===================== */ .experience-donut-chart-dark-mode .spinner { border-color: #2a355f; border-top-color: #60a5fa; @@ -768,41 +404,46 @@ border-color: #7f2a3a; } -.experience-donut-chart-dark-mode .no-data-message { - color: #dbe3ff; -} - -.experience-donut-chart-dark-mode .no-data-subtitle { - color: #b8c2ea; +/* ===================== Slices ===================== */ +.pie-cell { + transition: opacity 0.15s ease, filter 0.15s ease; } -/* ===================== Tooltip ===================== */ +/* ===================== Responsive ===================== */ +@media (width <= 640px) { + .filter-group, + .filter-actions { + max-width: 320px; + min-width: 220px; + } -/* Adjusted tooltip for Dark Mode: Scoped to .experience-donut-chart-dark-mode */ -.experience-donut-chart-dark-mode .custom-tooltip { - background-color: rgb(42 53 95 / 96%) !important; - color: #f1f5f9 !important; - border: 1px solid #3b4a75 !important; - box-shadow: 0 12px 28px rgb(0 0 0 / 45%) !important; + .chart-canvas { + max-width: 360px; + min-height: 260px; + } } -.experience-donut-chart-dark-mode .custom-tooltip strong { - color: #fff !important; -} +/* ===================== Accessibility ===================== */ +@media (prefers-contrast: more) { + .experience-chart-container { + border: 2px solid #000; + } -/* ===================== Print & prefers-color-scheme ===================== */ -@media print { - .experience-donut-chart-dark-mode .experience-chart-container { - background: #fff; - color: #0f172a; - box-shadow: none; - border: 1px solid #cbd5e1; + .filter-input, + .filter-select { + border: 2px solid #000; } } -/* Keep prefers-color-scheme consistent with your chosen dark */ -@media (prefers-color-scheme: dark) { - .experience-donut-chart-dark-mode .experience-chart-container { - background: #1c2441; +@media (prefers-reduced-motion: reduce) { + .spinner { + animation: none; + } + + .btn, + .filter-input, + .filter-select, + .pie-cell { + transition: none; } } diff --git a/src/components/Header/Header.jsx b/src/components/Header/Header.jsx index d06b7cca48..a8add71b10 100644 --- a/src/components/Header/Header.jsx +++ b/src/components/Header/Header.jsx @@ -39,6 +39,7 @@ import { BLUE_SQUARE_EMAIL_MANAGEMENT, DASHBOARD, JOB_ANALYTICS_REPORT, + BM_DASHBOARD, LOGOUT, OTHER_LINKS, PERMISSIONS_MANAGEMENT, @@ -57,7 +58,6 @@ import { VIEW_PROFILE, WEEKLY_SUMMARIES_REPORT, WELCOME, - BM_DASHBOARD } from '../../languages/en/ui'; import hasPermission, { cantUpdateDevAdminDetails } from '../../utils/permissions'; import PermissionWatcher from '../Auth/PermissionWatcher'; @@ -411,6 +411,7 @@ export function Header(props) { const showBMDashboard = location.pathname.startsWith('/bmdashboard'); + return (
@@ -507,11 +508,12 @@ export function Header(props) { - {showBMDashboard && ( - - {BM_DASHBOARD} - - + {showBMDashboard && ( + + + {BM_DASHBOARD} + + )} diff --git a/src/components/LBDashboard/BarGraphs/CompareGraphs.jsx b/src/components/LBDashboard/BarGraphs/CompareGraphs.jsx index 3c8453582f..5d015fbe82 100644 --- a/src/components/LBDashboard/BarGraphs/CompareGraphs.jsx +++ b/src/components/LBDashboard/BarGraphs/CompareGraphs.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import { ResponsiveContainer, BarChart, @@ -13,87 +12,6 @@ import { import { Card, CardBody } from 'reactstrap'; import styles from '../LBDashboard.module.css'; -const getHorizontalAxes = ({ - xDomain, - xTicks, - valueFormatter, - tickColor, - xLabel, - nameKey, - yCategoryWidth, - yTickFormatter, - showYAxisTitle, - yLabel, -}) => ({ - xAxis: ( - - ), - yAxis: ( - - ), -}); - -const getVerticalAxes = (nameKey, tickColor, xLabel, yDomain, yTicks, valueFormatter, yLabel) => ({ - xAxis: ( - - ), - yAxis: ( - - ), -}); - export function CompareBarGraph({ title, metricLabel, @@ -113,31 +31,20 @@ export function CompareBarGraph({ xTicks, yTicks, barSize, - height = 420, - yCategoryWidth = 70, - margins = { top: 16, right: 20, bottom: 46, left: 0 }, + height = 320, + yCategoryWidth = 140, + margins = { top: 8, right: 24, bottom: 36, left: 36 }, maxBars, showYAxisTitle = true, yTickFormatter, - darkMode = false, }) { const isHorizontal = orientation === 'horizontal'; - const tickColor = darkMode ? '#e1e1e1' : '#444'; - const gridColor = darkMode ? '#3a506b' : '#e0e0e0'; return ( - - + + {/* Title row + chips */} -
+
{title} {showMetricPill && ( @@ -149,14 +56,8 @@ export function CompareBarGraph({
{headerChips.map((c, i) => (
-
- {c.label} -
-
+
{c.label}
+
{String(c.value).toUpperCase()}
@@ -172,64 +73,58 @@ export function CompareBarGraph({ layout={isHorizontal ? 'vertical' : 'horizontal'} margin={margins} > - - {isHorizontal - ? (() => { - const axes = getHorizontalAxes({ - xDomain, - xTicks, - valueFormatter, - tickColor, - xLabel, - nameKey, - yCategoryWidth, - yTickFormatter, - showYAxisTitle, - yLabel, - }); - return ( - <> - {axes.xAxis} - {axes.yAxis} - - ); - })() - : (() => { - const axes = getVerticalAxes( - nameKey, - tickColor, - xLabel, - yDomain, - yTicks, - valueFormatter, - yLabel, - ); - return ( - <> - {axes.xAxis} - {axes.yAxis} - - ); - })()} + + {isHorizontal ? ( + <> + + + + ) : ( + <> + + + + )} [valueFormatter(v), tooltipLabel || metricLabel || title]} labelFormatter={lbl => `${lbl}`} - contentStyle={{ - background: darkMode ? '#1c2541' : '#fff', - border: `1px solid ${darkMode ? '#3a506b' : '#ccc'}`, - color: darkMode ? '#e1e1e1' : '#333', - }} - itemStyle={{ color: darkMode ? '#e1e1e1' : '#333' }} - labelStyle={{ color: darkMode ? '#e1e1e1' : '#333', fontWeight: 600 }} /> @@ -239,41 +134,3 @@ export function CompareBarGraph({ ); } - -CompareBarGraph.propTypes = { - title: PropTypes.string.isRequired, - metricLabel: PropTypes.string, - tooltipLabel: PropTypes.string, - showMetricPill: PropTypes.bool, - orientation: PropTypes.oneOf(['horizontal', 'vertical']).isRequired, - data: PropTypes.arrayOf(PropTypes.object).isRequired, - nameKey: PropTypes.string.isRequired, - valueKey: PropTypes.string.isRequired, - xLabel: PropTypes.string.isRequired, - yLabel: PropTypes.string.isRequired, - barColor: PropTypes.string, - valueFormatter: PropTypes.func, - headerChips: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.string.isRequired, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, - }), - ), - xDomain: PropTypes.array, - yDomain: PropTypes.array, - xTicks: PropTypes.array, - yTicks: PropTypes.array, - barSize: PropTypes.number, - height: PropTypes.number, - yCategoryWidth: PropTypes.number, - margins: PropTypes.shape({ - top: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - }), - maxBars: PropTypes.number, - showYAxisTitle: PropTypes.bool, - yTickFormatter: PropTypes.func, - darkMode: PropTypes.bool, -}; diff --git a/src/components/LBDashboard/BarGraphs/ComparePropertiesRatings.jsx b/src/components/LBDashboard/BarGraphs/ComparePropertiesRatings.jsx deleted file mode 100644 index 569895c604..0000000000 --- a/src/components/LBDashboard/BarGraphs/ComparePropertiesRatings.jsx +++ /dev/null @@ -1,184 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; -import moment from 'moment'; -import { CompareBarGraph } from './CompareGraphs'; -import { randomInt } from '../lbUtils'; - -export function ComparePropertiesRatings({ - fromDate, - toDate, - listingBiddingFilter, - selectedMetricKey, - darkMode, -}) { - const [propertiesData, setPropertiesData] = useState([]); - - useEffect(() => { - // Generate mock properties data based on filters - const generateMockProperties = () => { - const properties = [ - 'House AB', - 'Room A', - 'Room C', - 'Room A34', - 'Room 5', - 'Studio B12', - 'Apartment D', - ]; - - // Calculate a seed based on date range - const daysDiff = moment(toDate).diff(moment(fromDate), 'days'); - const dateSeed = daysDiff > 0 ? daysDiff / 365 : 0.1; - - // Different ranges based on listing/bidding filter - let multiplier = 1; - if (listingBiddingFilter === 'bidding') multiplier = 0.85; - else if (listingBiddingFilter === 'listing') multiplier = 1.1; - - return properties - .map(p => { - let value = 0; - - // Generate different data based on metric - switch (selectedMetricKey) { - case 'avgRating': { - // Generate ratings between 2.5 and 5.0, affected by filters - const baseRating = randomInt(250, 500) / 100; - value = Math.min(5, Math.max(1, baseRating * multiplier * dateSeed)); - break; - } - case 'pageVisits': - value = Math.floor(randomInt(20, 150) * multiplier * dateSeed); - break; - case 'numBids': - value = Math.floor(randomInt(5, 50) * multiplier * dateSeed); - break; - case 'avgBid': - case 'finalPrice': - value = randomInt(30000, 120000); - break; - case 'occupancyRate': - value = randomInt(60, 98); - break; - case 'avgStay': - value = randomInt(5, 40); - break; - default: - value = randomInt(10, 100); - } - - return { - property: p, - value: ['avgRating'].includes(selectedMetricKey) - ? Number(value.toFixed(2)) - : Math.floor(value), - }; - }) - .sort((a, b) => b.value - a.value) - .slice(0, 5); - }; - - setTimeout(() => { - setPropertiesData(generateMockProperties()); - }, 300); - }, [fromDate, toDate, listingBiddingFilter, selectedMetricKey]); - - // Determine labels and formatting based on metric - const getMetricConfig = () => { - switch (selectedMetricKey) { - case 'avgRating': - return { - yLabel: 'Average Rating', - yDomain: [0, 5], - yTicks: [0, 1, 2, 3, 4, 5], - valueFormatter: v => Number(v).toFixed(2), - }; - case 'pageVisits': - return { - yLabel: 'Page Visits', - yDomain: undefined, - yTicks: undefined, - valueFormatter: Number, - tooltipLabel: 'Page Visits', - }; - case 'numBids': - return { - yLabel: 'Number of Bids', - yDomain: undefined, - yTicks: undefined, - valueFormatter: Number, - tooltipLabel: 'Number of Bids', - }; - case 'avgBid': - case 'finalPrice': - return { - yLabel: selectedMetricKey === 'avgBid' ? 'Average Bid (₹)' : 'Final Price (₹)', - yDomain: undefined, - yTicks: undefined, - valueFormatter: v => `₹${Number(v).toLocaleString()}`, - tooltipLabel: selectedMetricKey === 'avgBid' ? 'Average Bid' : 'Final Price', - }; - case 'occupancyRate': - return { - yLabel: 'Occupancy Rate (%)', - yDomain: [0, 100], - yTicks: [0, 25, 50, 75, 100], - valueFormatter: v => `${v}%`, - tooltipLabel: 'Occupancy Rate', - }; - case 'avgStay': - return { - yLabel: 'Average Stay (days)', - yDomain: undefined, - yTicks: undefined, - valueFormatter: v => `${v} days`, - tooltipLabel: 'Average Stay', - }; - default: - return { - yLabel: 'Value', - yDomain: undefined, - yTicks: undefined, - valueFormatter: Number, - tooltipLabel: 'Value', - }; - } - }; - - const metricConfig = getMetricConfig(); - - return ( - - ); -} - -ComparePropertiesRatings.propTypes = { - fromDate: PropTypes.instanceOf(Date).isRequired, - toDate: PropTypes.instanceOf(Date).isRequired, - listingBiddingFilter: PropTypes.string.isRequired, - selectedMetricKey: PropTypes.string.isRequired, - darkMode: PropTypes.bool, -}; diff --git a/src/components/LBDashboard/LBDashboard.jsx b/src/components/LBDashboard/LBDashboard.jsx index 4c1b5c8f85..fbbb1f46d7 100644 --- a/src/components/LBDashboard/LBDashboard.jsx +++ b/src/components/LBDashboard/LBDashboard.jsx @@ -2,8 +2,6 @@ import { useState, useEffect, useMemo } from 'react'; import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import moment from 'moment'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; import { Container, @@ -20,27 +18,28 @@ import { import DemandOverTime from './LbAnalytics/DemandOverTime/DemandOverTime'; import WinningVsAverageBidChart from './LbAnalytics/WinningVsAverageBidChart/WinningVsAverageBidChart'; import ReviewWordCloud from './ReviewWordCloud/ReviewWordCloud'; +import { ComparePieChart } from './PieChart/ComparePieChart'; import RatingDistribution from './RatingDistribution/RatingDistribution'; import { CompareBarGraph } from './BarGraphs/CompareGraphs'; -import { ComparePropertiesRatings } from './BarGraphs/ComparePropertiesRatings'; +import httpService from '../../services/httpService'; +import { ApiEndpoint } from '../../utils/URL'; import styles from './LBDashboard.module.css'; import ConversionFunnel from './LbAnalytics/ConversionFunnel/ConversionFunnel'; -import { randomInt } from './lbUtils'; const METRIC_OPTIONS = { DEMAND: [ - { key: 'pageVisits', label: 'Page Visits', biddingOnly: false }, - { key: 'numBids', label: 'Number of Bids', biddingOnly: true }, - { key: 'avgRating', label: 'Average Rating', biddingOnly: false }, + { key: 'pageVisits', label: 'Page Visits' }, + { key: 'numBids', label: 'Number of Bids' }, + { key: 'avgRating', label: 'Average Rating' }, ], REVENUE: [ - { key: 'avgBid', label: 'Average Bid', biddingOnly: true }, - { key: 'finalPrice', label: 'Final Price / Income', biddingOnly: true }, + { key: 'avgBid', label: 'Average Bid' }, + { key: 'finalPrice', label: 'Final Price / Income' }, ], VACANCY: [ - { key: 'occupancyRate', label: 'Occupancy Rate (% days not vacant)', biddingOnly: false }, - { key: 'avgStay', label: 'Average Duration of Stay', biddingOnly: false }, + { key: 'occupancyRate', label: 'Occupancy Rate (% days not vacant)' }, + { key: 'avgStay', label: 'Average Duration of Stay' }, ], }; @@ -60,6 +59,24 @@ const DEFAULTS = { VACANCY: 'occupancyRate', }; +// Dummy data for the pie chart - matching the image specifications +const VILLAGE_COMPARISON_DATA = [ + { name: 'Earthbag', value: 10 }, + { name: 'Straw Bale', value: 50 }, + { name: 'Cob Village', value: 60 }, + { name: 'Tree House', value: 10 }, + { name: 'Recycle Materials', value: 70 }, +]; + +// Dummy data for Property graph (keep until backend is wired) +const propertiesData = [ + { property: 'House AB', value: 4.72 }, + { property: 'Room A', value: 4.5 }, + { property: 'Room C', value: 4.05 }, + { property: 'Room A34', value: 3.91 }, + { property: 'Room 5', value: 3.0 }, +]; + const getClassNames = (baseClass, darkClass, darkMode) => `${baseClass} ${darkMode ? darkClass : ''}`; function GraphCard({ title, metricLabel, darkMode }) { @@ -91,84 +108,60 @@ GraphCard.propTypes = { const CategoryControls = ({ categoryKey, label, + activeCategory, selectedMetricKey, openDD, darkMode, - listingBiddingFilter, onCategoryClick, onMetricPick, onToggleDD, -}) => { - const availableMetrics = METRIC_OPTIONS[categoryKey].filter( - m => listingBiddingFilter !== 'listing' || !m.biddingOnly, - ); - - const isDisabled = availableMetrics.length === 0; - const isSelectedCategory = METRIC_OPTIONS[categoryKey].some(m => m.key === selectedMetricKey); - - let btnActiveClass = ''; - if (isSelectedCategory) { - btnActiveClass = darkMode ? styles.darkActiveFilterBtn : styles.activeFilterBtn; - } else if (darkMode) { - btnActiveClass = styles.darkFilterBtn; - } - - let btnColor; - if (darkMode) { - btnColor = isSelectedCategory ? 'dark' : 'success'; - } - - return ( - <> - +}) => ( + <> + - !isDisabled && onToggleDD(categoryKey)} - className={`${styles.dd} ${isSelectedCategory ? styles.ddActive : ''}`} - > - - - {availableMetrics.map(m => ( - onMetricPick(categoryKey, m.key)} - className={`${styles.dropdownItem} ${ - selectedMetricKey === m.key ? styles.dropdownActive : '' - } ${darkMode ? styles.darkDropdownItem : ''}`} - > - {m.label} - - ))} - - - - ); -}; + onToggleDD(categoryKey)} + className={styles.dd} + > + + + {METRIC_OPTIONS[categoryKey].map(m => ( + onMetricPick(categoryKey, m.key)} + className={`${styles.dropdownItem} ${ + selectedMetricKey === m.key ? styles.dropdownActive : '' + } ${darkMode ? styles.darkDropdownItem : ''}`} + > + {m.label} + + ))} + + + +); CategoryControls.propTypes = { categoryKey: PropTypes.string.isRequired, label: PropTypes.string.isRequired, + activeCategory: PropTypes.string.isRequired, selectedMetricKey: PropTypes.string.isRequired, openDD: PropTypes.object.isRequired, darkMode: PropTypes.bool, - listingBiddingFilter: PropTypes.string.isRequired, onCategoryClick: PropTypes.func.isRequired, onMetricPick: PropTypes.func.isRequired, onToggleDD: PropTypes.func.isRequired, @@ -200,140 +193,53 @@ const FilterSection = ({ selectedMetricKey, openDD, metricLabel, - fromDate, - toDate, - compareType, - listingBiddingFilter, onCategoryClick, onMetricPick, onToggleDD, - onFromDateChange, - onToDateChange, - onCompareTypeChange, - onListingBiddingChange, }) => (
-
- {/* From Date */} -
- - -
- - {/* To Date */} -
- - -
- - {/* Compare By */} -
- - -
- - {/* Type */} -
- - -
+
+ Choose Metric to view
- {/* Metric Selection */} -
-
- Metric -
- - - - - -
- Current: {metricLabel} -
+ + + + + + +
+ Current metric: {metricLabel}
); @@ -344,17 +250,9 @@ FilterSection.propTypes = { selectedMetricKey: PropTypes.string.isRequired, openDD: PropTypes.object.isRequired, metricLabel: PropTypes.string, - fromDate: PropTypes.instanceOf(Date).isRequired, - toDate: PropTypes.instanceOf(Date).isRequired, - compareType: PropTypes.string.isRequired, - listingBiddingFilter: PropTypes.string.isRequired, onCategoryClick: PropTypes.func.isRequired, onMetricPick: PropTypes.func.isRequired, onToggleDD: PropTypes.func.isRequired, - onFromDateChange: PropTypes.func.isRequired, - onToDateChange: PropTypes.func.isRequired, - onCompareTypeChange: PropTypes.func.isRequired, - onListingBiddingChange: PropTypes.func.isRequired, }; const AnalysisSection = ({ title, darkMode, children }) => ( @@ -384,83 +282,66 @@ export function LBDashboard() { const [openDD, setOpenDD] = useState({ DEMAND: false, REVENUE: false, VACANCY: false }); const darkMode = useSelector(state => state.theme.darkMode); - // Date range state - default to last 365 days - const [fromDate, setFromDate] = useState( - moment() - .subtract(365, 'days') - .toDate(), - ); - const [toDate, setToDate] = useState(moment().toDate()); - - // Compare type: 'villages' or 'properties' - const [compareType, setCompareType] = useState('villages'); - - // Listing/Bidding filter: 'all', 'listing', 'bidding' - const [listingBiddingFilter, setListingBiddingFilter] = useState('all'); - - // --- Mock Villages data --- + // --- Villages backend data --- const [villagesRaw, setVillagesRaw] = useState([]); const [loadingVillages, setLoadingVillages] = useState(false); - const [villagesError] = useState(null); + const [villagesError, setVillagesError] = useState(null); useEffect(() => { - setLoadingVillages(true); - - // Generate mock data based on filters - const generateMockVillages = () => { - const villages = [ - { name: 'Tree House Village', regionId: '7' }, - { name: 'Cob Village', regionId: '3' }, - { name: 'Earthbag Village', regionId: '1' }, - { name: 'Recycled Materials Village', regionId: '6' }, - { name: 'Straw Bale Village', regionId: '2' }, - { name: 'Earth Block Village', regionId: '4' }, - { name: 'Duplicable City Center', regionId: 'C' }, - { name: 'Shipping Container Village', regionId: '5' }, - ]; - - // Calculate a seed based on date range - const daysDiff = moment(toDate).diff(moment(fromDate), 'days'); - const dateSeed = daysDiff > 0 ? daysDiff : 1; - - return villages.map(v => { - // Different ranges based on listing/bidding filter - let multiplier = 1; - if (listingBiddingFilter === 'bidding') multiplier = 0.6; - else if (listingBiddingFilter === 'listing') multiplier = 1.2; + let mounted = true; + + (async () => { + try { + setLoadingVillages(true); + setVillagesError(null); + + const res = await httpService.get(`${ApiEndpoint}/villages`); + if (!mounted) return; + + setVillagesRaw(Array.isArray(res?.data) ? res.data : []); + } catch (e) { + if (!mounted) return; + setVillagesError('Failed to load villages'); + setVillagesRaw([]); + } finally { + if (mounted) setLoadingVillages(false); + } + })(); - return { - _id: v.regionId, - name: v.name, - regionId: v.regionId, - analytics: { - pageVisits: Math.floor(randomInt(10, 80) * multiplier * (dateSeed / 30)), - numberOfBids: Math.floor(randomInt(5, 40) * multiplier * (dateSeed / 60)), - averageBid: randomInt(20000, 100000), - finalPrice: randomInt(50000, 150000), - occupancyRate: randomInt(50, 98), - averageStay: randomInt(3, 45), - }, - }; - }); + return () => { + mounted = false; }; + }, []); - setTimeout(() => { - setVillagesRaw(generateMockVillages()); - setLoadingVillages(false); - }, 300); - }, [fromDate, toDate, listingBiddingFilter]); - - const dateRange = useMemo(() => [moment(fromDate).startOf('day'), moment(toDate).endOf('day')], [ - fromDate, - toDate, - ]); + const dateRange = [ + moment() + .subtract(1, 'year') + .startOf('month'), + moment().endOf('month'), + ]; const getMetricLabel = () => { const all = Object.values(METRIC_OPTIONS).flat(); return (all.find(o => o.key === selectedMetricKey) || {}).label || ''; }; + // Decide which numeric value to calculate for the bar chart + const effectiveMetric = useMemo(() => { + switch (selectedMetricKey) { + case 'avgBid': + case 'finalPrice': + return 'avgCurrentBid'; + case 'pageVisits': + case 'numBids': + case 'avgRating': + case 'occupancyRate': + case 'avgStay': + return 'totalCurrentBid'; + default: + return 'totalCurrentBid'; + } + }, [selectedMetricKey]); + const valueFormatter = useMemo(() => { if (selectedMetricKey === 'avgRating') return v => Number(v).toFixed(2); if (selectedMetricKey === 'occupancyRate') return v => `${v}%`; @@ -477,41 +358,12 @@ export function LBDashboard() { return villagesRaw .map(v => { - const analytics = v.analytics || {}; - let value = 0; - - // Map selected metric to analytics field - switch (selectedMetricKey) { - case 'pageVisits': - value = analytics.pageVisits || 0; - break; - case 'numBids': - value = analytics.numberOfBids || 0; - break; - case 'avgBid': - value = analytics.averageBid || 0; - break; - case 'finalPrice': - value = analytics.finalPrice || 0; - break; - case 'avgRating': - value = analytics.averageRating || randomInt(30, 50) / 10; - break; - case 'occupancyRate': - value = analytics.occupancyRate || 0; - break; - case 'avgStay': - value = analytics.averageStay || 0; - break; - default: { - // Fallback to old logic for properties.currentBid - const props = Array.isArray(v.properties) ? v.properties : []; - const bids = props.map(p => Number(p?.currentBid || 0)); - const sum = bids.reduce((a, b) => a + b, 0); - value = sum; - break; - } - } + const props = Array.isArray(v.properties) ? v.properties : []; + const bids = props.map(p => Number(p?.currentBid || 0)); + const sum = bids.reduce((a, b) => a + b, 0); + const avg = bids.length ? sum / bids.length : 0; + + const value = effectiveMetric === 'avgCurrentBid' ? avg : sum; return { village: v.name || v.regionId || 'Unknown', @@ -520,7 +372,7 @@ export function LBDashboard() { }) .sort((a, b) => b.value - a.value) .slice(0, 20); - }, [villagesRaw, selectedMetricKey]); + }, [villagesRaw, effectiveMetric]); const stripVillageWord = s => { const str = String(s || ''); @@ -537,38 +389,33 @@ export function LBDashboard() { [villagesData], ); - const handleCategoryClick = category => { - setActiveCategory(category); - const availableMetrics = METRIC_OPTIONS[category].filter( - m => listingBiddingFilter !== 'listing' || !m.biddingOnly, - ); - const defaultMetric = availableMetrics.find(m => m.key === DEFAULTS[category]); - setSelectedMetricKey(defaultMetric ? defaultMetric.key : availableMetrics[0]?.key); + const getAvailableMetrics = () => { + return Object.values(METRIC_OPTIONS).flat(); }; - const handleMetricPick = (category, key) => { + const handleCategoryClick = category => { setActiveCategory(category); - setSelectedMetricKey(key); + setSelectedMetricKey(DEFAULTS[category]); }; - const handleListingBiddingChange = newFilter => { - setListingBiddingFilter(newFilter); - if (newFilter === 'listing') { - const currentOption = Object.values(METRIC_OPTIONS) - .flat() - .find(m => m.key === selectedMetricKey); - if (currentOption?.biddingOnly) { - const availableMetrics = METRIC_OPTIONS[activeCategory].filter(m => !m.biddingOnly); - if (availableMetrics.length > 0) { - setSelectedMetricKey(availableMetrics[0].key); - } else { - setActiveCategory('DEMAND'); - setSelectedMetricKey('pageVisits'); - } + const handleMetricChange = newMetricKey => { + setSelectedMetricKey(newMetricKey); + + // Update active category based on the selected metric + const allMetrics = Object.entries(METRIC_OPTIONS); + for (const [category, metrics] of allMetrics) { + if (metrics.some(m => m.key === newMetricKey)) { + setActiveCategory(category); + break; } } }; + const handleMetricPick = (category, key) => { + setActiveCategory(category); + setSelectedMetricKey(key); + }; + const toggleDD = category => setOpenDD(s => ({ ...s, [category]: !s[category] })); const goBack = () => globalThis.history.back(); @@ -588,102 +435,110 @@ export function LBDashboard() { selectedMetricKey={selectedMetricKey} openDD={openDD} metricLabel={metricLabel} - fromDate={fromDate} - toDate={toDate} - compareType={compareType} - listingBiddingFilter={listingBiddingFilter} onCategoryClick={handleCategoryClick} onMetricPick={handleMetricPick} onToggleDD={toggleDD} - onFromDateChange={setFromDate} - onToDateChange={setToDate} - onCompareTypeChange={setCompareType} - onListingBiddingChange={handleListingBiddingChange} /> - {compareType === 'villages' && ( - - - - - - - - {loadingVillages && ( -
- Loading villages… -
- )} - {villagesError && ( -
{villagesError}
- )} - - {!loadingVillages && !villagesError && ( - - )} - - - - - -
-
- )} - - {compareType === 'properties' && ( - - - - + + + + + + + {loadingVillages && ( +
Loading villages…
+ )} + {villagesError && ( +
{villagesError}
+ )} + + {!loadingVillages && !villagesError && ( + - - - - - -
-
- )} + )} + + + + + + + + + + + + + + + + Number(v).toFixed(2)} + tooltipLabel="Average Rating" + headerChips={[ + { label: 'List/Bid', value: 'ALL' }, + { label: 'Dates', value: 'ALL' }, + { label: 'Metric', value: 'ALL' }, + { label: 'Properties', value: 'ALL' }, + ]} + /> + + +
diff --git a/src/components/LBDashboard/LBDashboard.module.css b/src/components/LBDashboard/LBDashboard.module.css index f0aaa64afa..7ce7686307 100644 --- a/src/components/LBDashboard/LBDashboard.module.css +++ b/src/components/LBDashboard/LBDashboard.module.css @@ -69,34 +69,12 @@ body { background: #fff; border: 1px solid #ece7f4; border-radius: 14px; - padding: 16px; - margin: 0 15px 18px; -} - -.filterGrid { + padding: 14px 16px; display: grid; - grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); - gap: 16px; - margin-bottom: 16px; -} - -.filterGroup { - display: flex; - flex-direction: column; - gap: 6px; -} - -.metricSection { - display: flex; + grid-template-columns: auto 1fr auto; + gap: 12px 16px; align-items: center; - gap: 12px; - flex-wrap: wrap; - padding-top: 12px; - border-top: 1px solid #ece7f4; -} - -.darkFilterBar .metricSection { - border-top-color: #34495E; + margin: 0 15px 18px; } @media (max-width: 640px) { @@ -112,53 +90,12 @@ body { .filterLabel { font-size: 12px; color: #4a4a4a; - font-weight: 600; } .darkFilterLabel { color: #b0b0b0; } -.dateInput, -.selectInput { - padding: 8px 10px; - border: 1px solid #ccc; - border-radius: 6px; - font-size: 14px; - cursor: pointer; - background: #fff; -} - -.dateInput:focus, -.selectInput:focus { - outline: none; - border-color: #6a4ff7; -} - -.darkDateInput, -.darkSelectInput { - background: #34495E; - border-color: #4A5F7F; - color: #fff; -} - -.darkSelectInput option { - background: #34495E; - color: #fff; -} - -.selectInput { - appearance: none; - background-image: url('data:image/svg+xml;charset=UTF-8,'); - background-repeat: no-repeat; - background-position: right 10px center; - padding-right: 32px; -} - -.darkSelectInput { - background-image: url('data:image/svg+xml;charset=UTF-8,'); -} - .categoryGroup { display: flex; align-items: center; @@ -167,8 +104,8 @@ body { } .filterBtn { - background: #5cb85c !important; - border-color: #5cb85c !important; + background: #6a4ff7 !important; + border-color: #6a4ff7 !important; color: #fff !important; border-radius: 16px; padding: 6px 14px; @@ -177,8 +114,6 @@ body { } .filterBtn:hover { - background: #4cae4c !important; - border-color: #4cae4c !important; color: #fff !important; } @@ -193,32 +128,17 @@ body { color: #fff !important; } -.darkFilterBtn { - background: #5cb85c !important; - border-color: #5cb85c !important; -} - -.darkFilterBtn:hover { - background: #4cae4c !important; - border-color: #4cae4c !important; -} - -.darkFilterBtn.active { - background: #1e7e34 !important; - border-color: #1e7e34 !important; -} - .dd :global(.dropdown-toggle) { - background: #5cb85c !important; - border-color: #5cb85c !important; + background: #6a4ff7 !important; + border-color: #6a4ff7 !important; color: #fff !important; border-radius: 16px !important; padding: 6px 10px; } .dd :global(.dropdown-toggle:hover) { - background: #4cae4c !important; - border-color: #4cae4c !important; + background: #593dd8 !important; + border-color: #593dd8 !important; color: #fff !important; } @@ -226,24 +146,21 @@ body { .dropdownMenu { border-radius: 10px; padding: 4px 0; - background: #fff; } .darkDropdownMenu { - background: #1c2541 !important; - border-color: #225163 !important; + background: #1c2541; + border-color: #225163; } .dropdownItem { font-size: 14px; padding: 6px 14px; - color: #333; - background: #fff; } .dropdownItem:hover { - background: #eae4fb !important; - color: #6a4ff7 !important; + background: #eae4fb; + color: #6a4ff7; } .dropdownActive { @@ -251,36 +168,8 @@ body { color: #fff !important; } -/* Dedicated active-button classes — used instead of compound selectors */ -.activeFilterBtn { - background: #115b22 !important; - border-color: #0a4b18 !important; - color: #fff !important; - font-weight: 700 !important; -} - -.darkActiveFilterBtn { - background: #115b22 !important; - border-color: #0a4b18 !important; - color: #fff !important; - font-weight: 700 !important; - box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.25) !important; -} - -.ddActive :global(.dropdown-toggle) { - background: #115b22 !important; - border-color: #0a4b18 !important; - color: #fff !important; -} - -.ddActive :global(.dropdown-toggle:hover) { - background: #0d4720 !important; - border-color: #0d4720 !important; - color: #fff !important; -} - .currentMetric { - margin-left: auto; + justify-self: end; font-size: 14px; } @@ -315,25 +204,14 @@ body { min-height: 220px; } -.graphCardBody { - padding: 14px 22px 20px 6px; -} - .graphTitle { display: flex; align-items: center; justify-content: space-between; font-weight: 600; - font-size: 18px; margin-bottom: 8px; } -.graphCanvas { - margin-left: -18px; - margin-right: -8px; - width: calc(100% + 26px); -} - .metricPill { font-size: 12px; border: 1px solid #e2e2e2; @@ -712,15 +590,14 @@ body { } .darkFilterBtn { - background: #5cb85c !important; - border-color: #5cb85c !important; + background: #3a506b !important; + border-color: #3a506b !important; color: #fff !important; } .active.darkFilterBtn { - background: #1e7e34 !important; - border-color: #1e7e34 !important; - color: #fff !important; + background: #225163 !important; + border-color: #225163 !important; } .darkDropdown { @@ -730,18 +607,12 @@ body { } .darkDropdownItem { - color: #e1e1e1 !important; - background: #1c2541 !important; + color: #e1e1e1; + background: #1c2541; } .darkDropdownItem:hover { - background: #3a506b !important; - color: #fff !important; -} - -.darkDropdownItem.dropdownActive { - background: #6a4ff7 !important; - color: #fff !important; + background: #3a506b; } .darkText { @@ -790,44 +661,6 @@ body { box-shadow: 0 4px 16px rgb(28 37 65 / 25%); } -/* Mobile Responsiveness */ -@media (max-width: 768px) { - .filterGrid { - grid-template-columns: 1fr; - gap: 12px; - } - - .metricSection { - flex-direction: column; - align-items: flex-start; - } - - .categoryGroup { - width: 100%; - flex-direction: column; - } - - .filterBtn { - width: 100%; - text-align: center; - } - - .currentMetric { - margin-left: 0; - width: 100%; - } - - .dashboardHeader { - flex-direction: column; - gap: 10px; - align-items: flex-start; - } - - .title { - font-size: 20px; - } -} - /* Dark mode select dropdowns - remove double arrows and style properly */ .darkSelect { background-color: #2C3E50 !important; diff --git a/src/components/LBDashboard/LbAnalytics/ConversionFunnel/ConversionFunnel.module.css b/src/components/LBDashboard/LbAnalytics/ConversionFunnel/ConversionFunnel.module.css index f9f434d797..3f90588804 100644 --- a/src/components/LBDashboard/LbAnalytics/ConversionFunnel/ConversionFunnel.module.css +++ b/src/components/LBDashboard/LbAnalytics/ConversionFunnel/ConversionFunnel.module.css @@ -41,7 +41,7 @@ } .darkFiltersContainer .filterLabel { - color: #fff; + color: #ffffff; } /* Date Range Picker */ @@ -73,19 +73,19 @@ border-radius: 4px; font-size: 14px; width: 150px; - background-color: #fff; + background-color: #ffffff; color: #333; } .datePicker:focus { outline: none; border-color: #5DBEAF; - box-shadow: 0 0 0 2px rgb(93 190 175 / 20%); + box-shadow: 0 0 0 2px rgba(93, 190, 175, 0.2); } .darkDatePicker { background-color: #1c2541; - color: #fff; + color: #ffffff; border-color: #2e3f5c; } @@ -103,7 +103,7 @@ padding: 8px 16px; border: 1px solid #ccc; border-radius: 4px; - background-color: #fff; + background-color: #ffffff; color: #333; font-size: 14px; font-weight: 500; @@ -118,7 +118,7 @@ .categoryButtonActive { background-color: #5DBEAF; - color: #fff; + color: #ffffff; border-color: #5DBEAF; } @@ -128,7 +128,7 @@ .darkCategoryButton { background-color: #1c2541; - color: #fff; + color: #ffffff; border-color: #2e3f5c; } @@ -161,16 +161,16 @@ .darkMultiSelect :global(.dropdown-container) { background-color: #1c2541 !important; border-color: #2e3f5c !important; - color: #fff !important; + color: #ffffff !important; } .darkMultiSelect :global(.dropdown-heading) { background-color: #1c2541 !important; - color: #fff !important; + color: #ffffff !important; } .darkMultiSelect :global(.dropdown-heading-value span) { - color: #fff !important; + color: #ffffff !important; } /* Dark mode: dropdown panel */ @@ -187,7 +187,7 @@ /* Dark mode: search input */ .darkMultiSelect :global(.search input) { background-color: #162032 !important; - color: #fff !important; + color: #ffffff !important; border-color: #2e3f5c !important; } @@ -202,7 +202,7 @@ .darkMultiSelect :global(.option) { background-color: #1c2541 !important; - color: #fff !important; + color: #ffffff !important; } .darkMultiSelect :global(.option:hover), @@ -213,7 +213,7 @@ /* Dark mode: select-all item */ .darkMultiSelect :global(.select-item) { background-color: #1c2541 !important; - color: #fff !important; + color: #ffffff !important; } .darkMultiSelect :global(.select-item:hover) { @@ -273,7 +273,7 @@ } /* Responsive Design */ -@media (width <= 768px) { +@media (max-width: 768px) { .filtersContainer { flex-direction: column; } @@ -309,7 +309,7 @@ .darkContainer :global(.react-datepicker__current-month), .darkContainer :global(.react-datepicker__day-name), .darkContainer :global(.react-datepicker__day) { - color: #fff; + color: #ffffff; } .darkContainer :global(.react-datepicker__day:hover) { diff --git a/src/components/LBDashboard/LbAnalytics/WinningVsAverageBidChart/WinningVsAverageBidChart.module.css b/src/components/LBDashboard/LbAnalytics/WinningVsAverageBidChart/WinningVsAverageBidChart.module.css index 004da86448..69449efb7b 100644 --- a/src/components/LBDashboard/LbAnalytics/WinningVsAverageBidChart/WinningVsAverageBidChart.module.css +++ b/src/components/LBDashboard/LbAnalytics/WinningVsAverageBidChart/WinningVsAverageBidChart.module.css @@ -8,8 +8,8 @@ .card { border-radius: 8px; - box-shadow: 0 2px 8px rgb(0 0 0 / 10%); - background-color: #fff; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + background-color: #ffffff; } .darkCard { @@ -25,18 +25,18 @@ } .darkContainer .filtersSection { - border-bottom-color: #444; + border-bottom-color: #444444; } .filterTitle { margin-bottom: 12px; font-size: 16px; font-weight: 600; - color: #333; + color: #333333; } .darkText { - color: #fff !important; + color: #ffffff !important; } .applyBtn { @@ -70,18 +70,18 @@ align-items: center; justify-content: center; min-height: 400px; - color: #666; + color: #666666; font-size: 16px; } /* Responsive Design */ -@media (width <= 992px) { +@media (max-width: 992px) { .chartContainer { height: 450px; } } -@media (width <= 768px) { +@media (max-width: 768px) { .container { padding: 10px; } @@ -99,7 +99,7 @@ } } -@media (width <= 576px) { +@media (max-width: 576px) { .chartContainer { height: 350px; } diff --git a/src/components/LBDashboard/RatingDistribution/RatingDistribution.module.css b/src/components/LBDashboard/RatingDistribution/RatingDistribution.module.css index 747a4970d3..c0061cdd6c 100644 --- a/src/components/LBDashboard/RatingDistribution/RatingDistribution.module.css +++ b/src/components/LBDashboard/RatingDistribution/RatingDistribution.module.css @@ -1,7 +1,7 @@ .ratingCard { - background-color: #fff; + background-color: #ffffff; border-radius: 12px; - box-shadow: 0 2px 8px rgb(0 0 0 / 10%); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); padding: 16px; transition: all 0.3s ease; min-height: 220px; @@ -10,7 +10,7 @@ .darkCard { background-color: #1c2541 !important; - box-shadow: 0 2px 8px rgb(0 0 0 / 30%); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); border: none !important; } @@ -35,7 +35,7 @@ } .darkText { - color: #fff; + color: #ffffff; } .dateRangeSelector { @@ -106,7 +106,7 @@ .darkInput:focus { outline: none; border-color: #8b5cf6; - box-shadow: 0 0 0 3px rgb(139 92 246 / 10%); + box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.1); } .filtersSection { @@ -132,7 +132,7 @@ } /* Responsive Design */ -@media (width <= 768px) { +@media (max-width: 768px) { .header { flex-direction: column; align-items: flex-start; @@ -169,7 +169,7 @@ } } -@media (width <= 576px) { +@media (max-width: 576px) { .ratingCard { padding: 15px; } diff --git a/src/components/LBDashboard/lbUtils.js b/src/components/LBDashboard/lbUtils.js deleted file mode 100644 index 7a98b9a6cf..0000000000 --- a/src/components/LBDashboard/lbUtils.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Cryptographically uniform random integer in [min, max]. - * Uses crypto.getRandomValues for unbiased results. - */ -export function randomInt(min, max) { - const range = max - min + 1; - if (range <= 256) { - const arr = new Uint8Array(1); - const limit = 256 - (256 % range); - let r; - do { - crypto.getRandomValues(arr); - r = arr[0]; - } while (r >= limit); - return min + (r % range); - } - const arr = new Uint32Array(1); - const MAX = 0x100000000; - const limit = MAX - (MAX % range); - let r; - do { - crypto.getRandomValues(arr); - r = arr[0]; - } while (r >= limit); - return min + (r % range); -} diff --git a/src/components/Teams/CreateNewTeamPopup.jsx b/src/components/Teams/CreateNewTeamPopup.jsx index 995e10b5fa..255720222c 100644 --- a/src/components/Teams/CreateNewTeamPopup.jsx +++ b/src/components/Teams/CreateNewTeamPopup.jsx @@ -1,6 +1,5 @@ /* eslint-disable jsx-a11y/no-autofocus */ import React, { useState, useEffect } from 'react'; -import PropTypes from 'prop-types'; import { useSelector } from 'react-redux'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter, Input, Alert } from 'reactstrap'; import { boxStyle, boxStyleDark } from '~/styles'; @@ -33,9 +32,17 @@ export const CreateNewTeamPopup = React.memo(props => { } }; + //prettier-ignore + const formatSearchInput = text => text.toLowerCase().replace(/\s+/g, '').trim(); + const handleSubmit = async () => { + const teamNames = allTeams.filter(team => team.teamName).map(team => team.teamName); + const matchingTeams = teamNames.find( + team => formatSearchInput(team) === formatSearchInput(newTeam), + ); + if (newTeam !== '') { - if (!teamExists || props.isEdit) { + if (!matchingTeams || (props.isEdit && !matchingTeams)) { await props.onOkClick(newTeam, props.isEdit); } else { setTeamExists(true); @@ -75,29 +82,11 @@ export const CreateNewTeamPopup = React.memo(props => { > {newTeam.length}/{TEAM_NAME_MAX_LENGTH} characters - {!isValidTeam && ( - - Please enter a team name. - - )} - {teamExists && !props.isEdit && ( - - That's a great team name! So great that someone else already created that team. - Please choose a new name or use the existing team. + {!isValidTeam && Please enter a team name.} + {teamExists && ( + + That’s a great team name! So great that someone else already created that team. Please + choose a new name or use the existing team. )} @@ -115,19 +104,4 @@ export const CreateNewTeamPopup = React.memo(props => { CreateNewTeamPopup.displayName = 'CreateNewTeamPopup'; -CreateNewTeamPopup.propTypes = { - open: PropTypes.bool.isRequired, - teamName: PropTypes.string, - isEdit: PropTypes.bool, - onClose: PropTypes.func, - onOkClick: PropTypes.func, -}; - -CreateNewTeamPopup.defaultProps = { - teamName: '', - isEdit: false, - onClose: () => {}, - onOkClick: () => {}, -}; - export default CreateNewTeamPopup; diff --git a/src/components/Teams/Teams.jsx b/src/components/Teams/Teams.jsx index e1028ba9dc..b8767e945a 100644 --- a/src/components/Teams/Teams.jsx +++ b/src/components/Teams/Teams.jsx @@ -17,7 +17,6 @@ import { addTeamMember, updateTeamMemeberVisibility, clearTeamMembers, - postNewTeam, } from '../../actions/allTeamsAction'; import { getAllUserProfile } from '../../actions/userManagement'; import Loading from '../common/Loading'; @@ -29,7 +28,6 @@ import TeamMembersPopup from './TeamMembersPopup'; import DeleteTeamPopup from './DeleteTeamPopup'; import TeamStatusPopup from './TeamStatusPopup'; import AddTeamPopup from '../UserProfile/TeamsAndProjects/AddTeamPopup'; -import CreateNewTeamPopup from './CreateNewTeamPopup'; // constants const FILTER_ALL = 'all'; const FILTER_ACTIVE = 'active'; @@ -55,7 +53,6 @@ class Teams extends React.PureComponent { selectedFilter: FILTER_ALL, // Features from HEAD addTeamPopupOpen: false, - createNewTeamPopupOpen: false, isEdit: false, membersFetching: false, selectedTeamMembers: [], @@ -326,7 +323,8 @@ class Teams extends React.PureComponent { await this.props.getAllUserTeams(); await this.props.getAllUserProfile(); } catch (error) { - toast.error(error?.message || 'Error updating team list. Please refresh the page.'); + console.error('Error updating team list:', error); + toast.error('Error updating team list. Please refresh the page.'); } }} handleSubmit={() => {}} @@ -359,11 +357,6 @@ class Teams extends React.PureComponent { onConfirmClick={this.onConfirmClick} selectedTeamCode={selectedTeamCode} /> - ); }; @@ -435,26 +428,14 @@ class Teams extends React.PureComponent { }; onCreateNewTeamShow = () => { - this.setState({ createNewTeamPopupOpen: true }); - }; - - onCreateNewTeamPopupClose = () => { - this.setState({ createNewTeamPopupOpen: false }); - }; - - onCreateNewTeamOkClick = async teamName => { - try { - const res = await this.props.postNewTeam(teamName, true); - if (res?.status === 200) { - toast.success(`Team "${teamName}" created successfully!`); - this.setState({ createNewTeamPopupOpen: false }); - await this.props.getAllUserTeams(); - } else { - toast.error(res?.data?.error || 'Failed to create team. Please try again.'); - } - } catch (err) { - toast.error(err?.message || 'An unexpected error occurred. Please try again.'); - } + this.setState({ + addTeamPopupOpen: true, + isEdit: false, + selectedTeam: '', + selectedTeamId: undefined, + selectedTeamCode: '', + isActive: '', + }); }; onAddTeamPopupClose = () => { @@ -560,7 +541,6 @@ Teams.propTypes = { addTeamMember: PropTypes.func.isRequired, updateTeamMemeberVisibility: PropTypes.func.isRequired, clearTeamMembers: PropTypes.func.isRequired, - postNewTeam: PropTypes.func.isRequired, }; const mapStateToProps = state => ({ state }); @@ -575,5 +555,4 @@ export default connect(mapStateToProps, { addTeamMember, updateTeamMemeberVisibility, clearTeamMembers, - postNewTeam, })(Teams); diff --git a/src/components/UserManagement/ActiveCell.jsx b/src/components/UserManagement/ActiveCell.jsx index 3f9144d5ef..6f6b3d2b66 100644 --- a/src/components/UserManagement/ActiveCell.jsx +++ b/src/components/UserManagement/ActiveCell.jsx @@ -15,25 +15,23 @@ function ActiveCell(props) { const now = moment(); function deriveUserStatus({ isActive, reactivationDate, endDate }) { - - if (reactivationDate) return UserStatus.Paused; - - if (!isActive) return UserStatus.Inactive; - - if (!!endDate && moment(endDate).isAfter(now)) return UserStatus.Scheduled; - - return UserStatus.Active; -} - - const userStatus = deriveUserStatus({ - isActive, - reactivationDate, - endDate, - }); - - const isScheduled = userStatus === UserStatus.Scheduled; - const isPaused = userStatus === UserStatus.Paused; - const isSeparated = userStatus === UserStatus.Inactive; + if (reactivationDate) return UserStatus.Paused; + + + if (isActive === false) return UserStatus.Inactive; + + + if (isActive && !!endDate && moment(endDate).isAfter(now)) return UserStatus.Scheduled; + + if (endDate) return UserStatus.Inactive; + if (isActive) return UserStatus.Active; + + return UserStatus.Inactive; + } + + const isScheduled = deriveUserStatus({ isActive, reactivationDate, endDate }) === UserStatus.Scheduled; + const isPaused = deriveUserStatus({ isActive, reactivationDate, endDate }) === UserStatus.Paused; + const isSeparated = deriveUserStatus({ isActive, reactivationDate, endDate }) === UserStatus.Inactive; const className = (() => { diff --git a/src/components/UserManagement/UserManagement.jsx b/src/components/UserManagement/UserManagement.jsx index d644bc1cdc..3342fb6465 100644 --- a/src/components/UserManagement/UserManagement.jsx +++ b/src/components/UserManagement/UserManagement.jsx @@ -41,7 +41,10 @@ import SetUpFinalDayPopUp from './SetUpFinalDayPopUp'; import LogTimeOffPopUp from './logTimeOffPopUp'; import SetupNewUserPopup from './setupNewUserPopup'; import { getAllTimeOffRequests } from '../../actions/timeOffRequestAction'; -import { scheduleDeactivationAction, activateUserAction, deactivateImmediatelyAction } from '../../actions/userLifecycleActions'; +import { + scheduleDeactivationAction, + deactivateImmediatelyAction, +} from '../../actions/userLifecycleActions'; class UserManagement extends React.PureComponent { filteredUserDataCount = 0; @@ -417,12 +420,9 @@ class UserManagement extends React.PureComponent { }; reactivateUser = async (user = this.state.selectedUser) => { - await activateUserAction( - this.props.dispatch, - user, - this.props.getAllUserProfile, - ); -}; + await this.props.dispatch(updateUserPauseStatus(user, UserStatus.Active, Date.now())); + await this.props.getAllUserProfile(); + }; onUserUpdate = (updatedUser) => { const { userProfiles } = this.props.state.allUserProfiles; diff --git a/src/components/UserManagement/__tests__/UserManagement.test.jsx b/src/components/UserManagement/__tests__/UserManagement.test.jsx index f0597ffdcf..d6c64a19bf 100644 --- a/src/components/UserManagement/__tests__/UserManagement.test.jsx +++ b/src/components/UserManagement/__tests__/UserManagement.test.jsx @@ -5,6 +5,15 @@ import { fireEvent, render, screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import { Provider } from 'react-redux'; +import { updateUserPauseStatus } from '../../../actions/userManagement'; + +vi.mock('../../../actions/userManagement', async () => { + const actual = await vi.importActual('../../../actions/userManagement'); + return { + ...actual, + updateUserPauseStatus: vi.fn(() => async () => undefined), + }; +}); const createThunkStore = () => ({ getState: () => ({ @@ -70,10 +79,10 @@ describe('UserManagement Component', () => { expect(screen.getByTestId('user-management-table')).toBeInTheDocument(); }); - it('calls activateUserAction when resuming user', () => { + it('calls updateUserPauseStatus when resuming user', () => { renderUserManagement(); fireEvent.click(screen.getByTestId('pause-resume-button-0')); - expect(props.getAllUserProfile).toHaveBeenCalled(); + expect(updateUserPauseStatus).toHaveBeenCalled(); }); it('handles final day action when clicked', () => { diff --git a/src/constants/studentTasks.js b/src/constants/studentTasks.js index 789388782f..6838f193e7 100644 --- a/src/constants/studentTasks.js +++ b/src/constants/studentTasks.js @@ -7,4 +7,3 @@ export const FETCH_STUDENT_TASKS_START = 'FETCH_STUDENT_TASKS_START'; export const FETCH_STUDENT_TASKS_ERROR = 'FETCH_STUDENT_TASKS_ERROR'; export const RECEIVE_STUDENT_TASKS = 'RECEIVE_STUDENT_TASKS'; export const UPDATE_STUDENT_TASK = 'UPDATE_STUDENT_TASK'; -export const LOG_STUDENT_TASK_HOURS = 'LOG_STUDENT_TASK_HOURS'; diff --git a/src/reducers/studentTasksReducer.js b/src/reducers/studentTasksReducer.js index 4e6d2332e4..45128b7822 100644 --- a/src/reducers/studentTasksReducer.js +++ b/src/reducers/studentTasksReducer.js @@ -42,22 +42,6 @@ export const studentTasksReducer = (state = initialState, action) => { ), }; - case types.LOG_STUDENT_TASK_HOURS: - return { - ...state, - taskItems: state.taskItems.map(task => - task.id === action.taskId - ? { - ...task, - logged_hours: action.loggedHours, - suggested_total_hours: action.suggestedTotalHours, - status: action.status, - can_mark_done: action.canMarkDone, - } - : task, - ), - }; - default: return state; } diff --git a/src/routes.jsx b/src/routes.jsx index a587983d2b..6ca452b0af 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -352,17 +352,7 @@ export default ( /> - ( - <> - - - - - - )} - /> + @@ -716,11 +706,7 @@ export default ( {/* ----- BEGIN BM Dashboard Routing ----- */} - + @@ -893,11 +879,6 @@ export default ( exact component={EventParticipation} /> - `${APIEndpoint}/student/tasks`, STUDENT_TASK_MARK_DONE: taskId => `${APIEndpoint}/student/tasks/${taskId}/mark-done`, - STUDENT_TASK_LOG_HOURS: taskId => `${APIEndpoint}/student/tasks/${taskId}/log-hours`, // Intermediate Tasks (Education Portal) INTERMEDIATE_TASKS: () => `${APIEndpoint}/educator/intermediate-tasks`, @@ -381,7 +380,6 @@ export const ENDPOINTS = { BM_TOOLS_PURCHASE: `${APIEndpoint}/bm/tools/purchase`, POST_LESSON: `${APIEndpoint}/bm/lessons/new`, BM_LESSONS: `${APIEndpoint}/bm/lessons`, - BM_LESSONS_LEARNT: `${APIEndpoint}/bm/lessons-learnt`, BM_LESSON: `${APIEndpoint}/bm/lesson/`, BM_LESSON_LIKES: lessonId => `${APIEndpoint}/bm/lesson/${lessonId}/like`, BM_EXTERNAL_TEAM: `${APIEndpoint}/bm/externalTeam`, diff --git a/src/utils/lbDashboard/chartsUtils.js b/src/utils/lbDashboard/chartsUtils.js index 9fe78f9706..8111a80d67 100644 --- a/src/utils/lbDashboard/chartsUtils.js +++ b/src/utils/lbDashboard/chartsUtils.js @@ -1,15 +1,5 @@ import { CHART_COLORS, METRIC_LABELS, METRIC_CATEGORIES } from '../../constants/lbDashboard/chartsConstants'; -const CURRENCY_METRICS = new Set(['averageBid', 'finalPrice']); - -export function getMetricFormatter(metric) { - if (CURRENCY_METRICS.has(metric)) return v => `₹${Number(v).toLocaleString()}`; - if (metric === 'occupancyRate') return v => `${v}%`; - if (metric === 'averageDuration') return v => `${v} days`; - if (metric === 'averageRating') return v => Number(v).toFixed(1); - return v => v; -} - export function getItemColors(items) { const colorMap = {}; items.forEach((item, idx) => { @@ -19,7 +9,6 @@ export function getItemColors(items) { } export function createChartOptions(metric, darkMode) { - const fmt = getMetricFormatter(metric); return { responsive: true, maintainAspectRatio: false, @@ -37,13 +26,13 @@ export function createChartOptions(metric, darkMode) { offset: 4, clip: false, display: 'auto', - formatter: fmt, + formatter: value => value, }, tooltip: { enabled: true, callbacks: { label: function (context) { - return `${context.dataset.label}: ${fmt(context.parsed.y)}`; + return `${context.dataset.label}: ${context.parsed.y}`; }, }, }, @@ -77,11 +66,7 @@ export function createChartOptions(metric, darkMode) { grace: '12%', grid: { color: darkMode ? 'rgba(255,255,255,0.12)' : 'rgba(0,0,0,0.08)' }, border: { color: darkMode ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.15)' }, - ticks: { - font: { size: 12 }, - color: darkMode ? '#fff' : '#222', - callback: fmt, - }, + ticks: { font: { size: 12 }, color: darkMode ? '#fff' : '#222' }, }, }, };