Tweak financial calculations
This commit is contained in:
@@ -450,8 +450,9 @@ router.get('/financials', async (req, res) => {
|
|||||||
grossSales: calculateComparison(totals.grossSales, previousTotals.grossSales),
|
grossSales: calculateComparison(totals.grossSales, previousTotals.grossSales),
|
||||||
refunds: calculateComparison(totals.refunds, previousTotals.refunds),
|
refunds: calculateComparison(totals.refunds, previousTotals.refunds),
|
||||||
taxCollected: calculateComparison(totals.taxCollected, previousTotals.taxCollected),
|
taxCollected: calculateComparison(totals.taxCollected, previousTotals.taxCollected),
|
||||||
|
discounts: calculateComparison(totals.discounts, previousTotals.discounts),
|
||||||
cogs: calculateComparison(totals.cogs, previousTotals.cogs),
|
cogs: calculateComparison(totals.cogs, previousTotals.cogs),
|
||||||
netRevenue: calculateComparison(totals.netRevenue, previousTotals.netRevenue),
|
income: calculateComparison(totals.income, previousTotals.income),
|
||||||
profit: calculateComparison(totals.profit, previousTotals.profit),
|
profit: calculateComparison(totals.profit, previousTotals.profit),
|
||||||
margin: calculateComparison(totals.margin, previousTotals.margin),
|
margin: calculateComparison(totals.margin, previousTotals.margin),
|
||||||
};
|
};
|
||||||
@@ -706,10 +707,13 @@ function buildFinancialTotalsQuery(whereClause) {
|
|||||||
SELECT
|
SELECT
|
||||||
COALESCE(SUM(sale_amount), 0) as grossSales,
|
COALESCE(SUM(sale_amount), 0) as grossSales,
|
||||||
COALESCE(SUM(refund_amount), 0) as refunds,
|
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(tax_collected_amount), 0) as taxCollected,
|
||||||
|
COALESCE(SUM(discount_total_amount), 0) as discounts,
|
||||||
COALESCE(SUM(cogs_amount), 0) as cogs
|
COALESCE(SUM(cogs_amount), 0) as cogs
|
||||||
FROM report_sales_data
|
FROM report_sales_data
|
||||||
WHERE ${whereClause}
|
WHERE ${whereClause}
|
||||||
|
AND action IN (1, 2, 3)
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -719,10 +723,13 @@ function buildFinancialTrendQuery(whereClause) {
|
|||||||
DATE(date_change) as date,
|
DATE(date_change) as date,
|
||||||
SUM(sale_amount) as grossSales,
|
SUM(sale_amount) as grossSales,
|
||||||
SUM(refund_amount) as refunds,
|
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(tax_collected_amount) as taxCollected,
|
||||||
|
SUM(discount_total_amount) as discounts,
|
||||||
SUM(cogs_amount) as cogs
|
SUM(cogs_amount) as cogs
|
||||||
FROM report_sales_data
|
FROM report_sales_data
|
||||||
WHERE ${whereClause}
|
WHERE ${whereClause}
|
||||||
|
AND action IN (1, 2, 3)
|
||||||
GROUP BY DATE(date_change)
|
GROUP BY DATE(date_change)
|
||||||
ORDER BY date ASC
|
ORDER BY date ASC
|
||||||
`;
|
`;
|
||||||
@@ -731,20 +738,23 @@ function buildFinancialTrendQuery(whereClause) {
|
|||||||
function normalizeFinancialTotals(row = {}) {
|
function normalizeFinancialTotals(row = {}) {
|
||||||
const grossSales = parseFloat(row.grossSales || 0);
|
const grossSales = parseFloat(row.grossSales || 0);
|
||||||
const refunds = parseFloat(row.refunds || 0);
|
const refunds = parseFloat(row.refunds || 0);
|
||||||
|
const shippingFees = parseFloat(row.shippingFees || 0);
|
||||||
const taxCollected = parseFloat(row.taxCollected || 0);
|
const taxCollected = parseFloat(row.taxCollected || 0);
|
||||||
|
const discounts = parseFloat(row.discounts || 0);
|
||||||
const cogs = parseFloat(row.cogs || 0);
|
const cogs = parseFloat(row.cogs || 0);
|
||||||
const netSales = grossSales - refunds;
|
const productNet = grossSales - refunds - discounts;
|
||||||
const netRevenue = netSales - taxCollected;
|
const income = productNet + shippingFees;
|
||||||
const profit = netRevenue - cogs;
|
const profit = income - cogs;
|
||||||
const margin = netRevenue !== 0 ? (profit / netRevenue) * 100 : 0;
|
const margin = income !== 0 ? (profit / income) * 100 : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
grossSales,
|
grossSales,
|
||||||
refunds,
|
refunds,
|
||||||
|
shippingFees,
|
||||||
taxCollected,
|
taxCollected,
|
||||||
|
discounts,
|
||||||
cogs,
|
cogs,
|
||||||
netSales,
|
income,
|
||||||
netRevenue,
|
|
||||||
profit,
|
profit,
|
||||||
margin,
|
margin,
|
||||||
};
|
};
|
||||||
@@ -753,12 +763,14 @@ function normalizeFinancialTotals(row = {}) {
|
|||||||
function normalizeFinancialTrendRow(row = {}) {
|
function normalizeFinancialTrendRow(row = {}) {
|
||||||
const grossSales = parseFloat(row.grossSales || 0);
|
const grossSales = parseFloat(row.grossSales || 0);
|
||||||
const refunds = parseFloat(row.refunds || 0);
|
const refunds = parseFloat(row.refunds || 0);
|
||||||
|
const shippingFees = parseFloat(row.shippingFees || 0);
|
||||||
const taxCollected = parseFloat(row.taxCollected || 0);
|
const taxCollected = parseFloat(row.taxCollected || 0);
|
||||||
|
const discounts = parseFloat(row.discounts || 0);
|
||||||
const cogs = parseFloat(row.cogs || 0);
|
const cogs = parseFloat(row.cogs || 0);
|
||||||
const netSales = grossSales - refunds;
|
const productNet = grossSales - refunds - discounts;
|
||||||
const netRevenue = netSales - taxCollected;
|
const income = productNet + shippingFees;
|
||||||
const profit = netRevenue - cogs;
|
const profit = income - cogs;
|
||||||
const margin = netRevenue !== 0 ? (profit / netRevenue) * 100 : 0;
|
const margin = income !== 0 ? (profit / income) * 100 : 0;
|
||||||
let timestamp = null;
|
let timestamp = null;
|
||||||
|
|
||||||
if (row.date instanceof Date) {
|
if (row.date instanceof Date) {
|
||||||
@@ -771,10 +783,11 @@ function normalizeFinancialTrendRow(row = {}) {
|
|||||||
date: row.date,
|
date: row.date,
|
||||||
grossSales,
|
grossSales,
|
||||||
refunds,
|
refunds,
|
||||||
|
shippingFees,
|
||||||
taxCollected,
|
taxCollected,
|
||||||
|
discounts,
|
||||||
cogs,
|
cogs,
|
||||||
netSales,
|
income,
|
||||||
netRevenue,
|
|
||||||
profit,
|
profit,
|
||||||
margin,
|
margin,
|
||||||
timestamp,
|
timestamp,
|
||||||
|
|||||||
@@ -61,26 +61,28 @@ type ComparisonValue = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type FinancialTotals = {
|
type FinancialTotals = {
|
||||||
grossSales: number;
|
grossSales?: number;
|
||||||
refunds: number;
|
refunds?: number;
|
||||||
taxCollected: number;
|
taxCollected?: number;
|
||||||
|
shippingFees?: number;
|
||||||
|
discounts?: number;
|
||||||
cogs: number;
|
cogs: number;
|
||||||
netSales: number;
|
income?: number;
|
||||||
netRevenue: number;
|
|
||||||
profit: number;
|
profit: number;
|
||||||
margin: number;
|
margin: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type FinancialTrendPoint = {
|
type FinancialTrendPoint = {
|
||||||
date: string | Date | null;
|
date: string | Date | null;
|
||||||
grossSales: number;
|
income: number;
|
||||||
refunds: number;
|
|
||||||
taxCollected: number;
|
|
||||||
cogs: number;
|
cogs: number;
|
||||||
netSales: number;
|
|
||||||
netRevenue: number;
|
|
||||||
profit: number;
|
profit: number;
|
||||||
margin: number;
|
margin: number;
|
||||||
|
grossSales?: number;
|
||||||
|
refunds?: number;
|
||||||
|
shippingFees?: number;
|
||||||
|
taxCollected?: number;
|
||||||
|
discounts?: number;
|
||||||
timestamp: string | null;
|
timestamp: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -88,8 +90,9 @@ type FinancialComparison = {
|
|||||||
grossSales?: ComparisonValue;
|
grossSales?: ComparisonValue;
|
||||||
refunds?: ComparisonValue;
|
refunds?: ComparisonValue;
|
||||||
taxCollected?: ComparisonValue;
|
taxCollected?: ComparisonValue;
|
||||||
|
discounts?: ComparisonValue;
|
||||||
cogs?: ComparisonValue;
|
cogs?: ComparisonValue;
|
||||||
netRevenue?: ComparisonValue;
|
income?: ComparisonValue;
|
||||||
profit?: ComparisonValue;
|
profit?: ComparisonValue;
|
||||||
margin?: ComparisonValue;
|
margin?: ComparisonValue;
|
||||||
[key: string]: ComparisonValue | undefined;
|
[key: string]: ComparisonValue | undefined;
|
||||||
@@ -129,15 +132,14 @@ type YearPeriod = {
|
|||||||
|
|
||||||
type CustomPeriod = MonthPeriod | QuarterPeriod | 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 GroupByOption = "day" | "month" | "quarter" | "year";
|
||||||
|
|
||||||
type ChartPoint = {
|
type ChartPoint = {
|
||||||
label: string;
|
label: string;
|
||||||
timestamp: string | null;
|
timestamp: string | null;
|
||||||
grossSales: number | null;
|
income: number | null;
|
||||||
netRevenue: number | null;
|
|
||||||
cogs: number | null;
|
cogs: number | null;
|
||||||
profit: number | null;
|
profit: number | null;
|
||||||
margin: number | null;
|
margin: number | null;
|
||||||
@@ -146,18 +148,16 @@ type ChartPoint = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const chartColors: Record<ChartSeriesKey, string> = {
|
const chartColors: Record<ChartSeriesKey, string> = {
|
||||||
grossSales: "#7c3aed",
|
income: "#2563eb",
|
||||||
netRevenue: "#6366f1",
|
|
||||||
cogs: "#f97316",
|
cogs: "#f97316",
|
||||||
profit: "#10b981",
|
profit: "#10b981",
|
||||||
margin: "#0ea5e9",
|
margin: "#0ea5e9",
|
||||||
};
|
};
|
||||||
|
|
||||||
const SERIES_LABELS: Record<ChartSeriesKey, string> = {
|
const SERIES_LABELS: Record<ChartSeriesKey, string> = {
|
||||||
grossSales: "Gross Sales",
|
income: "Total Income",
|
||||||
netRevenue: "Net Revenue",
|
|
||||||
cogs: "COGS",
|
cogs: "COGS",
|
||||||
profit: "Profit",
|
profit: "Gross Profit",
|
||||||
margin: "Profit Margin",
|
margin: "Profit Margin",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -166,8 +166,7 @@ const SERIES_DEFINITIONS: Array<{
|
|||||||
label: string;
|
label: string;
|
||||||
type: "currency" | "percentage";
|
type: "currency" | "percentage";
|
||||||
}> = [
|
}> = [
|
||||||
{ key: "grossSales", label: SERIES_LABELS.grossSales, type: "currency" },
|
{ key: "income", label: SERIES_LABELS.income, type: "currency" },
|
||||||
{ key: "netRevenue", label: SERIES_LABELS.netRevenue, type: "currency" },
|
|
||||||
{ key: "cogs", label: SERIES_LABELS.cogs, type: "currency" },
|
{ key: "cogs", label: SERIES_LABELS.cogs, type: "currency" },
|
||||||
{ key: "profit", label: SERIES_LABELS.profit, type: "currency" },
|
{ key: "profit", label: SERIES_LABELS.profit, type: "currency" },
|
||||||
{ key: "margin", label: SERIES_LABELS.margin, type: "percentage" },
|
{ key: "margin", label: SERIES_LABELS.margin, type: "percentage" },
|
||||||
@@ -277,10 +276,12 @@ const formatPercentage = (value: number, digits = 1, suffix = "%") => {
|
|||||||
|
|
||||||
type RawTrendPoint = {
|
type RawTrendPoint = {
|
||||||
date: Date;
|
date: Date;
|
||||||
grossSales: number;
|
income: number;
|
||||||
netRevenue: number;
|
|
||||||
cogs: number;
|
cogs: number;
|
||||||
profit: number;
|
shippingFees: number;
|
||||||
|
taxCollected: number;
|
||||||
|
grossSales: number;
|
||||||
|
discounts: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type AggregatedTrendPoint = {
|
type AggregatedTrendPoint = {
|
||||||
@@ -289,8 +290,7 @@ type AggregatedTrendPoint = {
|
|||||||
tooltipLabel: string;
|
tooltipLabel: string;
|
||||||
detailLabel: string;
|
detailLabel: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
grossSales: number;
|
income: number;
|
||||||
netRevenue: number;
|
|
||||||
cogs: number;
|
cogs: number;
|
||||||
profit: number;
|
profit: number;
|
||||||
margin: number;
|
margin: number;
|
||||||
@@ -367,10 +367,8 @@ const aggregateTrendPoints = (points: RawTrendPoint[], groupBy: GroupByOption):
|
|||||||
key: string;
|
key: string;
|
||||||
start: Date;
|
start: Date;
|
||||||
end: Date;
|
end: Date;
|
||||||
grossSales: number;
|
income: number;
|
||||||
netRevenue: number;
|
|
||||||
cogs: number;
|
cogs: number;
|
||||||
profit: number;
|
|
||||||
}
|
}
|
||||||
>();
|
>();
|
||||||
|
|
||||||
@@ -382,10 +380,8 @@ const aggregateTrendPoints = (points: RawTrendPoint[], groupBy: GroupByOption):
|
|||||||
key,
|
key,
|
||||||
start: point.date,
|
start: point.date,
|
||||||
end: point.date,
|
end: point.date,
|
||||||
grossSales: point.grossSales,
|
income: point.income,
|
||||||
netRevenue: point.netRevenue,
|
|
||||||
cogs: point.cogs,
|
cogs: point.cogs,
|
||||||
profit: point.profit,
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -397,18 +393,17 @@ const aggregateTrendPoints = (points: RawTrendPoint[], groupBy: GroupByOption):
|
|||||||
bucket.end = point.date;
|
bucket.end = point.date;
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket.grossSales += point.grossSales;
|
bucket.income += point.income;
|
||||||
bucket.netRevenue += point.netRevenue;
|
|
||||||
bucket.cogs += point.cogs;
|
bucket.cogs += point.cogs;
|
||||||
bucket.profit += point.profit;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(bucketMap.values())
|
return Array.from(bucketMap.values())
|
||||||
.sort((a, b) => a.start.getTime() - b.start.getTime())
|
.sort((a, b) => a.start.getTime() - b.start.getTime())
|
||||||
.map((bucket) => {
|
.map((bucket) => {
|
||||||
const { axisLabel, tooltipLabel, detailLabel } = buildGroupLabels(bucket.start, bucket.end, groupBy);
|
const { axisLabel, tooltipLabel, detailLabel } = buildGroupLabels(bucket.start, bucket.end, groupBy);
|
||||||
const { netRevenue, profit } = bucket;
|
const income = bucket.income;
|
||||||
const margin = netRevenue !== 0 ? (profit / netRevenue) * 100 : 0;
|
const profit = income - bucket.cogs;
|
||||||
|
const margin = income !== 0 ? (profit / income) * 100 : 0;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: bucket.key,
|
id: bucket.key,
|
||||||
@@ -416,8 +411,7 @@ const aggregateTrendPoints = (points: RawTrendPoint[], groupBy: GroupByOption):
|
|||||||
tooltipLabel,
|
tooltipLabel,
|
||||||
detailLabel,
|
detailLabel,
|
||||||
timestamp: bucket.start.toISOString(),
|
timestamp: bucket.start.toISOString(),
|
||||||
grossSales: bucket.grossSales,
|
income,
|
||||||
netRevenue,
|
|
||||||
cogs: bucket.cogs,
|
cogs: bucket.cogs,
|
||||||
profit,
|
profit,
|
||||||
margin,
|
margin,
|
||||||
@@ -516,8 +510,7 @@ const extendAggregatedTrendPoints = (
|
|||||||
tooltipLabel,
|
tooltipLabel,
|
||||||
detailLabel,
|
detailLabel,
|
||||||
timestamp: bucketStart.toISOString(),
|
timestamp: bucketStart.toISOString(),
|
||||||
grossSales: 0,
|
income: 0,
|
||||||
netRevenue: 0,
|
|
||||||
cogs: 0,
|
cogs: 0,
|
||||||
profit: 0,
|
profit: 0,
|
||||||
margin: 0,
|
margin: 0,
|
||||||
@@ -536,14 +529,21 @@ const monthsBetween = (start: Date, end: Date) => {
|
|||||||
|
|
||||||
const buildTrendLabel = (
|
const buildTrendLabel = (
|
||||||
comparison?: ComparisonValue | null,
|
comparison?: ComparisonValue | null,
|
||||||
options?: { isPercentage?: boolean }
|
options?: { isPercentage?: boolean; invertDirection?: boolean }
|
||||||
): TrendSummary | null => {
|
): TrendSummary | null => {
|
||||||
if (!comparison || comparison.absolute === null) {
|
if (!comparison || comparison.absolute === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { absolute, percentage } = comparison;
|
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);
|
const absoluteValue = Math.abs(absolute);
|
||||||
|
|
||||||
if (options?.isPercentage) {
|
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 generateYearOptions = (span: number) => {
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
return Array.from({ length: span }, (_, index) => currentYear - index);
|
return Array.from({ length: span }, (_, index) => currentYear - index);
|
||||||
@@ -638,9 +671,8 @@ const FinancialOverview = () => {
|
|||||||
count: 1,
|
count: 1,
|
||||||
});
|
});
|
||||||
const [metrics, setMetrics] = useState<Record<ChartSeriesKey, boolean>>({
|
const [metrics, setMetrics] = useState<Record<ChartSeriesKey, boolean>>({
|
||||||
grossSales: true,
|
income: true,
|
||||||
netRevenue: true,
|
cogs: true,
|
||||||
cogs: false,
|
|
||||||
profit: true,
|
profit: true,
|
||||||
margin: true,
|
margin: true,
|
||||||
});
|
});
|
||||||
@@ -724,12 +756,25 @@ const FinancialOverview = () => {
|
|||||||
return null;
|
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 {
|
return {
|
||||||
date,
|
date,
|
||||||
grossSales: toNumber(point.grossSales),
|
income: incomeValue,
|
||||||
netRevenue: toNumber(point.netRevenue),
|
shippingFees: shippingFeesValue,
|
||||||
|
taxCollected: taxCollectedValue,
|
||||||
|
grossSales: grossSalesValue,
|
||||||
|
discounts: discountsValue,
|
||||||
cogs: toNumber(point.cogs),
|
cogs: toNumber(point.cogs),
|
||||||
profit: toNumber(point.profit),
|
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.filter((value): value is RawTrendPoint => Boolean(value))
|
.filter((value): value is RawTrendPoint => Boolean(value))
|
||||||
@@ -848,45 +893,74 @@ const FinancialOverview = () => {
|
|||||||
|
|
||||||
const totals = data.totals;
|
const totals = data.totals;
|
||||||
const previous = data.previousTotals ?? undefined;
|
const previous = data.previousTotals ?? undefined;
|
||||||
const comparison = data.comparison ?? {};
|
|
||||||
|
|
||||||
const safeCurrency = (value: number | undefined | null, digits = 0) =>
|
const safeCurrency = (value: number | undefined | null, digits = 0) =>
|
||||||
typeof value === "number" && Number.isFinite(value) ? formatCurrency(value, digits) : "—";
|
typeof value === "number" && Number.isFinite(value) ? formatCurrency(value, digits) : "—";
|
||||||
|
|
||||||
const safePercentage = (value: number | undefined | null, digits = 1) =>
|
const safePercentage = (value: number | undefined | null, digits = 1) =>
|
||||||
typeof value === "number" && Number.isFinite(value) ? formatPercentage(value, digits) : "—";
|
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 [
|
return [
|
||||||
{
|
{
|
||||||
key: "grossSales",
|
key: "income",
|
||||||
title: "Gross Sales",
|
title: "Total Income",
|
||||||
value: safeCurrency(totals.grossSales, 0),
|
value: safeCurrency(totalIncome, 0),
|
||||||
description: previous?.grossSales != null ? `Previous: ${safeCurrency(previous.grossSales, 0)}` : undefined,
|
description: previousIncome != null ? `Previous: ${safeCurrency(previousIncome, 0)}` : undefined,
|
||||||
trend: buildTrendLabel(comparison.grossSales),
|
trend: buildTrendLabel(comparison?.income ?? buildComparisonFromValues(totalIncome, previousIncome ?? null)),
|
||||||
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),
|
|
||||||
accentClass: "text-indigo-600 dark:text-indigo-400",
|
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",
|
key: "profit",
|
||||||
title: "Profit",
|
title: "Gross Profit",
|
||||||
value: safeCurrency(totals.profit, 0),
|
value: safeCurrency(profitValue, 0),
|
||||||
description: previous?.profit != null ? `Previous: ${safeCurrency(previous.profit, 0)}` : undefined,
|
description: previousProfitValue != null ? `Previous: ${safeCurrency(previousProfitValue, 0)}` : undefined,
|
||||||
trend: buildTrendLabel(comparison.profit),
|
trend: buildTrendLabel(comparison?.profit ?? buildComparisonFromValues(profitValue, previousProfitValue ?? null)),
|
||||||
accentClass: "text-emerald-600 dark:text-emerald-400",
|
accentClass: "text-emerald-600 dark:text-emerald-400",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "margin",
|
key: "margin",
|
||||||
title: "Profit Margin",
|
title: "Profit Margin",
|
||||||
value: safePercentage(totals.margin, 1),
|
value: safePercentage(marginValue, 1),
|
||||||
description: previous?.margin != null ? `Previous: ${safePercentage(previous.margin, 1)}` : undefined,
|
description: previousMarginValue != null ? `Previous: ${safePercentage(previousMarginValue, 1)}` : undefined,
|
||||||
trend: buildTrendLabel(comparison.margin, { isPercentage: true }),
|
trend: buildTrendLabel(
|
||||||
|
comparison?.margin ?? buildComparisonFromValues(marginValue, previousMarginValue ?? null),
|
||||||
|
{ isPercentage: true }
|
||||||
|
),
|
||||||
accentClass: "text-sky-600 dark:text-sky-400",
|
accentClass: "text-sky-600 dark:text-sky-400",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -907,8 +981,7 @@ const FinancialOverview = () => {
|
|||||||
return aggregatedPoints.map((point) => ({
|
return aggregatedPoints.map((point) => ({
|
||||||
label: point.label,
|
label: point.label,
|
||||||
timestamp: point.timestamp,
|
timestamp: point.timestamp,
|
||||||
grossSales: point.isFuture ? null : point.grossSales,
|
income: point.isFuture ? null : point.income,
|
||||||
netRevenue: point.isFuture ? null : point.netRevenue,
|
|
||||||
cogs: point.isFuture ? null : point.cogs,
|
cogs: point.isFuture ? null : point.cogs,
|
||||||
profit: point.isFuture ? null : point.profit,
|
profit: point.isFuture ? null : point.profit,
|
||||||
margin: point.isFuture ? null : point.margin,
|
margin: point.isFuture ? null : point.margin,
|
||||||
@@ -977,8 +1050,7 @@ const FinancialOverview = () => {
|
|||||||
id: point.id,
|
id: point.id,
|
||||||
label: point.detailLabel,
|
label: point.detailLabel,
|
||||||
timestamp: point.timestamp,
|
timestamp: point.timestamp,
|
||||||
grossSales: point.grossSales,
|
income: point.income,
|
||||||
netRevenue: point.netRevenue,
|
|
||||||
cogs: point.cogs,
|
cogs: point.cogs,
|
||||||
profit: point.profit,
|
profit: point.profit,
|
||||||
margin: point.margin,
|
margin: point.margin,
|
||||||
@@ -1174,18 +1246,15 @@ const FinancialOverview = () => {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Date</TableHead>
|
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Date</TableHead>
|
||||||
{metrics.grossSales && (
|
{metrics.income && (
|
||||||
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Gross Sales</TableHead>
|
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Total Income</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.cogs && (
|
{metrics.cogs && (
|
||||||
<TableHead className="px-6 py-3 whitespace-nowrap text-center">COGS</TableHead>
|
<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 && (
|
{metrics.margin && (
|
||||||
<TableHead className="px-6 py-3 whitespace-nowrap text-center">Margin</TableHead>
|
<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">
|
<TableCell className="px-6 py-3 whitespace-nowrap text-center text-sm text-muted-foreground">
|
||||||
{row.label || "—"}
|
{row.label || "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
{metrics.grossSales && (
|
{metrics.income && (
|
||||||
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
|
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
|
||||||
{row.isFuture ? "—" : formatCurrency(row.grossSales, 0)}
|
{row.isFuture ? "—" : formatCurrency(row.income, 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)}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
{metrics.cogs && (
|
{metrics.cogs && (
|
||||||
@@ -1217,6 +1276,11 @@ const FinancialOverview = () => {
|
|||||||
{row.isFuture ? "—" : formatCurrency(row.cogs, 0)}
|
{row.isFuture ? "—" : formatCurrency(row.cogs, 0)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
)}
|
)}
|
||||||
|
{metrics.profit && (
|
||||||
|
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
|
||||||
|
{row.isFuture ? "—" : formatCurrency(row.profit, 0)}
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
{metrics.margin && (
|
{metrics.margin && (
|
||||||
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
|
<TableCell className="px-6 py-3 whitespace-nowrap text-center font-medium">
|
||||||
{row.isFuture ? "—" : formatPercentage(row.margin, 1)}
|
{row.isFuture ? "—" : formatPercentage(row.margin, 1)}
|
||||||
@@ -1387,13 +1451,9 @@ const FinancialOverview = () => {
|
|||||||
<ResponsiveContainer width="100%" height="100%">
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
<ComposedChart data={chartData} margin={{ top: 10, right: 30, left: -10, bottom: 0 }}>
|
<ComposedChart data={chartData} margin={{ top: 10, right: 30, left: -10, bottom: 0 }}>
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="financialGrossSales" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="financialIncome" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="5%" stopColor={chartColors.grossSales} stopOpacity={0.35} />
|
<stop offset="5%" stopColor={chartColors.income} stopOpacity={0.3} />
|
||||||
<stop offset="95%" stopColor={chartColors.grossSales} stopOpacity={0.05} />
|
<stop offset="95%" stopColor={chartColors.income} 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>
|
</linearGradient>
|
||||||
<linearGradient id="financialCogs" x1="0" y1="0" x2="0" y2="1">
|
<linearGradient id="financialCogs" x1="0" y1="0" x2="0" y2="1">
|
||||||
<stop offset="5%" stopColor={chartColors.cogs} stopOpacity={0.25} />
|
<stop offset="5%" stopColor={chartColors.cogs} stopOpacity={0.25} />
|
||||||
@@ -1424,25 +1484,14 @@ const FinancialOverview = () => {
|
|||||||
)}
|
)}
|
||||||
<Tooltip content={<FinancialTooltip />} />
|
<Tooltip content={<FinancialTooltip />} />
|
||||||
<Legend formatter={(value: string) => SERIES_LABELS[value as ChartSeriesKey] ?? value} />
|
<Legend formatter={(value: string) => SERIES_LABELS[value as ChartSeriesKey] ?? value} />
|
||||||
{metrics.grossSales ? (
|
{metrics.income ? (
|
||||||
<Area
|
<Area
|
||||||
yAxisId="left"
|
yAxisId="left"
|
||||||
type="monotone"
|
type="monotone"
|
||||||
dataKey="grossSales"
|
dataKey="income"
|
||||||
name={SERIES_LABELS.grossSales}
|
name={SERIES_LABELS.income}
|
||||||
stroke={chartColors.grossSales}
|
stroke={chartColors.income}
|
||||||
fill="url(#financialGrossSales)"
|
fill="url(#financialIncome)"
|
||||||
strokeWidth={2}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
{metrics.netRevenue ? (
|
|
||||||
<Area
|
|
||||||
yAxisId="left"
|
|
||||||
type="monotone"
|
|
||||||
dataKey="netRevenue"
|
|
||||||
name={SERIES_LABELS.netRevenue}
|
|
||||||
stroke={chartColors.netRevenue}
|
|
||||||
fill="url(#financialNetRevenue)"
|
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
Reference in New Issue
Block a user