From 5d46a2a7e5c6d66e6077657608bdcdc2b1ebf6f9 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 20 Sep 2025 17:40:34 -0400 Subject: [PATCH] Tweak financial calculations --- .../dashboard/acot-server/routes/events.js | 39 ++- .../dashboard/FinancialOverview.tsx | 285 ++++++++++-------- 2 files changed, 193 insertions(+), 131 deletions(-) diff --git a/inventory-server/dashboard/acot-server/routes/events.js b/inventory-server/dashboard/acot-server/routes/events.js index c340c23..5cdb84b 100644 --- a/inventory-server/dashboard/acot-server/routes/events.js +++ b/inventory-server/dashboard/acot-server/routes/events.js @@ -450,8 +450,9 @@ router.get('/financials', async (req, res) => { grossSales: calculateComparison(totals.grossSales, previousTotals.grossSales), refunds: calculateComparison(totals.refunds, previousTotals.refunds), taxCollected: calculateComparison(totals.taxCollected, previousTotals.taxCollected), + discounts: calculateComparison(totals.discounts, previousTotals.discounts), cogs: calculateComparison(totals.cogs, previousTotals.cogs), - netRevenue: calculateComparison(totals.netRevenue, previousTotals.netRevenue), + income: calculateComparison(totals.income, previousTotals.income), profit: calculateComparison(totals.profit, previousTotals.profit), margin: calculateComparison(totals.margin, previousTotals.margin), }; @@ -706,10 +707,13 @@ function buildFinancialTotalsQuery(whereClause) { SELECT COALESCE(SUM(sale_amount), 0) as grossSales, COALESCE(SUM(refund_amount), 0) as refunds, + COALESCE(SUM(shipping_collected_amount + small_order_fee_amount + rush_fee_amount), 0) as shippingFees, COALESCE(SUM(tax_collected_amount), 0) as taxCollected, + COALESCE(SUM(discount_total_amount), 0) as discounts, COALESCE(SUM(cogs_amount), 0) as cogs FROM report_sales_data WHERE ${whereClause} + AND action IN (1, 2, 3) `; } @@ -719,10 +723,13 @@ function buildFinancialTrendQuery(whereClause) { DATE(date_change) as date, SUM(sale_amount) as grossSales, SUM(refund_amount) as refunds, + SUM(shipping_collected_amount + small_order_fee_amount + rush_fee_amount) as shippingFees, SUM(tax_collected_amount) as taxCollected, + SUM(discount_total_amount) as discounts, SUM(cogs_amount) as cogs FROM report_sales_data WHERE ${whereClause} + AND action IN (1, 2, 3) GROUP BY DATE(date_change) ORDER BY date ASC `; @@ -731,20 +738,23 @@ function buildFinancialTrendQuery(whereClause) { function normalizeFinancialTotals(row = {}) { const grossSales = parseFloat(row.grossSales || 0); const refunds = parseFloat(row.refunds || 0); + const shippingFees = parseFloat(row.shippingFees || 0); const taxCollected = parseFloat(row.taxCollected || 0); + const discounts = parseFloat(row.discounts || 0); const cogs = parseFloat(row.cogs || 0); - const netSales = grossSales - refunds; - const netRevenue = netSales - taxCollected; - const profit = netRevenue - cogs; - const margin = netRevenue !== 0 ? (profit / netRevenue) * 100 : 0; + const productNet = grossSales - refunds - discounts; + const income = productNet + shippingFees; + const profit = income - cogs; + const margin = income !== 0 ? (profit / income) * 100 : 0; return { grossSales, refunds, + shippingFees, taxCollected, + discounts, cogs, - netSales, - netRevenue, + income, profit, margin, }; @@ -753,12 +763,14 @@ function normalizeFinancialTotals(row = {}) { function normalizeFinancialTrendRow(row = {}) { const grossSales = parseFloat(row.grossSales || 0); const refunds = parseFloat(row.refunds || 0); + const shippingFees = parseFloat(row.shippingFees || 0); const taxCollected = parseFloat(row.taxCollected || 0); + const discounts = parseFloat(row.discounts || 0); const cogs = parseFloat(row.cogs || 0); - const netSales = grossSales - refunds; - const netRevenue = netSales - taxCollected; - const profit = netRevenue - cogs; - const margin = netRevenue !== 0 ? (profit / netRevenue) * 100 : 0; + const productNet = grossSales - refunds - discounts; + const income = productNet + shippingFees; + const profit = income - cogs; + const margin = income !== 0 ? (profit / income) * 100 : 0; let timestamp = null; if (row.date instanceof Date) { @@ -771,10 +783,11 @@ function normalizeFinancialTrendRow(row = {}) { date: row.date, grossSales, refunds, + shippingFees, taxCollected, + discounts, cogs, - netSales, - netRevenue, + income, profit, margin, timestamp, diff --git a/inventory/src/components/dashboard/FinancialOverview.tsx b/inventory/src/components/dashboard/FinancialOverview.tsx index 86c65a5..73a1cbb 100644 --- a/inventory/src/components/dashboard/FinancialOverview.tsx +++ b/inventory/src/components/dashboard/FinancialOverview.tsx @@ -61,26 +61,28 @@ type ComparisonValue = { }; type FinancialTotals = { - grossSales: number; - refunds: number; - taxCollected: number; + grossSales?: number; + refunds?: number; + taxCollected?: number; + shippingFees?: number; + discounts?: number; cogs: number; - netSales: number; - netRevenue: number; + income?: number; profit: number; margin: number; }; type FinancialTrendPoint = { date: string | Date | null; - grossSales: number; - refunds: number; - taxCollected: number; + income: number; cogs: number; - netSales: number; - netRevenue: number; profit: number; margin: number; + grossSales?: number; + refunds?: number; + shippingFees?: number; + taxCollected?: number; + discounts?: number; timestamp: string | null; }; @@ -88,8 +90,9 @@ type FinancialComparison = { grossSales?: ComparisonValue; refunds?: ComparisonValue; taxCollected?: ComparisonValue; + discounts?: ComparisonValue; cogs?: ComparisonValue; - netRevenue?: ComparisonValue; + income?: ComparisonValue; profit?: ComparisonValue; margin?: ComparisonValue; [key: string]: ComparisonValue | undefined; @@ -129,15 +132,14 @@ type YearPeriod = { type CustomPeriod = MonthPeriod | QuarterPeriod | YearPeriod; -type ChartSeriesKey = "grossSales" | "netRevenue" | "cogs" | "profit" | "margin"; +type ChartSeriesKey = "income" | "cogs" | "profit" | "margin"; type GroupByOption = "day" | "month" | "quarter" | "year"; type ChartPoint = { label: string; timestamp: string | null; - grossSales: number | null; - netRevenue: number | null; + income: number | null; cogs: number | null; profit: number | null; margin: number | null; @@ -146,18 +148,16 @@ type ChartPoint = { }; const chartColors: Record = { - grossSales: "#7c3aed", - netRevenue: "#6366f1", + income: "#2563eb", cogs: "#f97316", profit: "#10b981", margin: "#0ea5e9", }; const SERIES_LABELS: Record = { - grossSales: "Gross Sales", - netRevenue: "Net Revenue", + income: "Total Income", cogs: "COGS", - profit: "Profit", + profit: "Gross Profit", margin: "Profit Margin", }; @@ -166,8 +166,7 @@ const SERIES_DEFINITIONS: Array<{ label: string; type: "currency" | "percentage"; }> = [ - { key: "grossSales", label: SERIES_LABELS.grossSales, type: "currency" }, - { key: "netRevenue", label: SERIES_LABELS.netRevenue, type: "currency" }, + { key: "income", label: SERIES_LABELS.income, type: "currency" }, { key: "cogs", label: SERIES_LABELS.cogs, type: "currency" }, { key: "profit", label: SERIES_LABELS.profit, type: "currency" }, { key: "margin", label: SERIES_LABELS.margin, type: "percentage" }, @@ -277,10 +276,12 @@ const formatPercentage = (value: number, digits = 1, suffix = "%") => { type RawTrendPoint = { date: Date; - grossSales: number; - netRevenue: number; + income: number; cogs: number; - profit: number; + shippingFees: number; + taxCollected: number; + grossSales: number; + discounts: number; }; type AggregatedTrendPoint = { @@ -289,8 +290,7 @@ type AggregatedTrendPoint = { tooltipLabel: string; detailLabel: string; timestamp: string; - grossSales: number; - netRevenue: number; + income: number; cogs: number; profit: number; margin: number; @@ -367,10 +367,8 @@ const aggregateTrendPoints = (points: RawTrendPoint[], groupBy: GroupByOption): key: string; start: Date; end: Date; - grossSales: number; - netRevenue: number; + income: number; cogs: number; - profit: number; } >(); @@ -382,10 +380,8 @@ const aggregateTrendPoints = (points: RawTrendPoint[], groupBy: GroupByOption): key, start: point.date, end: point.date, - grossSales: point.grossSales, - netRevenue: point.netRevenue, + income: point.income, cogs: point.cogs, - profit: point.profit, }); return; } @@ -397,18 +393,17 @@ const aggregateTrendPoints = (points: RawTrendPoint[], groupBy: GroupByOption): bucket.end = point.date; } - bucket.grossSales += point.grossSales; - bucket.netRevenue += point.netRevenue; + bucket.income += point.income; bucket.cogs += point.cogs; - bucket.profit += point.profit; }); return Array.from(bucketMap.values()) .sort((a, b) => a.start.getTime() - b.start.getTime()) .map((bucket) => { const { axisLabel, tooltipLabel, detailLabel } = buildGroupLabels(bucket.start, bucket.end, groupBy); - const { netRevenue, profit } = bucket; - const margin = netRevenue !== 0 ? (profit / netRevenue) * 100 : 0; + const income = bucket.income; + const profit = income - bucket.cogs; + const margin = income !== 0 ? (profit / income) * 100 : 0; return { id: bucket.key, @@ -416,8 +411,7 @@ const aggregateTrendPoints = (points: RawTrendPoint[], groupBy: GroupByOption): tooltipLabel, detailLabel, timestamp: bucket.start.toISOString(), - grossSales: bucket.grossSales, - netRevenue, + income, cogs: bucket.cogs, profit, margin, @@ -516,8 +510,7 @@ const extendAggregatedTrendPoints = ( tooltipLabel, detailLabel, timestamp: bucketStart.toISOString(), - grossSales: 0, - netRevenue: 0, + income: 0, cogs: 0, profit: 0, margin: 0, @@ -536,14 +529,21 @@ const monthsBetween = (start: Date, end: Date) => { const buildTrendLabel = ( comparison?: ComparisonValue | null, - options?: { isPercentage?: boolean } + options?: { isPercentage?: boolean; invertDirection?: boolean } ): TrendSummary | null => { if (!comparison || comparison.absolute === null) { return null; } const { absolute, percentage } = comparison; - const direction: TrendDirection = absolute > 0 ? "up" : absolute < 0 ? "down" : "flat"; + const rawDirection: TrendDirection = absolute > 0 ? "up" : absolute < 0 ? "down" : "flat"; + const direction: TrendDirection = options?.invertDirection + ? rawDirection === "up" + ? "down" + : rawDirection === "down" + ? "up" + : "flat" + : rawDirection; const absoluteValue = Math.abs(absolute); if (options?.isPercentage) { @@ -566,6 +566,39 @@ const buildTrendLabel = ( }; }; +const safeNumeric = (value: number | null | undefined) => + typeof value === "number" && Number.isFinite(value) ? value : 0; + +const computeTotalIncome = (totals?: FinancialTotals | null) => { + if (!totals || typeof totals.income !== "number" || !Number.isFinite(totals.income)) { + return 0; + } + + return totals.income; +}; + +const computeProfitFrom = (income: number, cogs?: number | null | undefined) => income - safeNumeric(cogs); + +const computeMarginFrom = (profit: number, income: number) => (income !== 0 ? (profit / income) * 100 : 0); + +const buildComparisonFromValues = (current?: number | null, previous?: number | null): ComparisonValue | null => { + if (current == null || previous == null) { + return null; + } + + if (!Number.isFinite(current) || !Number.isFinite(previous)) { + return null; + } + + const absolute = current - previous; + const percentage = previous !== 0 ? (absolute / Math.abs(previous)) * 100 : null; + + return { + absolute, + percentage, + }; +}; + const generateYearOptions = (span: number) => { const currentYear = new Date().getFullYear(); return Array.from({ length: span }, (_, index) => currentYear - index); @@ -638,9 +671,8 @@ const FinancialOverview = () => { count: 1, }); const [metrics, setMetrics] = useState>({ - grossSales: true, - netRevenue: true, - cogs: false, + income: true, + cogs: true, profit: true, margin: true, }); @@ -724,12 +756,25 @@ const FinancialOverview = () => { return null; } + if (typeof point.income !== "number" || !Number.isFinite(point.income)) { + return null; + } + + const grossSalesValue = toNumber((point as { grossSales?: number }).grossSales); + const refundsValue = toNumber((point as { refunds?: number }).refunds); + const shippingFeesValue = toNumber((point as { shippingFees?: number }).shippingFees); + const taxCollectedValue = toNumber((point as { taxCollected?: number }).taxCollected); + const discountsValue = toNumber((point as { discounts?: number }).discounts); + const incomeValue = point.income; + return { date, - grossSales: toNumber(point.grossSales), - netRevenue: toNumber(point.netRevenue), + income: incomeValue, + shippingFees: shippingFeesValue, + taxCollected: taxCollectedValue, + grossSales: grossSalesValue, + discounts: discountsValue, cogs: toNumber(point.cogs), - profit: toNumber(point.profit), }; }) .filter((value): value is RawTrendPoint => Boolean(value)) @@ -848,45 +893,74 @@ const FinancialOverview = () => { const totals = data.totals; const previous = data.previousTotals ?? undefined; - const comparison = data.comparison ?? {}; - const safeCurrency = (value: number | undefined | null, digits = 0) => typeof value === "number" && Number.isFinite(value) ? formatCurrency(value, digits) : "—"; const safePercentage = (value: number | undefined | null, digits = 1) => typeof value === "number" && Number.isFinite(value) ? formatPercentage(value, digits) : "—"; + const comparison = data.comparison ?? {}; + + const totalIncome = computeTotalIncome(totals); + const previousIncome = previous ? computeTotalIncome(previous) : null; + + const cogsValue = safeNumeric(totals.cogs); + const previousCogs = previous?.cogs != null ? safeNumeric(previous.cogs) : null; + + const profitValue = Number.isFinite(totals.profit) + ? safeNumeric(totals.profit) + : computeProfitFrom(totalIncome, totals.cogs); + const previousProfitValue = previousIncome != null + ? Number.isFinite(previous?.profit) + ? safeNumeric(previous?.profit) + : computeProfitFrom(previousIncome, previous?.cogs) + : null; + + const marginValue = Number.isFinite(totals.margin) + ? safeNumeric(totals.margin) + : computeMarginFrom(profitValue, totalIncome); + const previousMarginValue = previousIncome != null + ? Number.isFinite(previous?.margin) + ? safeNumeric(previous?.margin) + : computeMarginFrom(previousProfitValue ?? 0, previousIncome) + : null; + return [ { - key: "grossSales", - title: "Gross Sales", - value: safeCurrency(totals.grossSales, 0), - description: previous?.grossSales != null ? `Previous: ${safeCurrency(previous.grossSales, 0)}` : undefined, - trend: buildTrendLabel(comparison.grossSales), - accentClass: "text-emerald-600 dark:text-emerald-400", - }, - { - key: "netRevenue", - title: "Net Revenue", - value: safeCurrency(totals.netRevenue, 0), - description: previous?.netRevenue != null ? `Previous: ${safeCurrency(previous.netRevenue, 0)}` : undefined, - trend: buildTrendLabel(comparison.netRevenue), + key: "income", + title: "Total Income", + value: safeCurrency(totalIncome, 0), + description: previousIncome != null ? `Previous: ${safeCurrency(previousIncome, 0)}` : undefined, + trend: buildTrendLabel(comparison?.income ?? buildComparisonFromValues(totalIncome, previousIncome ?? null)), accentClass: "text-indigo-600 dark:text-indigo-400", }, + { + key: "cogs", + title: "COGS", + value: safeCurrency(cogsValue, 0), + description: previousCogs != null ? `Previous: ${safeCurrency(previousCogs, 0)}` : undefined, + trend: buildTrendLabel(comparison?.cogs ?? buildComparisonFromValues(cogsValue, previousCogs ?? null), { + invertDirection: true, + }), + accentClass: "text-amber-600 dark:text-amber-400", + }, { key: "profit", - title: "Profit", - value: safeCurrency(totals.profit, 0), - description: previous?.profit != null ? `Previous: ${safeCurrency(previous.profit, 0)}` : undefined, - trend: buildTrendLabel(comparison.profit), + title: "Gross Profit", + value: safeCurrency(profitValue, 0), + description: previousProfitValue != null ? `Previous: ${safeCurrency(previousProfitValue, 0)}` : undefined, + trend: buildTrendLabel(comparison?.profit ?? buildComparisonFromValues(profitValue, previousProfitValue ?? null)), accentClass: "text-emerald-600 dark:text-emerald-400", }, { key: "margin", title: "Profit Margin", - value: safePercentage(totals.margin, 1), - description: previous?.margin != null ? `Previous: ${safePercentage(previous.margin, 1)}` : undefined, - trend: buildTrendLabel(comparison.margin, { isPercentage: true }), + value: safePercentage(marginValue, 1), + description: previousMarginValue != null ? `Previous: ${safePercentage(previousMarginValue, 1)}` : undefined, + trend: buildTrendLabel( + comparison?.margin ?? buildComparisonFromValues(marginValue, previousMarginValue ?? null), + { isPercentage: true } + ), accentClass: "text-sky-600 dark:text-sky-400", }, ]; @@ -907,8 +981,7 @@ const FinancialOverview = () => { return aggregatedPoints.map((point) => ({ label: point.label, timestamp: point.timestamp, - grossSales: point.isFuture ? null : point.grossSales, - netRevenue: point.isFuture ? null : point.netRevenue, + income: point.isFuture ? null : point.income, cogs: point.isFuture ? null : point.cogs, profit: point.isFuture ? null : point.profit, margin: point.isFuture ? null : point.margin, @@ -977,8 +1050,7 @@ const FinancialOverview = () => { id: point.id, label: point.detailLabel, timestamp: point.timestamp, - grossSales: point.grossSales, - netRevenue: point.netRevenue, + income: point.income, cogs: point.cogs, profit: point.profit, margin: point.margin, @@ -1174,18 +1246,15 @@ const FinancialOverview = () => { Date - {metrics.grossSales && ( - Gross Sales - )} - {metrics.netRevenue && ( - Net Revenue - )} - {metrics.profit && ( - Profit + {metrics.income && ( + Total Income )} {metrics.cogs && ( COGS )} + {metrics.profit && ( + Gross Profit + )} {metrics.margin && ( Margin )} @@ -1197,19 +1266,9 @@ const FinancialOverview = () => { {row.label || "—"} - {metrics.grossSales && ( + {metrics.income && ( - {row.isFuture ? "—" : formatCurrency(row.grossSales, 0)} - - )} - {metrics.netRevenue && ( - - {row.isFuture ? "—" : formatCurrency(row.netRevenue, 0)} - - )} - {metrics.profit && ( - - {row.isFuture ? "—" : formatCurrency(row.profit, 0)} + {row.isFuture ? "—" : formatCurrency(row.income, 0)} )} {metrics.cogs && ( @@ -1217,6 +1276,11 @@ const FinancialOverview = () => { {row.isFuture ? "—" : formatCurrency(row.cogs, 0)} )} + {metrics.profit && ( + + {row.isFuture ? "—" : formatCurrency(row.profit, 0)} + + )} {metrics.margin && ( {row.isFuture ? "—" : formatPercentage(row.margin, 1)} @@ -1387,13 +1451,9 @@ const FinancialOverview = () => { - - - - - - - + + + @@ -1424,25 +1484,14 @@ const FinancialOverview = () => { )} } /> SERIES_LABELS[value as ChartSeriesKey] ?? value} /> - {metrics.grossSales ? ( + {metrics.income ? ( - ) : null} - {metrics.netRevenue ? ( - ) : null}