From 7432e8c7484a0941d441d44cc89c7ededc97589f Mon Sep 17 00:00:00 2001 From: saikrishnaraoyadagiri Date: Wed, 3 Sep 2025 19:17:39 -0400 Subject: [PATCH 1/6] feat: adding donut chart for planned cost breakdown --- .../PlannedCostDonutChart.css | 342 ++++++++++++++++++ .../PlannedCostDonutChart.jsx | 293 +++++++++++++++ src/components/PlannedCostDonutChart/index.js | 1 + .../plannedCostService.js | 133 +++++++ src/routes.jsx | 4 +- 5 files changed, 771 insertions(+), 2 deletions(-) create mode 100644 src/components/PlannedCostDonutChart/PlannedCostDonutChart.css create mode 100644 src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx create mode 100644 src/components/PlannedCostDonutChart/index.js create mode 100644 src/components/PlannedCostDonutChart/plannedCostService.js diff --git a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.css b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.css new file mode 100644 index 0000000000..2d4964e594 --- /dev/null +++ b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.css @@ -0,0 +1,342 @@ +.planned-cost-container { + padding: 2rem; + background: #ffffff; + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + margin: 1rem 0; + transition: all 0.3s ease; + overflow: visible; + height: auto; + min-height: auto; +} + +.planned-cost-container.dark-mode { + background: #2d3748; + color: #e2e8f0; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); +} + +.chart-title { + text-align: center; + margin-bottom: 2rem; + color: #2d3748; + font-weight: 600; + font-size: 1.75rem; +} + +.dark-mode .chart-title { + color: #e2e8f0; +} + +.filter-section { + margin-bottom: 2rem; + padding: 1rem; + background: #f7fafc; + border-radius: 8px; +} + +.dark-mode .filter-section { + background: #4a5568; +} + +.filter-section label { + font-weight: 600; + margin-bottom: 0.5rem; + color: #4a5568; +} + +.dark-mode .filter-section label { + color: #e2e8f0; +} + +.project-select { + border: 2px solid #e2e8f0; + border-radius: 6px; + padding: 0.75rem; + font-size: 1rem; + transition: border-color 0.3s ease; +} + +.project-select:focus { + border-color: #4299e1; + box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1); + outline: none; +} + +.dark-mode .project-select { + background: #4a5568; + border-color: #718096; + color: #e2e8f0; +} + +.dark-mode .project-select:focus { + border-color: #63b3ed; +} + +.chart-area { + display: flex; + flex-direction: column; + align-items: center; + overflow: visible; +} + +.loading-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 200px; + padding: 2rem; + color: #718096; +} + +.dark-mode .loading-container { + color: #a0aec0; +} + +.loading-container p { + margin-top: 1rem; + font-size: 1.1rem; +} + +.no-data-container, +.select-project-container { + display: flex; + align-items: center; + justify-content: center; + min-height: 200px; + padding: 2rem; + color: #718096; + font-size: 1.1rem; + text-align: center; +} + +.dark-mode .no-data-container, +.dark-mode .select-project-container { + color: #a0aec0; +} + +/* Chart center info styling */ +.chart-center-info { + text-align: center; + margin: 1rem 0; +} + +.total-cost-display { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.total-label { + font-size: 1rem; + color: #718096; + font-weight: 500; +} + +.dark-mode .total-label { + color: #a0aec0; +} + +.total-amount { + font-size: 2rem; + font-weight: 700; + color: #2d3748; +} + +.dark-mode .total-amount { + color: #e2e8f0; +} + +/* Legend styling */ +.legend-container { + margin-top: 2rem; + width: 100%; + max-width: 600px; +} + +.legend-container h4 { + text-align: center; + margin-bottom: 1rem; + color: #2d3748; + font-weight: 600; +} + +.dark-mode .legend-container h4 { + color: #e2e8f0; +} + +.legend-items { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; +} + +.legend-item { + display: flex; + align-items: center; + padding: 0.75rem; + background: #f7fafc; + border-radius: 6px; + transition: transform 0.2s ease; +} + +.legend-item:hover { + transform: translateY(-2px); +} + +.dark-mode .legend-item { + background: #4a5568; +} + +.legend-color { + width: 20px; + height: 20px; + border-radius: 50%; + margin-right: 0.75rem; + flex-shrink: 0; +} + +.legend-label { + font-weight: 600; + color: #2d3748; + margin-right: auto; + min-width: 80px; +} + +.dark-mode .legend-label { + color: #e2e8f0; +} + +.legend-value { + color: #718096; + font-size: 0.9rem; + text-align: right; +} + +.dark-mode .legend-value { + color: #a0aec0; +} + +/* Tooltip styling */ +.planned-cost-tooltip { + background: rgba(0, 0, 0, 0.9); + color: white; + padding: 0.75rem; + border-radius: 6px; + font-size: 0.9rem; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); +} + +.tooltip-category { + font-weight: 600; + margin: 0 0 0.5rem 0; + color: #ffffff; +} + +.tooltip-cost { + margin: 0 0 0.25rem 0; + color: #e2e8f0; +} + +.tooltip-percentage { + margin: 0; + color: #a0aec0; +} + +/* Ensure no unnecessary scrollbars */ +html, body { + overflow-x: hidden; +} + +/* Responsive design */ +@media (max-width: 768px) { + .planned-cost-container { + padding: 1rem; + margin: 0.5rem 0; + } + + .chart-title { + font-size: 1.5rem; + margin-bottom: 1.5rem; + } + + .filter-section { + margin-bottom: 1.5rem; + padding: 0.75rem; + } + + .legend-items { + grid-template-columns: 1fr; + gap: 0.75rem; + } + + .legend-item { + padding: 0.5rem; + } + + .legend-label { + min-width: 60px; + font-size: 0.9rem; + } + + .legend-value { + font-size: 0.8rem; + } +} + +@media (max-width: 480px) { + .planned-cost-container { + padding: 0.75rem; + } + + .chart-title { + font-size: 1.25rem; + margin-bottom: 1rem; + } + + .filter-section { + padding: 0.5rem; + } + + .project-select { + padding: 0.5rem; + font-size: 0.9rem; + } +} + +/* Animation for chart loading */ +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.chart-area > * { + animation: fadeIn 0.5s ease-out; +} + +/* Hover effects for interactive elements */ +.project-select:hover { + border-color: #cbd5e0; +} + +.dark-mode .project-select:hover { + border-color: #a0aec0; +} + +/* Focus states for accessibility */ +.project-select:focus-visible { + outline: 2px solid #4299e1; + outline-offset: 2px; +} + +.dark-mode .project-select:focus-visible { + outline-color: #63b3ed; +} diff --git a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx new file mode 100644 index 0000000000..4ca641eae5 --- /dev/null +++ b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx @@ -0,0 +1,293 @@ +import React, { useState, useEffect } from 'react'; +import { useSelector } from 'react-redux'; +import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts'; +import { Input, Label, Row, Col, Alert, Spinner } from 'reactstrap'; +import { fetchProjects, fetchPlannedCostBreakdown } from './plannedCostService'; +import './PlannedCostDonutChart.css'; + +const COLORS = { + Plumbing: '#FF6384', + Electrical: '#36A2EB', + Structural: '#FFCE56', + Mechanical: '#4BC0C0', +}; + +const RADIAN = Math.PI / 180; + +const CustomTooltip = ({ active, payload, totalCost }) => { + if (active && payload && payload.length) { + const data = payload[0]; + const percentage = ((data.value / totalCost) * 100).toFixed(1); + + return ( +
+

{data.name}

+

Planned Cost: ${data.value.toLocaleString()}

+

Percentage: {percentage}%

+
+ ); + } + return null; +}; + +const PlannedCostDonutChart = () => { + const [selectedProject, setSelectedProject] = useState(''); + const [chartData, setChartData] = useState([]); + const [totalCost, setTotalCost] = useState(0); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [projects, setProjects] = useState([]); + + const darkMode = useSelector(state => state.theme?.darkMode); + + // Fetch projects list + useEffect(() => { + const getProjects = async () => { + try { + const projectsData = await fetchProjects(); + setProjects(projectsData); + } catch (err) { + setError(err.message); + } + }; + + getProjects(); + }, []); + + // Fetch planned cost breakdown when project changes + useEffect(() => { + if (!selectedProject) { + setChartData([]); + setTotalCost(0); + return; + } + + const getPlannedCostBreakdown = async () => { + setLoading(true); + setError(null); + + try { + const breakdown = await fetchPlannedCostBreakdown(selectedProject); + + // Transform data for the chart - handle both array and object formats + let transformedData = []; + let total = 0; + + if (Array.isArray(breakdown)) { + // If backend returns array of objects like [{category: 'Plumbing', plannedCost: 5500}, ...] + const categoryMap = {}; + breakdown.forEach(item => { + if (item.category && typeof item.plannedCost === 'number') { + categoryMap[item.category] = item.plannedCost; + } + }); + + transformedData = [ + { name: 'Plumbing', value: categoryMap.Plumbing || 0 }, + { name: 'Electrical', value: categoryMap.Electrical || 0 }, + { name: 'Structural', value: categoryMap.Structural || 0 }, + { name: 'Mechanical', value: categoryMap.Mechanical || 0 }, + ]; + + total = Object.values(categoryMap).reduce((sum, cost) => sum + cost, 0); + } else { + // If backend returns object like {total: 90000, breakdown: {plumbing: 5500, ...}} + if (breakdown.total && breakdown.breakdown) { + // Use the total from backend and breakdown for chart data + // Handle case-insensitive matching + const breakdownData = breakdown.breakdown; + + // Check if we actually have valid data + const hasValidData = Object.values(breakdownData).some( + value => typeof value === 'number' && value > 0, + ); + + if (hasValidData) { + transformedData = [ + { name: 'Plumbing', value: breakdownData.Plumbing || breakdownData.plumbing || 0 }, + { + name: 'Electrical', + value: breakdownData.Electrical || breakdownData.electrical || 0, + }, + { + name: 'Structural', + value: breakdownData.Structural || breakdownData.structural || 0, + }, + { + name: 'Mechanical', + value: breakdownData.Mechanical || breakdownData.mechanical || 0, + }, + ]; + + total = breakdown.total; // Use the total from backend + } else { + // No valid data available + transformedData = []; + total = 0; + } + } else { + // Fallback to old format + transformedData = [ + { name: 'Plumbing', value: breakdown.plumbing || 0 }, + { name: 'Electrical', value: breakdown.electrical || 0 }, + { name: 'Structural', value: breakdown.structural || 0 }, + { name: 'Mechanical', value: breakdown.mechanical || 0 }, + ]; + + total = Object.values(breakdown).reduce((sum, cost) => sum + (cost || 0), 0); + } + } + + setChartData(transformedData); + setTotalCost(total); + } catch (err) { + setError(err.message); + setChartData([]); + setTotalCost(0); + } finally { + setLoading(false); + } + }; + + getPlannedCostBreakdown(); + }, [selectedProject]); + + const renderCustomizedLabel = ({ + cx, + cy, + midAngle, + innerRadius, + outerRadius, + percent, + value, + }) => { + if (value === 0) return null; + + const radius = innerRadius + (outerRadius - innerRadius) * 0.5; + const x = cx + radius * Math.cos(-midAngle * RADIAN); + const y = cy + radius * Math.sin(-midAngle * RADIAN); + + return ( + + {percent > 0.05 ? `${(percent * 100).toFixed(0)}%` : ''} + + ); + }; + + if (error && !selectedProject) { + return ( +
+ {error} +
+ ); + } + + return ( +
+

Planned Cost Breakdown by Type of Expenditure

+ + {/* Project Filter */} + + + + setSelectedProject(e.target.value)} + className="project-select" + > + + {projects.map(project => ( + + ))} + + + + + {/* Chart Area */} +
+ {loading ? ( +
+ +

Loading planned cost data...

+
+ ) : selectedProject && chartData.length > 0 && totalCost > 0 ? ( + <> + + + + {chartData.map((entry, index) => ( + + ))} + + + } /> + + + + {/* Center display showing total */} +
+
+ Total Planned Cost + ${totalCost.toLocaleString()} +
+
+ + {/* Legend */} +
+
+ {chartData.map((entry, index) => ( +
+
+ {entry.name} + + ${entry.value.toLocaleString()} ( + {((entry.value / totalCost) * 100).toFixed(1)}%) + +
+ ))} +
+
+ + ) : selectedProject ? ( +
+

No planned cost data available for this project.

+
+ ) : ( +
+

Please select a project to view the planned cost breakdown.

+
+ )} +
+ + {error && ( + + {error} + + )} +
+ ); +}; + +export default PlannedCostDonutChart; diff --git a/src/components/PlannedCostDonutChart/index.js b/src/components/PlannedCostDonutChart/index.js new file mode 100644 index 0000000000..4d37658614 --- /dev/null +++ b/src/components/PlannedCostDonutChart/index.js @@ -0,0 +1 @@ +export { default } from './PlannedCostDonutChart'; diff --git a/src/components/PlannedCostDonutChart/plannedCostService.js b/src/components/PlannedCostDonutChart/plannedCostService.js new file mode 100644 index 0000000000..81f2f1f90d --- /dev/null +++ b/src/components/PlannedCostDonutChart/plannedCostService.js @@ -0,0 +1,133 @@ +import axios from 'axios'; + +const API_BASE_URL = process.env.REACT_APP_APIENDPOINT || 'http://localhost:4500/api'; + +/** + * Fetch all available projects + * @returns {Promise} Array of projects + */ +export const fetchProjects = async () => { + try { + const token = localStorage.getItem('token'); + if (!token) { + throw new Error('No authentication token found'); + } + + const response = await axios.get(`${API_BASE_URL}/projects`, { + headers: { Authorization: token }, + }); + + return response.data || []; + } catch (error) { + console.error('Error fetching projects:', error); + throw new Error('Failed to fetch projects'); + } +}; + +/** + * Fetch planned cost breakdown for a specific project + * @param {string} projectId - The project ID + * @returns {Promise} Planned cost breakdown data + */ +export const fetchPlannedCostBreakdown = async projectId => { + try { + const token = localStorage.getItem('token'); + if (!token) { + throw new Error('No authentication token found'); + } + + const response = await axios.get( + `${API_BASE_URL}/projects/${projectId}/planned-cost-breakdown`, + { + headers: { Authorization: token }, + }, + ); + + return response.data; + } catch (error) { + console.error('Error fetching planned cost breakdown:', error); + throw new Error(error.response?.data?.message || 'Failed to fetch planned cost breakdown'); + } +}; + +/** + * Create or update planned cost for a project category + * @param {string} projectId - The project ID + * @param {string} category - The category (Plumbing, Electrical, Structural, Mechanical) + * @param {number} plannedCost - The planned cost amount + * @returns {Promise} Response data + */ +export const createOrUpdatePlannedCost = async (projectId, category, plannedCost) => { + try { + const token = localStorage.getItem('token'); + if (!token) { + throw new Error('No authentication token found'); + } + + const response = await axios.post( + `${API_BASE_URL}/projects/${projectId}/planned-costs`, + { + category, + plannedCost, + }, + { + headers: { Authorization: token }, + }, + ); + + return response.data; + } catch (error) { + console.error('Error creating/updating planned cost:', error); + throw new Error(error.response?.data?.message || 'Failed to create/update planned cost'); + } +}; + +/** + * Delete planned cost for a project category + * @param {string} projectId - The project ID + * @param {string} category - The category to delete + * @returns {Promise} Response data + */ +export const deletePlannedCost = async (projectId, category) => { + try { + const token = localStorage.getItem('token'); + if (!token) { + throw new Error('No authentication token found'); + } + + const response = await axios.delete( + `${API_BASE_URL}/projects/${projectId}/planned-costs/${category}`, + { + headers: { Authorization: token }, + }, + ); + + return response.data; + } catch (error) { + console.error('Error deleting planned cost:', error); + throw new Error(error.response?.data?.message || 'Failed to delete planned cost'); + } +}; + +/** + * Get all planned costs for a project (detailed view) + * @param {string} projectId - The project ID + * @returns {Promise} Array of planned cost records + */ +export const getAllPlannedCostsForProject = async projectId => { + try { + const token = localStorage.getItem('token'); + if (!token) { + throw new Error('No authentication token found'); + } + + const response = await axios.get(`${API_BASE_URL}/projects/${projectId}/planned-costs`, { + headers: { Authorization: token }, + }); + + return response.data || []; + } catch (error) { + console.error('Error fetching all planned costs:', error); + throw new Error(error.response?.data?.message || 'Failed to fetch planned costs'); + } +}; diff --git a/src/routes.jsx b/src/routes.jsx index 8e66e78d53..568b53ef16 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -42,7 +42,7 @@ import TSAFormPage7 from './components/TSAForm/pages/TSAFormPage7'; import TSAFormPage8 from './components/TSAForm/pages/TSAFormPage8'; import Timelog from './components/Timelog'; import UserProfileEdit from './components/UserProfile/UserProfileEdit'; - +import PlannedCostDonutChart from './components/PlannedCostDonutChart'; import Dashboard from './components/Dashboard'; import Logout from './components/Logout/Logout'; import Login from './components/Login'; @@ -749,7 +749,7 @@ export default ( exact component={PromotionEligibility} /> - + From f75aa25ad400e7c1d75344b492b1c8186948e77f Mon Sep 17 00:00:00 2001 From: Sai Krishna Date: Sun, 12 Oct 2025 03:37:22 +0000 Subject: [PATCH 2/6] updated css file name --- src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx | 2 +- ...annedCostDonutChart.css => PlannedCostDonutChart.module.css} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/components/PlannedCostDonutChart/{PlannedCostDonutChart.css => PlannedCostDonutChart.module.css} (100%) diff --git a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx index 4ca641eae5..27b8c79975 100644 --- a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx +++ b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx @@ -3,7 +3,7 @@ import { useSelector } from 'react-redux'; import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts'; import { Input, Label, Row, Col, Alert, Spinner } from 'reactstrap'; import { fetchProjects, fetchPlannedCostBreakdown } from './plannedCostService'; -import './PlannedCostDonutChart.css'; +import './PlannedCostDonutChart.module.css'; const COLORS = { Plumbing: '#FF6384', diff --git a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.css b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.module.css similarity index 100% rename from src/components/PlannedCostDonutChart/PlannedCostDonutChart.css rename to src/components/PlannedCostDonutChart/PlannedCostDonutChart.module.css From 7e1bc30b4a98c7758b3a533658bef5e605223d0f Mon Sep 17 00:00:00 2001 From: saikrishnaraoyadagiri Date: Sun, 22 Feb 2026 13:00:36 -0500 Subject: [PATCH 3/6] implementing requested changes --- .../PlannedCostDonutChart.jsx | 95 +++++++++--- .../PlannedCostDonutChart.module.css | 143 ++++++++++++++++-- 2 files changed, 211 insertions(+), 27 deletions(-) diff --git a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx index 27b8c79975..d70adc2e4e 100644 --- a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx +++ b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx @@ -1,7 +1,7 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts'; -import { Input, Label, Row, Col, Alert, Spinner } from 'reactstrap'; +import { Label, Row, Col, Alert, Spinner } from 'reactstrap'; import { fetchProjects, fetchPlannedCostBreakdown } from './plannedCostService'; import './PlannedCostDonutChart.module.css'; @@ -32,13 +32,32 @@ const CustomTooltip = ({ active, payload, totalCost }) => { const PlannedCostDonutChart = () => { const [selectedProject, setSelectedProject] = useState(''); + const [selectedProjectName, setSelectedProjectName] = useState(''); const [chartData, setChartData] = useState([]); const [totalCost, setTotalCost] = useState(0); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [projects, setProjects] = useState([]); + const [dropdownOpen, setDropdownOpen] = useState(false); + const [searchTerm, setSearchTerm] = useState(''); const darkMode = useSelector(state => state.theme?.darkMode); + const dropdownRef = useRef(null); + + const filteredProjects = projects.filter(project => { + const name = project.name || project.projectName || ''; + return name.toLowerCase().includes(searchTerm.toLowerCase()); + }); + + useEffect(() => { + const handleClickOutside = event => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setDropdownOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); // Fetch projects list useEffect(() => { @@ -177,7 +196,7 @@ const PlannedCostDonutChart = () => { fontSize="12" fontWeight="bold" > - {percent > 0.05 ? `${(percent * 100).toFixed(0)}%` : ''} + {percent > 0.05 ? `${parseFloat((percent * 100).toFixed(1))}%` : ''} ); }; @@ -197,21 +216,61 @@ const PlannedCostDonutChart = () => { {/* Project Filter */} - - setSelectedProject(e.target.value)} - className="project-select" - > - - {projects.map(project => ( - - ))} - + +
+ { + setSearchTerm(e.target.value); + if (!dropdownOpen) setDropdownOpen(true); + }} + onFocus={() => { + setDropdownOpen(true); + setSearchTerm(''); + }} + /> + + {dropdownOpen && ( +
+ {filteredProjects.length > 0 ? ( + filteredProjects.map(project => ( +
{ + setSelectedProject(project._id); + setSelectedProjectName(project.name || project.projectName); + setDropdownOpen(false); + setSearchTerm(''); + }} + onKeyDown={e => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + setSelectedProject(project._id); + setSelectedProjectName(project.name || project.projectName); + setDropdownOpen(false); + setSearchTerm(''); + } + }} + > + {project.name || project.projectName} +
+ )) + ) : ( +
No projects found
+ )} +
+ )} +
diff --git a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.module.css b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.module.css index 2d4964e594..00c0f1da85 100644 --- a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.module.css +++ b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.module.css @@ -49,30 +49,130 @@ color: #e2e8f0; } -.project-select { +/* Searchable dropdown */ +.searchable-dropdown { + position: relative; + width: 100%; +} + +.dropdown-search-input { + width: 100%; border: 2px solid #e2e8f0; border-radius: 6px; padding: 0.75rem; + padding-right: 2.5rem; font-size: 1rem; transition: border-color 0.3s ease; + background: #ffffff; + color: #2d3748; +} + +.dropdown-search-input::placeholder { + color: #a0aec0; } -.project-select:focus { +.dropdown-search-input:focus { border-color: #4299e1; box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.1); outline: none; } -.dark-mode .project-select { +.dark-mode .dropdown-search-input { background: #4a5568; border-color: #718096; color: #e2e8f0; } -.dark-mode .project-select:focus { +.dark-mode .dropdown-search-input::placeholder { + color: #a0aec0; +} + +.dark-mode .dropdown-search-input:focus { border-color: #63b3ed; } +.dropdown-arrow { + position: absolute; + right: 0.75rem; + top: 50%; + transform: translateY(-50%); + pointer-events: none; + color: #718096; + font-size: 0.8rem; + transition: transform 0.2s ease; +} + +.dropdown-arrow.open { + transform: translateY(-50%) rotate(180deg); +} + +.dark-mode .dropdown-arrow { + color: #a0aec0; +} + +.dropdown-options { + position: absolute; + top: 100%; + left: 0; + right: 0; + max-height: 250px; + overflow-y: auto; + background: #ffffff; + border: 2px solid #e2e8f0; + border-top: none; + border-radius: 0 0 6px 6px; + z-index: 1000; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); +} + +.dark-mode .dropdown-options { + background: #2d3748; + border-color: #718096; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); +} + +.dropdown-option { + padding: 0.6rem 0.75rem; + cursor: pointer; + font-size: 0.95rem; + color: #2d3748; + transition: background 0.15s ease; +} + +.dropdown-option:hover { + background: #edf2f7; +} + +.dropdown-option.selected { + background: #ebf4ff; + font-weight: 600; + color: #2b6cb0; +} + +.dark-mode .dropdown-option { + color: #e2e8f0; +} + +.dark-mode .dropdown-option:hover { + background: #4a5568; +} + +.dark-mode .dropdown-option.selected { + background: #2a4365; + color: #90cdf4; +} + +.dropdown-no-results { + padding: 0.75rem; + color: #a0aec0; + text-align: center; + font-size: 0.9rem; +} + +.dark-mode .dropdown-no-results { + color: #718096; +} + .chart-area { display: flex; flex-direction: column; @@ -245,6 +345,31 @@ color: #a0aec0; } +/* Dark mode scrollbar for dropdown */ +.dark-mode .dropdown-options::-webkit-scrollbar { + width: 6px; +} + +.dark-mode .dropdown-options::-webkit-scrollbar-track { + background: #2d3748; +} + +.dark-mode .dropdown-options::-webkit-scrollbar-thumb { + background: #718096; + border-radius: 3px; +} + +.dark-mode .dropdown-options::-webkit-scrollbar-thumb:hover { + background: #a0aec0; +} + +/* Dark mode for Alert */ +.dark-mode .alert-danger { + background: #742a2a; + color: #feb2b2; + border-color: #9b2c2c; +} + /* Ensure no unnecessary scrollbars */ html, body { overflow-x: hidden; @@ -300,7 +425,7 @@ html, body { padding: 0.5rem; } - .project-select { + .dropdown-search-input { padding: 0.5rem; font-size: 0.9rem; } @@ -323,20 +448,20 @@ html, body { } /* Hover effects for interactive elements */ -.project-select:hover { +.dropdown-search-input:hover { border-color: #cbd5e0; } -.dark-mode .project-select:hover { +.dark-mode .dropdown-search-input:hover { border-color: #a0aec0; } /* Focus states for accessibility */ -.project-select:focus-visible { +.dropdown-search-input:focus-visible { outline: 2px solid #4299e1; outline-offset: 2px; } -.dark-mode .project-select:focus-visible { +.dark-mode .dropdown-search-input:focus-visible { outline-color: #63b3ed; } From a0b2b6bcac2dcd24be40094180c3b6da0224daee Mon Sep 17 00:00:00 2001 From: saikrishnaraoyadagiri Date: Sun, 22 Feb 2026 14:48:49 -0500 Subject: [PATCH 4/6] implementing requested changes --- .../PlannedCostDonutChart.jsx | 90 ++++++++++--------- .../PlannedCostDonutChart.module.css | 49 +++++++--- 2 files changed, 84 insertions(+), 55 deletions(-) diff --git a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx index d70adc2e4e..f989b84b52 100644 --- a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx +++ b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx @@ -1,9 +1,9 @@ import React, { useState, useEffect, useRef } from 'react'; import { useSelector } from 'react-redux'; import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts'; -import { Label, Row, Col, Alert, Spinner } from 'reactstrap'; +import { Alert, Spinner } from 'reactstrap'; import { fetchProjects, fetchPlannedCostBreakdown } from './plannedCostService'; -import './PlannedCostDonutChart.module.css'; +import styles from './PlannedCostDonutChart.module.css'; const COLORS = { Plumbing: '#FF6384', @@ -20,10 +20,10 @@ const CustomTooltip = ({ active, payload, totalCost }) => { const percentage = ((data.value / totalCost) * 100).toFixed(1); return ( -
-

