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

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

View File

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

View File

@@ -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<ValueType, NameT
const isRevenue = name.includes("revenue");
const isMargin = name.includes("margin");
const isAov = name.includes("aov");
const isProfit = name.includes("profit");
const isCogs = name.includes("cogs");
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 valueFormatted = formatNumber(Number(entry.value));
@@ -523,7 +521,7 @@ export function BlackFridayDashboard() {
[currentYear]
);
const [selectedYears, setSelectedYears] = useState<number[]>(
const [selectedYears] = useState<number[]>(
availableYears.slice(0, 6)
);
const [dataByYear, setDataByYear] = useState<Record<number, YearMetrics>>({});
@@ -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 = () => (
<div className="flex items-center justify-between gap-4 flex-wrap">
<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>
<span className="text-sm text-muted-foreground">{currentYearData?.range.label}</span>
</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>
);
@@ -920,7 +875,7 @@ export function BlackFridayDashboard() {
if (!currentYearData) return null;
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) => {
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 = () => (
<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 */}
<Card className="border-muted">
<CardHeader className="pb-2 pt-3 px-4">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<DollarSign className="h-4 w-4 text-muted-foreground" />
Revenue by Day
Revenue
</CardTitle>
</CardHeader>
<CardContent className="p-2 pt-0">
<div className="h-[180px] w-full">
<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))" />
@@ -1064,16 +1019,16 @@ export function BlackFridayDashboard() {
</CardContent>
</Card>
{/* Orders Chart */}
{/* Profit 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 by Day
<Wallet className="h-4 w-4 text-emerald-500" />
Profit
</CardTitle>
</CardHeader>
<CardContent className="p-2 pt-0">
<div className="h-[180px] w-full">
<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))" />
@@ -1085,11 +1040,11 @@ export function BlackFridayDashboard() {
dy={5}
/>
<YAxis
tickFormatter={(value) => 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}
/>
<RechartsTooltip content={<CustomTooltip />} />
{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 }}
/>
))}
</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)}
strokeWidth={year === currentYear ? 2.5 : 1.5}
strokeOpacity={year === currentYear ? 1 : 0.6}
@@ -1116,11 +1118,11 @@ export function BlackFridayDashboard() {
<CardHeader className="pb-2 pt-3 px-4">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Percent className="h-4 w-4 text-muted-foreground" />
Margin by Day
Margin
</CardTitle>
</CardHeader>
<CardContent className="p-2 pt-0">
<div className="h-[180px] w-full">
<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))" />
@@ -1158,16 +1160,63 @@ export function BlackFridayDashboard() {
</CardContent>
</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 */}
<Card className="border-muted">
<CardHeader className="pb-2 pt-3 px-4">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<Zap className="h-4 w-4 text-muted-foreground" />
AOV by Day
AOV
</CardTitle>
</CardHeader>
<CardContent className="p-2 pt-0">
<div className="h-[180px] w-full">
<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))" />
@@ -1365,7 +1414,7 @@ export function BlackFridayDashboard() {
{/* Charts with legend */}
<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>
{renderLegend()}
</div>