Add in financial overview component with related routes
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user