diff --git a/inventory-server/dashboard/acot-server/routes/events.js b/inventory-server/dashboard/acot-server/routes/events.js index 5d03ba9..5070cec 100644 --- a/inventory-server/dashboard/acot-server/routes/events.js +++ b/inventory-server/dashboard/acot-server/routes/events.js @@ -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) `; diff --git a/inventory/src/pages/BlackFridayDashboard.tsx b/inventory/src/pages/BlackFridayDashboard.tsx index 23aabaa..bcf0df2 100644 --- a/inventory/src/pages/BlackFridayDashboard.tsx +++ b/inventory/src/pages/BlackFridayDashboard.tsx @@ -7,7 +7,6 @@ import { CardTitle, } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; import { Alert, AlertDescription, @@ -30,23 +29,20 @@ import { Tooltip as RechartsTooltip, XAxis, YAxis, - Legend, TooltipProps, } from "recharts"; import { - RefreshCw, TrendingUp, - Sparkles, DollarSign, ShoppingBag, Percent, ArrowUpRight, ArrowDownRight, - Trophy, - Activity, Clock3, Zap, Users, + Wallet, + Package, } from "lucide-react"; import { acotService } from "@/services/dashboard/acotService"; import { cn } from "@/lib/utils"; @@ -472,9 +468,11 @@ const CustomTooltip = ({ active, payload, label }: TooltipProps( + const [selectedYears] = useState( availableYears.slice(0, 6) ); const [dataByYear, setDataByYear] = useState>({}); @@ -564,11 +562,13 @@ export function BlackFridayDashboard() { metric: "revenue", eventType: "PLACED_ORDER", daily: true, + excludeCherryBox: true, }), acotService.getFinancials({ timeRange: "custom", startDate: params.startDate, endDate: params.endDate, + excludeCherryBox: true, }), ]); @@ -722,6 +722,8 @@ export function BlackFridayDashboard() { point[`${year}-orders`] = day?.orders ?? 0; point[`${year}-margin`] = day?.margin ?? null; point[`${year}-aov`] = day?.avgOrderValue ?? 0; + point[`${year}-profit`] = day?.profit ?? 0; + point[`${year}-cogs`] = day?.cogs ?? 0; point[`color-${year}`] = COLOR_PALETTE[colorIndex % COLOR_PALETTE.length]; }); return point; @@ -733,16 +735,6 @@ export function BlackFridayDashboard() { return bucketDebug.slice(0, 200); // keep overlay readable }, [bucketDebug]); - const toggleYear = (year: number) => { - setSelectedYears((prev) => { - if (prev.includes(year)) { - if (prev.length === 1) return prev; // keep at least one year selected - return prev.filter((y) => y !== year); - } - return [...prev, year].sort((a, b) => b - a); - }); - }; - const getStrokeForYear = (year: number) => { const index = sortedYears.indexOf(year); return COLOR_PALETTE[index % COLOR_PALETTE.length]; @@ -755,46 +747,9 @@ export function BlackFridayDashboard() { const renderLiveHeader = () => (
-
-
- - -
- LIVE -
-

Black Friday {currentYear}

{currentYearData?.range.label}
- -
-
- {availableYears.map((year) => ( - - ))} -
- -
); @@ -920,7 +875,7 @@ export function BlackFridayDashboard() { if (!currentYearData) return null; return ( -
+
{currentYearData.days.map((day, idx) => { const lastYearDay = lastYearData?.days[idx]; const change = lastYearDay ? percentChange(day.revenue, lastYearDay.revenue) : null; @@ -1014,19 +969,19 @@ export function BlackFridayDashboard() { ); }; - // Compact charts - 2x2 grid + // Compact charts - 3x2 grid (Revenue/Profit on top, COGS/Margin in middle, Orders/AOV on bottom) const renderCharts = () => ( -
+
{/* Revenue Chart */} - Revenue by Day + Revenue -
+
@@ -1064,16 +1019,16 @@ export function BlackFridayDashboard() { - {/* Orders Chart */} + {/* Profit Chart */} - - Orders by Day + + Profit -
+
@@ -1085,11 +1040,11 @@ export function BlackFridayDashboard() { dy={5} /> formatNumber(Number(value))} + tickFormatter={(value) => `$${(value as number / 1000).toFixed(0)}k`} axisLine={false} tickLine={false} tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 10 }} - width={40} + width={45} /> } /> {sortedYears.map((year) => ( @@ -1097,7 +1052,54 @@ export function BlackFridayDashboard() { key={year} type="monotone" name={`${year}`} - dataKey={`${year}-orders`} + dataKey={`${year}-profit`} + stroke={getStrokeForYear(year)} + strokeWidth={year === currentYear ? 2.5 : 1.5} + strokeOpacity={year === currentYear ? 1 : 0.6} + dot={year === currentYear ? { r: 3, strokeWidth: 2, fill: "hsl(var(--background))" } : false} + activeDot={{ r: 4, strokeWidth: 0 }} + /> + ))} + + +
+ + + + {/* COGS Chart */} + + + + + COGS + + + +
+ + + + + `$${(value as number / 1000).toFixed(0)}k`} + axisLine={false} + tickLine={false} + tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 10 }} + width={45} + /> + } /> + {sortedYears.map((year) => ( + - Margin by Day + Margin -
+
@@ -1158,16 +1160,63 @@ export function BlackFridayDashboard() { + {/* Orders Chart */} + + + + + Orders + + + +
+ + + + + formatNumber(Number(value))} + axisLine={false} + tickLine={false} + tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 10 }} + width={40} + /> + } /> + {sortedYears.map((year) => ( + + ))} + + +
+
+
+ {/* AOV Chart */} - AOV by Day + AOV -
+
@@ -1365,7 +1414,7 @@ export function BlackFridayDashboard() { {/* Charts with legend */}
-
+

Daily Trends

{renderLegend()}