Fix and restyle salesmetrics component
This commit is contained in:
@@ -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
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user