Tweak financial calculations

This commit is contained in:
2025-09-20 17:40:34 -04:00
parent 512b351429
commit 5d46a2a7e5
2 changed files with 193 additions and 131 deletions

View File

@@ -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,

View File

@@ -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<ChartSeriesKey, string> = {
grossSales: "#7c3aed",
netRevenue: "#6366f1",
income: "#2563eb",
cogs: "#f97316",
profit: "#10b981",
margin: "#0ea5e9",
};
const SERIES_LABELS: Record<ChartSeriesKey, string> = {
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<Record<ChartSeriesKey, boolean>>({
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 = () => {
<TableHeader>
<TableRow>
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Date</TableHead>
{metrics.grossSales && (
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Gross Sales</TableHead>
)}
{metrics.netRevenue && (
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Net Revenue</TableHead>
)}
{metrics.profit && (
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Profit</TableHead>
{metrics.income && (
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Total Income</TableHead>
)}
{metrics.cogs && (
<TableHead className="px-6 py-3 whitespace-nowrap text-center">COGS</TableHead>
)}
{metrics.profit && (
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Gross Profit</TableHead>
)}
{metrics.margin && (
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Margin</TableHead>
)}
@@ -1197,19 +1266,9 @@ const FinancialOverview = () => {
<TableCell className="px-6 py-3 whitespace-nowrap text-center text-sm text-muted-foreground">
{row.label || "—"}
</TableCell>
{metrics.grossSales && (
{metrics.income && (
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
{row.isFuture ? "—" : formatCurrency(row.grossSales, 0)}
</TableCell>
)}
{metrics.netRevenue && (
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
{row.isFuture ? "—" : formatCurrency(row.netRevenue, 0)}
</TableCell>
)}
{metrics.profit && (
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
{row.isFuture ? "—" : formatCurrency(row.profit, 0)}
{row.isFuture ? "—" : formatCurrency(row.income, 0)}
</TableCell>
)}
{metrics.cogs && (
@@ -1217,6 +1276,11 @@ const FinancialOverview = () => {
{row.isFuture ? "—" : formatCurrency(row.cogs, 0)}
</TableCell>
)}
{metrics.profit && (
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
{row.isFuture ? "—" : formatCurrency(row.profit, 0)}
</TableCell>
)}
{metrics.margin && (
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
{row.isFuture ? "—" : formatPercentage(row.margin, 1)}
@@ -1387,13 +1451,9 @@ const FinancialOverview = () => {
<ResponsiveContainer width="100%" height="100%">
<ComposedChart data={chartData} margin={{ top: 10, right: 30, left: -10, bottom: 0 }}>
<defs>
<linearGradient id="financialGrossSales" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={chartColors.grossSales} stopOpacity={0.35} />
<stop offset="95%" stopColor={chartColors.grossSales} stopOpacity={0.05} />
</linearGradient>
<linearGradient id="financialNetRevenue" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={chartColors.netRevenue} stopOpacity={0.3} />
<stop offset="95%" stopColor={chartColors.netRevenue} stopOpacity={0.05} />
<linearGradient id="financialIncome" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={chartColors.income} stopOpacity={0.3} />
<stop offset="95%" stopColor={chartColors.income} stopOpacity={0.05} />
</linearGradient>
<linearGradient id="financialCogs" x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={chartColors.cogs} stopOpacity={0.25} />
@@ -1424,25 +1484,14 @@ const FinancialOverview = () => {
)}
<Tooltip content={<FinancialTooltip />} />
<Legend formatter={(value: string) => SERIES_LABELS[value as ChartSeriesKey] ?? value} />
{metrics.grossSales ? (
{metrics.income ? (
<Area
yAxisId="left"
type="monotone"
dataKey="grossSales"
name={SERIES_LABELS.grossSales}
stroke={chartColors.grossSales}
fill="url(#financialGrossSales)"
strokeWidth={2}
/>
) : null}
{metrics.netRevenue ? (
<Area
yAxisId="left"
type="monotone"
dataKey="netRevenue"
name={SERIES_LABELS.netRevenue}
stroke={chartColors.netRevenue}
fill="url(#financialNetRevenue)"
dataKey="income"
name={SERIES_LABELS.income}
stroke={chartColors.income}
fill="url(#financialIncome)"
strokeWidth={2}
/>
) : null}