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

View File

@@ -7,7 +7,6 @@ import {
CardTitle, CardTitle,
} from "@/components/ui/card"; } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { import {
Alert, Alert,
AlertDescription, AlertDescription,
@@ -30,23 +29,20 @@ import {
Tooltip as RechartsTooltip, Tooltip as RechartsTooltip,
XAxis, XAxis,
YAxis, YAxis,
Legend,
TooltipProps, TooltipProps,
} from "recharts"; } from "recharts";
import { import {
RefreshCw,
TrendingUp, TrendingUp,
Sparkles,
DollarSign, DollarSign,
ShoppingBag, ShoppingBag,
Percent, Percent,
ArrowUpRight, ArrowUpRight,
ArrowDownRight, ArrowDownRight,
Trophy,
Activity,
Clock3, Clock3,
Zap, Zap,
Users, Users,
Wallet,
Package,
} from "lucide-react"; } from "lucide-react";
import { acotService } from "@/services/dashboard/acotService"; import { acotService } from "@/services/dashboard/acotService";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -472,9 +468,11 @@ const CustomTooltip = ({ active, payload, label }: TooltipProps<ValueType, NameT
const isRevenue = name.includes("revenue"); const isRevenue = name.includes("revenue");
const isMargin = name.includes("margin"); const isMargin = name.includes("margin");
const isAov = name.includes("aov"); const isAov = name.includes("aov");
const isProfit = name.includes("profit");
const isCogs = name.includes("cogs");
let valueFormatted = ""; let valueFormatted = "";
if (isRevenue || isAov) valueFormatted = formatCurrency(Number(entry.value)); if (isRevenue || isAov || isProfit || isCogs) valueFormatted = formatCurrency(Number(entry.value));
else if (isMargin) valueFormatted = `${Number(entry.value).toFixed(1)}%`; else if (isMargin) valueFormatted = `${Number(entry.value).toFixed(1)}%`;
else valueFormatted = formatNumber(Number(entry.value)); else valueFormatted = formatNumber(Number(entry.value));
@@ -523,7 +521,7 @@ export function BlackFridayDashboard() {
[currentYear] [currentYear]
); );
const [selectedYears, setSelectedYears] = useState<number[]>( const [selectedYears] = useState<number[]>(
availableYears.slice(0, 6) availableYears.slice(0, 6)
); );
const [dataByYear, setDataByYear] = useState<Record<number, YearMetrics>>({}); const [dataByYear, setDataByYear] = useState<Record<number, YearMetrics>>({});
@@ -564,11 +562,13 @@ export function BlackFridayDashboard() {
metric: "revenue", metric: "revenue",
eventType: "PLACED_ORDER", eventType: "PLACED_ORDER",
daily: true, daily: true,
excludeCherryBox: true,
}), }),
acotService.getFinancials({ acotService.getFinancials({
timeRange: "custom", timeRange: "custom",
startDate: params.startDate, startDate: params.startDate,
endDate: params.endDate, endDate: params.endDate,
excludeCherryBox: true,
}), }),
]); ]);
@@ -722,6 +722,8 @@ export function BlackFridayDashboard() {
point[`${year}-orders`] = day?.orders ?? 0; point[`${year}-orders`] = day?.orders ?? 0;
point[`${year}-margin`] = day?.margin ?? null; point[`${year}-margin`] = day?.margin ?? null;
point[`${year}-aov`] = day?.avgOrderValue ?? 0; 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]; point[`color-${year}`] = COLOR_PALETTE[colorIndex % COLOR_PALETTE.length];
}); });
return point; return point;
@@ -733,16 +735,6 @@ export function BlackFridayDashboard() {
return bucketDebug.slice(0, 200); // keep overlay readable return bucketDebug.slice(0, 200); // keep overlay readable
}, [bucketDebug]); }, [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 getStrokeForYear = (year: number) => {
const index = sortedYears.indexOf(year); const index = sortedYears.indexOf(year);
return COLOR_PALETTE[index % COLOR_PALETTE.length]; return COLOR_PALETTE[index % COLOR_PALETTE.length];
@@ -755,46 +747,9 @@ export function BlackFridayDashboard() {
const renderLiveHeader = () => ( const renderLiveHeader = () => (
<div className="flex items-center justify-between gap-4 flex-wrap"> <div className="flex items-center justify-between gap-4 flex-wrap">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<div className="relative flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
</div>
<span className="text-sm font-semibold text-emerald-600 dark:text-emerald-400">LIVE</span>
</div>
<div className="h-4 w-px bg-border" />
<h1 className="text-xl font-bold tracking-tight">Black Friday {currentYear}</h1> <h1 className="text-xl font-bold tracking-tight">Black Friday {currentYear}</h1>
<span className="text-sm text-muted-foreground">{currentYearData?.range.label}</span> <span className="text-sm text-muted-foreground">{currentYearData?.range.label}</span>
</div> </div>
<div className="flex items-center gap-2">
<div className="flex items-center gap-1 bg-muted/60 px-1 py-0.5 rounded-md border">
{availableYears.map((year) => (
<Button
key={year}
size="sm"
variant={selectedYears.includes(year) ? (year === currentYear ? "default" : "secondary") : "ghost"}
onClick={() => toggleYear(year)}
className={cn(
"h-6 px-2 text-xs font-medium rounded transition-all",
year === currentYear && selectedYears.includes(year) && "bg-emerald-600 hover:bg-emerald-700",
!selectedYears.includes(year) && "text-muted-foreground hover:text-foreground"
)}
>
{year}
</Button>
))}
</div>
<Button
size="sm"
variant="ghost"
onClick={() => setRefreshToken((token) => token + 1)}
disabled={loading}
className="h-6 w-6 p-0"
>
<RefreshCw className={cn("h-3.5 w-3.5", loading && "animate-spin")} />
</Button>
</div>
</div> </div>
); );
@@ -920,7 +875,7 @@ export function BlackFridayDashboard() {
if (!currentYearData) return null; if (!currentYearData) return null;
return ( return (
<div className="grid grid-cols-6 gap-2"> <div className="grid grid-cols-3 lg:grid-cols-6 gap-2">
{currentYearData.days.map((day, idx) => { {currentYearData.days.map((day, idx) => {
const lastYearDay = lastYearData?.days[idx]; const lastYearDay = lastYearData?.days[idx];
const change = lastYearDay ? percentChange(day.revenue, lastYearDay.revenue) : null; 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 = () => ( const renderCharts = () => (
<div className="grid gap-3 grid-cols-1 lg:grid-cols-2"> <div className="grid gap-3 grid-cols-1 md:grid-cols-2">
{/* Revenue Chart */} {/* Revenue Chart */}
<Card className="border-muted"> <Card className="border-muted">
<CardHeader className="pb-2 pt-3 px-4"> <CardHeader className="pb-2 pt-3 px-4">
<CardTitle className="text-sm font-medium flex items-center gap-2"> <CardTitle className="text-sm font-medium flex items-center gap-2">
<DollarSign className="h-4 w-4 text-muted-foreground" /> <DollarSign className="h-4 w-4 text-muted-foreground" />
Revenue by Day Revenue
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="p-2 pt-0"> <CardContent className="p-2 pt-0">
<div className="h-[180px] w-full"> <div className="h-[160px] w-full">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}> <LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" /> <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
@@ -1064,16 +1019,16 @@ export function BlackFridayDashboard() {
</CardContent> </CardContent>
</Card> </Card>
{/* Orders Chart */} {/* Profit Chart */}
<Card className="border-muted"> <Card className="border-muted">
<CardHeader className="pb-2 pt-3 px-4"> <CardHeader className="pb-2 pt-3 px-4">
<CardTitle className="text-sm font-medium flex items-center gap-2"> <CardTitle className="text-sm font-medium flex items-center gap-2">
<ShoppingBag className="h-4 w-4 text-muted-foreground" /> <Wallet className="h-4 w-4 text-emerald-500" />
Orders by Day Profit
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="p-2 pt-0"> <CardContent className="p-2 pt-0">
<div className="h-[180px] w-full"> <div className="h-[160px] w-full">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}> <LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" /> <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
@@ -1085,11 +1040,11 @@ export function BlackFridayDashboard() {
dy={5} dy={5}
/> />
<YAxis <YAxis
tickFormatter={(value) => formatNumber(Number(value))} tickFormatter={(value) => `$${(value as number / 1000).toFixed(0)}k`}
axisLine={false} axisLine={false}
tickLine={false} tickLine={false}
tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 10 }} tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 10 }}
width={40} width={45}
/> />
<RechartsTooltip content={<CustomTooltip />} /> <RechartsTooltip content={<CustomTooltip />} />
{sortedYears.map((year) => ( {sortedYears.map((year) => (
@@ -1097,7 +1052,54 @@ export function BlackFridayDashboard() {
key={year} key={year}
type="monotone" type="monotone"
name={`${year}`} 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 }}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
{/* COGS Chart */}
<Card className="border-muted">
<CardHeader className="pb-2 pt-3 px-4">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Package className="h-4 w-4 text-muted-foreground" />
COGS
</CardTitle>
</CardHeader>
<CardContent className="p-2 pt-0">
<div className="h-[160px] w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<XAxis
dataKey="day"
axisLine={false}
tickLine={false}
tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 10 }}
dy={5}
/>
<YAxis
tickFormatter={(value) => `$${(value as number / 1000).toFixed(0)}k`}
axisLine={false}
tickLine={false}
tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 10 }}
width={45}
/>
<RechartsTooltip content={<CustomTooltip />} />
{sortedYears.map((year) => (
<Line
key={year}
type="monotone"
name={`${year}`}
dataKey={`${year}-cogs`}
stroke={getStrokeForYear(year)} stroke={getStrokeForYear(year)}
strokeWidth={year === currentYear ? 2.5 : 1.5} strokeWidth={year === currentYear ? 2.5 : 1.5}
strokeOpacity={year === currentYear ? 1 : 0.6} strokeOpacity={year === currentYear ? 1 : 0.6}
@@ -1116,11 +1118,11 @@ export function BlackFridayDashboard() {
<CardHeader className="pb-2 pt-3 px-4"> <CardHeader className="pb-2 pt-3 px-4">
<CardTitle className="text-sm font-medium flex items-center gap-2"> <CardTitle className="text-sm font-medium flex items-center gap-2">
<Percent className="h-4 w-4 text-muted-foreground" /> <Percent className="h-4 w-4 text-muted-foreground" />
Margin by Day Margin
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="p-2 pt-0"> <CardContent className="p-2 pt-0">
<div className="h-[180px] w-full"> <div className="h-[160px] w-full">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}> <LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" /> <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
@@ -1158,16 +1160,63 @@ export function BlackFridayDashboard() {
</CardContent> </CardContent>
</Card> </Card>
{/* Orders Chart */}
<Card className="border-muted">
<CardHeader className="pb-2 pt-3 px-4">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<ShoppingBag className="h-4 w-4 text-muted-foreground" />
Orders
</CardTitle>
</CardHeader>
<CardContent className="p-2 pt-0">
<div className="h-[160px] w-full">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
<XAxis
dataKey="day"
axisLine={false}
tickLine={false}
tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 10 }}
dy={5}
/>
<YAxis
tickFormatter={(value) => formatNumber(Number(value))}
axisLine={false}
tickLine={false}
tick={{ fill: "hsl(var(--muted-foreground))", fontSize: 10 }}
width={40}
/>
<RechartsTooltip content={<CustomTooltip />} />
{sortedYears.map((year) => (
<Line
key={year}
type="monotone"
name={`${year}`}
dataKey={`${year}-orders`}
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 }}
/>
))}
</LineChart>
</ResponsiveContainer>
</div>
</CardContent>
</Card>
{/* AOV Chart */} {/* AOV Chart */}
<Card className="border-muted"> <Card className="border-muted">
<CardHeader className="pb-2 pt-3 px-4"> <CardHeader className="pb-2 pt-3 px-4">
<CardTitle className="text-sm font-medium flex items-center gap-2"> <CardTitle className="text-sm font-medium flex items-center gap-2">
<Zap className="h-4 w-4 text-muted-foreground" /> <Zap className="h-4 w-4 text-muted-foreground" />
AOV by Day AOV
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="p-2 pt-0"> <CardContent className="p-2 pt-0">
<div className="h-[180px] w-full"> <div className="h-[160px] w-full">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}> <LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 0 }}>
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" /> <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="hsl(var(--border))" />
@@ -1365,7 +1414,7 @@ export function BlackFridayDashboard() {
{/* Charts with legend */} {/* Charts with legend */}
<div className="space-y-2"> <div className="space-y-2">
<div className="flex items-center justify-between"> <div className="flex flex-col lg:flex-row items-center justify-between">
<h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Daily Trends</h3> <h3 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">Daily Trends</h3>
{renderLegend()} {renderLegend()}
</div> </div>