From 2098570a239ddd772572ca7204a776f061dfbb07 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 23 Apr 2026 21:03:22 +0700 Subject: [PATCH] feat: add dynamic date filtering for analysis dashboard metrics and charts --- app/Http/Controllers/AnalysisController.php | 69 ++++--- resources/js/pages/analysis.tsx | 216 +++++++++++++++++--- resources/js/types/dashboard.ts | 5 + 3 files changed, 242 insertions(+), 48 deletions(-) diff --git a/app/Http/Controllers/AnalysisController.php b/app/Http/Controllers/AnalysisController.php index 082ba05..7f31d93 100644 --- a/app/Http/Controllers/AnalysisController.php +++ b/app/Http/Controllers/AnalysisController.php @@ -10,23 +10,41 @@ use App\Models\OrderItem; use App\Models\Payroll; use App\Models\Purchase; +use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Inertia\Inertia; 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 = [ - 'total_sales' => $this->getStats(fn () => Order::count()), - 'total_revenue' => $this->getStats(fn () => Order::sum('total')), - 'total_expenses' => $this->getStats(fn () => Expense::sum('amount')), - 'total_payrolls' => $this->getStats(fn () => Payroll::sum('total_salary')), - 'cogs' => $this->getStats(fn () => Order::sum('hpp')), - 'total_discount' => $this->getStats(fn () => Order::sum('discount')), - 'total_purchases' => $this->getStats(fn () => Purchase::sum('total')), - 'products_sold' => $this->getStats(fn () => OrderItem::sum('qty')), - 'total_customers' => $this->getStats(fn () => Order::distinct('customer_name')->count('customer_name')), + 'total_sales' => $this->getStats(fn () => $applyFilter(Order::query())->count()), + 'total_revenue' => $this->getStats(fn () => $applyFilter(Order::query())->sum('total')), + 'total_expenses' => $this->getStats(fn () => $applyFilter(Expense::query())->sum('amount')), + 'total_payrolls' => $this->getStats(fn () => $applyFilter(Payroll::query(), 'period_month')->sum('total_salary')), + 'cogs' => $this->getStats(fn () => $applyFilter(Order::query())->sum('hpp')), + 'total_discount' => $this->getStats(fn () => $applyFilter(Order::query())->sum('discount')), + 'total_purchases' => $this->getStats(fn () => $applyFilter(Purchase::query())->sum('total')), + 'products_sold' => $this->getStats(fn () => $applyFilter(OrderItem::query())->sum('qty')), + 'total_customers' => $this->getStats(fn () => $applyFilter(Order::query())->distinct('customer_name')->count('customer_name')), ]; $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']); - // Revenue vs Purchases per month (this year) - $revenueByMonth = DB::table('orders') + // Revenue vs Purchases per month + $revenueByMonth = $applyFilter(DB::table('orders')) ->select( DB::raw('MONTH(created_at) as month'), DB::raw('SUM(total) as total') @@ -47,7 +65,7 @@ public function __invoke() ->groupBy(DB::raw('MONTH(created_at)')) ->get(); - $purchasesByMonth = DB::table('purchases') + $purchasesByMonth = $applyFilter(DB::table('purchases')) ->select( DB::raw('MONTH(created_at) as month'), 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')) ->groupBy('payment_method') ->get() @@ -80,7 +98,7 @@ public function __invoke() return $item; }); - $orderStatuses = DB::table('orders') + $orderStatuses = $applyFilter(DB::table('orders')) ->select('order_status as name', DB::raw('COUNT(*) as total')) ->groupBy('order_status') ->get() @@ -92,7 +110,7 @@ public function __invoke() return $item; }); - $orderChannels = DB::table('orders') + $orderChannels = $applyFilter(DB::table('orders')) ->select('order_channel as name', DB::raw('COUNT(*) as total')) ->groupBy('order_channel') ->get() @@ -104,7 +122,7 @@ public function __invoke() 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') ->select('products.name as name', DB::raw('SUM(order_items.qty) as total')) ->groupBy('products.name') @@ -112,7 +130,7 @@ public function __invoke() ->limit(5) ->get(); - $topCustomers = DB::table('orders') + $topCustomers = $applyFilter(DB::table('orders'), 'orders.created_at') ->select('customer_name as name', DB::raw('SUM(total) as total')) ->whereNotNull('customer_name') ->groupBy('customer_name') @@ -120,7 +138,7 @@ public function __invoke() ->limit(5) ->get(); - $salesByHourRaw = DB::table('orders') + $salesByHourRaw = $applyFilter(DB::table('orders')) ->select( DB::raw('HOUR(created_at) as hour'), DB::raw('COUNT(*) as total') @@ -138,7 +156,7 @@ public function __invoke() }); // Revenue & Profit & Volume per month - $ordersByMonth = DB::table('orders') + $ordersByMonth = $applyFilter(DB::table('orders')) ->select( DB::raw('MONTH(created_at) as month'), DB::raw('SUM(total) as revenue'), @@ -148,7 +166,7 @@ public function __invoke() ->groupBy(DB::raw('MONTH(created_at)')) ->get(); - $expensesByMonth = DB::table('expenses') + $expensesByMonth = $applyFilter(DB::table('expenses')) ->select( DB::raw('MONTH(created_at) as month'), DB::raw('SUM(amount) as total') @@ -156,7 +174,7 @@ public function __invoke() ->groupBy(DB::raw('MONTH(created_at)')) ->get(); - $payrollsByMonth = DB::table('payrolls') + $payrollsByMonth = $applyFilter(DB::table('payrolls'), 'period_month') ->select( DB::raw('MONTH(period_month) as month'), 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('category_product', 'products.id', '=', 'category_product.product_id') ->join('categories', 'category_product.category_id', '=', 'categories.id') @@ -219,6 +237,11 @@ public function __invoke() 'topProducts' => $topProducts, 'topCustomers' => $topCustomers, 'topCategories' => $topCategories, + 'filters' => [ + 'period' => $period, + 'start_date' => $startDate, + 'end_date' => $endDate, + ], ]); } diff --git a/resources/js/pages/analysis.tsx b/resources/js/pages/analysis.tsx index 0cf9aeb..fd7fd4c 100644 --- a/resources/js/pages/analysis.tsx +++ b/resources/js/pages/analysis.tsx @@ -1,27 +1,193 @@ 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 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 { CustomPieChart } from '../components/charts/pie-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 { SalesByHourChart } from '../components/charts/sales-by-hour-chart'; +import { TransactionVolumeChart } from '../components/charts/transaction-volume-chart'; 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().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 ( <>
-
-

Analisa

+
+
+

Analisa

+

Lihat performa bisnis Anda secara mendalam.

+
+ +
+ + +
+ + + + + + { + setStartDate(date ? formatToYmd(date) : ''); + setIsStartOpen(false); + }} + captionLayout="dropdown" + initialFocus + /> + + + + s/d + + + + + + + { + setEndDate(date ? formatToYmd(date) : ''); + setIsEndOpen(false); + }} + captionLayout="dropdown" + initialFocus + /> + + + + +
+ + {(period !== 'all' || (startDate && endDate)) && ( + + )} +
{/* Stats Grid */} @@ -56,36 +222,36 @@ export default function Analisa() { {/* Main Trends Grid */}
- -
{/* Activity & Volume Grid */}
- -
@@ -124,13 +290,13 @@ export default function Analisa() {