Add in financial overview component with related routes

This commit is contained in:
2025-09-18 12:04:20 -04:00
parent 7da2b304b4
commit c61115f665
5 changed files with 1547 additions and 5 deletions

View File

@@ -309,9 +309,9 @@ router.get('/stats/details', async (req, res) => {
const { timeRange, startDate, endDate, metric, daily } = req.query;
const { connection, release: releaseConn } = await getDbConnection();
release = releaseConn;
const { whereClause, params } = getTimeRangeConditions(timeRange, startDate, endDate);
// Daily breakdown query
const dailyQuery = `
SELECT
@@ -410,6 +410,68 @@ router.get('/stats/details', async (req, res) => {
}
});
// Financial performance endpoint
router.get('/financials', async (req, res) => {
let release;
try {
const { timeRange, startDate, endDate } = req.query;
const { connection, release: releaseConn } = await getDbConnection();
release = releaseConn;
const { whereClause, params, dateRange } = getTimeRangeConditions(timeRange, startDate, endDate);
const financialWhere = whereClause.replace(/date_placed/g, 'date_change');
const [totalsRows] = await connection.execute(
buildFinancialTotalsQuery(financialWhere),
params
);
const totals = normalizeFinancialTotals(totalsRows[0]);
const [trendRows] = await connection.execute(
buildFinancialTrendQuery(financialWhere),
params
);
const trend = trendRows.map(normalizeFinancialTrendRow);
let previousTotals = null;
let comparison = null;
const previousRange = getPreviousPeriodRange(timeRange, startDate, endDate);
if (previousRange) {
const prevWhere = previousRange.whereClause.replace(/date_placed/g, 'date_change');
const [previousRows] = await connection.execute(
buildFinancialTotalsQuery(prevWhere),
previousRange.params
);
previousTotals = normalizeFinancialTotals(previousRows[0]);
comparison = {
grossSales: calculateComparison(totals.grossSales, previousTotals.grossSales),
refunds: calculateComparison(totals.refunds, previousTotals.refunds),
taxCollected: calculateComparison(totals.taxCollected, previousTotals.taxCollected),
cogs: calculateComparison(totals.cogs, previousTotals.cogs),
netRevenue: calculateComparison(totals.netRevenue, previousTotals.netRevenue),
profit: calculateComparison(totals.profit, previousTotals.profit),
margin: calculateComparison(totals.margin, previousTotals.margin),
};
}
res.json({
dateRange,
totals,
previousTotals,
comparison,
trend,
});
} catch (error) {
console.error('Error in /financials:', error);
res.status(500).json({ error: error.message });
} finally {
if (release) release();
}
});
// Products endpoint - replaces /api/klaviyo/events/products
router.get('/products', async (req, res) => {
let release;
@@ -639,6 +701,132 @@ function calculatePeriodProgress(timeRange) {
}
}
function buildFinancialTotalsQuery(whereClause) {
return `
SELECT
COALESCE(SUM(sale_amount), 0) as grossSales,
COALESCE(SUM(refund_amount), 0) as refunds,
COALESCE(SUM(tax_collected_amount), 0) as taxCollected,
COALESCE(SUM(cogs_amount), 0) as cogs
FROM report_sales_data
WHERE ${whereClause}
`;
}
function buildFinancialTrendQuery(whereClause) {
return `
SELECT
DATE(date_change) as date,
SUM(sale_amount) as grossSales,
SUM(refund_amount) as refunds,
SUM(tax_collected_amount) as taxCollected,
SUM(cogs_amount) as cogs
FROM report_sales_data
WHERE ${whereClause}
GROUP BY DATE(date_change)
ORDER BY date ASC
`;
}
function normalizeFinancialTotals(row = {}) {
const grossSales = parseFloat(row.grossSales || 0);
const refunds = parseFloat(row.refunds || 0);
const taxCollected = parseFloat(row.taxCollected || 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;
return {
grossSales,
refunds,
taxCollected,
cogs,
netSales,
netRevenue,
profit,
margin,
};
}
function normalizeFinancialTrendRow(row = {}) {
const grossSales = parseFloat(row.grossSales || 0);
const refunds = parseFloat(row.refunds || 0);
const taxCollected = parseFloat(row.taxCollected || 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;
let timestamp = null;
if (row.date instanceof Date) {
timestamp = new Date(row.date.getTime()).toISOString();
} else if (typeof row.date === 'string') {
timestamp = new Date(`${row.date}T00:00:00Z`).toISOString();
}
return {
date: row.date,
grossSales,
refunds,
taxCollected,
cogs,
netSales,
netRevenue,
profit,
margin,
timestamp,
};
}
function calculateComparison(currentValue, previousValue) {
if (typeof previousValue !== 'number') {
return { absolute: null, percentage: null };
}
const absolute = typeof currentValue === 'number' ? currentValue - previousValue : null;
const percentage =
absolute !== null && previousValue !== 0
? (absolute / Math.abs(previousValue)) * 100
: null;
return { absolute, percentage };
}
function getPreviousPeriodRange(timeRange, startDate, endDate) {
if (timeRange && timeRange !== 'custom') {
const prevTimeRange = getPreviousTimeRange(timeRange);
if (!prevTimeRange || prevTimeRange === timeRange) {
return null;
}
return getTimeRangeConditions(prevTimeRange);
}
const hasCustomDates = (timeRange === 'custom' || !timeRange) && startDate && endDate;
if (!hasCustomDates) {
return null;
}
const start = new Date(startDate);
const end = new Date(endDate);
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
return null;
}
const duration = end.getTime() - start.getTime();
if (!Number.isFinite(duration) || duration <= 0) {
return null;
}
const prevEnd = new Date(start.getTime() - 1);
const prevStart = new Date(prevEnd.getTime() - duration);
return getTimeRangeConditions('custom', prevStart.toISOString(), prevEnd.toISOString());
}
async function getPreviousPeriodData(connection, timeRange, startDate, endDate) {
// Calculate previous period dates
let prevWhereClause, prevParams;
@@ -764,4 +952,4 @@ router.get('/debug/pool', (req, res) => {
});
});
module.exports = router;
module.exports = router;