Clean up build errors, better mobile styles for Black Friday, remove cherry box orders and add profit/cogs charts
This commit is contained in:
@@ -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)
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user