{data.name}

-

Planned Cost: ${data.value.toLocaleString()}

-

Percentage: {percentage}%

+
+

{data.name}

+

Planned Cost: ${data.value.toLocaleString()}

+

Percentage: {percentage}%

); } @@ -93,7 +93,6 @@ const PlannedCostDonutChart = () => { let total = 0; if (Array.isArray(breakdown)) { - // If backend returns array of objects like [{category: 'Plumbing', plannedCost: 5500}, ...] const categoryMap = {}; breakdown.forEach(item => { if (item.category && typeof item.plannedCost === 'number') { @@ -110,13 +109,9 @@ const PlannedCostDonutChart = () => { total = Object.values(categoryMap).reduce((sum, cost) => sum + cost, 0); } else { - // If backend returns object like {total: 90000, breakdown: {plumbing: 5500, ...}} if (breakdown.total && breakdown.breakdown) { - // Use the total from backend and breakdown for chart data - // Handle case-insensitive matching const breakdownData = breakdown.breakdown; - // Check if we actually have valid data const hasValidData = Object.values(breakdownData).some( value => typeof value === 'number' && value > 0, ); @@ -138,14 +133,12 @@ const PlannedCostDonutChart = () => { }, ]; - total = breakdown.total; // Use the total from backend + total = breakdown.total; } else { - // No valid data available transformedData = []; total = 0; } } else { - // Fallback to old format transformedData = [ { name: 'Plumbing', value: breakdown.plumbing || 0 }, { name: 'Electrical', value: breakdown.electrical || 0 }, @@ -203,25 +196,29 @@ const PlannedCostDonutChart = () => { if (error && !selectedProject) { return ( -
+
{error}
); } return ( -
-

Planned Cost Breakdown by Type of Expenditure

+
+

Planned Cost Breakdown by Type of Expenditure

{/* Project Filter */} - - - -
+
+
+ +
{ @@ -233,9 +230,13 @@ const PlannedCostDonutChart = () => { setSearchTerm(''); }} /> - + + ▾ + {dropdownOpen && ( -
+
{filteredProjects.length > 0 ? ( filteredProjects.map(project => (
{ role="option" tabIndex={0} aria-selected={selectedProject === project._id} - className={`dropdown-option ${ - selectedProject === project._id ? 'selected' : '' + className={`${styles['dropdown-option']} ${ + selectedProject === project._id ? styles.selected : '' }`} onClick={() => { setSelectedProject(project._id); @@ -266,18 +267,18 @@ const PlannedCostDonutChart = () => {
)) ) : ( -
No projects found
+
No projects found
)}
)}
- - +
+
{/* Chart Area */} -
+
{loading ? ( -
+

Loading planned cost data...

@@ -306,21 +307,24 @@ const PlannedCostDonutChart = () => { {/* Center display showing total */} -
-
- Total Planned Cost - ${totalCost.toLocaleString()} +
+
+ Total Planned Cost + ${totalCost.toLocaleString()}
{/* Legend */} -
-
+
+
{chartData.map((entry, index) => ( -
-
- {entry.name} - +
+
+ {entry.name} + ${entry.value.toLocaleString()} ( {((entry.value / totalCost) * 100).toFixed(1)}%) @@ -330,11 +334,11 @@ const PlannedCostDonutChart = () => {
) : selectedProject ? ( -
+

No planned cost data available for this project.

) : ( -
+

Please select a project to view the planned cost breakdown.

)} diff --git a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.module.css b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.module.css index 00c0f1da85..e524ad3bf8 100644 --- a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.module.css +++ b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.module.css @@ -1,13 +1,15 @@ .planned-cost-container { padding: 2rem; background: #ffffff; - border-radius: 12px; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); - margin: 1rem 0; + border-radius: 0; + box-shadow: none; + margin: 0; + width: 100%; transition: all 0.3s ease; overflow: visible; - height: auto; - min-height: auto; + min-height: calc(100vh - 80px); + display: flex; + flex-direction: column; } .planned-cost-container.dark-mode { @@ -33,19 +35,27 @@ padding: 1rem; background: #f7fafc; border-radius: 8px; + display: flex; + justify-content: center; } .dark-mode .filter-section { background: #4a5568; } -.filter-section label { +.filter-col { + width: 100%; + max-width: 500px; +} + +.filter-label { + display: block; font-weight: 600; margin-bottom: 0.5rem; color: #4a5568; } -.dark-mode .filter-section label { +.dark-mode .filter-label { color: #e2e8f0; } @@ -78,17 +88,30 @@ } .dark-mode .dropdown-search-input { - background: #4a5568; + background-color: #4a5568 !important; border-color: #718096; - color: #e2e8f0; + color: #e2e8f0 !important; + -webkit-text-fill-color: #e2e8f0; } .dark-mode .dropdown-search-input::placeholder { color: #a0aec0; + -webkit-text-fill-color: #a0aec0; } .dark-mode .dropdown-search-input:focus { + background-color: #4a5568 !important; border-color: #63b3ed; + color: #e2e8f0 !important; + -webkit-text-fill-color: #e2e8f0; +} + +.dark-mode .dropdown-search-input:-webkit-autofill, +.dark-mode .dropdown-search-input:-webkit-autofill:hover, +.dark-mode .dropdown-search-input:-webkit-autofill:focus { + -webkit-box-shadow: 0 0 0 1000px #4a5568 inset !important; + -webkit-text-fill-color: #e2e8f0 !important; + transition: background-color 5000s ease-in-out 0s; } .dropdown-arrow { @@ -178,6 +201,7 @@ flex-direction: column; align-items: center; overflow: visible; + flex: 1; } .loading-container { @@ -204,10 +228,11 @@ display: flex; align-items: center; justify-content: center; - min-height: 200px; + flex: 1; + min-height: 300px; padding: 2rem; color: #718096; - font-size: 1.1rem; + font-size: 1.2rem; text-align: center; } @@ -364,7 +389,7 @@ } /* Dark mode for Alert */ -.dark-mode .alert-danger { +.dark-mode :global(.alert-danger) { background: #742a2a; color: #feb2b2; border-color: #9b2c2c; From 7d905eed150b38078647999fb6578a2668da4a1e Mon Sep 17 00:00:00 2001 From: saikrishnaraoyadagiri Date: Sun, 22 Feb 2026 15:47:46 -0500 Subject: [PATCH 5/6] routes fix --- src/routes.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes.jsx b/src/routes.jsx index 410a0e8296..3ec84437c6 100644 --- a/src/routes.jsx +++ b/src/routes.jsx @@ -893,7 +893,6 @@ export default ( allowedRoles={[UserRole.Administrator, UserRole.CoreTeam, UserRole.Owner]} routePermissions={RoutePermissions.accessHgnSkillsDashboard} /> - From 894f640cdb467a01b4786d3337b2a20f6fa78e86 Mon Sep 17 00:00:00 2001 From: saikrishnaraoyadagiri Date: Sun, 22 Feb 2026 17:06:31 -0500 Subject: [PATCH 6/6] lint issues --- .../PlannedCostDonutChart/PlannedCostDonutChart.jsx | 8 ++------ .../PlannedCostDonutChart/plannedCostService.js | 5 +++++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx index f989b84b52..e225fc7fdc 100644 --- a/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx +++ b/src/components/PlannedCostDonutChart/PlannedCostDonutChart.jsx @@ -203,9 +203,7 @@ const PlannedCostDonutChart = () => { } return ( -
+

Planned Cost Breakdown by Type of Expenditure

{/* Project Filter */} @@ -230,9 +228,7 @@ const PlannedCostDonutChart = () => { setSearchTerm(''); }} /> - + {dropdownOpen && ( diff --git a/src/components/PlannedCostDonutChart/plannedCostService.js b/src/components/PlannedCostDonutChart/plannedCostService.js index 81f2f1f90d..d603026ffc 100644 --- a/src/components/PlannedCostDonutChart/plannedCostService.js +++ b/src/components/PlannedCostDonutChart/plannedCostService.js @@ -19,6 +19,7 @@ export const fetchProjects = async () => { return response.data || []; } catch (error) { + // eslint-disable-next-line no-console console.error('Error fetching projects:', error); throw new Error('Failed to fetch projects'); } @@ -45,6 +46,7 @@ export const fetchPlannedCostBreakdown = async projectId => { return response.data; } catch (error) { + // eslint-disable-next-line no-console console.error('Error fetching planned cost breakdown:', error); throw new Error(error.response?.data?.message || 'Failed to fetch planned cost breakdown'); } @@ -77,6 +79,7 @@ export const createOrUpdatePlannedCost = async (projectId, category, plannedCost return response.data; } catch (error) { + // eslint-disable-next-line no-console console.error('Error creating/updating planned cost:', error); throw new Error(error.response?.data?.message || 'Failed to create/update planned cost'); } @@ -104,6 +107,7 @@ export const deletePlannedCost = async (projectId, category) => { return response.data; } catch (error) { + // eslint-disable-next-line no-console console.error('Error deleting planned cost:', error); throw new Error(error.response?.data?.message || 'Failed to delete planned cost'); } @@ -127,6 +131,7 @@ export const getAllPlannedCostsForProject = async projectId => { return response.data || []; } catch (error) { + // eslint-disable-next-line no-console console.error('Error fetching all planned costs:', error); throw new Error(error.response?.data?.message || 'Failed to fetch planned costs'); }