Files
WooNooW/admin-spa/src/routes/Dashboard/Taxes.tsx
dwindown 0dace90597 refactor: Smart contextual headers - only when they add value
Implemented intelligent header rules based on user feedback.

Problem Analysis:
1. Dashboard submenu tabs already show page names (Overview, Revenue, Orders...)
2. Showing "Orders" header is ambiguous (Analytics or Management?)
3. Wasted vertical space for redundant information
4. FAB already handles actions on management pages

Solution: Headers ONLY When They Add Value

Rules Implemented:

1. Dashboard Pages: NO HEADERS
   - Submenu tabs are sufficient
   - Saves vertical space
   - No ambiguity

   Before:
   Dashboard → Overview  = "Dashboard" header (redundant!)
   Dashboard → Orders    = "Orders" header (confusing!)

   After:
   Dashboard → Overview  = No header (tabs show "Overview")
   Dashboard → Orders    = No header (tabs show "Orders")

2. Settings Pages: HEADERS ONLY WITH ACTIONS
   - Store Details + [Save] = Show header ✓
   - Payments + [Refresh]   = Show header ✓
   - Pages without actions  = No header (save space)

   Logic: If there is an action button, we need a place to put it → header
          If no action button, header is just wasting space → remove it

3. Management Pages: NO HEADERS
   - FAB handles actions ([+ Add Order])
   - No need for redundant header with action button

4. Payments Exception: REMOVED
   - Treat Payments like any other settings page
   - Has action (Refresh) = show header
   - Consistent with other pages

Implementation:

Dashboard Pages (7 files):
- Removed usePageHeader hook
- Removed useEffect for setting header
- Removed unused imports (useEffect, usePageHeader)
- Result: Clean, no headers, tabs are enough

PageHeader Component:
- Removed Payments special case detection
- Removed useLocation import
- Simplified logic: hideOnDesktop prop only

SettingsLayout Component:
- Changed logic: Only set header when onSave OR action exists
- If no action: clearPageHeader() instead of setPageHeader(title)
- Result: Headers only appear when needed

Benefits:

 Saves vertical space (no redundant headers)
 No ambiguity (Dashboard Orders vs Orders Management)
 Consistent logic (action = header, no action = no header)
 Cleaner UI (less visual clutter)
 FAB handles management page actions

Files Modified:
- Dashboard/index.tsx (Overview)
- Dashboard/Revenue.tsx
- Dashboard/Orders.tsx
- Dashboard/Products.tsx
- Dashboard/Customers.tsx
- Dashboard/Coupons.tsx
- Dashboard/Taxes.tsx
- PageHeader.tsx
- SettingsLayout.tsx

Result: Smart headers that only appear when they add value! 🎯
2025-11-06 23:11:59 +07:00

269 lines
9.0 KiB
TypeScript

