diff --git a/backend/src/routes/adminDashboard.js b/backend/src/routes/adminDashboard.js index bc5aa379..6a67b771 100644 --- a/backend/src/routes/adminDashboard.js +++ b/backend/src/routes/adminDashboard.js @@ -439,6 +439,10 @@ router.get('/crm-stats', adminAuth, async (req, res) => { const monthCutoff = new Date(now - 30 * DAY); const quarterCutoff = new Date(now - 90 * DAY); const yearCutoff = new Date(now - 365 * DAY); + // Calendar year-to-date (Jan 1 of the current year, local time) — + // the dashboard's revenue "year" tile can toggle between this and + // the trailing-365-day window. + const calendarYearCutoff = new Date(new Date(now).getFullYear(), 0, 1); // ---- quotes: counts by status --------------------------------- let quoteCounts = { draft: 0, sent: 0, accepted: 0, declined: 0, expired: 0, converted: 0 }; @@ -459,6 +463,7 @@ router.get('/crm-stats', adminAuth, async (req, res) => { let revenueMonthMinor = 0; let revenueQuarterMinor = 0; let revenueYearMinor = 0; + let revenueCalendarYearMinor = 0; let outstandingTotalMinor = 0; let outstandingCount = 0; @@ -504,9 +509,10 @@ router.get('/crm-stats', adminAuth, async (req, res) => { .first(); return Number(row?.total || 0); }; - revenueMonthMinor = await winSum(monthCutoff); - revenueQuarterMinor = await winSum(quarterCutoff); - revenueYearMinor = await winSum(yearCutoff); + revenueMonthMinor = await winSum(monthCutoff); + revenueQuarterMinor = await winSum(quarterCutoff); + revenueYearMinor = await winSum(yearCutoff); + revenueCalendarYearMinor = await winSum(calendarYearCutoff); // Outstanding: every invoice that's been sent but not fully // paid (sent + overdue). Outstanding = total - paid. We sum @@ -568,9 +574,10 @@ router.get('/crm-stats', adminAuth, async (req, res) => { quotes: quoteCounts, invoices: invoiceCounts, revenue: { - monthMinor: revenueMonthMinor, - quarterMinor: revenueQuarterMinor, - yearMinor: revenueYearMinor, + monthMinor: revenueMonthMinor, + quarterMinor: revenueQuarterMinor, + yearMinor: revenueYearMinor, + calendarYearMinor: revenueCalendarYearMinor, }, outstanding: { totalMinor: outstandingTotalMinor, diff --git a/frontend/src/components/admin/CrmOverviewSection.tsx b/frontend/src/components/admin/CrmOverviewSection.tsx index 88f7ea7d..34a17978 100644 --- a/frontend/src/components/admin/CrmOverviewSection.tsx +++ b/frontend/src/components/admin/CrmOverviewSection.tsx @@ -50,6 +50,10 @@ export const CrmOverviewSection: React.FC = () => { // publicSettings finishes loading — shows everything, then settles. const showRevenue = publicSettings?.crm_overview_show_revenue !== false; const showOutstanding = publicSettings?.crm_overview_show_outstanding !== false; + // Revenue "year" tile toggles in place between the trailing-365-day + // window and calendar year-to-date — keeps the dashboard to four + // tiles instead of adding a fifth. + const [revYearMode, setRevYearMode] = React.useState<'rolling' | 'calendar'>('rolling'); const showQuotes = publicSettings?.crm_overview_show_quotes !== false; const showInvoices = publicSettings?.crm_overview_show_invoices !== false; // Compute which sub-sections actually render so we can skip the @@ -121,8 +125,15 @@ export const CrmOverviewSection: React.FC = () => { /> } - label={t('crmOverview.revenue.year', 'Revenue · last 365 days')} - value={formatMoney(d.revenue.yearMinor, cur)} + label={revYearMode === 'calendar' + ? t('crmOverview.revenue.yearCalendar', 'Revenue · this year') + : t('crmOverview.revenue.year', 'Revenue · last 365 days')} + value={formatMoney( + revYearMode === 'calendar' ? d.revenue.calendarYearMinor : d.revenue.yearMinor, + cur, + )} + sub={t('crmOverview.revenue.toggleHint', 'Tap to switch window')} + onClick={() => setRevYearMode((m) => (m === 'rolling' ? 'calendar' : 'rolling'))} /> )} @@ -249,8 +260,11 @@ interface StatCardProps { value: string | number; sub?: string; to?: string; + /** Makes the whole tile a button (mutually exclusive with `to`). + * Used by the revenue tile to toggle its window in place. */ + onClick?: () => void; } -const StatCard: React.FC = ({ icon, label, value, sub, to }) => { +const StatCard: React.FC = ({ icon, label, value, sub, to, onClick }) => { const inner = (
@@ -266,6 +280,13 @@ const StatCard: React.FC = ({ icon, label, value, sub, to }) => { if (to) { return {inner}; } + if (onClick) { + return ( + + ); + } return inner; }; diff --git a/frontend/src/services/bills.service.ts b/frontend/src/services/bills.service.ts index a4d0fffe..d64cb3c8 100644 --- a/frontend/src/services/bills.service.ts +++ b/frontend/src/services/bills.service.ts @@ -386,6 +386,9 @@ export interface CrmOverviewStats { monthMinor: number; quarterMinor: number; yearMinor: number; + /** Revenue since Jan 1 of the current year (calendar YTD). The + * dashboard's "year" tile toggles between this and yearMinor. */ + calendarYearMinor: number; }; outstanding: { totalMinor: number;