Clean up build errors, better mobile styles for Black Friday, remove cherry box orders and add profit/cogs charts

This commit is contained in:
2025-11-29 01:24:54 -05:00
parent b81dfb9649
commit f5b2b4e421
2 changed files with 216 additions and 111 deletions

View File

@@ -13,6 +13,13 @@ const {
const TIMEZONE = 'America/New_York';
const BUSINESS_DAY_START_HOUR = timeHelpers?.BUSINESS_DAY_START_HOUR ?? 1;
// Cherry Box order types to exclude when excludeCherryBox=true is passed
// 3 = cherrybox_subscription, 4 = cherrybox_sending, 5 = cherrybox_subscription_renew, 7 = cherrybox_refund
const EXCLUDED_ORDER_TYPES = [3, 4, 5, 7];
const getCherryBoxClause = (exclude) => exclude ? `order_type NOT IN (${EXCLUDED_ORDER_TYPES.join(', ')})` : '1=1';
const getCherryBoxClauseAliased = (alias, exclude) => exclude ? `${alias}.order_type NOT IN (${EXCLUDED_ORDER_TYPES.join(', ')})` : '1=1';
const parseBoolParam = (value) => value === 'true' || value === '1';
// Image URL generation utility
const getImageUrls = (pid, iid = 1) => {
const imageUrlBase = 'https://sbing.com/i/products/0000/';
@@ -39,14 +46,15 @@ router.get('/stats', async (req, res) => {
try {
const mainOperation = async () => {
const { timeRange, startDate, endDate } = req.query;
console.log(`[STATS] Getting DB connection...`);
const { timeRange, startDate, endDate, excludeCherryBox } = req.query;
const excludeCB = parseBoolParam(excludeCherryBox);
console.log(`[STATS] Getting DB connection... (excludeCherryBox: ${excludeCB})`);
const { connection, release } = await getDbConnection();
console.log(`[STATS] DB connection obtained in ${Date.now() - startTime}ms`);
const { whereClause, params, dateRange } = getTimeRangeConditions(timeRange, startDate, endDate);
// Main order stats query
// Main order stats query (optionally excludes Cherry Box orders)
const mainStatsQuery = `
SELECT
COUNT(*) as orderCount,
@@ -61,32 +69,32 @@ router.get('/stats', async (req, res) => {
SUM(CASE WHEN order_status = 15 THEN 1 ELSE 0 END) as cancelledCount,
SUM(CASE WHEN order_status = 15 THEN summary_total ELSE 0 END) as cancelledTotal
FROM _order
WHERE order_status > 15 AND ${whereClause}
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause}
`;
const [mainStats] = await connection.execute(mainStatsQuery, params);
const stats = mainStats[0];
// Refunds query
// Refunds query (optionally excludes Cherry Box orders)
const refundsQuery = `
SELECT
COUNT(*) as refundCount,
ABS(SUM(payment_amount)) as refundTotal
FROM order_payment op
JOIN _order o ON op.order_id = o.order_id
WHERE payment_amount < 0 AND o.order_status > 15 AND ${whereClause.replace('date_placed', 'o.date_placed')}
WHERE payment_amount < 0 AND o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace('date_placed', 'o.date_placed')}
`;
const [refundStats] = await connection.execute(refundsQuery, params);
// Best revenue day query
// Best revenue day query (optionally excludes Cherry Box orders)
const bestDayQuery = `
SELECT
DATE(date_placed) as date,
SUM(summary_total) as revenue,
COUNT(*) as orders
FROM _order
WHERE order_status > 15 AND ${whereClause}
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause}
GROUP BY DATE(date_placed)
ORDER BY revenue DESC
LIMIT 1
@@ -94,7 +102,7 @@ router.get('/stats', async (req, res) => {
const [bestDayResult] = await connection.execute(bestDayQuery, params);
// Peak hour query (for single day periods)
// Peak hour query (for single day periods, optionally excludes Cherry Box orders)
let peakHour = null;
if (['today', 'yesterday'].includes(timeRange)) {
const peakHourQuery = `
@@ -102,7 +110,7 @@ router.get('/stats', async (req, res) => {
HOUR(date_placed) as hour,
COUNT(*) as count
FROM _order
WHERE order_status > 15 AND ${whereClause}
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause}
GROUP BY HOUR(date_placed)
ORDER BY count DESC
LIMIT 1
@@ -122,7 +130,7 @@ router.get('/stats', async (req, res) => {
}
// Brands and categories query - simplified for now since we don't have the category tables
// We'll use a simple approach without company table for now
// We'll use a simple approach without company table for now (optionally excludes Cherry Box orders)
const brandsQuery = `
SELECT
'Various Brands' as brandName,
@@ -132,13 +140,13 @@ router.get('/stats', async (req, res) => {
FROM order_items oi
JOIN _order o ON oi.order_id = o.order_id
JOIN products p ON oi.prod_pid = p.pid
WHERE o.order_status > 15 AND ${whereClause.replace('date_placed', 'o.date_placed')}
WHERE o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace('date_placed', 'o.date_placed')}
HAVING revenue > 0
`;
const [brandsResult] = await connection.execute(brandsQuery, params);
// For categories, we'll use a simplified approach
// For categories, we'll use a simplified approach (optionally excludes Cherry Box orders)
const categoriesQuery = `
SELECT
'General' as categoryName,
@@ -148,13 +156,13 @@ router.get('/stats', async (req, res) => {
FROM order_items oi
JOIN _order o ON oi.order_id = o.order_id
JOIN products p ON oi.prod_pid = p.pid
WHERE o.order_status > 15 AND ${whereClause.replace('date_placed', 'o.date_placed')}
WHERE o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace('date_placed', 'o.date_placed')}
HAVING revenue > 0
`;
const [categoriesResult] = await connection.execute(categoriesQuery, params);
// Shipping locations query
// Shipping locations query (optionally excludes Cherry Box orders)
const shippingQuery = `
SELECT
ship_country,
@@ -162,7 +170,7 @@ router.get('/stats', async (req, res) => {
ship_method_selected,
COUNT(*) as count
FROM _order
WHERE order_status IN (100, 92) AND ${whereClause}
WHERE order_status IN (100, 92) AND ${getCherryBoxClause(excludeCB)} AND ${whereClause}
GROUP BY ship_country, ship_state, ship_method_selected
`;
@@ -171,13 +179,13 @@ router.get('/stats', async (req, res) => {
// Process shipping data
const shippingStats = processShippingData(shippingResult, stats.shippedCount);
// Order value range query
// Order value range query (optionally excludes Cherry Box orders)
const orderRangeQuery = `
SELECT
MIN(summary_total) as smallest,
MAX(summary_total) as largest
FROM _order
WHERE order_status > 15 AND ${whereClause}
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause}
`;
const [orderRangeResult] = await connection.execute(orderRangeQuery, params);
@@ -189,7 +197,7 @@ router.get('/stats', async (req, res) => {
}
// Previous period comparison data
const prevPeriodData = await getPreviousPeriodData(connection, timeRange, startDate, endDate);
const prevPeriodData = await getPreviousPeriodData(connection, timeRange, startDate, endDate, excludeCB);
const response = {
timeRange: dateRange,
@@ -316,13 +324,14 @@ router.get('/stats', async (req, res) => {
router.get('/stats/details', async (req, res) => {
let release;
try {
const { timeRange, startDate, endDate, metric, daily } = req.query;
const { timeRange, startDate, endDate, metric, daily, excludeCherryBox } = req.query;
const excludeCB = parseBoolParam(excludeCherryBox);
const { connection, release: releaseConn } = await getDbConnection();
release = releaseConn;
const { whereClause, params } = getTimeRangeConditions(timeRange, startDate, endDate);
// Daily breakdown query
// Daily breakdown query (optionally excludes Cherry Box orders)
const dailyQuery = `
SELECT
DATE(date_placed) as date,
@@ -331,7 +340,7 @@ router.get('/stats/details', async (req, res) => {
AVG(summary_total) as averageOrderValue,
SUM(stats_prod_pieces) as itemCount
FROM _order
WHERE order_status > 15 AND ${whereClause}
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause}
GROUP BY DATE(date_placed)
ORDER BY DATE(date_placed)
`;
@@ -359,7 +368,7 @@ router.get('/stats/details', async (req, res) => {
prevParams = [prevStart.toISOString(), prevEnd.toISOString()];
}
// Get previous period daily data
// Get previous period daily data (optionally excludes Cherry Box orders)
const prevQuery = `
SELECT
DATE(date_placed) as date,
@@ -367,7 +376,7 @@ router.get('/stats/details', async (req, res) => {
SUM(summary_total) as prevRevenue,
AVG(summary_total) as prevAvgOrderValue
FROM _order
WHERE order_status > 15 AND ${prevWhereClause}
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${prevWhereClause}
GROUP BY DATE(date_placed)
`;
@@ -424,7 +433,8 @@ router.get('/stats/details', async (req, res) => {
router.get('/financials', async (req, res) => {
let release;
try {
const { timeRange, startDate, endDate } = req.query;
const { timeRange, startDate, endDate, excludeCherryBox } = req.query;
const excludeCB = parseBoolParam(excludeCherryBox);
const { connection, release: releaseConn } = await getDbConnection();
release = releaseConn;
@@ -450,7 +460,7 @@ router.get('/financials', async (req, res) => {
});
const [totalsRows] = await connection.execute(
buildFinancialTotalsQuery(financialWhere),
buildFinancialTotalsQuery(financialWhere, excludeCB),
params
);
@@ -462,7 +472,7 @@ router.get('/financials', async (req, res) => {
});
const [trendRows] = await connection.execute(
buildFinancialTrendQuery(financialWhere),
buildFinancialTrendQuery(financialWhere, excludeCB),
params
);
@@ -489,7 +499,7 @@ router.get('/financials', async (req, res) => {
});
const prevWhere = previousRange.whereClause.replace(/date_placed/g, 'date_change');
const [previousRows] = await connection.execute(
buildFinancialTotalsQuery(prevWhere),
buildFinancialTotalsQuery(prevWhere, excludeCB),
previousRange.params
);
previousTotals = normalizeFinancialTotals(previousRows[0]);
@@ -549,12 +559,14 @@ router.get('/financials', async (req, res) => {
router.get('/products', async (req, res) => {
let release;
try {
const { timeRange, startDate, endDate } = req.query;
const { timeRange, startDate, endDate, excludeCherryBox } = req.query;
const excludeCB = parseBoolParam(excludeCherryBox);
const { connection, release: releaseConn } = await getDbConnection();
release = releaseConn;
const { whereClause, params } = getTimeRangeConditions(timeRange, startDate, endDate);
// Products query (optionally excludes Cherry Box orders)
const productsQuery = `
SELECT
p.pid,
@@ -566,7 +578,7 @@ router.get('/products', async (req, res) => {
FROM order_items oi
JOIN _order o ON oi.order_id = o.order_id
JOIN products p ON oi.prod_pid = p.pid
WHERE o.order_status > 15 AND ${whereClause.replace('date_placed', 'o.date_placed')}
WHERE o.order_status > 15 AND ${getCherryBoxClauseAliased('o', excludeCB)} AND ${whereClause.replace('date_placed', 'o.date_placed')}
GROUP BY p.pid, p.description
ORDER BY totalRevenue DESC
LIMIT 500
@@ -609,7 +621,8 @@ router.get('/products', async (req, res) => {
router.get('/projection', async (req, res) => {
let release;
try {
const { timeRange, startDate, endDate } = req.query;
const { timeRange, startDate, endDate, excludeCherryBox } = req.query;
const excludeCB = parseBoolParam(excludeCherryBox);
// Only provide projections for incomplete periods
if (!['today', 'thisWeek', 'thisMonth'].includes(timeRange)) {
@@ -622,19 +635,20 @@ router.get('/projection', async (req, res) => {
// Get current period data
const { whereClause, params } = getTimeRangeConditions(timeRange, startDate, endDate);
// Current period query (optionally excludes Cherry Box orders)
const currentQuery = `
SELECT
SUM(summary_total) as currentRevenue,
COUNT(*) as currentOrders
FROM _order
WHERE order_status > 15 AND ${whereClause}
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCB)} AND ${whereClause}
`;
const [currentResult] = await connection.execute(currentQuery, params);
const current = currentResult[0];
// Get historical data for the same period type
const historicalQuery = await getHistoricalProjectionData(connection, timeRange);
const historicalQuery = await getHistoricalProjectionData(connection, timeRange, excludeCB);
// Calculate projection based on current progress and historical patterns
const periodProgress = calculatePeriodProgress(timeRange);
@@ -765,7 +779,24 @@ function calculatePeriodProgress(timeRange) {
return Math.min(100, Math.max(0, (elapsed / total) * 100));
}
function buildFinancialTotalsQuery(whereClause) {
function buildFinancialTotalsQuery(whereClause, excludeCherryBox = false) {
// Optionally join to _order to exclude Cherry Box orders
if (excludeCherryBox) {
return `
SELECT
COALESCE(SUM(r.sale_amount), 0) as grossSales,
COALESCE(SUM(r.refund_amount), 0) as refunds,
COALESCE(SUM(r.shipping_collected_amount + r.small_order_fee_amount + r.rush_fee_amount), 0) as shippingFees,
COALESCE(SUM(r.tax_collected_amount), 0) as taxCollected,
COALESCE(SUM(r.discount_total_amount), 0) as discounts,
COALESCE(SUM(r.cogs_amount), 0) as cogs
FROM report_sales_data r
JOIN _order o ON r.order_id = o.order_id
WHERE ${whereClause.replace(/date_change/g, 'r.date_change')}
AND r.action IN (1, 2, 3)
AND ${getCherryBoxClauseAliased('o', true)}
`;
}
return `
SELECT
COALESCE(SUM(sale_amount), 0) as grossSales,
@@ -780,8 +811,31 @@ function buildFinancialTotalsQuery(whereClause) {
`;
}
function buildFinancialTrendQuery(whereClause) {
function buildFinancialTrendQuery(whereClause, excludeCherryBox = false) {
const businessDayOffset = BUSINESS_DAY_START_HOUR;
// Optionally join to _order to exclude Cherry Box orders
if (excludeCherryBox) {
return `
SELECT
DATE_FORMAT(
DATE_SUB(r.date_change, INTERVAL ${businessDayOffset} HOUR),
'%Y-%m-%d'
) as businessDate,
SUM(r.sale_amount) as grossSales,
SUM(r.refund_amount) as refunds,
SUM(r.shipping_collected_amount + r.small_order_fee_amount + r.rush_fee_amount) as shippingFees,
SUM(r.tax_collected_amount) as taxCollected,
SUM(r.discount_total_amount) as discounts,
SUM(r.cogs_amount) as cogs
FROM report_sales_data r
JOIN _order o ON r.order_id = o.order_id
WHERE ${whereClause.replace(/date_change/g, 'r.date_change')}
AND r.action IN (1, 2, 3)
AND ${getCherryBoxClauseAliased('o', true)}
GROUP BY businessDate
ORDER BY businessDate ASC
`;
}
return `
SELECT
DATE_FORMAT(
@@ -940,7 +994,7 @@ function getPreviousPeriodRange(timeRange, startDate, endDate) {
return getTimeRangeConditions('custom', prevStart.toISOString(), prevEnd.toISOString());
}
async function getPreviousPeriodData(connection, timeRange, startDate, endDate) {
async function getPreviousPeriodData(connection, timeRange, startDate, endDate, excludeCherryBox = false) {
// Calculate previous period dates
let prevWhereClause, prevParams;
@@ -962,13 +1016,14 @@ async function getPreviousPeriodData(connection, timeRange, startDate, endDate)
prevParams = [prevStart.toISOString(), prevEnd.toISOString()];
}
// Previous period query (optionally excludes Cherry Box orders)
const prevQuery = `
SELECT
COUNT(*) as orderCount,
SUM(summary_total) as revenue,
AVG(summary_total) as averageOrderValue
FROM _order
WHERE order_status > 15 AND ${prevWhereClause}
WHERE order_status > 15 AND ${getCherryBoxClause(excludeCherryBox)} AND ${prevWhereClause}
`;
const [prevResult] = await connection.execute(prevQuery, prevParams);
@@ -994,8 +1049,8 @@ function getPreviousTimeRange(timeRange) {
return map[timeRange] || timeRange;
}
async function getHistoricalProjectionData(connection, timeRange) {
// Get historical data for projection calculations
async function getHistoricalProjectionData(connection, timeRange, excludeCherryBox = false) {
// Get historical data for projection calculations (optionally excludes Cherry Box orders)
// This is a simplified version - you could make this more sophisticated
const historicalQuery = `
SELECT
@@ -1003,6 +1058,7 @@ async function getHistoricalProjectionData(connection, timeRange) {
COUNT(*) as orders
FROM _order
WHERE order_status > 15
AND ${getCherryBoxClause(excludeCherryBox)}
AND date_placed >= DATE_SUB(NOW(), INTERVAL 30 DAY)
AND date_placed < DATE_SUB(NOW(), INTERVAL 1 DAY)
`;