import React, { useState, useMemo } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import { DollarSign, FileText, ShoppingCart, TrendingUp } from 'lucide-react';
import { __ } from '@/lib/i18n';
import { formatMoney, getStoreCurrency } from '@/lib/currency';
import { useDashboardPeriod } from '@/hooks/useDashboardPeriod';
import { useTaxesAnalytics } from '@/hooks/useAnalytics';
import { ErrorCard } from '@/components/ErrorCard';
import { getPageLoadErrorMessage } from '@/lib/errorHandling';
import { StatCard } from './components/StatCard';
import { ChartCard } from './components/ChartCard';
import { DataTable, Column } from './components/DataTable';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { DUMMY_TAXES_DATA, TaxesData, TaxByRate, TaxByLocation } from './data/dummyTaxes';
export default function TaxesReport() {
const { period } = useDashboardPeriod();
const store = getStoreCurrency();
// Fetch real data or use dummy data based on toggle
const { data, isLoading, error, refetch } = useTaxesAnalytics(DUMMY_TAXES_DATA);
const chartData = useMemo(() => {
return period === 'all' ? data.chart_data : data.chart_data.slice(-parseInt(period));
}, [data.chart_data, period]);
// Calculate period metrics
const periodMetrics = useMemo(() => {
if (period === 'all') {
const totalTax = data.chart_data.reduce((sum: number, d: any) => sum + d.tax, 0);
const totalOrders = data.chart_data.reduce((sum: number, d: any) => sum + d.orders, 0);
return {
total_tax: totalTax,
avg_tax_per_order: totalOrders > 0 ? totalTax / totalOrders : 0,
orders_with_tax: totalOrders,
change_percent: undefined,
};
}
const periodData = data.chart_data.slice(-parseInt(period));
const previousData = data.chart_data.slice(-parseInt(period) * 2, -parseInt(period));
const totalTax = periodData.reduce((sum: number, d: any) => sum + d.tax, 0);
const totalOrders = periodData.reduce((sum: number, d: any) => sum + d.orders, 0);
const prevTotalTax = previousData.reduce((sum: number, d: any) => sum + d.tax, 0);
const prevTotalOrders = previousData.reduce((sum: number, d: any) => sum + d.orders, 0);
const avgTaxPerOrder = totalOrders > 0 ? totalTax / totalOrders : 0;
const prevAvgTaxPerOrder = prevTotalOrders > 0 ? prevTotalTax / prevTotalOrders : 0;
return {
total_tax: totalTax,
avg_tax_per_order: avgTaxPerOrder,
orders_with_tax: totalOrders,
change_percent: prevTotalTax > 0 ? ((totalTax - prevTotalTax) / prevTotalTax) * 100 : 0,
avg_tax_per_order_change: prevAvgTaxPerOrder > 0 ? ((avgTaxPerOrder - prevAvgTaxPerOrder) / prevAvgTaxPerOrder) * 100 : 0,
orders_with_tax_change: prevTotalOrders > 0 ? ((totalOrders - prevTotalOrders) / prevTotalOrders) * 100 : 0,
};
}, [data.chart_data, period]);
// Filter table data by period
const filteredByRate = useMemo(() => {
const factor = period === 'all' ? 1 : parseInt(period) / 30;
return data.by_rate.map((r: any) => ({
...r,
orders: Math.round(r.orders * factor),
tax_amount: Math.round(r.tax_amount * factor),
}));
}, [data.by_rate, period]);
const filteredByLocation = useMemo(() => {
const factor = period === 'all' ? 1 : parseInt(period) / 30;
return data.by_location.map((l: any) => ({
...l,
orders: Math.round(l.orders * factor),
tax_amount: Math.round(l.tax_amount * factor),
}));
}, [data.by_location, period]);
// Show loading state
if (isLoading) {
return (
<div className="flex items-center justify-center h-96">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-muted-foreground">{__('Loading analytics...')}</p>
</div>
</div>
);
}
// Show error state
if (error) {
return (
<ErrorCard
title={__('Failed to load taxes analytics')}
message={getPageLoadErrorMessage(error)}
onRetry={() => refetch()}
/>
);
}
const formatCurrency = (value: number) => {
return formatMoney(value, {
currency: store.currency,
symbol: store.symbol,
thousandSep: store.thousand_sep,
decimalSep: store.decimal_sep,
decimals: 0,
preferSymbol: true,
});
};
const rateColumns: Column<TaxByRate>[] = [
{ key: 'rate', label: __('Tax Rate'), sortable: true },
{
key: 'percentage',
label: __('Rate %'),
sortable: true,
align: 'right',
render: (value) => `${value.toFixed(1)}%`,
},
{
key: 'orders',
label: __('Orders'),
sortable: true,
align: 'right',
},
{
key: 'tax_amount',
label: __('Tax Collected'),
sortable: true,
align: 'right',
render: (value) => formatCurrency(value),
},
];
const locationColumns: Column<TaxByLocation>[] = [
{ key: 'state_name', label: __('Location'), sortable: true },
{
key: 'orders',
label: __('Orders'),
sortable: true,
align: 'right',
},
{
key: 'tax_amount',
label: __('Tax Collected'),
sortable: true,
align: 'right',
render: (value) => formatCurrency(value),
},
{
key: 'percentage',
label: __('% of Total'),
sortable: true,
align: 'right',
render: (value) => `${value.toFixed(1)}%`,
},
];
return (
<div className="space-y-6">
<div className="mb-6">
<h1 className="text-2xl font-bold">{__('Taxes Report')}</h1>
<p className="text-sm text-muted-foreground">{__('Tax collection and breakdowns')}</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<StatCard
title={__('Total Tax Collected')}
value={periodMetrics.total_tax}
change={periodMetrics.change_percent}
icon={DollarSign}
format="money"
period={period}
/>
<StatCard
title={__('Avg Tax per Order')}
value={periodMetrics.avg_tax_per_order}
change={periodMetrics.avg_tax_per_order_change}
icon={TrendingUp}
format="money"
period={period}
/>
<StatCard
title={__('Orders with Tax')}
value={periodMetrics.orders_with_tax}
change={periodMetrics.orders_with_tax_change}
icon={ShoppingCart}
format="number"
period={period}
/>
</div>
<ChartCard
title={__('Tax Collection Over Time')}
description={__('Daily tax collection and order count')}
>
<ResponsiveContainer width="100%" height={300}>
<LineChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" className="stroke-muted" />
<XAxis
dataKey="date"
className="text-xs"
tickFormatter={(value) => {
const date = new Date(value);
return `${date.getMonth() + 1}/${date.getDate()}`;
}}
/>
<YAxis className="text-xs" />
<Tooltip
content={({ active, payload }) => {
if (!active || !payload || !payload.length) return null;
return (
<div className="rounded-lg border bg-background p-3 shadow-lg">
<p className="text-sm font-medium mb-2">
{new Date(payload[0].payload.date).toLocaleDateString()}
</p>
<div className="flex items-center justify-between gap-4 text-sm">
<span>{__('Tax')}:</span>
<span className="font-medium">{formatCurrency(payload[0].payload.tax)}</span>
</div>
<div className="flex items-center justify-between gap-4 text-sm">
<span>{__('Orders')}:</span>
<span className="font-medium">{payload[0].payload.orders}</span>
</div>
</div>
);
}}
/>
<Line
type="monotone"
dataKey="tax"
name={__('Tax Collected')}
stroke="#3b82f6"
strokeWidth={2}
/>
</LineChart>
</ResponsiveContainer>
</ChartCard>
<div className="grid gap-6 md:grid-cols-2">
<ChartCard
title={__('Tax by Rate')}
description={__('Breakdown by tax rate')}
>
<DataTable
data={filteredByRate}
columns={rateColumns}
/>
</ChartCard>
<ChartCard
title={__('Tax by Location')}
description={__('Breakdown by state/province')}
>
<DataTable
data={filteredByLocation}
columns={locationColumns}
/>
</ChartCard>
</div>
</div>
);
}