diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureCard.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureCard.jsx new file mode 100644 index 0000000000..7b9e9f3f59 --- /dev/null +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureCard.jsx @@ -0,0 +1,113 @@ +import { useEffect, useState } from 'react'; +import axios from 'axios'; +import { ENDPOINTS } from '../../../../utils/URL'; +import ExpenditureChart from './ExpenditureChart'; +import styles from './ExpenditureCard.module.css'; + +function useProjectList() { + const [projectList, setProjectList] = useState([]); + const [selectedProject, setSelectedProject] = useState(''); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + + axios + .get(ENDPOINTS.BM_EXPENDITURE_PROJECTS) + .then(({ data }) => { + if (cancelled) return; + const labeled = Array.isArray(data) + ? data.map((id, index) => ({ + id, + name: `Project ${String.fromCodePoint(65 + index)}`, + })) + : []; + setProjectList(labeled); + if (labeled.length > 0) setSelectedProject(labeled[0].id); + }) + .catch(err => { + if (!cancelled) { + setError(err?.response?.data?.message || 'Failed to load projects'); + } + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + + return () => { + cancelled = true; + }; + }, []); + + return { projectList, selectedProject, setSelectedProject, loading, error }; +} + +/** + * ExpenditureCard — unified card for both layout variants. + * + * Props: + * mode ('comparison' | 'stacked') — layout variant; defaults to 'stacked' + * pieType ('actual' | 'planned') — required when mode === 'stacked' + */ +function ExpenditureCard({ mode = 'stacked', pieType }) { + const { projectList, selectedProject, setSelectedProject, loading, error } = useProjectList(); + + const isStacked = mode === 'stacked'; + const cardClass = isStacked ? `${styles.card} ${styles.cardStacked}` : styles.card; + const selectId = isStacked ? `sec-project-select-${pieType}` : 'ft-project-select'; + + if (loading) { + return ( +
+ {/* is the semantic equivalent of role="status" with implicit aria-live="polite" */} + Loading project list… +
+ ); + } + + if (error) { + return ( +
+

+ {error} +

+
+ ); + } + + return ( +
+
+
+ + +
+
+ + {selectedProject && ( + + )} +
+ ); +} + +export default ExpenditureCard; diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureCard.module.css b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureCard.module.css new file mode 100644 index 0000000000..2d9598b057 --- /dev/null +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureCard.module.css @@ -0,0 +1,97 @@ +/* ============================================================ + ExpenditureCard — Shared CSS Module + Used by both FinancialsTrackingCard (comparison mode) and + SingleExpenditureCard (stacked mode). + All colours use CSS custom properties that cascade from + WeeklyProjectSummary.module.css (:root / .darkMode). + Light / dark switching is therefore automatic. + ============================================================ */ + +/* ── Outer card ──────────────────────────────────────────────── */ +.card { + background: var(--card-bg); + border-radius: 10px; + padding: 20px; + box-shadow: 0 2px 8px var(--card-shadow); + width: 100%; + box-sizing: border-box; + color: var(--text-color); +} + +/* Stacked-mode card needs fixed min-height and column flex */ +.cardStacked { + min-height: 340px; + display: flex; + flex-direction: column; +} + +/* ── Controls row ────────────────────────────────────────────── */ +.controls { + margin-bottom: 20px; +} + +.controlsStacked { + margin-bottom: 12px; +} + +/* ── Project select group ────────────────────────────────────── */ +.selectGroup { + display: flex; + flex-direction: column; + gap: 4px; + max-width: 280px; +} + +.selectGroupStacked { + max-width: 240px; +} + +.selectLabel { + font-size: 12px; + font-weight: 500; + color: var(--text-color); +} + +.select { + padding: 6px 10px; + background: var(--section-bg); + color: var(--text-color); + border: 1px solid var(--border-color, #cccccc); + border-radius: 6px; + font-size: 14px; + cursor: pointer; + transition: border-color 0.15s ease; +} + +.select:focus { + outline: none; + border-color: var(--focus-border-color, #3b82f6); +} + +/* ── Loading / error states ──────────────────────────────────── */ +.stateMessage { + display: flex; + align-items: center; + justify-content: center; + padding: 48px 20px; + font-size: 14px; + color: var(--text-color); + text-align: center; +} + +.errorMessage { + color: var(--neg-color, #b91c1c); +} + +/* ── Responsive ─────────────────────────────────────────────── */ +@media (max-width: 640px) { + .card { + padding: 16px; + } + + .selectGroup, + .selectGroupStacked { + max-width: 100%; + width: 100%; + } +} diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.jsx index e5d032a325..b84ebe86dd 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.jsx @@ -1,98 +1,353 @@ -import { useEffect, useState } from 'react'; -import { PieChart, Pie, Cell, Tooltip, Legend } from 'recharts'; +import { useCallback, useEffect, useState } from 'react'; import axios from 'axios'; +import { useSelector } from 'react-redux'; +import { Cell, Legend, Pie, PieChart, ResponsiveContainer, Tooltip } from 'recharts'; +import { ENDPOINTS } from '../../../../utils/URL'; import styles from './ExpenditureChart.module.css'; +// Category → colour mapping (Labour=blue, Equipment=green, Materials=yellow) const COLORS = ['#6777EF', '#A0CD61', '#F5CD4B']; -const CATEGORIES = ['Labor', 'Equipment', 'Materials']; +const CATEGORY_NAMES = ['Labor', 'Equipment', 'Materials']; function normalizeData(data) { - return CATEGORIES.map(cat => { - const found = data.find(d => d.category === cat); + return CATEGORY_NAMES.map(cat => { + const found = Array.isArray(data) ? data.find(d => d.category === cat) : undefined; return found || { category: cat, amount: 0 }; }); } -const renderCustomLabel = ({ cx, cy, midAngle, outerRadius, percent, name }) => { - const RADIAN = Math.PI / 180; - const radius = outerRadius * 0.6; - const x = cx + radius * Math.cos(-midAngle * RADIAN); - const y = cy + radius * Math.sin(-midAngle * RADIAN); +/** + * PieChartPanel — renders one pie (Actual or Planned). + * Works as a standalone stacked card and as one half of the comparison layout. + */ +function PieChartPanel({ data, title, darkMode, compact }) { + const normalizedData = normalizeData(data); + const hasData = normalizedData.some(d => d.amount > 0); + + /** + * Renders a label inside each pie slice. + * - Color: white in dark mode, black in light mode. + * - Single line when the chord width allows ≥ 8 px font. + * - Two lines (name / percentage) otherwise, each independently sized to + * fit within the chord so text never overflows the slice. + */ + const labelRenderer = useCallback( + ({ cx, cy, midAngle, outerRadius, percent, name }) => { + if (percent === 0) return null; + + const RADIAN = Math.PI / 180; + const labelRadius = outerRadius * 0.65; + const x = cx + labelRadius * Math.cos(-midAngle * RADIAN); + const y = cy + labelRadius * Math.sin(-midAngle * RADIAN); + + const fill = darkMode ? '#ffffff' : '#000000'; + const percentLabel = `${(percent * 100).toFixed(1)}%`; + const fullLabel = `${name}: ${percentLabel}`; + + // ── Available horizontal width at the label position ────────────── + // + // Constraint 1 — circle boundary: + // The pie circle limits how far text can extend left/right at the + // label's y-position. For a label near the left/right edge of the + // pie (e.g. large slices whose mid-angle points sideways), this is + // the binding constraint that the simple arc-chord formula misses. + const dy = y - cy; + const pieHalfWidth = Math.sqrt(Math.max(0, outerRadius * outerRadius - dy * dy)); + const circleAvailableWidth = Math.max(0, 2 * (pieHalfWidth - Math.abs(x - cx))); + + // Constraint 2 — angular chord: + // Prevents text from bleeding into adjacent slices. The chord at + // labelRadius is a conservative approximation; it over-constrains + // left/right-oriented slices but that only makes the font slightly + // smaller, which is a safe trade-off. + const sliceAngle = percent * 2 * Math.PI; + const angularChord = 2 * labelRadius * Math.sin(sliceAngle / 2); + + // The tighter of the two constraints is the actual available width. + const availableWidth = Math.min(circleAvailableWidth, angularChord); + + // ── Choose single-line or two-line rendering ────────────────────── + // Avg character width ≈ 0.58× font-size for mixed alphanumeric text. + const AVG_CHAR_RATIO = 0.58; + const singleLineFontSize = availableWidth / (fullLabel.length * AVG_CHAR_RATIO); + + if (singleLineFontSize >= 8) { + return ( + + {fullLabel} + + ); + } + + // Two-line fallback: name on top, percentage on bottom. + // Size to the longer string so both lines stay within availableWidth. + const longerLen = Math.max(name.length, percentLabel.length); + const twoLineFontSize = Math.max( + 7, + Math.min(11, availableWidth / (longerLen * AVG_CHAR_RATIO)), + ); + const lineSpacing = twoLineFontSize * 1.3; + + // If the minimum legible font (7 px) still can't fit the longer line, + // suppress the label entirely rather than rendering unreadable overflow. + if (availableWidth < longerLen * AVG_CHAR_RATIO * 7) return null; + + return ( + + + {name} + + + {percentLabel} + + + ); + }, + [darkMode], + ); + + // Inline styles for recharts elements that can't be controlled via CSS vars + const legendStyle = { + color: darkMode ? '#ffffff' : '#000000', + fontSize: '12px', + paddingTop: '8px', + }; + + const tooltipContentStyle = { + backgroundColor: darkMode ? '#2b3e59' : '#ffffff', + borderColor: darkMode ? '#4a5a77' : '#cccccc', + borderRadius: '6px', + fontSize: '12px', + }; + + const tooltipItemStyle = { color: darkMode ? '#ffffff' : '#333333' }; + const tooltipLabelStyle = { color: darkMode ? '#ffffff' : '#333333', fontWeight: 600 }; return ( - - {`${name}: ${(percent * 100).toFixed(1)}%`} - +
+

{title}

+ + {hasData ? ( +
+ + + + {normalizedData.map((entry, index) => ( + + ))} + + + + + +
+ ) : ( + // is the semantic element for role="status"; aria-live="polite" is implicit + + No expenditure data for this project + + )} +
); -}; +} + +/** + * ExpenditureChart — fetches /bm/expenditure/:projectId/pie once, + * then renders: + * • pieType set — single PieChartPanel (actual OR planned), no card wrapper + * • stacked — two separate cards, each with one pie + * • comparison — side-by-side on desktop, tab-switched on mobile (≤640 px) + * + * Props: + * projectId (string) — selected project ObjectId + * viewMode ('stacked'|'comparison') — ignored when pieType is set + * pieType ('actual'|'planned'|undefined) — when set, renders one pie only + */ +function ExpenditureChart({ projectId, viewMode, pieType }) { + const darkMode = useSelector(state => state.theme.darkMode); -function ExpenditureChart({ projectId }) { const [actual, setActual] = useState([]); const [planned, setPlanned] = useState([]); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + // activeTab controls which panel is shown on mobile in comparison mode + const [activeTab, setActiveTab] = useState('actual'); useEffect(() => { - const fetchData = async () => { - if (!projectId) return; - setLoading(true); - setError(null); - try { - const res = await axios.get( - `${process.env.REACT_APP_APIENDPOINT}/bm/expenditure/${projectId}/pie`, - ); - setActual(res.data.actual); - setPlanned(res.data.planned); - } catch (err) { - setError('Failed to load expenditure data'); - } finally { - setLoading(false); - } + if (!projectId) return undefined; + + let cancelled = false; + setLoading(true); + setError(null); + + axios + .get(ENDPOINTS.BM_EXPENDITURE_PIE(projectId)) + .then(({ data }) => { + if (!cancelled) { + setActual(Array.isArray(data.actual) ? data.actual : []); + setPlanned(Array.isArray(data.planned) ? data.planned : []); + } + }) + .catch(() => { + if (!cancelled) setError('Failed to load expenditure data'); + }) + .finally(() => { + if (!cancelled) setLoading(false); + }); + + return () => { + cancelled = true; }; - fetchData(); }, [projectId]); - const renderChart = (data, title) => ( -
-

{title}

- - + Select a project to view expenditure data +
+ ); + } + + if (loading) { + // is the semantic element for role="status" (aria-live="polite" is implicit) + return ( + + + ); + } + + if (error) { + return ( +
+ {error} +
+ ); + } + + // ── Single-pie mode (used by SingleExpenditureCard) ────────── + if (pieType) { + const panelData = pieType === 'actual' ? actual : planned; + const panelTitle = pieType === 'actual' ? 'Actual Expenditure' : 'Planned Expenditure'; + return ; + } + + // ── Comparison mode ────────────────────────────────────────── + if (viewMode === 'comparison') { + return ( +
+ {/* + Tab bar — hidden via CSS on desktop (>640 px), + visible on mobile to switch between the two panels. + */} +
- {data.map((entry, index) => ( - - ))} - - - - -
- ); + + +
+ + {/* Side-by-side row (desktop) / single visible panel (mobile) */} +
+
+ +
- if (loading) - return
Loading expenditure data...
; - if (error) return
{error}
; +
+ +
+
+ + ); + } + // ── Stacked mode (default) ─────────────────────────────────── return ( -
- {renderChart(normalizeData(actual), 'Actual Expenditure')} - {renderChart(normalizeData(planned), 'Planned Expenditure')} +
+
+ +
+
+ +
); } diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.module.css b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.module.css index 8b58a523e4..1aceebe5d5 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.module.css +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/ExpenditureChart.module.css @@ -1,63 +1,227 @@ +/* ============================================================ + ExpenditureChart — CSS Module + All colors use CSS custom properties defined in + WeeklyProjectSummary.module.css (:root / .darkMode scope) + so light/dark switching is automatic for anything that uses + var(--*). Recharts elements that need dark-mode colours are + handled via inline styles in JSX using the darkMode prop. + ============================================================ */ + +/* ── Stacked mode: outer flex row ─────────────────────────── */ .expenditure-chart-wrapper { display: flex; - justify-content: center; flex-wrap: wrap; - gap: 16px; + gap: 20px; width: 100%; - padding: 0; - margin: 0; } +/* ── Stacked mode: individual pie card ─────────────────────── */ .expenditure-chart-card { - flex: 1 1 220px; - max-width: 260px; - padding: 12px 10px; - min-height: 300px; + flex: 1 1 260px; + max-width: 520px; background: var(--card-bg); - box-shadow: 0 2px 4px var(--card-shadow); - border-radius: 8px; + border-radius: 10px; + padding: 16px 14px 12px; + box-shadow: 0 2px 8px var(--card-shadow); display: flex; flex-direction: column; - align-items: center; - justify-content: flex-start; + align-items: stretch; + min-height: 340px; } -.expenditure-chart-title { - font-size: 1.1rem; - font-weight: bold; +/* ── Comparison mode: full-width layout wrapper ─────────────── */ +.expenditure-chart-comparison { + width: 100%; + display: flex; + flex-direction: column; + gap: 0; +} + +/* ── Comparison mode: tab bar (hidden on desktop) ───────────── */ +.expenditure-chart-tab-bar { + display: none; /* shown only on mobile via media query below */ + gap: 8px; + margin-bottom: 16px; +} + +.expenditure-chart-tab { + flex: 1; + padding: 8px 16px; + background: var(--section-bg); + color: var(--text-color); + border: 1px solid var(--border-color, #ccc); + border-radius: 20px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease; text-align: center; - margin-bottom: 6px; } -.recharts-legend-wrapper { - margin-top: -10px !important; +.expenditure-chart-tab:hover { + background: var(--section-title-hover); +} + +.expenditure-chart-tab--active { + background: var(--button-bg); + color: #fff; + border-color: var(--button-bg); +} + +.expenditure-chart-tab:focus-visible { + outline: 2px solid var(--focus-border-color, #3b82f6); + outline-offset: 2px; +} + +/* ── Comparison mode: panes row (side-by-side on desktop) ──── */ +.expenditure-chart-panes { + display: flex; + flex-direction: row; + gap: 20px; + width: 100%; } -.recharts-legend-item-text { - font-size: 12px !important; - white-space: nowrap; +/* ── Comparison mode: each half-panel ─────────────────────── */ +.expenditure-chart-panel { + flex: 1 1 0; + min-width: 0; } -.recharts-pie-label-text { - font-size: 10px !important; +/* Hidden panel class — only activates on mobile (see @media) */ +.expenditure-chart-panel--hidden-mobile { + /* no-op on desktop; hidden on mobile via media query */ +} + +/* ── Shared chart-pane (title + ResponsiveContainer) ────────── */ +.chart-pane { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + height: 100%; +} + +.chart-pane__title { + font-size: 1rem; font-weight: 600; - fill: white; - text-anchor: middle; - alignment-baseline: central; - white-space: nowrap; + color: var(--text-color); + text-align: center; + margin: 0 0 10px 0; } -.dark-mode .expenditure-chart-card { - background: var(--card-bg); - box-shadow: 0 2px 5px rgba(255, 255, 255, 0.08); +/* ResponsiveContainer wrapper — must have explicit height */ +.chart-pane__container { + width: 100%; + height: 280px; + max-height: 340px; + flex: 1 1 auto; +} + +/* Compact variant used in comparison mode panes */ +.chart-pane--compact .chart-pane__container { + height: 260px; } -.dark-mode .expenditure-chart-title { +/* No-data message inside a chart pane */ +.chart-pane__empty { + display: flex; + justify-content: center; + align-items: center; + height: 200px; + font-size: 14px; color: var(--text-color); + text-align: center; + opacity: 0.7; + padding: 16px; } -.dark-mode .recharts-default-tooltip { - background-color: #333 !important; - color: #fff !important; - border: 1px solid #555 !important; +/* ── Loading state ──────────────────────────────────────────── */ +.expenditure-chart-loading { + display: flex; + align-items: center; + justify-content: center; + gap: 12px; + padding: 48px 20px; + font-size: 14px; + color: var(--text-color); + text-align: center; +} + +/* CSS spinner */ +.expenditure-chart-spinner { + display: inline-block; + width: 20px; + height: 20px; + border: 3px solid var(--card-shadow); + border-top-color: var(--button-bg); + border-radius: 50%; + flex-shrink: 0; + animation: expenditure-spin 0.75s linear infinite; +} + +@keyframes expenditure-spin { + to { + transform: rotate(360deg); + } +} + +/* ── Error state ────────────────────────────────────────────── */ +.expenditure-chart-error { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + padding: 40px 20px; + font-size: 14px; + color: var(--neg-color, #b91c1c); + text-align: center; +} + +/* ── No-project selected state ──────────────────────────────── */ +.expenditure-chart-empty { + display: flex; + align-items: center; + justify-content: center; + padding: 40px 20px; + font-size: 14px; + color: var(--text-color); + text-align: center; + opacity: 0.7; +} + +/* ── Responsive: small screens ─────────────────────────────── */ +@media (max-width: 640px) { + /* Show tab bar in comparison mode */ + .expenditure-chart-tab-bar { + display: flex; + } + + /* Stack the two panes vertically (only one is visible at a time via JS) */ + .expenditure-chart-panes { + flex-direction: column; + } + + /* Hide inactive panel in comparison mode on mobile */ + .expenditure-chart-panel--hidden-mobile { + display: none; + } + + /* Stacked mode cards go full-width */ + .expenditure-chart-wrapper { + flex-direction: column; + } + + .expenditure-chart-card { + max-width: 100%; + flex-basis: auto; + } + + /* Slightly shorter charts on small screens */ + .chart-pane__container { + height: 240px; + } + + .chart-pane--compact .chart-pane__container { + height: 220px; + } } diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingCard.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingCard.jsx index d1eb5e79f5..ea6f7c7080 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingCard.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingCard.jsx @@ -1,58 +1,7 @@ -import { useEffect, useState } from 'react'; -import axios from 'axios'; -import ExpenditureChart from './ExpenditureChart'; +import ExpenditureCard from './ExpenditureCard'; function FinancialsTrackingCard() { - const [projectList, setProjectList] = useState([]); - const [selectedProject, setSelectedProject] = useState(''); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchProjects = async () => { - try { - setLoading(true); - const res = await axios.get(`${process.env.REACT_APP_APIENDPOINT}/bm/expenditure/projects`); - const labeledProjects = res.data.map((id, index) => ({ - id, - name: `Project ${String.fromCharCode(65 + index)}`, - })); - setProjectList(labeledProjects); - if (labeledProjects.length > 0) { - setSelectedProject(labeledProjects[0].id); - } - } catch (err) { - // console.error('Error fetching project IDs:', err); - setError('Failed to load projects'); - } finally { - setLoading(false); - } - }; - fetchProjects(); - }, []); - - if (loading) return
Loading project list...
; - if (error) return
{error}
; - - return ( -
-
- - -
- {selectedProject && } -
- ); + return ; } export default FinancialsTrackingCard; diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingSection.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingSection.jsx new file mode 100644 index 0000000000..314c135769 --- /dev/null +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingSection.jsx @@ -0,0 +1,112 @@ +import { useState } from 'react'; +import ActualVsPlannedCost from '../ActualVsPlannedCost/ActualVsPlannedCost'; +import FinancialsTrackingCard from './FinancialsTrackingCard'; +import SingleExpenditureCard from './SingleExpenditureCard'; +import styles from './FinancialsTrackingSection.module.css'; + +/** + * FinancialsTrackingSection + * + * Owns the stacked / comparison layout toggle for the Financials + * Tracking section. Renders different card grids based on viewMode: + * + * Stacked (default) — 4 independent cards in a 2×2 grid: + * [Actual Expenditure] [Planned Expenditure] + * [Actual vs Planned] [Placeholder] + * + * Comparison — 3 cards (combined card spans full width at top): + * [Combined Expenditure Card — shared filter, both pies S-by-S] + * [Actual vs Planned] [Placeholder] + */ +function FinancialsTrackingSection() { + const [viewMode, setViewMode] = useState('stacked'); + + return ( +
+ {/* ── View-mode toggle ─────────────────────────────────────── */} +
+ + +
+ + {viewMode === 'stacked' ? ( + /* ── Stacked: 2×2 grid of independent cards ─────────────── */ +
+ + +
+ +
+
+

Coming Soon

+
+
+ ) : ( + /* ── Comparison: combined card on top, two cards below ───── */ +
+ +
+
+ +
+
+

Coming Soon

+
+
+
+ )} +
+ ); +} + +export default FinancialsTrackingSection; diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingSection.module.css b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingSection.module.css new file mode 100644 index 0000000000..e02ac1a0bc --- /dev/null +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/FinancialsTrackingSection.module.css @@ -0,0 +1,175 @@ +/* ============================================================ + FinancialsTrackingSection — CSS Module + Manages the 2×2 grid (stacked) and top+bottom (comparison) + layouts for the Financials Tracking section. + All colours cascade from WeeklyProjectSummary.module.css. + ============================================================ */ + +/* ── Section wrapper ─────────────────────────────────────────── */ +.section { + width: 100%; + display: flex; + flex-direction: column; + gap: 16px; +} + +/* ── View-mode toggle ────────────────────────────────────────── */ +.toggleGroup { + display: flex; + gap: 8px; + flex-wrap: wrap; + align-items: center; +} + +.toggleBtn { + padding: 7px 18px; + background: var(--section-bg); + color: var(--text-color); + border: 1px solid var(--border-color, #cccccc); + border-radius: 20px; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: background 0.18s ease, color 0.18s ease, border-color 0.18s ease; + white-space: nowrap; +} + +.toggleBtn:hover { + background: var(--section-title-hover); +} + +.toggleBtn:focus-visible { + outline: 2px solid var(--focus-border-color, #3b82f6); + outline-offset: 2px; +} + +/* Active / pressed state */ +.toggleBtnActive { + background: var(--button-bg); + color: #ffffff; + border-color: var(--button-bg); +} + +.toggleBtnActive:hover { + background: var(--button-hover); + border-color: var(--button-hover); +} + +/* ── Stacked mode: 2×2 grid ──────────────────────────────────── */ +.grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + width: 100%; +} + +/* ── Comparison mode: column layout ─────────────────────────── */ +.comparisonLayout { + display: flex; + flex-direction: column; + gap: 20px; + width: 100%; +} + +/* ── Comparison mode: bottom row (2 cards side-by-side) ─────── */ +.bottomRow { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 20px; + width: 100%; +} + +/* ── Shared card wrapper for non-expenditure charts ─────────── */ +.chartCard { + background: var(--card-bg); + border-radius: 10px; + padding: 16px; + box-shadow: 0 2px 8px var(--card-shadow); + min-height: 340px; + display: flex; + flex-direction: column; +} + +/* ── Placeholder card ────────────────────────────────────────── */ +.placeholderCard { + background: var(--card-bg); + border-radius: 10px; + padding: 20px; + box-shadow: 0 2px 8px var(--card-shadow); + min-height: 340px; + display: flex; + align-items: center; + justify-content: center; + border: 2px dashed var(--border-color, #cccccc); + box-sizing: border-box; +} + +.placeholderText { + font-size: 14px; + color: var(--text-color); + opacity: 0.5; + text-align: center; + margin: 0; +} + +/* ── Button label tiers (responsive text) ────────────────────── */ +.labelFull { + display: inline; +} + +.labelMed { + display: none; +} + +.labelMin { + display: none; + align-items: center; + gap: 5px; +} + +.btnIcon { + flex-shrink: 0; + vertical-align: middle; +} + +/* ── Responsive ──────────────────────────────────────────────── */ +@media (max-width: 768px) { + .grid { + grid-template-columns: 1fr; + } + + .bottomRow { + grid-template-columns: 1fr; + } +} + +@media (max-width: 640px) { + .toggleGroup { + width: 100%; + } + + .toggleBtn { + flex: 1; + text-align: center; + } + + /* Switch to medium labels on tablet */ + .labelFull { + display: none; + } + + .labelMed { + display: inline; + } +} + +@media (max-width: 420px) { + /* Switch to icon + short label on small phones */ + .labelMed { + display: none; + } + + .labelMin { + display: inline-flex; + } +} diff --git a/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/SingleExpenditureCard.jsx b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/SingleExpenditureCard.jsx new file mode 100644 index 0000000000..604de2c814 --- /dev/null +++ b/src/components/BMDashboard/WeeklyProjectSummary/ExpenditureChart/SingleExpenditureCard.jsx @@ -0,0 +1,7 @@ +import ExpenditureCard from './ExpenditureCard'; + +function SingleExpenditureCard({ pieType }) { + return ; +} + +export default SingleExpenditureCard; diff --git a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx index 48818a5f28..49a112e824 100644 --- a/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx +++ b/src/components/BMDashboard/WeeklyProjectSummary/WeeklyProjectSummary.jsx @@ -14,6 +14,8 @@ import IssuesBreakdownChart from './IssuesBreakdownChart'; import InjuryCategoryBarChart from './GroupedBarGraphInjurySeverity/InjuryCategoryBarChart'; import ToolsHorizontalBarChart from './Tools/ToolsHorizontalBarChart'; import ExpenseBarChart from './Financials/ExpenseBarChart'; +import FinancialStatButtons from './Financials/FinancialStatButtons'; +import FinancialsTrackingSection from './ExpenditureChart/FinancialsTrackingSection'; import ActualVsPlannedCost from './ActualVsPlannedCost/ActualVsPlannedCost'; import TotalMaterialCostPerProject from './TotalMaterialCostPerProject/TotalMaterialCostPerProject'; import InteractiveMap from '../InteractiveMap/InteractiveMap'; @@ -328,22 +330,8 @@ function WeeklyProjectSummary() { key: 'Financials Tracking', className: 'full', content: ( -
- {[1, 2, 3, 4].map((_, index) => { - const uniqueId = uuidv4(); - return ( -
- {(() => { - if (index === 2) return ; - if (index === 3) return ; - return '📊 Card'; - })()} -
- ); - })} +
+
), }, @@ -386,8 +374,12 @@ function WeeklyProjectSummary() { // Remove interactive elements for PDF clonedContent - .querySelectorAll('button, .weekly-project-summary-dropdown-icon, .no-print, iframe') - .forEach(el => el.parentNode?.removeChild(el)); + .querySelectorAll( + 'button, .weekly-project-summary-dropdown-icon, .no-print, .weekly-summary-header-controls', + ) + .forEach(el => { + el.parentNode?.removeChild(el); + }); // Ensure charts are visible const styleElem = document.createElement('style'); diff --git a/src/utils/URL.js b/src/utils/URL.js index 641784a311..40fda66abe 100644 --- a/src/utils/URL.js +++ b/src/utils/URL.js @@ -326,6 +326,8 @@ export const ENDPOINTS = { BM_UPDATE_MATERIAL_BULK: `${APIEndpoint}/bm/updateMaterialRecordBulk`, BM_UPDATE_MATERIAL_STATUS: `${APIEndpoint}/bm/updateMaterialStatus`, BM_MATERIAL_STOCK_OUT_RISK: `${APIEndpoint}/bm/materials/stock-out-risk`, + BM_EXPENDITURE_PROJECTS: `${APIEndpoint}/bm/expenditure/projects`, + BM_EXPENDITURE_PIE: projectId => `${APIEndpoint}/bm/expenditure/${projectId}/pie`, BM_UPDATE_REUSABLE: `${APIEndpoint}/bm/updateReusableRecord`, BM_UPDATE_REUSABLE_BULK: `${APIEndpoint}/bm/updateReusableRecordBulk`, BM_TOOL_TYPES: `${APIEndpoint}/bm/invtypes/tools`,