Fix and restyle salesmetrics component

This commit is contained in:
2025-01-18 01:12:44 -05:00
parent 9003300d0d
commit b5a354a1de
4 changed files with 52 additions and 129 deletions

View File

@@ -640,18 +640,8 @@ router.get('/best-sellers', async (req, res) => {
router.get('/sales/metrics', async (req, res) => { router.get('/sales/metrics', async (req, res) => {
const { startDate, endDate } = req.query; const { startDate, endDate } = req.query;
try { try {
const [dailyData] = await executeQuery(` // Get daily sales data
SELECT JSON_ARRAYAGG( const [dailyRows] = await executeQuery(`
JSON_OBJECT(
'date', sale_date,
'orders', COALESCE(total_orders, 0),
'units', COALESCE(total_units, 0),
'revenue', COALESCE(total_revenue, 0),
'cogs', COALESCE(total_cogs, 0),
'profit', COALESCE(total_profit, 0)
)
) as daily_data
FROM (
SELECT SELECT
DATE(o.date) as sale_date, DATE(o.date) as sale_date,
COUNT(DISTINCT o.order_number) as total_orders, COUNT(DISTINCT o.order_number) as total_orders,
@@ -664,71 +654,33 @@ router.get('/sales/metrics', async (req, res) => {
WHERE o.canceled = false WHERE o.canceled = false
AND o.date BETWEEN ? AND ? AND o.date BETWEEN ? AND ?
GROUP BY DATE(o.date) GROUP BY DATE(o.date)
) d ORDER BY sale_date
`, [startDate, endDate]);
const [categoryData] = await executeQuery(`
SELECT JSON_ARRAYAGG(
JSON_OBJECT(
'category', category_name,
'orders', COALESCE(category_orders, 0),
'units', COALESCE(category_units, 0),
'revenue', COALESCE(category_revenue, 0)
)
) as category_data
FROM (
SELECT
c.name as category_name,
COUNT(DISTINCT o.order_number) as category_orders,
SUM(o.quantity) as category_units,
SUM(o.price * o.quantity) as category_revenue
FROM orders o
JOIN products p ON o.product_id = p.product_id
JOIN product_categories pc ON p.product_id = pc.product_id
JOIN categories c ON pc.category_id = c.id
WHERE o.canceled = false
AND o.date BETWEEN ? AND ?
GROUP BY c.id, c.name
) c
`, [startDate, endDate]); `, [startDate, endDate]);
// Get summary metrics
const [metrics] = await executeQuery(` const [metrics] = await executeQuery(`
SELECT SELECT
COALESCE(COUNT(DISTINCT DATE(o.date)), 0) as days_with_sales, COUNT(DISTINCT o.order_number) as total_orders,
COALESCE(COUNT(DISTINCT o.order_number), 0) as total_orders, SUM(o.quantity) as total_units,
COALESCE(SUM(o.quantity), 0) as total_units, SUM(o.price * o.quantity) as total_revenue,
COALESCE(SUM(o.price * o.quantity), 0) as total_revenue, SUM(p.cost_price * o.quantity) as total_cogs,
COALESCE(SUM(p.cost_price * o.quantity), 0) as total_cogs, SUM((o.price - p.cost_price) * o.quantity) as total_profit
COALESCE(SUM((o.price - p.cost_price) * o.quantity), 0) as total_profit,
COALESCE(AVG(daily.orders), 0) as avg_daily_orders,
COALESCE(AVG(daily.units), 0) as avg_daily_units,
COALESCE(AVG(daily.revenue), 0) as avg_daily_revenue
FROM orders o FROM orders o
JOIN products p ON o.product_id = p.product_id JOIN products p ON o.product_id = p.product_id
LEFT JOIN (
SELECT
DATE(date) as sale_date,
COUNT(DISTINCT order_number) as orders,
SUM(quantity) as units,
SUM(price * quantity) as revenue
FROM orders
WHERE canceled = false
GROUP BY DATE(date)
) daily ON DATE(o.date) = daily.sale_date
WHERE o.canceled = false WHERE o.canceled = false
AND o.date BETWEEN ? AND ? AND o.date BETWEEN ? AND ?
`, [startDate, endDate]); `, [startDate, endDate]);
const response = { const response = {
totalOrders: parseInt(metrics.total_orders) || 0, totalOrders: parseInt(metrics[0]?.total_orders) || 0,
totalUnitsSold: parseInt(metrics.total_units) || 0, totalUnitsSold: parseInt(metrics[0]?.total_units) || 0,
totalRevenue: parseFloat(metrics.total_revenue) || 0, totalCogs: parseFloat(metrics[0]?.total_cogs) || 0,
totalCogs: parseFloat(metrics.total_cogs) || 0, totalRevenue: parseFloat(metrics[0]?.total_revenue) || 0,
dailySales: JSON.parse(dailyData.daily_data || '[]').map(day => ({ dailySales: dailyRows.map(day => ({
date: day.date, date: day.sale_date,
units: parseInt(day.units) || 0, units: parseInt(day.total_units) || 0,
revenue: parseFloat(day.revenue) || 0, revenue: parseFloat(day.total_revenue) || 0,
cogs: parseFloat(day.cogs) || 0 cogs: parseFloat(day.total_cogs) || 0
})) }))
}; };

View File

@@ -66,7 +66,7 @@ export function BestSellers() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<TabsContent value="products"> <TabsContent value="products">
<ScrollArea className="h-[400px] w-full"> <ScrollArea className="h-[385px] w-full">
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>

View File

@@ -115,8 +115,8 @@ export function ForecastMetrics() {
type="monotone" type="monotone"
dataKey="revenue" dataKey="revenue"
name="Revenue" name="Revenue"
stroke="#00C49F" stroke="#8884D8"
fill="#00C49F" fill="#8884D8"
fillOpacity={0.2} fillOpacity={0.2}
/> />
</AreaChart> </AreaChart>

View File

@@ -6,7 +6,7 @@ import config from "@/config"
import { formatCurrency } from "@/lib/utils" import { formatCurrency } from "@/lib/utils"
import { ClipboardList, Package, DollarSign, ShoppingCart } from "lucide-react" import { ClipboardList, Package, DollarSign, ShoppingCart } from "lucide-react"
import { DateRange } from "react-day-picker" import { DateRange } from "react-day-picker"
import { addDays } from "date-fns" import { addDays, format } from "date-fns"
import { DateRangePicker } from "@/components/ui/date-range-picker-narrow" import { DateRangePicker } from "@/components/ui/date-range-picker-narrow"
interface SalesData { interface SalesData {
@@ -45,7 +45,7 @@ export function SalesMetrics() {
return ( return (
<> <>
<CardHeader className="flex flex-row items-center justify-between pr-4"> <CardHeader className="flex flex-row items-center justify-between pr-5">
<CardTitle className="text-xl font-medium">Sales Overview</CardTitle> <CardTitle className="text-xl font-medium">Sales Overview</CardTitle>
<div className="w-[230px]"> <div className="w-[230px]">
<DateRangePicker <DateRangePicker
@@ -57,80 +57,60 @@ export function SalesMetrics() {
/> />
</div> </div>
</CardHeader> </CardHeader>
<CardContent> <CardContent className="py-0 -mb-2">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ClipboardList className="h-4 w-4 text-muted-foreground" /> <ClipboardList className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Total Orders</p> <p className="text-sm font-medium text-muted-foreground">Total Orders</p>
</div> </div>
<p className="text-2xl font-bold">{data?.totalOrders.toLocaleString() || 0}</p> <p className="text-lg font-bold">{data?.totalOrders.toLocaleString() || 0}</p>
</div> </div>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Package className="h-4 w-4 text-muted-foreground" /> <Package className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Units Sold</p> <p className="text-sm font-medium text-muted-foreground">Units Sold</p>
</div> </div>
<p className="text-2xl font-bold">{data?.totalUnitsSold.toLocaleString() || 0}</p> <p className="text-lg font-bold">{data?.totalUnitsSold.toLocaleString() || 0}</p>
</div> </div>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<DollarSign className="h-4 w-4 text-muted-foreground" /> <DollarSign className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Cost of Goods</p> <p className="text-sm font-medium text-muted-foreground">Cost of Goods</p>
</div> </div>
<p className="text-2xl font-bold">{formatCurrency(data?.totalCogs || 0)}</p> <p className="text-lg font-bold">{formatCurrency(data?.totalCogs || 0)}</p>
</div> </div>
<div className="flex items-baseline justify-between"> <div className="flex items-baseline justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ShoppingCart className="h-4 w-4 text-muted-foreground" /> <ShoppingCart className="h-4 w-4 text-muted-foreground" />
<p className="text-sm font-medium text-muted-foreground">Revenue</p> <p className="text-sm font-medium text-muted-foreground">Revenue</p>
</div> </div>
<p className="text-2xl font-bold">{formatCurrency(data?.totalRevenue || 0)}</p> <p className="text-lg font-bold">{formatCurrency(data?.totalRevenue || 0)}</p>
</div> </div>
</div> </div>
<div className="h-[300px] w-full"> <div className="h-[250px] w-full">
<ResponsiveContainer width="100%" height="100%"> <ResponsiveContainer width="100%" height="100%">
<AreaChart data={data?.dailySales || []}> <AreaChart
data={data?.dailySales || []}
margin={{ top: 30, right: 0, left: -60, bottom: 0 }}
>
<XAxis <XAxis
dataKey="date" dataKey="date"
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
fontSize={12} tick={false}
/> />
<YAxis <YAxis
yAxisId="left"
tickLine={false} tickLine={false}
axisLine={false} axisLine={false}
fontSize={12} tick={false}
tickFormatter={(value) => value.toLocaleString()}
/>
<YAxis
yAxisId="right"
orientation="right"
tickLine={false}
axisLine={false}
fontSize={12}
tickFormatter={(value) => formatCurrency(value)}
/> />
<Tooltip <Tooltip
formatter={(value: number, name: string) => [ formatter={(value: number) => [formatCurrency(value), "Revenue"]}
name === "units" ? value.toLocaleString() : formatCurrency(value), labelFormatter={(date) => format(new Date(date), 'MMM d, yyyy')}
name === "units" ? "Units" : name === "revenue" ? "Revenue" : "COGS"
]}
labelFormatter={(label) => `Date: ${label}`}
/> />
<Area <Area
yAxisId="left"
type="monotone"
dataKey="units"
name="Units"
stroke="#0088FE"
fill="#0088FE"
fillOpacity={0.2}
/>
<Area
yAxisId="right"
type="monotone" type="monotone"
dataKey="revenue" dataKey="revenue"
name="Revenue" name="Revenue"
@@ -138,15 +118,6 @@ export function SalesMetrics() {
fill="#00C49F" fill="#00C49F"
fillOpacity={0.2} fillOpacity={0.2}
/> />
<Area
yAxisId="right"
type="monotone"
dataKey="cogs"
name="COGS"
stroke="#FF8042"
fill="#FF8042"
fillOpacity={0.2}
/>
</AreaChart> </AreaChart>
</ResponsiveContainer> </ResponsiveContainer>
</div> </div>