Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 106 additions & 16 deletions src/components/BMDashboard/WeeklyProjectSummary/CostPredictionChart.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ReferenceLine,
} from 'recharts';
import styles from './CostPredictionChart.module.css';
import { fetchBMProjects } from '../../../actions/bmdashboard/projectActions';
import projectCostService from '../../../services/projectCostService';

// Custom dot renderer (unchanged)
Expand Down Expand Up @@ -69,12 +70,30 @@ function CostPredictionChart({ projectId }) {
const [chartData, setChartData] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// Date filters
const [startDate, setStartDate] = useState('');
const [endDate, setEndDate] = useState('');
const darkMode = useSelector(state => state.theme.darkMode);
//Project list for dropdown
const projects = useSelector(state => state.bmProjects) || [];
const [selectedProject, setSelectedProject] = useState('');
const legendItems = [
{ label: 'Planned Cost', color: '#7acba6', type: 'circle' },
{ label: 'Actual Cost', color: '#9aa6ff', type: 'circle' },
{ label: 'Predicted Cost', color: '#ff8c2a', type: 'dash' },
];
// Reset all the filters
const resetFilters = () => {
setSelectedProject('');
setStartDate('');
setEndDate('');
setError('');
};
//Project dropdown
useEffect(() => {
dispatch(fetchBMProjects());
}, [dispatch]);

useEffect(() => {
const fetchData = async () => {
try {
Expand Down Expand Up @@ -106,8 +125,9 @@ function CostPredictionChart({ projectId }) {
}
};

if (projectId) fetchData();
}, [projectId]);
const effectiveProjectId = selectedProject || projectId; //fetches data when either the prop projectId or the dropdown selection changes
if (effectiveProjectId) fetchData();
}, [projectId, selectedProject]);

if (loading) return <div>Loading chart data...</div>;
if (error) return <div>Error: {error}</div>;
Expand All @@ -117,17 +137,74 @@ function CostPredictionChart({ projectId }) {
year: 'numeric',
});

// Filter data by date range
const filteredData = chartData.filter(item => {
const itemDate = new Date(item.month);
const start = startDate ? new Date(startDate) : null;
const end = endDate ? new Date(endDate) : null;
return (!start || itemDate >= start) && (!end || itemDate <= end);
});

// THEME COLORS — only for grid, axis ticks/lines, legend
const gridColor = darkMode ? '#e5e7eb' : '#9ca3af'; // grid lines
const tickColor = darkMode ? '#e5e7eb' : '#9ca3af'; // tick text
const axisLineCol = darkMode ? '#e5e7eb' : '#9ca3af'; // axis baseline & tick marks
const legendColor = darkMode ? '#e5e7eb' : '#9ca3af'; // legend text
console.log('ticl', tickColor);

return (
<div className={styles.titleContainer}>
<h2 className={styles.title}>Planned Vs Actual costs tracking</h2>
{/* Project Filter Dropdown */}
<div style={{ display: 'flex', gap: '10px', marginBottom: '10px' }}>
<div className={styles.selectorGroup}>
<label htmlFor="projectSelect">Project: </label>
<select
id="projectSelect"
value={selectedProject}
onChange={e => setSelectedProject(e.target.value)}
>
<option value="">Select a Project</option>
{projects.map(project => (
<option key={project._id} value={project._id}>
{project.name}
</option>
))}
</select>
</div>
</div>

{/* Date Filters */}
<div style={{ display: 'flex', gap: '10px', marginBottom: '10px' }}>
<div className={styles.selectorGroup}>
<label htmlFor="startDate">Start Date: </label>
<input type="date" value={startDate} onChange={e => setStartDate(e.target.value)} />
</div>
<div className={styles.selectorGroup}>
<label htmlFor="endDate">End Date: </label>
<input type="date" value={endDate} onChange={e => setEndDate(e.target.value)} />
</div>
</div>
{/* Reset Filters button */}
<div style={{ minWidth: '120px' }}>
<button
type="button"
onClick={resetFilters}
style={{
padding: '0.2rem 0.8rem',
borderRadius: '6px',
border: '1px solid #d9d2d2ff',
background: '#dededeff',
cursor: 'pointer',
fontSize: '0.95rem',
}}
aria-label="Reset filters"
title="Reset filters"
>
Reset
</button>
</div>
<ResponsiveContainer width="100%" height={400}>
<LineChart data={chartData} margin={{ top: 20, right: 30, left: 20, bottom: 40 }}>
<LineChart data={filteredData} margin={{ top: 20, right: 30, left: 20, bottom: 40 }}>
{/* Grid */}
<CartesianGrid
strokeDasharray="3 3"
Expand All @@ -140,18 +217,28 @@ function CostPredictionChart({ projectId }) {
{/* Axes: tick text, tick marks, baseline lines */}
<XAxis
dataKey="month"
tick={({ x, y, payload }) => (
<text
x={x}
y={y + 15} // push text down so it doesn’t overlap axis line
textAnchor="middle"
fill={darkMode ? '#e5e7eb' : '#9ca3af'}
>
{payload.value}
</text>
)}
tickMargin={0} // apply margin at XAxis level, not inside <text>
tick={({ x, y, payload }) => {
// Converts "February 2024" to "Feb 24"
const shortLabel = payload.value.replace(
/(\w{3})\w*\s(\d{4})/,
(_, m, y) => `${m} ${y.slice(-2)}`,
);

return (
<text
x={x}
y={y + 15} // push text down so it doesn’t overlap axis line
textAnchor="middle"
fill={darkMode ? '#e5e7eb' : '#9ca3af'}
fontSize={15}
>
{shortLabel}
</text>
);
}}
tickMargin={10}
/>

<YAxis
tick={({ x, y, payload }) => (
<text x={x} y={y} textAnchor="end" fill={darkMode ? '#e5e7eb' : '#9ca3af'}>
Expand All @@ -160,7 +247,10 @@ function CostPredictionChart({ projectId }) {
)}
/>
{/* Tooltip & Legend */}
<Tooltip />
<Tooltip
labelFormatter={label => ` ${label}`}
formatter={(value, name) => [`$${Number(value).toLocaleString()}`, name]}
/>
<Legend
verticalAlign="bottom"
height={48}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
.title {
margin: 0 0 8px 0;
text-align: center;
font-size: 24px;
font-size: 20px;
font-weight: normal;
color: var(--text-color);
}
Expand All @@ -14,6 +14,31 @@
height: 100%;
padding: 20px;
}
/* === selectors layout === */
.selectorGroup {
flex: 1;
display: flex;
flex-direction: column;
}

.selectorGroup label {
font-size: small;
margin-bottom: 3px;
color: var(--text-color);
}

.selectorGroup select {
font-size: small;
width: 100%;
max-height: 200px;
overflow-y: auto;
background-color: var(--card-bg);
color: var(--text-color);
border: 1px solid var(--button-hover);
border-radius: 4px;
padding: 4px 8px;
box-sizing: border-box;
}

.legendList {
display: flex;
Expand All @@ -36,4 +61,4 @@
display: flex;
align-items: center;
gap: 8px;
}
}
Loading