feat: add dynamic date filtering for analysis dashboard metrics and charts
This commit is contained in:
parent
e74eff77f2
commit
2098570a23
@ -10,23 +10,41 @@
|
|||||||
use App\Models\OrderItem;
|
use App\Models\OrderItem;
|
||||||
use App\Models\Payroll;
|
use App\Models\Payroll;
|
||||||
use App\Models\Purchase;
|
use App\Models\Purchase;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
|
||||||
class AnalysisController extends Controller
|
class AnalysisController extends Controller
|
||||||
{
|
{
|
||||||
public function __invoke()
|
public function __invoke(Request $request)
|
||||||
{
|
{
|
||||||
|
$period = $request->input('period', 'all');
|
||||||
|
$startDate = $request->input('start_date');
|
||||||
|
$endDate = $request->input('end_date');
|
||||||
|
|
||||||
|
$applyFilter = function ($query, $column = 'created_at') use ($period, $startDate, $endDate) {
|
||||||
|
if ($startDate && $endDate) {
|
||||||
|
return $query->whereBetween($column, [$startDate.' 00:00:00', $endDate.' 23:59:59']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($period) {
|
||||||
|
'week' => $query->whereBetween($column, [now()->startOfWeek(), now()->endOfWeek()]),
|
||||||
|
'month' => $query->whereMonth($column, now()->month)->whereYear($column, now()->year),
|
||||||
|
'year' => $query->whereYear($column, now()->year),
|
||||||
|
default => $query,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
$stats = [
|
$stats = [
|
||||||
'total_sales' => $this->getStats(fn () => Order::count()),
|
'total_sales' => $this->getStats(fn () => $applyFilter(Order::query())->count()),
|
||||||
'total_revenue' => $this->getStats(fn () => Order::sum('total')),
|
'total_revenue' => $this->getStats(fn () => $applyFilter(Order::query())->sum('total')),
|
||||||
'total_expenses' => $this->getStats(fn () => Expense::sum('amount')),
|
'total_expenses' => $this->getStats(fn () => $applyFilter(Expense::query())->sum('amount')),
|
||||||
'total_payrolls' => $this->getStats(fn () => Payroll::sum('total_salary')),
|
'total_payrolls' => $this->getStats(fn () => $applyFilter(Payroll::query(), 'period_month')->sum('total_salary')),
|
||||||
'cogs' => $this->getStats(fn () => Order::sum('hpp')),
|
'cogs' => $this->getStats(fn () => $applyFilter(Order::query())->sum('hpp')),
|
||||||
'total_discount' => $this->getStats(fn () => Order::sum('discount')),
|
'total_discount' => $this->getStats(fn () => $applyFilter(Order::query())->sum('discount')),
|
||||||
'total_purchases' => $this->getStats(fn () => Purchase::sum('total')),
|
'total_purchases' => $this->getStats(fn () => $applyFilter(Purchase::query())->sum('total')),
|
||||||
'products_sold' => $this->getStats(fn () => OrderItem::sum('qty')),
|
'products_sold' => $this->getStats(fn () => $applyFilter(OrderItem::query())->sum('qty')),
|
||||||
'total_customers' => $this->getStats(fn () => Order::distinct('customer_name')->count('customer_name')),
|
'total_customers' => $this->getStats(fn () => $applyFilter(Order::query())->distinct('customer_name')->count('customer_name')),
|
||||||
];
|
];
|
||||||
|
|
||||||
$stats['aov'] = $this->calculateAov($stats['total_revenue'], $stats['total_sales']);
|
$stats['aov'] = $this->calculateAov($stats['total_revenue'], $stats['total_sales']);
|
||||||
@ -38,8 +56,8 @@ public function __invoke()
|
|||||||
];
|
];
|
||||||
$stats['profit_margin'] = $this->calculateMargin($stats['net_profit'], $stats['total_revenue']);
|
$stats['profit_margin'] = $this->calculateMargin($stats['net_profit'], $stats['total_revenue']);
|
||||||
|
|
||||||
// Revenue vs Purchases per month (this year)
|
// Revenue vs Purchases per month
|
||||||
$revenueByMonth = DB::table('orders')
|
$revenueByMonth = $applyFilter(DB::table('orders'))
|
||||||
->select(
|
->select(
|
||||||
DB::raw('MONTH(created_at) as month'),
|
DB::raw('MONTH(created_at) as month'),
|
||||||
DB::raw('SUM(total) as total')
|
DB::raw('SUM(total) as total')
|
||||||
@ -47,7 +65,7 @@ public function __invoke()
|
|||||||
->groupBy(DB::raw('MONTH(created_at)'))
|
->groupBy(DB::raw('MONTH(created_at)'))
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$purchasesByMonth = DB::table('purchases')
|
$purchasesByMonth = $applyFilter(DB::table('purchases'))
|
||||||
->select(
|
->select(
|
||||||
DB::raw('MONTH(created_at) as month'),
|
DB::raw('MONTH(created_at) as month'),
|
||||||
DB::raw('SUM(total) as total')
|
DB::raw('SUM(total) as total')
|
||||||
@ -68,7 +86,7 @@ public function __invoke()
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$paymentMethods = DB::table('orders')
|
$paymentMethods = $applyFilter(DB::table('orders'))
|
||||||
->select('payment_method as name', DB::raw('COUNT(*) as total'))
|
->select('payment_method as name', DB::raw('COUNT(*) as total'))
|
||||||
->groupBy('payment_method')
|
->groupBy('payment_method')
|
||||||
->get()
|
->get()
|
||||||
@ -80,7 +98,7 @@ public function __invoke()
|
|||||||
return $item;
|
return $item;
|
||||||
});
|
});
|
||||||
|
|
||||||
$orderStatuses = DB::table('orders')
|
$orderStatuses = $applyFilter(DB::table('orders'))
|
||||||
->select('order_status as name', DB::raw('COUNT(*) as total'))
|
->select('order_status as name', DB::raw('COUNT(*) as total'))
|
||||||
->groupBy('order_status')
|
->groupBy('order_status')
|
||||||
->get()
|
->get()
|
||||||
@ -92,7 +110,7 @@ public function __invoke()
|
|||||||
return $item;
|
return $item;
|
||||||
});
|
});
|
||||||
|
|
||||||
$orderChannels = DB::table('orders')
|
$orderChannels = $applyFilter(DB::table('orders'))
|
||||||
->select('order_channel as name', DB::raw('COUNT(*) as total'))
|
->select('order_channel as name', DB::raw('COUNT(*) as total'))
|
||||||
->groupBy('order_channel')
|
->groupBy('order_channel')
|
||||||
->get()
|
->get()
|
||||||
@ -104,7 +122,7 @@ public function __invoke()
|
|||||||
return $item;
|
return $item;
|
||||||
});
|
});
|
||||||
|
|
||||||
$topProducts = DB::table('order_items')
|
$topProducts = $applyFilter(DB::table('order_items'), 'order_items.created_at')
|
||||||
->join('products', 'order_items.product_id', '=', 'products.id')
|
->join('products', 'order_items.product_id', '=', 'products.id')
|
||||||
->select('products.name as name', DB::raw('SUM(order_items.qty) as total'))
|
->select('products.name as name', DB::raw('SUM(order_items.qty) as total'))
|
||||||
->groupBy('products.name')
|
->groupBy('products.name')
|
||||||
@ -112,7 +130,7 @@ public function __invoke()
|
|||||||
->limit(5)
|
->limit(5)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$topCustomers = DB::table('orders')
|
$topCustomers = $applyFilter(DB::table('orders'), 'orders.created_at')
|
||||||
->select('customer_name as name', DB::raw('SUM(total) as total'))
|
->select('customer_name as name', DB::raw('SUM(total) as total'))
|
||||||
->whereNotNull('customer_name')
|
->whereNotNull('customer_name')
|
||||||
->groupBy('customer_name')
|
->groupBy('customer_name')
|
||||||
@ -120,7 +138,7 @@ public function __invoke()
|
|||||||
->limit(5)
|
->limit(5)
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$salesByHourRaw = DB::table('orders')
|
$salesByHourRaw = $applyFilter(DB::table('orders'))
|
||||||
->select(
|
->select(
|
||||||
DB::raw('HOUR(created_at) as hour'),
|
DB::raw('HOUR(created_at) as hour'),
|
||||||
DB::raw('COUNT(*) as total')
|
DB::raw('COUNT(*) as total')
|
||||||
@ -138,7 +156,7 @@ public function __invoke()
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Revenue & Profit & Volume per month
|
// Revenue & Profit & Volume per month
|
||||||
$ordersByMonth = DB::table('orders')
|
$ordersByMonth = $applyFilter(DB::table('orders'))
|
||||||
->select(
|
->select(
|
||||||
DB::raw('MONTH(created_at) as month'),
|
DB::raw('MONTH(created_at) as month'),
|
||||||
DB::raw('SUM(total) as revenue'),
|
DB::raw('SUM(total) as revenue'),
|
||||||
@ -148,7 +166,7 @@ public function __invoke()
|
|||||||
->groupBy(DB::raw('MONTH(created_at)'))
|
->groupBy(DB::raw('MONTH(created_at)'))
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$expensesByMonth = DB::table('expenses')
|
$expensesByMonth = $applyFilter(DB::table('expenses'))
|
||||||
->select(
|
->select(
|
||||||
DB::raw('MONTH(created_at) as month'),
|
DB::raw('MONTH(created_at) as month'),
|
||||||
DB::raw('SUM(amount) as total')
|
DB::raw('SUM(amount) as total')
|
||||||
@ -156,7 +174,7 @@ public function __invoke()
|
|||||||
->groupBy(DB::raw('MONTH(created_at)'))
|
->groupBy(DB::raw('MONTH(created_at)'))
|
||||||
->get();
|
->get();
|
||||||
|
|
||||||
$payrollsByMonth = DB::table('payrolls')
|
$payrollsByMonth = $applyFilter(DB::table('payrolls'), 'period_month')
|
||||||
->select(
|
->select(
|
||||||
DB::raw('MONTH(period_month) as month'),
|
DB::raw('MONTH(period_month) as month'),
|
||||||
DB::raw('SUM(total_salary) as total')
|
DB::raw('SUM(total_salary) as total')
|
||||||
@ -196,7 +214,7 @@ public function __invoke()
|
|||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$topCategories = DB::table('order_items')
|
$topCategories = $applyFilter(DB::table('order_items'), 'order_items.created_at')
|
||||||
->join('products', 'order_items.product_id', '=', 'products.id')
|
->join('products', 'order_items.product_id', '=', 'products.id')
|
||||||
->join('category_product', 'products.id', '=', 'category_product.product_id')
|
->join('category_product', 'products.id', '=', 'category_product.product_id')
|
||||||
->join('categories', 'category_product.category_id', '=', 'categories.id')
|
->join('categories', 'category_product.category_id', '=', 'categories.id')
|
||||||
@ -219,6 +237,11 @@ public function __invoke()
|
|||||||
'topProducts' => $topProducts,
|
'topProducts' => $topProducts,
|
||||||
'topCustomers' => $topCustomers,
|
'topCustomers' => $topCustomers,
|
||||||
'topCategories' => $topCategories,
|
'topCategories' => $topCategories,
|
||||||
|
'filters' => [
|
||||||
|
'period' => $period,
|
||||||
|
'start_date' => $startDate,
|
||||||
|
'end_date' => $endDate,
|
||||||
|
],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +1,193 @@
|
|||||||
import { StatCard } from '@/components/cards/stat-card';
|
import { StatCard } from '@/components/cards/stat-card';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Calendar } from '@/components/ui/calendar';
|
||||||
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { analysis } from '@/routes';
|
import { analysis } from '@/routes';
|
||||||
import type { AnalysisPageProps } from '@/types';
|
import type { AnalysisPageProps } from '@/types';
|
||||||
import { Head, usePage } from '@inertiajs/react';
|
import { Head, router, usePage } from '@inertiajs/react';
|
||||||
|
import { CalendarIcon, Filter, FilterX } from 'lucide-react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { AovTrendChart } from '../components/charts/aov-trend-chart';
|
||||||
import { CustomBarChart } from '../components/charts/bar-chart';
|
import { CustomBarChart } from '../components/charts/bar-chart';
|
||||||
import { CustomPieChart } from '../components/charts/pie-chart';
|
import { CustomPieChart } from '../components/charts/pie-chart';
|
||||||
import { RevenueVsProfitChart } from '../components/charts/revenue-vs-profit-chart';
|
import { RevenueVsProfitChart } from '../components/charts/revenue-vs-profit-chart';
|
||||||
import { TransactionVolumeChart } from '../components/charts/transaction-volume-chart';
|
|
||||||
import { AovTrendChart } from '../components/charts/aov-trend-chart';
|
|
||||||
import { RevenueVsPurchasesChart } from '../components/charts/revenue-vs-purchases-chart';
|
import { RevenueVsPurchasesChart } from '../components/charts/revenue-vs-purchases-chart';
|
||||||
import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
|
import { SalesByHourChart } from '../components/charts/sales-by-hour-chart';
|
||||||
|
import { TransactionVolumeChart } from '../components/charts/transaction-volume-chart';
|
||||||
|
|
||||||
export default function Analisa() {
|
export default function Analisa() {
|
||||||
const { stats, salesByMonth, revenueVsProfit, transactionVolume, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers, topCategories } =
|
const { stats, salesByMonth, revenueVsProfit, transactionVolume, salesByHour, paymentMethods, orderStatuses, orderChannels, topProducts, topCustomers, topCategories, filters } =
|
||||||
usePage<AnalysisPageProps>().props;
|
usePage<AnalysisPageProps>().props;
|
||||||
|
|
||||||
|
const [period, setPeriod] = useState(filters.period || 'all');
|
||||||
|
const [startDate, setStartDate] = useState(filters.start_date || '');
|
||||||
|
const [endDate, setEndDate] = useState(filters.end_date || '');
|
||||||
|
const [isStartOpen, setIsStartOpen] = useState(false);
|
||||||
|
const [isEndOpen, setIsEndOpen] = useState(false);
|
||||||
|
|
||||||
|
const formatDate = (dateStr: string) => {
|
||||||
|
if (!dateStr) return null;
|
||||||
|
return new Intl.DateTimeFormat("id-ID", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "long",
|
||||||
|
year: "numeric",
|
||||||
|
}).format(new Date(dateStr));
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatToYmd = (date: Date) => {
|
||||||
|
return date.getFullYear() + "-" + String(date.getMonth() + 1).padStart(2, '0') + "-" + String(date.getDate()).padStart(2, '0');
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyFilters = (newParams: any) => {
|
||||||
|
router.get(analysis().url, {
|
||||||
|
...filters,
|
||||||
|
...newParams
|
||||||
|
}, {
|
||||||
|
preserveState: true,
|
||||||
|
preserveScroll: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePeriodChange = (value: string) => {
|
||||||
|
setPeriod(value);
|
||||||
|
setStartDate('');
|
||||||
|
setEndDate('');
|
||||||
|
applyFilters({ period: value, start_date: '', end_date: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCustomFilter = () => {
|
||||||
|
if (startDate && endDate) {
|
||||||
|
setPeriod('custom');
|
||||||
|
applyFilters({ period: 'custom', start_date: startDate, end_date: endDate });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
setPeriod('all');
|
||||||
|
setStartDate('');
|
||||||
|
setEndDate('');
|
||||||
|
applyFilters({ period: 'all', start_date: '', end_date: '' });
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFilterDescription = (baseDesc: string) => {
|
||||||
|
if (filters.start_date && filters.end_date) {
|
||||||
|
return `${baseDesc} (Periode: ${filters.start_date} s/d ${filters.end_date})`;
|
||||||
|
}
|
||||||
|
switch (filters.period) {
|
||||||
|
case 'week': return `${baseDesc} (Pekan Ini)`;
|
||||||
|
case 'month': return `${baseDesc} (Bulan Ini)`;
|
||||||
|
case 'year': return `${baseDesc} (Tahun Ini)`;
|
||||||
|
default: return `${baseDesc} (Semua Waktu)`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title="Analisa" />
|
<Head title="Analisa" />
|
||||||
|
|
||||||
<div className="flex h-full min-h-screen flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-6 @container/main">
|
<div className="flex h-full min-h-screen flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-6 @container/main">
|
||||||
|
|
||||||
<div>
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<h1 className="text-3xl font-bold tracking-tight">Analisa</h1>
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold tracking-tight">Analisa</h1>
|
||||||
|
<p className="text-muted-foreground text-sm">Lihat performa bisnis Anda secara mendalam.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<Select value={period} onValueChange={handlePeriodChange}>
|
||||||
|
<SelectTrigger className="w-[140px] bg-card">
|
||||||
|
<SelectValue placeholder="Pilih Periode" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="all">Semua Waktu</SelectItem>
|
||||||
|
<SelectItem value="week">Pekan Ini</SelectItem>
|
||||||
|
<SelectItem value="month">Bulan Ini</SelectItem>
|
||||||
|
<SelectItem value="year">Tahun Ini</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 rounded-md border bg-card p-1">
|
||||||
|
<Popover open={isStartOpen} onOpenChange={setIsStartOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-[140px] justify-start px-2 font-normal"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-3 w-3 text-muted-foreground" />
|
||||||
|
<span className="truncate">
|
||||||
|
{startDate ? formatDate(startDate) : "Mulai"}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={startDate ? new Date(startDate) : undefined}
|
||||||
|
onSelect={(date) => {
|
||||||
|
setStartDate(date ? formatToYmd(date) : '');
|
||||||
|
setIsStartOpen(false);
|
||||||
|
}}
|
||||||
|
captionLayout="dropdown"
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<span className="text-muted-foreground text-xs">s/d</span>
|
||||||
|
|
||||||
|
<Popover open={isEndOpen} onOpenChange={setIsEndOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 w-[140px] justify-start px-2 font-normal"
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-3 w-3 text-muted-foreground" />
|
||||||
|
<span className="truncate">
|
||||||
|
{endDate ? formatDate(endDate) : "Selesai"}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={endDate ? new Date(endDate) : undefined}
|
||||||
|
onSelect={(date) => {
|
||||||
|
setEndDate(date ? formatToYmd(date) : '');
|
||||||
|
setIsEndOpen(false);
|
||||||
|
}}
|
||||||
|
captionLayout="dropdown"
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="default"
|
||||||
|
className="h-8 px-3"
|
||||||
|
onClick={handleCustomFilter}
|
||||||
|
disabled={!startDate || !endDate}
|
||||||
|
>
|
||||||
|
<Filter className="mr-2 h-4 w-4" />
|
||||||
|
Terapkan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(period !== 'all' || (startDate && endDate)) && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={clearFilters}
|
||||||
|
className="h-9"
|
||||||
|
>
|
||||||
|
<FilterX className="mr-2 h-4 w-4" />
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats Grid */}
|
{/* Stats Grid */}
|
||||||
@ -56,36 +222,36 @@ export default function Analisa() {
|
|||||||
|
|
||||||
{/* Main Trends Grid */}
|
{/* Main Trends Grid */}
|
||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
<RevenueVsProfitChart
|
<RevenueVsProfitChart
|
||||||
data={revenueVsProfit}
|
data={revenueVsProfit}
|
||||||
title="Pendapatan vs Laba"
|
title="Pendapatan vs Laba"
|
||||||
description="Analisis pendapatan kotor dibandingkan dengan laba bersih."
|
description={getFilterDescription("Analisis pendapatan kotor dibandingkan dengan laba bersih.")}
|
||||||
/>
|
/>
|
||||||
<RevenueVsPurchasesChart
|
<RevenueVsPurchasesChart
|
||||||
data={salesByMonth}
|
data={salesByMonth}
|
||||||
title="Penjualan vs Belanja"
|
title="Penjualan vs Belanja"
|
||||||
description="Perbandingan total nilai penjualan dan belanja stok barang."
|
description={getFilterDescription("Perbandingan total nilai penjualan dan belanja stok barang.")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Activity & Volume Grid */}
|
{/* Activity & Volume Grid */}
|
||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
<TransactionVolumeChart
|
<TransactionVolumeChart
|
||||||
data={transactionVolume}
|
data={transactionVolume}
|
||||||
title="Volume Transaksi"
|
title="Volume Transaksi"
|
||||||
description="Total jumlah pesanan yang diproses per bulan."
|
description={getFilterDescription("Total jumlah pesanan yang diproses per bulan.")}
|
||||||
/>
|
/>
|
||||||
<AovTrendChart
|
<AovTrendChart
|
||||||
data={transactionVolume}
|
data={transactionVolume}
|
||||||
title="Tren AOV"
|
title="Tren AOV"
|
||||||
description="Rata-rata nilai belanja per pesanan pelanggan setiap bulannya."
|
description={getFilterDescription("Rata-rata nilai belanja per pesanan pelanggan setiap bulannya.")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SalesByHourChart
|
<SalesByHourChart
|
||||||
data={salesByHour}
|
data={salesByHour}
|
||||||
title="Jam Sibuk"
|
title="Jam Sibuk"
|
||||||
description="Grafik aktivitas transaksi berdasarkan waktu (jam) dalam sehari."
|
description={getFilterDescription("Grafik aktivitas transaksi berdasarkan waktu (jam) dalam sehari.")}
|
||||||
config={{
|
config={{
|
||||||
total: {
|
total: {
|
||||||
label: "Total Penjualan",
|
label: "Total Penjualan",
|
||||||
@ -97,25 +263,25 @@ export default function Analisa() {
|
|||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 @4xl/main:grid-cols-4">
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 @4xl/main:grid-cols-4">
|
||||||
<CustomPieChart
|
<CustomPieChart
|
||||||
title="Metode Pembayaran"
|
title="Metode Pembayaran"
|
||||||
description="Distribusi transaksi berdasarkan metode pembayaran yang digunakan."
|
description={getFilterDescription("Distribusi transaksi berdasarkan metode pembayaran yang digunakan.")}
|
||||||
data={paymentMethods}
|
data={paymentMethods}
|
||||||
colorOffset={0}
|
colorOffset={0}
|
||||||
/>
|
/>
|
||||||
<CustomPieChart
|
<CustomPieChart
|
||||||
title="Status Pesanan"
|
title="Status Pesanan"
|
||||||
description="Status terkini dari seluruh pesanan yang masuk."
|
description={getFilterDescription("Status terkini dari seluruh pesanan yang masuk.")}
|
||||||
data={orderStatuses}
|
data={orderStatuses}
|
||||||
colorOffset={120}
|
colorOffset={120}
|
||||||
/>
|
/>
|
||||||
<CustomPieChart
|
<CustomPieChart
|
||||||
title="Channel Pesanan"
|
title="Channel Pesanan"
|
||||||
description="Sumber pesanan berdasarkan platform atau saluran penjualan."
|
description={getFilterDescription("Sumber pesanan berdasarkan platform atau saluran penjualan.")}
|
||||||
data={orderChannels}
|
data={orderChannels}
|
||||||
colorOffset={240}
|
colorOffset={240}
|
||||||
/>
|
/>
|
||||||
<CustomPieChart
|
<CustomPieChart
|
||||||
title="Top 5 Kategori"
|
title="Top 5 Kategori"
|
||||||
description="Kategori produk yang paling banyak menyumbang penjualan."
|
description={getFilterDescription("Kategori produk yang paling banyak menyumbang penjualan.")}
|
||||||
data={topCategories}
|
data={topCategories}
|
||||||
colorOffset={60}
|
colorOffset={60}
|
||||||
/>
|
/>
|
||||||
@ -124,13 +290,13 @@ export default function Analisa() {
|
|||||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||||
<CustomBarChart
|
<CustomBarChart
|
||||||
title="Top 5 Produk"
|
title="Top 5 Produk"
|
||||||
description="Daftar produk dengan volume penjualan tertinggi."
|
description={getFilterDescription("Daftar produk dengan volume penjualan tertinggi.")}
|
||||||
data={topProducts}
|
data={topProducts}
|
||||||
colorOffset={45}
|
colorOffset={45}
|
||||||
/>
|
/>
|
||||||
<CustomBarChart
|
<CustomBarChart
|
||||||
title="Top 5 Pelanggan"
|
title="Top 5 Pelanggan"
|
||||||
description="Pelanggan dengan total nominal belanja tertinggi (setia)."
|
description={getFilterDescription("Pelanggan dengan total nominal belanja tertinggi (setia).")}
|
||||||
data={topCustomers}
|
data={topCustomers}
|
||||||
colorOffset={180}
|
colorOffset={180}
|
||||||
isCurrency={true}
|
isCurrency={true}
|
||||||
|
|||||||
@ -41,5 +41,10 @@ export interface AnalysisPageProps {
|
|||||||
topProducts: any[];
|
topProducts: any[];
|
||||||
topCustomers: any[];
|
topCustomers: any[];
|
||||||
topCategories: any[];
|
topCategories: any[];
|
||||||
|
filters: {
|
||||||
|
period: string;
|
||||||
|
start_date: string | null;
|
||||||
|
end_date: string | null;
|
||||||
|
};
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user