feat: Add tooltips and hover effects to dashboard cards and stats for improved user experience, and include a description for the outlet statistic.

This commit is contained in:
Yoga Pangestu 2026-02-12 15:11:48 +07:00
parent 6477684540
commit 3b289588f2
4 changed files with 498 additions and 187 deletions

View File

@ -83,11 +83,15 @@ class Analysis extends Component
public function mount(): void public function mount(): void
{ {
$this->outlets = auth()->user()->outlets()->get()->pluck('name', 'id')->toArray(); /** @var \App\Models\User $user */
$user = auth()->user();
$this->outlets = $user->outlets()->get()->pluck('name', 'id')->toArray();
} }
public function render(): View public function render(): View
{ {
/** @var \App\Models\User $user */
$user = auth()->user();
$outletIds = $this->selectedOutletIds; $outletIds = $this->selectedOutletIds;
$period = $this->period; $period = $this->period;
$range = $this->range; $range = $this->range;
@ -119,79 +123,90 @@ public function render(): View
[ [
'title' => 'Outlet', 'title' => 'Outlet',
'value' => Outlet::when($dateQuery || ($startDate && $endDate), $filterDate)->count(), 'value' => Outlet::when($dateQuery || ($startDate && $endDate), $filterDate)->count(),
'description' => 'Total jumlah outlet atau cabang toko yang terdaftar.',
], ],
[ [
'title' => 'Pegawai', 'title' => 'Pegawai',
'value' => User::whereHas('employee') 'value' => User::whereHas('employee')
->when(! empty($outletIds), fn ($q) => $q->whereHas('outlets', fn ($q2) => $q2->whereIn('outlets.id', $outletIds))) ->when(! empty($outletIds), fn($q) => $q->whereHas('outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
->when($dateQuery || ($startDate && $endDate), $filterDate) ->when($dateQuery || ($startDate && $endDate), $filterDate)
->withoutDeveloper() ->withoutDeveloper()
->count(), ->count(),
'description' => 'Jumlah karyawan aktif yang terdaftar di outlet terpilih.',
], ],
[ [
'title' => 'Tier', 'title' => 'Tier',
'value' => Tier::when($dateQuery || ($startDate && $endDate), $filterDate)->count(), 'value' => Tier::when($dateQuery || ($startDate && $endDate), $filterDate)->count(),
'description' => 'Jumlah tingkatan loyalitas member (seperti Bronze, Silver, Gold).',
], ],
[ [
'title' => 'Voucher Aktif', 'title' => 'Voucher Aktif',
'value' => Voucher::when(! empty($outletIds), fn ($q) => $q->whereHas('outlets', fn ($q2) => $q2->whereIn('outlets.id', $outletIds))) 'value' => Voucher::when(! empty($outletIds), fn($q) => $q->whereHas('outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
->when($dateQuery || ($startDate && $endDate), $filterDate) ->when($dateQuery || ($startDate && $endDate), $filterDate)
->active() ->active()
->count(), ->count(),
'description' => 'Jumlah kode promo atau voucher yang masih berlaku saat ini.',
], ],
[ [
'title' => 'Member', 'title' => 'Member',
'value' => User::whereHas('customer') 'value' => User::whereHas('customer')
->when(! empty($outletIds), fn ($q) => $q->whereHas('outlets', fn ($q2) => $q2->whereIn('outlets.id', $outletIds))) ->when(! empty($outletIds), fn($q) => $q->whereHas('outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
->when($dateQuery || ($startDate && $endDate), $filterDate) ->when($dateQuery || ($startDate && $endDate), $filterDate)
->count(), ->count(),
'description' => 'Total pelanggan yang sudah mendaftar sebagai member resmi.',
], ],
[ [
'title' => 'Customer', 'title' => 'Customer',
'value' => Customer::whereDoesntHave('user') 'value' => Customer::whereDoesntHave('user')
->when($dateQuery || ($startDate && $endDate), $filterDate) ->when($dateQuery || ($startDate && $endDate), $filterDate)
->count(), ->count(),
'description' => 'Jumlah pelanggan umum (guest) yang pernah melakukan transaksi.',
], ],
[ [
'title' => 'Kategori', 'title' => 'Kategori',
'value' => Category::when($dateQuery || ($startDate && $endDate), $filterDate)->count(), 'value' => Category::when($dateQuery || ($startDate && $endDate), $filterDate)->count(),
'description' => 'Jumlah kategori barang (seperti Parfum, Sabun, dll).',
], ],
[ [
'title' => 'Merek', 'title' => 'Merek',
'value' => Brand::when($dateQuery || ($startDate && $endDate), $filterDate)->count(), 'value' => Brand::when($dateQuery || ($startDate && $endDate), $filterDate)->count(),
'description' => 'Jumlah merek parfum yang terdaftar di sistem.',
], ],
[ [
'title' => 'Parfum', 'title' => 'Parfum',
'value' => Perfume::when(! empty($outletIds), fn ($q) => $q->whereHas('outlets', fn ($q2) => $q2->whereIn('outlets.id', $outletIds))) 'value' => Perfume::when(! empty($outletIds), fn($q) => $q->whereHas('outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
->when($dateQuery || ($startDate && $endDate), $filterDate) ->when($dateQuery || ($startDate && $endDate), $filterDate)
->count(), ->count(),
'description' => 'Jumlah varian bibit parfum yang tersedia dalam katalog.',
], ],
[ [
'title' => 'Produk', 'title' => 'Produk',
'value' => Product::when(! empty($outletIds), fn ($q) => $q->whereHas('outlets', fn ($q2) => $q2->whereIn('outlets.id', $outletIds))) 'value' => Product::when(! empty($outletIds), fn($q) => $q->whereHas('outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
->when($dateQuery || ($startDate && $endDate), $filterDate) ->when($dateQuery || ($startDate && $endDate), $filterDate)
->count(), ->count(),
'description' => 'Jumlah item produk jadi (seperti aksesoris) yang terdaftar.',
], ],
[ [
'title' => 'Botol', 'title' => 'Botol',
'value' => Bottle::when(! empty($outletIds), fn ($q) => $q->whereHas('outlets', fn ($q2) => $q2->whereIn('outlets.id', $outletIds))) 'value' => Bottle::when(! empty($outletIds), fn($q) => $q->whereHas('outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
->when($dateQuery || ($startDate && $endDate), $filterDate) ->when($dateQuery || ($startDate && $endDate), $filterDate)
->count(), ->count(),
'description' => 'Jumlah jenis botol parfum yang tersedia.',
], ],
]; ];
$orderQuery = Order::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $orderQuery = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->when($dateQuery || ($startDate && $endDate), $filterDate); ->when($dateQuery || ($startDate && $endDate), $filterDate);
$totalIncome = (clone $orderQuery)->sum('total'); $totalIncome = (clone $orderQuery)->sum('total');
$totalCogs = (clone $orderQuery)->sum('cogs'); $totalCogs = (clone $orderQuery)->sum('cogs');
$totalDiscount = (clone $orderQuery)->sum(DB::raw('discount + COALESCE(voucher_discount, 0)')); $totalDiscount = (clone $orderQuery)->sum(DB::raw('discount + COALESCE(voucher_discount, 0)'));
$expenseQuery = Expense::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $expenseQuery = Expense::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->when($dateQuery || ($startDate && $endDate), $filterDate); ->when($dateQuery || ($startDate && $endDate), $filterDate);
$totalExpense = $expenseQuery->sum('amount'); $totalExpense = $expenseQuery->sum('amount');
$payrollQuery = Payroll::when(! empty($outletIds), fn ($q) => $q->whereHas('user.outlets', fn ($q2) => $q2->whereIn('outlets.id', $outletIds))) $payrollQuery = Payroll::when(! empty($outletIds), fn($q) => $q->whereHas('user.outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
->when($dateQuery || ($startDate && $endDate), $filterDate) ->when($dateQuery || ($startDate && $endDate), $filterDate)
->paid(); ->paid();
$totalPayroll = $payrollQuery->sum('total_salary'); $totalPayroll = $payrollQuery->sum('total_salary');
@ -200,7 +215,7 @@ public function render(): View
$netProfit = $grossProfit - $totalExpense - $totalPayroll; $netProfit = $grossProfit - $totalExpense - $totalPayroll;
$totalOrdersCount = (clone $orderQuery)->count(); $totalOrdersCount = (clone $orderQuery)->count();
$totalItemsCount = OrderItem::whereHas('order', fn ($q) => $q->when(! empty($outletIds), fn ($q2) => $q2->whereIn('outlet_id', $outletIds))->when($dateQuery || ($startDate && $endDate), $filterDate))->sum('quantity'); $totalItemsCount = OrderItem::whereHas('order', fn($q) => $q->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))->when($dateQuery || ($startDate && $endDate), $filterDate))->sum('quantity');
$aov = $totalOrdersCount > 0 ? $totalIncome / $totalOrdersCount : 0; $aov = $totalOrdersCount > 0 ? $totalIncome / $totalOrdersCount : 0;
$profitMargin = $totalIncome > 0 ? ($netProfit / $totalIncome) * 100 : 0; $profitMargin = $totalIncome > 0 ? ($netProfit / $totalIncome) * 100 : 0;
@ -210,7 +225,7 @@ public function render(): View
->whereNotNull('customer_id') ->whereNotNull('customer_id')
->groupBy('customer_id') ->groupBy('customer_id')
->get(); ->get();
$repeatCustomers = $customerOrders->filter(fn ($c) => $c->count > 1)->count(); $repeatCustomers = $customerOrders->filter(fn($c) => $c->count > 1)->count();
$totalCustomers = $customerOrders->count(); $totalCustomers = $customerOrders->count();
$loyaltyRate = $totalCustomers > 0 ? ($repeatCustomers / $totalCustomers) * 100 : 0; $loyaltyRate = $totalCustomers > 0 ? ($repeatCustomers / $totalCustomers) * 100 : 0;
@ -218,76 +233,90 @@ public function render(): View
[ [
'title' => 'Average Order Value (AOV)', 'title' => 'Average Order Value (AOV)',
'value' => formatCurrencyNumber($aov, 'Rp'), 'value' => formatCurrencyNumber($aov, 'Rp'),
'description' => 'Rata-rata nilai belanja pelanggan dalam satu kali checkout.',
], ],
auth()->user()->hasRole(['Developer', 'Owner']) ? [ $user->hasRole(['Developer', 'Owner']) ? [
'title' => 'Profit Margin', 'title' => 'Profit Margin',
'value' => round($profitMargin, 1).'%', 'value' => round($profitMargin, 1) . '%',
'description' => 'Persentase keuntungan bersih dibandingkan dengan total pendapatan.',
] : null, ] : null,
[ [
'title' => 'Loyalty Rate', 'title' => 'Loyalty Rate',
'value' => round($loyaltyRate, 1).'%', 'value' => round($loyaltyRate, 1) . '%',
'description' => 'Persentase pelanggan yang melakukan pembelian lebih dari satu kali.',
], ],
[ [
'title' => 'Item per Transaksi', 'title' => 'Item per Transaksi',
'value' => round($itemsPerOrder, 1), 'value' => round($itemsPerOrder, 1),
'description' => 'Rata-rata jumlah barang yang dibeli dalam satu transaksi.',
], ],
[ [
'title' => 'Total Order', 'title' => 'Total Order',
'value' => formatCurrencyNumber($totalOrdersCount), 'value' => formatCurrencyNumber($totalOrdersCount),
'description' => 'Total frekuensi transaksi atau pesanan yang masuk selama periode ini.',
], ],
[ [
'title' => 'Pendapatan', 'title' => 'Pendapatan',
'value' => formatCurrencyNumber($totalIncome, 'Rp'), 'value' => formatCurrencyNumber($totalIncome, 'Rp'),
'description' => 'Total nominal uang yang masuk dari penjualan (Omzet).',
], ],
auth()->user()->hasRole(['Developer', 'Owner']) ? [ $user->hasRole(['Developer', 'Owner']) ? [
'title' => 'HPP', 'title' => 'HPP',
'value' => formatCurrencyNumber($totalCogs, 'Rp'), 'value' => formatCurrencyNumber($totalCogs, 'Rp'),
'description' => 'Total modal barang atau Harga Pokok Penjualan (HPP).',
] : null, ] : null,
auth()->user()->hasRole(['Developer', 'Owner']) ? [ $user->hasRole(['Developer', 'Owner']) ? [
'title' => 'Laba Kotor', 'title' => 'Laba Kotor',
'value' => formatCurrencyNumber($grossProfit, 'Rp'), 'value' => formatCurrencyNumber($grossProfit, 'Rp'),
'description' => 'Pendapatan dikurangi dengan HPP dan Diskon.',
] : null, ] : null,
auth()->user()->hasRole(['Developer', 'Owner']) ? [ $user->hasRole(['Developer', 'Owner']) ? [
'title' => 'Laba Bersih', 'title' => 'Laba Bersih',
'value' => formatCurrencyNumber($netProfit, 'Rp'), 'value' => formatCurrencyNumber($netProfit, 'Rp'),
'description' => 'Laba kotor dikurangi dengan beban operasional dan gaji.',
] : null, ] : null,
[ [
'title' => 'Diskon', 'title' => 'Diskon',
'value' => formatCurrencyNumber($totalDiscount, 'Rp'), 'value' => formatCurrencyNumber($totalDiscount, 'Rp'),
'description' => 'Total nilai potongan harga (diskon) yang diberikan kepada pelanggan.',
], ],
[ [
'title' => 'Voucher Terpakai', 'title' => 'Voucher Terpakai',
'value' => formatCurrencyNumber( 'value' => formatCurrencyNumber(
(clone $orderQuery)->whereNotNull('voucher_id')->count() (clone $orderQuery)->whereNotNull('voucher_id')->count()
), ),
'description' => 'Jumlah transaksi yang menggunakan voucher diskon selama periode ini.',
], ],
[ [
'title' => 'Parfum Terjual', 'title' => 'Parfum Terjual',
'value' => formatCurrencyNumber( 'value' => formatCurrencyNumber(
OrderItem::where('orderable_type', Perfume::class) OrderItem::where('orderable_type', Perfume::class)
->whereHas('order', fn ($q) => $q->when(! empty($outletIds), fn ($q2) => $q2->whereIn('outlet_id', $outletIds))->when($dateQuery || ($startDate && $endDate), $filterDate)) ->whereHas('order', fn($q) => $q->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))->when($dateQuery || ($startDate && $endDate), $filterDate))
->sum('quantity') ->sum('quantity')
).' ml', ) . ' ml',
'description' => 'Total volume cairan parfum yang terjual dalam satuan mililiter (ml).',
], ],
[ [
'title' => 'Produk Terjual', 'title' => 'Produk Terjual',
'value' => formatCurrencyNumber( 'value' => formatCurrencyNumber(
OrderItem::where('orderable_type', Product::class) OrderItem::where('orderable_type', Product::class)
->whereHas('order', fn ($q) => $q->when(! empty($outletIds), fn ($q2) => $q2->whereIn('outlet_id', $outletIds))->when($dateQuery || ($startDate && $endDate), $filterDate)) ->whereHas('order', fn($q) => $q->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))->when($dateQuery || ($startDate && $endDate), $filterDate))
->sum('quantity') ->sum('quantity')
).' pcs', ) . ' pcs',
'description' => 'Total jumlah unit produk yang terjual dalam satuan pcs.',
], ],
[ [
'title' => 'Botol Terjual', 'title' => 'Botol Terjual',
'value' => formatCurrencyNumber( 'value' => formatCurrencyNumber(
OrderItem::where('orderable_type', Bottle::class) OrderItem::where('orderable_type', Bottle::class)
->whereHas('order', fn ($q) => $q->when(! empty($outletIds), fn ($q2) => $q2->whereIn('outlet_id', $outletIds))->when($dateQuery || ($startDate && $endDate), $filterDate)) ->whereHas('order', fn($q) => $q->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))->when($dateQuery || ($startDate && $endDate), $filterDate))
->sum('quantity') ->sum('quantity')
).' pcs', ) . ' pcs',
'description' => 'Total jumlah unit botol yang terjual dalam satuan pcs.',
], ],
]); ]);
@ -295,37 +324,40 @@ public function render(): View
[ [
'title' => 'Beban Toko', 'title' => 'Beban Toko',
'value' => formatCurrencyNumber( 'value' => formatCurrencyNumber(
Expense::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) Expense::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->when($dateQuery || ($startDate && $endDate), $filterDate) ->when($dateQuery || ($startDate && $endDate), $filterDate)
->sum('amount'), ->sum('amount'),
'Rp' 'Rp'
), ),
'description' => 'Total biaya operasional atau pengeluaran toko yang tercatat selama periode ini.',
], ],
auth()->user()->hasRole(['Developer', 'Owner']) ? [ $user->hasRole(['Developer', 'Owner']) ? [
'title' => 'Belanja Barang', 'title' => 'Belanja Barang',
'value' => formatCurrencyNumber( 'value' => formatCurrencyNumber(
Purchase::when($dateQuery || ($startDate && $endDate), $filterDate) Purchase::when($dateQuery || ($startDate && $endDate), $filterDate)
->sum('total'), ->sum('total'),
'Rp' 'Rp'
), ),
'description' => 'Total pengeluaran untuk pembelian stok barang atau inventaris selama periode ini.',
] : null, ] : null,
[ [
'title' => 'Penggajian', 'title' => 'Penggajian',
'value' => formatCurrencyNumber( 'value' => formatCurrencyNumber(
Payroll::when(! empty($outletIds), fn ($q) => $q->whereHas('user.outlets', fn ($q2) => $q2->whereIn('outlets.id', $outletIds))) Payroll::when(! empty($outletIds), fn($q) => $q->whereHas('user.outlets', fn($q2) => $q2->whereIn('outlets.id', $outletIds)))
->when($dateQuery || ($startDate && $endDate), $filterDate) ->when($dateQuery || ($startDate && $endDate), $filterDate)
->paid() ->paid()
->sum('total_salary'), ->sum('total_salary'),
'Rp' 'Rp'
), ),
'description' => 'Total pengeluaran untuk gaji karyawan.',
], ],
]); ]);
$topPerfumes = OrderItem::where('orderable_type', Perfume::class) $topPerfumes = OrderItem::where('orderable_type', Perfume::class)
->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) { ->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) {
$q->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $q->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->when($dateQuery || ($startDate && $endDate), $filterDate); ->when($dateQuery || ($startDate && $endDate), $filterDate);
}) })
->with('orderable') ->with('orderable')
@ -334,7 +366,7 @@ public function render(): View
->orderByDesc('total_sales_value') ->orderByDesc('total_sales_value')
->take(5) ->take(5)
->get() ->get()
->map(fn ($item) => [ ->map(fn($item) => [
'name' => $item->orderable?->name ?? 'Unknown', 'name' => $item->orderable?->name ?? 'Unknown',
'sale_price' => formatCurrencyNumber($item->orderable?->sale_price ?? 0, 'Rp'), 'sale_price' => formatCurrencyNumber($item->orderable?->sale_price ?? 0, 'Rp'),
'total_sold' => formatCurrencyNumber($item->total_sold), 'total_sold' => formatCurrencyNumber($item->total_sold),
@ -344,7 +376,7 @@ public function render(): View
$topProducts = OrderItem::where('orderable_type', Product::class) $topProducts = OrderItem::where('orderable_type', Product::class)
->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) { ->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) {
$q->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $q->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->when($dateQuery || ($startDate && $endDate), $filterDate); ->when($dateQuery || ($startDate && $endDate), $filterDate);
}) })
->with('orderable') ->with('orderable')
@ -353,7 +385,7 @@ public function render(): View
->orderByDesc('total_sales_value') ->orderByDesc('total_sales_value')
->take(5) ->take(5)
->get() ->get()
->map(fn ($item) => [ ->map(fn($item) => [
'name' => $item->orderable?->name ?? 'Unknown', 'name' => $item->orderable?->name ?? 'Unknown',
'sale_price' => formatCurrencyNumber($item->orderable?->sale_price ?? 0, 'Rp'), 'sale_price' => formatCurrencyNumber($item->orderable?->sale_price ?? 0, 'Rp'),
'total_sold' => formatCurrencyNumber($item->total_sold), 'total_sold' => formatCurrencyNumber($item->total_sold),
@ -363,7 +395,7 @@ public function render(): View
$topBottles = OrderItem::where('orderable_type', Bottle::class) $topBottles = OrderItem::where('orderable_type', Bottle::class)
->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) { ->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) {
$q->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $q->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->when($dateQuery || ($startDate && $endDate), $filterDate); ->when($dateQuery || ($startDate && $endDate), $filterDate);
}) })
->with('orderable') ->with('orderable')
@ -372,7 +404,7 @@ public function render(): View
->orderByDesc('total_sales_value') ->orderByDesc('total_sales_value')
->take(5) ->take(5)
->get() ->get()
->map(fn ($item) => [ ->map(fn($item) => [
'name' => $item->orderable?->name ?? 'Unknown', 'name' => $item->orderable?->name ?? 'Unknown',
'sale_price' => formatCurrencyNumber($item->orderable?->sale_price ?? 0, 'Rp'), 'sale_price' => formatCurrencyNumber($item->orderable?->sale_price ?? 0, 'Rp'),
'total_sold' => formatCurrencyNumber($item->total_sold), 'total_sold' => formatCurrencyNumber($item->total_sold),
@ -383,9 +415,9 @@ public function render(): View
// --- Chart Data Logic --- // --- Chart Data Logic ---
// 1. Revenue & Profit Trend // 1. Revenue & Profit Trend
$trendDates = collect(range(29, 0))->map(fn ($i) => now()->subDays($i)->format('Y-m-d')); $trendDates = collect(range(29, 0))->map(fn($i) => now()->subDays($i)->format('Y-m-d'));
$ordersTrend = Order::query() $ordersTrend = Order::query()
->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->where('created_at', '>=', now()->subDays(30)->startOfDay()) ->where('created_at', '>=', now()->subDays(30)->startOfDay())
->select(DB::raw('DATE(created_at) as date'), DB::raw('SUM(total) as revenue'), DB::raw('SUM(total) - SUM(cogs) - SUM(discount) - SUM(COALESCE(voucher_discount,0)) as gross_profit'), DB::raw('count(*) as count')) ->select(DB::raw('DATE(created_at) as date'), DB::raw('SUM(total) as revenue'), DB::raw('SUM(total) - SUM(cogs) - SUM(discount) - SUM(COALESCE(voucher_discount,0)) as gross_profit'), DB::raw('count(*) as count'))
->groupBy('date') ->groupBy('date')
@ -393,7 +425,7 @@ public function render(): View
->keyBy('date'); ->keyBy('date');
$expensesTrend = Expense::query() $expensesTrend = Expense::query()
->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->where('created_at', '>=', now()->subDays(30)->startOfDay()) ->where('created_at', '>=', now()->subDays(30)->startOfDay())
->select(DB::raw('DATE(created_at) as date'), DB::raw('SUM(amount) as total_expense')) ->select(DB::raw('DATE(created_at) as date'), DB::raw('SUM(amount) as total_expense'))
->groupBy('date') ->groupBy('date')
@ -401,9 +433,9 @@ public function render(): View
->keyBy('date'); ->keyBy('date');
$this->revenueTrendChart = [ $this->revenueTrendChart = [
'labels' => $trendDates->map(fn ($d) => Carbon::parse($d)->format('d M'))->toArray(), 'labels' => $trendDates->map(fn($d) => Carbon::parse($d)->format('d M'))->toArray(),
'revenue' => $trendDates->map(fn ($d) => (int) ($ordersTrend->get($d)?->revenue ?? 0))->toArray(), 'revenue' => $trendDates->map(fn($d) => (int) ($ordersTrend->get($d)?->revenue ?? 0))->toArray(),
'profit' => $trendDates->map(fn ($d) => (int) (($ordersTrend->get($d)?->gross_profit ?? 0) - ($expensesTrend->get($d)?->total_expense ?? 0)))->toArray(), 'profit' => $trendDates->map(fn($d) => (int) (($ordersTrend->get($d)?->gross_profit ?? 0) - ($expensesTrend->get($d)?->total_expense ?? 0)))->toArray(),
]; ];
// 2. busiest hour // 2. busiest hour
@ -416,26 +448,26 @@ public function render(): View
->keyBy('hour'); ->keyBy('hour');
$this->hourlySalesChart = [ $this->hourlySalesChart = [
'labels' => collect(range(0, 23))->map(fn ($h) => str_pad($h, 2, '0', STR_PAD_LEFT).':00')->toArray(), 'labels' => collect(range(0, 23))->map(fn($h) => str_pad($h, 2, '0', STR_PAD_LEFT) . ':00')->toArray(),
'data' => collect(range(0, 23))->map(fn ($h) => $hourlySales->get($h)?->count ?? 0)->toArray(), 'data' => collect(range(0, 23))->map(fn($h) => $hourlySales->get($h)?->count ?? 0)->toArray(),
]; ];
// 3. Product Category Distribution // 3. Product Category Distribution
$perfumeSales = OrderItem::where('orderable_type', Perfume::class) $perfumeSales = OrderItem::where('orderable_type', Perfume::class)
->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) { ->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) {
$q->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $q->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->when($dateQuery || ($startDate && $endDate), $filterDate); ->when($dateQuery || ($startDate && $endDate), $filterDate);
})->sum(DB::raw('unit_price * quantity')); })->sum(DB::raw('unit_price * quantity'));
$productSales = OrderItem::where('orderable_type', Product::class) $productSales = OrderItem::where('orderable_type', Product::class)
->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) { ->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) {
$q->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $q->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->when($dateQuery || ($startDate && $endDate), $filterDate); ->when($dateQuery || ($startDate && $endDate), $filterDate);
})->sum(DB::raw('unit_price * quantity')); })->sum(DB::raw('unit_price * quantity'));
$bottleSales = OrderItem::where('orderable_type', Bottle::class) $bottleSales = OrderItem::where('orderable_type', Bottle::class)
->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) { ->whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) {
$q->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $q->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->when($dateQuery || ($startDate && $endDate), $filterDate); ->when($dateQuery || ($startDate && $endDate), $filterDate);
})->sum(DB::raw('unit_price * quantity')); })->sum(DB::raw('unit_price * quantity'));
@ -446,7 +478,7 @@ public function render(): View
// 4. Payment Method // 4. Payment Method
$paymentMethods = Payment::whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) { $paymentMethods = Payment::whereHas('order', function ($q) use ($outletIds, $dateQuery, $startDate, $endDate, $filterDate) {
$q->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $q->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->when($dateQuery || ($startDate && $endDate), $filterDate); ->when($dateQuery || ($startDate && $endDate), $filterDate);
}) })
->select('method', DB::raw('SUM(amount) as total')) ->select('method', DB::raw('SUM(amount) as total'))
@ -454,13 +486,13 @@ public function render(): View
->get(); ->get();
$this->paymentMethodChart = [ $this->paymentMethodChart = [
'labels' => $paymentMethods->map(fn ($pm) => $pm->method?->label() ?? 'Unknown')->toArray(), 'labels' => $paymentMethods->map(fn($pm) => $pm->method?->label() ?? 'Unknown')->toArray(),
'data' => $paymentMethods->pluck('total')->toArray(), 'data' => $paymentMethods->pluck('total')->toArray(),
]; ];
// 5. Outlet Performance // 5. Outlet Performance
$outletPerformance = Order::query() $outletPerformance = Order::query()
->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->when($dateQuery || ($startDate && $endDate), $filterDate) ->when($dateQuery || ($startDate && $endDate), $filterDate)
->select('outlet_id', DB::raw('SUM(total) as revenue')) ->select('outlet_id', DB::raw('SUM(total) as revenue'))
->groupBy('outlet_id') ->groupBy('outlet_id')
@ -468,7 +500,7 @@ public function render(): View
->get(); ->get();
$this->outletPerformanceChart = [ $this->outletPerformanceChart = [
'labels' => $outletPerformance->map(fn ($op) => $op->outlet?->name ?? 'Outlet #'.$op->outlet_id)->toArray(), 'labels' => $outletPerformance->map(fn($op) => $op->outlet?->name ?? 'Outlet #' . $op->outlet_id)->toArray(),
'data' => $outletPerformance->pluck('revenue')->toArray(), 'data' => $outletPerformance->pluck('revenue')->toArray(),
]; ];
@ -514,7 +546,7 @@ public function render(): View
$this->dayOfWeekSalesChart = [ $this->dayOfWeekSalesChart = [
'labels' => array_values($days), 'labels' => array_values($days),
'data' => collect(range(1, 7))->map(fn ($d) => (int) ($dayOfWeekSales->get($d)?->revenue ?? 0))->toArray(), 'data' => collect(range(1, 7))->map(fn($d) => (int) ($dayOfWeekSales->get($d)?->revenue ?? 0))->toArray(),
]; ];
// 8. Customer Type (Member vs Guest) // 8. Customer Type (Member vs Guest)
@ -528,13 +560,13 @@ public function render(): View
// 9. Transaction Trend (Daily Count) // 9. Transaction Trend (Daily Count)
$this->transactionTrendChart = [ $this->transactionTrendChart = [
'labels' => $trendDates->map(fn ($d) => Carbon::parse($d)->format('d M'))->toArray(), 'labels' => $trendDates->map(fn($d) => Carbon::parse($d)->format('d M'))->toArray(),
'data' => $trendDates->map(fn ($d) => (int) ($ordersTrend->get($d)?->count ?? 0))->toArray(), 'data' => $trendDates->map(fn($d) => (int) ($ordersTrend->get($d)?->count ?? 0))->toArray(),
]; ];
// 10. AOV Trend (Daily Revenue / Daily Count) // 10. AOV Trend (Daily Revenue / Daily Count)
$this->aovTrendChart = [ $this->aovTrendChart = [
'labels' => $trendDates->map(fn ($d) => Carbon::parse($d)->format('d M'))->toArray(), 'labels' => $trendDates->map(fn($d) => Carbon::parse($d)->format('d M'))->toArray(),
'data' => $trendDates->map(function ($d) use ($ordersTrend) { 'data' => $trendDates->map(function ($d) use ($ordersTrend) {
$rev = $ordersTrend->get($d)?->revenue ?? 0; $rev = $ordersTrend->get($d)?->revenue ?? 0;
$count = $ordersTrend->get($d)?->count ?? 0; $count = $ordersTrend->get($d)?->count ?? 0;
@ -563,7 +595,7 @@ public function render(): View
// 14. Voucher Usage Trend // 14. Voucher Usage Trend
$vouchersTrend = Order::query() $vouchersTrend = Order::query()
->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->where('created_at', '>=', now()->subDays(30)->startOfDay()) ->where('created_at', '>=', now()->subDays(30)->startOfDay())
->whereNotNull('voucher_id') ->whereNotNull('voucher_id')
->select(DB::raw('DATE(created_at) as date'), DB::raw('count(*) as count')) ->select(DB::raw('DATE(created_at) as date'), DB::raw('count(*) as count'))
@ -572,8 +604,8 @@ public function render(): View
->keyBy('date'); ->keyBy('date');
$this->voucherUsageTrendChart = [ $this->voucherUsageTrendChart = [
'labels' => $trendDates->map(fn ($d) => Carbon::parse($d)->format('d M'))->toArray(), 'labels' => $trendDates->map(fn($d) => Carbon::parse($d)->format('d M'))->toArray(),
'data' => $trendDates->map(fn ($d) => (int) ($vouchersTrend->get($d)?->count ?? 0))->toArray(), 'data' => $trendDates->map(fn($d) => (int) ($vouchersTrend->get($d)?->count ?? 0))->toArray(),
]; ];
$this->countingData = $countingData; $this->countingData = $countingData;

View File

@ -44,64 +44,68 @@ class Overview extends Component
public function mount(): void public function mount(): void
{ {
$this->outlets = auth()->user()->outlets()->get()->pluck('name', 'id')->toArray(); /** @var \App\Models\User $user */
$user = auth()->user();
$this->outlets = $user->outlets()->get()->pluck('name', 'id')->toArray();
} }
public function render(): View public function render(): View
{ {
/** @var \App\Models\User $user */
$user = auth()->user();
$outletIds = $this->selectedOutletIds; $outletIds = $this->selectedOutletIds;
if (count($this->outlets) === 1) { if (count($this->outlets) === 1) {
$outletIds = auth()->user()->outlets()->pluck('outlets.id')->toArray(); $outletIds = $user->outlets()->pluck('outlets.id')->toArray();
} }
$todayOrder = Order::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $todayOrder = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->today() ->today()
->count(); ->count();
$yesterdayOrder = Order::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $yesterdayOrder = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->yesterday() ->yesterday()
->count(); ->count();
$todayIncome = Order::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $todayIncome = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->today() ->today()
->sum('total'); ->sum('total');
$yesterdayIncome = Order::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $yesterdayIncome = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->yesterday() ->yesterday()
->sum('total'); ->sum('total');
$todayCogs = Order::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $todayCogs = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->today() ->today()
->sum('cogs'); ->sum('cogs');
$yesterdayCogs = Order::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $yesterdayCogs = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->yesterday() ->yesterday()
->sum('cogs'); ->sum('cogs');
$todayDiscount = Order::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $todayDiscount = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->today() ->today()
->sum(DB::raw('discount + COALESCE(voucher_discount, 0)')); ->sum(DB::raw('discount + COALESCE(voucher_discount, 0)'));
$yesterdayDiscount = Order::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $yesterdayDiscount = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->yesterday() ->yesterday()
->sum(DB::raw('discount + COALESCE(voucher_discount, 0)')); ->sum(DB::raw('discount + COALESCE(voucher_discount, 0)'));
$todayVoucher = Order::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $todayVoucher = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->today() ->today()
->whereNotNull('voucher_id') ->whereNotNull('voucher_id')
->count(); ->count();
$yesterdayVoucher = Order::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $yesterdayVoucher = Order::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->yesterday() ->yesterday()
->whereNotNull('voucher_id') ->whereNotNull('voucher_id')
->count(); ->count();
$todayExpense = Expense::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $todayExpense = Expense::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->today() ->today()
->sum('amount'); ->sum('amount');
$yesterdayExpense = Expense::when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) $yesterdayExpense = Expense::when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->yesterday() ->yesterday()
->sum('amount'); ->sum('amount');
@ -118,21 +122,21 @@ public function render(): View
$todaySoldPerfume = OrderItem::where('orderable_type', Perfume::class) $todaySoldPerfume = OrderItem::where('orderable_type', Perfume::class)
->whereHas( ->whereHas(
'order', 'order',
fn ($q) => $q->today()->when(! empty($outletIds), fn ($q2) => $q2->whereIn('outlet_id', $outletIds)) fn($q) => $q->today()->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))
) )
->sum('quantity'); ->sum('quantity');
$yesterdaySoldPerfume = OrderItem::where('orderable_type', Perfume::class) $yesterdaySoldPerfume = OrderItem::where('orderable_type', Perfume::class)
->whereHas( ->whereHas(
'order', 'order',
fn ($q) => $q->yesterday()->when(! empty($outletIds), fn ($q2) => $q2->whereIn('outlet_id', $outletIds)) fn($q) => $q->yesterday()->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))
) )
->sum('quantity'); ->sum('quantity');
$todayTopPerfume = OrderItem::where('orderable_type', Perfume::class) $todayTopPerfume = OrderItem::where('orderable_type', Perfume::class)
->whereHas( ->whereHas(
'order', 'order',
fn ($q) => $q->today()->when(! empty($outletIds), fn ($q2) => $q2->whereIn('outlet_id', $outletIds)) fn($q) => $q->today()->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))
) )
->select('orderable_id', 'orderable_type', DB::raw('SUM(quantity) as total_sold')) ->select('orderable_id', 'orderable_type', DB::raw('SUM(quantity) as total_sold'))
->groupBy('orderable_id', 'orderable_type') ->groupBy('orderable_id', 'orderable_type')
@ -143,7 +147,7 @@ public function render(): View
$yesterdayTopPerfume = OrderItem::where('orderable_type', Perfume::class) $yesterdayTopPerfume = OrderItem::where('orderable_type', Perfume::class)
->whereHas( ->whereHas(
'order', 'order',
fn ($q) => $q->yesterday()->when(! empty($outletIds), fn ($q2) => $q2->whereIn('outlet_id', $outletIds)) fn($q) => $q->yesterday()->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds))
) )
->select('orderable_id', 'orderable_type', DB::raw('SUM(quantity) as total_sold')) ->select('orderable_id', 'orderable_type', DB::raw('SUM(quantity) as total_sold'))
->groupBy('orderable_id', 'orderable_type') ->groupBy('orderable_id', 'orderable_type')
@ -168,26 +172,26 @@ public function render(): View
$hourFunc = DB::getDriverName() === 'sqlite' ? "CAST(strftime('%H', created_at) AS INTEGER)" : 'HOUR(created_at)'; $hourFunc = DB::getDriverName() === 'sqlite' ? "CAST(strftime('%H', created_at) AS INTEGER)" : 'HOUR(created_at)';
$todayHourly = Order::whereDate('created_at', now()) $todayHourly = Order::whereDate('created_at', now())
->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->select(DB::raw("$hourFunc as hour"), DB::raw('count(*) as count')) ->select(DB::raw("$hourFunc as hour"), DB::raw('count(*) as count'))
->groupBy('hour') ->groupBy('hour')
->get()->keyBy('hour'); ->get()->keyBy('hour');
$yesterdayHourly = Order::whereDate('created_at', now()->yesterday()) $yesterdayHourly = Order::whereDate('created_at', now()->yesterday())
->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->select(DB::raw("$hourFunc as hour"), DB::raw('count(*) as count')) ->select(DB::raw("$hourFunc as hour"), DB::raw('count(*) as count'))
->groupBy('hour') ->groupBy('hour')
->get()->keyBy('hour'); ->get()->keyBy('hour');
$this->hourlyComparisonChart = [ $this->hourlyComparisonChart = [
'labels' => collect(range(0, 23))->map(fn ($h) => str_pad($h, 2, '0', STR_PAD_LEFT).':00')->toArray(), 'labels' => collect(range(0, 23))->map(fn($h) => str_pad($h, 2, '0', STR_PAD_LEFT) . ':00')->toArray(),
'today' => collect(range(0, 23))->map(fn ($h) => $todayHourly->get($h)?->count ?? 0)->toArray(), 'today' => collect(range(0, 23))->map(fn($h) => $todayHourly->get($h)?->count ?? 0)->toArray(),
'yesterday' => collect(range(0, 23))->map(fn ($h) => $yesterdayHourly->get($h)?->count ?? 0)->toArray(), 'yesterday' => collect(range(0, 23))->map(fn($h) => $yesterdayHourly->get($h)?->count ?? 0)->toArray(),
]; ];
// 2. Today's Distribution (Category) // 2. Today's Distribution (Category)
$categorySales = OrderItem::whereHas('order', function ($q) use ($outletIds) { $categorySales = OrderItem::whereHas('order', function ($q) use ($outletIds) {
$q->today()->when(! empty($outletIds), fn ($q2) => $q2->whereIn('outlet_id', $outletIds)); $q->today()->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds));
}) })
->select('orderable_type', DB::raw('SUM(unit_price * quantity) as total')) ->select('orderable_type', DB::raw('SUM(unit_price * quantity) as total'))
->groupBy('orderable_type') ->groupBy('orderable_type')
@ -204,25 +208,25 @@ public function render(): View
// 3. Today's Payment Methods // 3. Today's Payment Methods
$paymentMethods = Payment::whereHas('order', function ($q) use ($outletIds) { $paymentMethods = Payment::whereHas('order', function ($q) use ($outletIds) {
$q->today()->when(! empty($outletIds), fn ($q2) => $q2->whereIn('outlet_id', $outletIds)); $q->today()->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds));
}) })
->select('method', DB::raw('SUM(amount) as total')) ->select('method', DB::raw('SUM(amount) as total'))
->groupBy('method') ->groupBy('method')
->get(); ->get();
$this->todayPaymentChart = [ $this->todayPaymentChart = [
'labels' => $paymentMethods->map(fn ($pm) => $pm->method?->label() ?? 'Unknown')->toArray(), 'labels' => $paymentMethods->map(fn($pm) => $pm->method?->label() ?? 'Unknown')->toArray(),
'data' => $paymentMethods->pluck('total')->toArray(), 'data' => $paymentMethods->pluck('total')->toArray(),
]; ];
// 4. Today's Customer Type // 4. Today's Customer Type
$todayMemberOrders = Order::query() $todayMemberOrders = Order::query()
->today() ->today()
->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->whereNotNull('customer_id')->count(); ->whereNotNull('customer_id')->count();
$todayGuestOrders = Order::query() $todayGuestOrders = Order::query()
->today() ->today()
->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->whereNull('customer_id')->count(); ->whereNull('customer_id')->count();
$this->todayCustomerTypeChart = [ $this->todayCustomerTypeChart = [
@ -232,7 +236,7 @@ public function render(): View
// 5. Today's Top 5 Selling Items // 5. Today's Top 5 Selling Items
$todayTopItems = OrderItem::whereHas('order', function ($q) use ($outletIds) { $todayTopItems = OrderItem::whereHas('order', function ($q) use ($outletIds) {
$q->today()->when(! empty($outletIds), fn ($q2) => $q2->whereIn('outlet_id', $outletIds)); $q->today()->when(! empty($outletIds), fn($q2) => $q2->whereIn('outlet_id', $outletIds));
}) })
->with('orderable') ->with('orderable')
->select('orderable_id', 'orderable_type', DB::raw('SUM(quantity) as total_sold')) ->select('orderable_id', 'orderable_type', DB::raw('SUM(quantity) as total_sold'))
@ -242,14 +246,14 @@ public function render(): View
->get(); ->get();
$this->todayTopSellingChart = [ $this->todayTopSellingChart = [
'labels' => $todayTopItems->map(fn ($item) => $item->orderable?->name ?? 'Unknown')->toArray(), 'labels' => $todayTopItems->map(fn($item) => $item->orderable?->name ?? 'Unknown')->toArray(),
'data' => $todayTopItems->pluck('total_sold')->map(fn ($val) => (int) $val)->toArray(), 'data' => $todayTopItems->pluck('total_sold')->map(fn($val) => (int) $val)->toArray(),
]; ];
// 6. Today's Top 5 Customers // 6. Today's Top 5 Customers
$todayTopCustomers = Order::query() $todayTopCustomers = Order::query()
->today() ->today()
->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->whereNotNull('customer_id') ->whereNotNull('customer_id')
->select('customer_id', DB::raw('SUM(total) as total_spent')) ->select('customer_id', DB::raw('SUM(total) as total_spent'))
->groupBy('customer_id') ->groupBy('customer_id')
@ -259,18 +263,18 @@ public function render(): View
->get(); ->get();
$this->todayTopCustomersChart = [ $this->todayTopCustomersChart = [
'labels' => $todayTopCustomers->map(fn ($o) => $o->customer?->user?->employee?->full_name ?? $o->customer?->user?->name ?? 'Customer #'.$o->customer_id)->toArray(), 'labels' => $todayTopCustomers->map(fn($o) => $o->customer?->user?->employee?->full_name ?? $o->customer?->user?->name ?? 'Customer #' . $o->customer_id)->toArray(),
'data' => $todayTopCustomers->pluck('total_spent')->map(fn ($val) => (int) $val)->toArray(), 'data' => $todayTopCustomers->pluck('total_spent')->map(fn($val) => (int) $val)->toArray(),
]; ];
// 7. Today's Discount Distribution (With Discount vs Regular) // 7. Today's Discount Distribution (With Discount vs Regular)
$withDiscount = Order::query() $withDiscount = Order::query()
->today() ->today()
->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->where('discount', '>', 0)->count(); ->where('discount', '>', 0)->count();
$noDiscount = Order::query() $noDiscount = Order::query()
->today() ->today()
->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) ->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds))
->where('discount', '<=', 0)->count(); ->where('discount', '<=', 0)->count();
$this->todayDiscountChart = [ $this->todayDiscountChart = [
@ -279,24 +283,26 @@ public function render(): View
]; ];
$this->stats = array_values(array_filter([ $this->stats = array_values(array_filter([
auth()->user()->hasRole(['Developer', 'Owner']) ? [ $user->hasRole(['Developer', 'Owner']) ? [
'title' => 'Average Order Value (AOV)', 'title' => 'Average Order Value (AOV)',
'value' => formatCurrencyNumber($todayAov, 'Rp'), 'value' => formatCurrencyNumber($todayAov, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayAov, 'Rp'), 'previous' => formatCurrencyNumber($yesterdayAov, 'Rp'),
'trend' => $yesterdayAov > 0 'trend' => $yesterdayAov > 0
? round((($todayAov - $yesterdayAov) / $yesterdayAov) * 100, 1).'%' ? round((($todayAov - $yesterdayAov) / $yesterdayAov) * 100, 1) . '%'
: '∞%', : '∞%',
'trendUp' => $todayAov > $yesterdayAov, 'trendUp' => $todayAov > $yesterdayAov,
'description' => 'Rata-rata nilai belanja pelanggan dalam satu kali checkout.',
] : null, ] : null,
auth()->user()->hasRole(['Developer', 'Owner']) ? [ $user->hasRole(['Developer', 'Owner']) ? [
'title' => 'Profit Margin', 'title' => 'Profit Margin',
'value' => round($todayProfitMargin, 1).'%', 'value' => round($todayProfitMargin, 1) . '%',
'previous' => round($yesterdayProfitMargin, 1).'%', 'previous' => round($yesterdayProfitMargin, 1) . '%',
'trend' => $yesterdayProfitMargin > 0 'trend' => $yesterdayProfitMargin > 0
? round($todayProfitMargin - $yesterdayProfitMargin, 1).'%' ? round($todayProfitMargin - $yesterdayProfitMargin, 1) . '%'
: '∞%', : '∞%',
'trendUp' => $todayProfitMargin > $yesterdayProfitMargin, 'trendUp' => $todayProfitMargin > $yesterdayProfitMargin,
'description' => 'Persentase keuntungan bersih dibandingkan dengan total pendapatan.',
] : null, ] : null,
[ [
@ -304,9 +310,10 @@ public function render(): View
'value' => $todayOrder, 'value' => $todayOrder,
'previous' => $yesterdayOrder, 'previous' => $yesterdayOrder,
'trend' => $yesterdayOrder > 0 'trend' => $yesterdayOrder > 0
? round((($todayOrder - $yesterdayOrder) / $yesterdayOrder) * 100, 1).'%' ? round((($todayOrder - $yesterdayOrder) / $yesterdayOrder) * 100, 1) . '%'
: '∞%', : '∞%',
'trendUp' => $todayOrder > $yesterdayOrder, 'trendUp' => $todayOrder > $yesterdayOrder,
'description' => 'Total frekuensi transaksi atau pesanan yang masuk selama periode ini.',
], ],
[ [
@ -314,31 +321,34 @@ public function render(): View
'value' => formatCurrencyNumber($todayIncome, 'Rp'), 'value' => formatCurrencyNumber($todayIncome, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayIncome, 'Rp'), 'previous' => formatCurrencyNumber($yesterdayIncome, 'Rp'),
'trend' => $yesterdayIncome > 0 'trend' => $yesterdayIncome > 0
? round((($todayIncome - $yesterdayIncome) / $yesterdayIncome) * 100, 1).'%' ? round((($todayIncome - $yesterdayIncome) / $yesterdayIncome) * 100, 1) . '%'
: '∞%', : '∞%',
'trendUp' => $todayIncome > $yesterdayIncome, 'trendUp' => $todayIncome > $yesterdayIncome,
'formatted' => formatCurrencyNumber($todayIncome, 'Rp'), 'formatted' => formatCurrencyNumber($todayIncome, 'Rp'),
'description' => 'Total nominal uang yang masuk dari penjualan (Omzet).',
], ],
auth()->user()->hasRole(['Developer', 'Owner']) ? [ $user->hasRole(['Developer', 'Owner']) ? [
'title' => 'HPP', 'title' => 'HPP',
'value' => formatCurrencyNumber($todayCogs, 'Rp'), 'value' => formatCurrencyNumber($todayCogs, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayCogs, 'Rp'), 'previous' => formatCurrencyNumber($yesterdayCogs, 'Rp'),
'trend' => $yesterdayCogs > 0 'trend' => $yesterdayCogs > 0
? round((($todayCogs - $yesterdayCogs) / $yesterdayCogs) * 100, 1).'%' ? round((($todayCogs - $yesterdayCogs) / $yesterdayCogs) * 100, 1) . '%'
: '∞%', : '∞%',
'trendUp' => $todayCogs > $yesterdayCogs, 'trendUp' => $todayCogs > $yesterdayCogs,
'formatted' => formatCurrencyNumber($todayCogs, 'Rp'), 'formatted' => formatCurrencyNumber($todayCogs, 'Rp'),
'description' => 'Harga Pokok Penjualan, yaitu total modal dari barang-barang yang terjual.',
] : null, ] : null,
[ [
'title' => 'Member Baru', 'title' => 'Member Baru',
'value' => $todayNewMembers.' Orang', 'value' => $todayNewMembers . ' Orang',
'previous' => $yesterdayNewMembers.' Orang', 'previous' => $yesterdayNewMembers . ' Orang',
'trend' => $yesterdayNewMembers > 0 'trend' => $yesterdayNewMembers > 0
? round((($todayNewMembers - $yesterdayNewMembers) / $yesterdayNewMembers) * 100, 1).'%' ? round((($todayNewMembers - $yesterdayNewMembers) / $yesterdayNewMembers) * 100, 1) . '%'
: '∞%', : '∞%',
'trendUp' => $todayNewMembers > $yesterdayNewMembers, 'trendUp' => $todayNewMembers > $yesterdayNewMembers,
'description' => 'Jumlah pelanggan baru yang mendaftar menjadi member selama periode ini.',
], ],
[ [
@ -346,10 +356,11 @@ public function render(): View
'value' => formatCurrencyNumber($todayDiscount, 'Rp'), 'value' => formatCurrencyNumber($todayDiscount, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayDiscount, 'Rp'), 'previous' => formatCurrencyNumber($yesterdayDiscount, 'Rp'),
'trend' => $yesterdayDiscount > 0 'trend' => $yesterdayDiscount > 0
? round((($todayDiscount - $yesterdayDiscount) / $yesterdayDiscount) * 100, 1).'%' ? round((($todayDiscount - $yesterdayDiscount) / $yesterdayDiscount) * 100, 1) . '%'
: '∞%', : '∞%',
'trendUp' => $todayDiscount > $yesterdayDiscount, 'trendUp' => $todayDiscount > $yesterdayDiscount,
'formatted' => formatCurrencyNumber($todayDiscount, 'Rp'), 'formatted' => formatCurrencyNumber($todayDiscount, 'Rp'),
'description' => 'Total nilai potongan harga (diskon) yang diberikan kepada pelanggan.',
], ],
[ [
@ -357,9 +368,10 @@ public function render(): View
'value' => $todayVoucher, 'value' => $todayVoucher,
'previous' => $yesterdayVoucher, 'previous' => $yesterdayVoucher,
'trend' => $yesterdayVoucher > 0 'trend' => $yesterdayVoucher > 0
? round((($todayVoucher - $yesterdayVoucher) / $yesterdayVoucher) * 100, 1).'%' ? round((($todayVoucher - $yesterdayVoucher) / $yesterdayVoucher) * 100, 1) . '%'
: '∞%', : '∞%',
'trendUp' => $todayVoucher > $yesterdayVoucher, 'trendUp' => $todayVoucher > $yesterdayVoucher,
'description' => 'Jumlah transaksi yang menggunakan voucher diskon selama periode ini.',
], ],
[ [
@ -367,59 +379,65 @@ public function render(): View
'value' => formatCurrencyNumber($todayExpense, 'Rp'), 'value' => formatCurrencyNumber($todayExpense, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayExpense, 'Rp'), 'previous' => formatCurrencyNumber($yesterdayExpense, 'Rp'),
'trend' => $yesterdayExpense > 0 'trend' => $yesterdayExpense > 0
? round((($todayExpense - $yesterdayExpense) / $yesterdayExpense) * 100, 1).'%' ? round((($todayExpense - $yesterdayExpense) / $yesterdayExpense) * 100, 1) . '%'
: '∞%', : '∞%',
'trendUp' => $todayExpense > $yesterdayExpense, 'trendUp' => $todayExpense > $yesterdayExpense,
'description' => 'Total biaya operasional atau pengeluaran toko yang tercatat selama periode ini.',
], ],
auth()->user()->hasRole(['Developer', 'Owner']) ? [ $user->hasRole(['Developer', 'Owner']) ? [
'title' => 'Belanja Barang', 'title' => 'Belanja Barang',
'value' => formatCurrencyNumber($todayPurchase, 'Rp'), 'value' => formatCurrencyNumber($todayPurchase, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayPurchase, 'Rp'), 'previous' => formatCurrencyNumber($yesterdayPurchase, 'Rp'),
'trend' => $yesterdayPurchase > 0 'trend' => $yesterdayPurchase > 0
? round((($todayPurchase - $yesterdayPurchase) / $yesterdayPurchase) * 100, 1).'%' ? round((($todayPurchase - $yesterdayPurchase) / $yesterdayPurchase) * 100, 1) . '%'
: '∞%', : '∞%',
'trendUp' => $todayPurchase > $yesterdayPurchase, 'trendUp' => $todayPurchase > $yesterdayPurchase,
'description' => 'Total pengeluaran untuk pembelian stok barang atau inventaris selama periode ini.',
] : null, ] : null,
auth()->user()->hasRole(['Developer', 'Owner']) ? [ $user->hasRole(['Developer', 'Owner']) ? [
'title' => 'Laba Kotor', 'title' => 'Laba Kotor',
'value' => formatCurrencyNumber($todayGrossProfit, 'Rp'), 'value' => formatCurrencyNumber($todayGrossProfit, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayGrossProfit, 'Rp'), 'previous' => formatCurrencyNumber($yesterdayGrossProfit, 'Rp'),
'trend' => $yesterdayGrossProfit > 0 'trend' => $yesterdayGrossProfit > 0
? round((($todayGrossProfit - $yesterdayGrossProfit) / $yesterdayGrossProfit) * 100, 1).'%' ? round((($todayGrossProfit - $yesterdayGrossProfit) / $yesterdayGrossProfit) * 100, 1) . '%'
: '∞%', : '∞%',
'trendUp' => $todayGrossProfit > $yesterdayGrossProfit, 'trendUp' => $todayGrossProfit > $yesterdayGrossProfit,
'description' => 'Pendapatan dikurangi dengan HPP dan Diskon.',
] : null, ] : null,
auth()->user()->hasRole(['Developer', 'Owner']) ? [ $user->hasRole(['Developer', 'Owner']) ? [
'title' => 'Laba Bersih', 'title' => 'Laba Bersih',
'value' => formatCurrencyNumber($todayNetProfit, 'Rp'), 'value' => formatCurrencyNumber($todayNetProfit, 'Rp'),
'previous' => formatCurrencyNumber($yesterdayNetProfit, 'Rp'), 'previous' => formatCurrencyNumber($yesterdayNetProfit, 'Rp'),
'trend' => $yesterdayNetProfit > 0 'trend' => $yesterdayNetProfit > 0
? round((($todayNetProfit - $yesterdayNetProfit) / $yesterdayNetProfit) * 100, 1).'%' ? round((($todayNetProfit - $yesterdayNetProfit) / $yesterdayNetProfit) * 100, 1) . '%'
: '∞%', : '∞%',
'trendUp' => $todayNetProfit > $yesterdayNetProfit, 'trendUp' => $todayNetProfit > $yesterdayNetProfit,
'description' => 'Laba kotor dikurangi dengan beban operasional dan gaji.',
] : null, ] : null,
[ [
'title' => 'Parfum Terjual', 'title' => 'Parfum Terjual',
'value' => $todaySoldPerfume.' ml', 'value' => $todaySoldPerfume . ' ml',
'previous' => $yesterdaySoldPerfume.' ml', 'previous' => $yesterdaySoldPerfume . ' ml',
'trend' => $yesterdaySoldPerfume > 0 'trend' => $yesterdaySoldPerfume > 0
? round((($todaySoldPerfume - $yesterdaySoldPerfume) / $yesterdaySoldPerfume) * 100, 1).'%' ? round((($todaySoldPerfume - $yesterdaySoldPerfume) / $yesterdaySoldPerfume) * 100, 1) . '%'
: '∞%', : '∞%',
'trendUp' => $todaySoldPerfume > $yesterdaySoldPerfume, 'trendUp' => $todaySoldPerfume > $yesterdaySoldPerfume,
'description' => 'Total volume cairan parfum yang terjual dalam satuan mililiter (ml).',
], ],
[ [
'title' => 'Top Parfum', 'title' => 'Top Parfum',
'value' => optional($todayTopPerfume?->orderable)->name.' ('.$todayTopPerfume?->total_sold.'ml)' ?? '-', 'value' => optional($todayTopPerfume?->orderable)->name . ' (' . $todayTopPerfume?->total_sold . 'ml)' ?? '-',
'previous' => optional($yesterdayTopPerfume?->orderable)->name.'('.$yesterdayTopPerfume?->total_sold.'ml)' ?? '-', 'previous' => optional($yesterdayTopPerfume?->orderable)->name . '(' . $yesterdayTopPerfume?->total_sold . 'ml)' ?? '-',
'trend' => ($yesterdayTopPerfume && $yesterdayTopPerfume->total_sold > 0) 'trend' => ($yesterdayTopPerfume && $yesterdayTopPerfume->total_sold > 0)
? round((($todayTopPerfume?->total_sold - $yesterdayTopPerfume?->total_sold) / $yesterdayTopPerfume?->total_sold) * 100, 1).'%' ? round((($todayTopPerfume?->total_sold - $yesterdayTopPerfume?->total_sold) / $yesterdayTopPerfume?->total_sold) * 100, 1) . '%'
: '∞%', : '∞%',
'trendUp' => ($todayTopPerfume?->total_sold ?? 0) > ($yesterdayTopPerfume?->total_sold ?? 0), 'trendUp' => ($todayTopPerfume?->total_sold ?? 0) > ($yesterdayTopPerfume?->total_sold ?? 0),
'description' => 'Nama varian parfum yang paling banyak terjual hari ini.',
], ],
])); ]));

View File

@ -45,8 +45,20 @@
<flux:heading>Counting Data</flux:heading> <flux:heading>Counting Data</flux:heading>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-3 mb-6 mt-2"> <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-3 mb-6 mt-2">
@foreach ($countingData as $item) @foreach ($countingData as $item)
<div class="rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700"> <div
class="relative rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700 transition-all duration-300 hover:scale-[1.02] hover:shadow-lg dark:hover:shadow-zinc-900/50">
<div class="flex items-start justify-between">
<flux:subheading>{{ $item['title'] }}</flux:subheading> <flux:subheading>{{ $item['title'] }}</flux:subheading>
@if (isset($item['description']))
<div class="absolute top-2 right-2">
<flux:tooltip toggleable content="{{ $item['description'] }}">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent hover:bg-zinc-300 dark:hover:bg-zinc-600" />
</flux:tooltip>
</div>
@endif
</div>
<flux:heading size="xl" class="mb-2">{{ $item['value'] }}</flux:heading> <flux:heading size="xl" class="mb-2">{{ $item['value'] }}</flux:heading>
</div> </div>
@endforeach @endforeach
@ -55,8 +67,20 @@
<flux:heading>Order</flux:heading> <flux:heading>Order</flux:heading>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 mb-6 mt-2"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 mb-6 mt-2">
@foreach ($orders as $order) @foreach ($orders as $order)
<div class="rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700"> <div
class="relative rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700 transition-all duration-300 hover:scale-[1.02] hover:shadow-lg dark:hover:shadow-zinc-900/50">
<div class="flex items-start justify-between">
<flux:subheading>{{ $order['title'] }}</flux:subheading> <flux:subheading>{{ $order['title'] }}</flux:subheading>
@if (isset($order['description']))
<div class="absolute top-2 right-2">
<flux:tooltip toggleable content="{{ $order['description'] }}">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent hover:bg-zinc-300 dark:hover:bg-zinc-600" />
</flux:tooltip>
</div>
@endif
</div>
<flux:heading size="xl" class="mb-2">{{ $order['value'] }}</flux:heading> <flux:heading size="xl" class="mb-2">{{ $order['value'] }}</flux:heading>
</div> </div>
@endforeach @endforeach
@ -65,8 +89,20 @@
<flux:heading>Pengeluaran</flux:heading> <flux:heading>Pengeluaran</flux:heading>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6 mt-2"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6 mt-2">
@foreach ($expenses as $expense) @foreach ($expenses as $expense)
<div class="rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700"> <div
class="relative rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700 transition-all duration-300 hover:scale-[1.02] hover:shadow-lg dark:hover:shadow-zinc-900/50">
<div class="flex items-start justify-between">
<flux:subheading>{{ $expense['title'] }}</flux:subheading> <flux:subheading>{{ $expense['title'] }}</flux:subheading>
@if (isset($expense['description']))
<div class="absolute top-2 right-2">
<flux:tooltip toggleable content="{{ $expense['description'] }}">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent hover:bg-zinc-300 dark:hover:bg-zinc-600" />
</flux:tooltip>
</div>
@endif
</div>
<flux:heading size="xl" class="mb-2">{{ $expense['value'] }}</flux:heading> <flux:heading size="xl" class="mb-2">{{ $expense['value'] }}</flux:heading>
</div> </div>
@endforeach @endforeach
@ -74,31 +110,75 @@
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8" x-data="analysisCharts"> <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8" x-data="analysisCharts">
<!-- 1. Revenue & Profit Trend --> <!-- 1. Revenue & Profit Trend -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Trend perbandingan pendapatan dan laba bersih perusahaan selama 30 hari terakhir.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Trend Pendapatan & Laba (30 Hari Terakhir)</flux:heading> <flux:heading class="mb-4">Trend Pendapatan & Laba (30 Hari Terakhir)</flux:heading>
<div class="h-80"><canvas id="revenueTrendChart"></canvas></div> <div class="h-80"><canvas id="revenueTrendChart"></canvas></div>
</flux:card> </flux:card>
<!-- 6. Member Growth (Area Chart) - Cumulative --> <!-- 6. Member Growth (Area Chart) - Cumulative -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable content="Grafik akumulasi pertumbuhan jumlah member dari waktu ke waktu.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Pertumbuhan Member (Akumulasi)</flux:heading> <flux:heading class="mb-4">Pertumbuhan Member (Akumulasi)</flux:heading>
<div class="h-80"><canvas id="memberGrowthChart"></canvas></div> <div class="h-80"><canvas id="memberGrowthChart"></canvas></div>
</flux:card> </flux:card>
<!-- 2. busiest hour --> <!-- 2. busiest hour -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Identifikasi jam-jam dengan volume transaksi tertinggi berdasarkan waktu pemesanan.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Jam Sibuk (Total Order per Jam)</flux:heading> <flux:heading class="mb-4">Jam Sibuk (Total Order per Jam)</flux:heading>
<div class="h-80"><canvas id="hourlySalesChart"></canvas></div> <div class="h-80"><canvas id="hourlySalesChart"></canvas></div>
</flux:card> </flux:card>
<!-- 5. Outlet Performance --> <!-- 5. Outlet Performance -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Perbandingan total pendapatan antar outlet yang terdaftar di sistem.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Performa Outlet (Pendapatan)</flux:heading> <flux:heading class="mb-4">Performa Outlet (Pendapatan)</flux:heading>
<div class="h-80"><canvas id="outletPerformanceChart"></canvas></div> <div class="h-80"><canvas id="outletPerformanceChart"></canvas></div>
</flux:card> </flux:card>
<!-- 3. Product Category Distribution --> <!-- 3. Product Category Distribution -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Pembagian pendapatan berdasarkan kategori produk (Parfum, Produk, Botol).">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Distribusi Penjualan</flux:heading> <flux:heading class="mb-4">Distribusi Penjualan</flux:heading>
<div class="h-80 flex items-center justify-center"> <div class="h-80 flex items-center justify-center">
<canvas id="categoryDistributionChart"></canvas> <canvas id="categoryDistributionChart"></canvas>
@ -106,7 +186,16 @@
</flux:card> </flux:card>
<!-- 4. Payment Method --> <!-- 4. Payment Method -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Persentase penggunaan berbagai metode pembayaran oleh pelanggan.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Metode Pembayaran</flux:heading> <flux:heading class="mb-4">Metode Pembayaran</flux:heading>
<div class="h-80 flex items-center justify-center"> <div class="h-80 flex items-center justify-center">
<canvas id="paymentMethodChart"></canvas> <canvas id="paymentMethodChart"></canvas>
@ -114,13 +203,31 @@
</flux:card> </flux:card>
<!-- 7. Day of Week Sales --> <!-- 7. Day of Week Sales -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Analisa hari dalam seminggu saat volume penjualan paling tinggi.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Performa Penjualan per Hari</flux:heading> <flux:heading class="mb-4">Performa Penjualan per Hari</flux:heading>
<div class="h-80"><canvas id="dayOfWeekSalesChart"></canvas></div> <div class="h-80"><canvas id="dayOfWeekSalesChart"></canvas></div>
</flux:card> </flux:card>
<!-- 8. Customer Type Distribution --> <!-- 8. Customer Type Distribution -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Perbandingan jumlah transaksi antara pelanggan terdaftar (Member) dan pelanggan umum (Guest).">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Segmentasi Pelanggan (Member vs Guest)</flux:heading> <flux:heading class="mb-4">Segmentasi Pelanggan (Member vs Guest)</flux:heading>
<div class="h-80 flex items-center justify-center"> <div class="h-80 flex items-center justify-center">
<canvas id="customerTypeChart"></canvas> <canvas id="customerTypeChart"></canvas>
@ -128,19 +235,44 @@
</flux:card> </flux:card>
<!-- 9. Transaction Trend --> <!-- 9. Transaction Trend -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable content="Trend jumlah transaksi harian dalam sebulan terakhir.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Trend Volume Transaksi (30 Hari)</flux:heading> <flux:heading class="mb-4">Trend Volume Transaksi (30 Hari)</flux:heading>
<div class="h-80"><canvas id="transactionTrendChart"></canvas></div> <div class="h-80"><canvas id="transactionTrendChart"></canvas></div>
</flux:card> </flux:card>
<!-- 10. AOV Trend --> <!-- 10. AOV Trend -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable content="Trend rata-rata nilai transaksi (Average Order Value).">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Trend Rata-rata Belanja (AOV)</flux:heading> <flux:heading class="mb-4">Trend Rata-rata Belanja (AOV)</flux:heading>
<div class="h-80"><canvas id="aovTrendChart"></canvas></div> <div class="h-80"><canvas id="aovTrendChart"></canvas></div>
</flux:card> </flux:card>
<!-- 11. Expense Breakdown --> <!-- 11. Expense Breakdown -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Perbandingan antara biaya operasional toko dengan biaya gaji karyawan.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Komposisi Biaya Operasional</flux:heading> <flux:heading class="mb-4">Komposisi Biaya Operasional</flux:heading>
<div class="h-80 flex items-center justify-center"> <div class="h-80 flex items-center justify-center">
<canvas id="expenseBreakdownChart"></canvas> <canvas id="expenseBreakdownChart"></canvas>
@ -148,7 +280,16 @@
</flux:card> </flux:card>
<!-- 12. Revenue vs Cost --> <!-- 12. Revenue vs Cost -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Perbandingan antara pendapatan (omzet) dengan total seluruh pengeluaran.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Omzet vs Total Biaya</flux:heading> <flux:heading class="mb-4">Omzet vs Total Biaya</flux:heading>
<div class="h-80 flex items-center justify-center"> <div class="h-80 flex items-center justify-center">
<canvas id="revenueVsCostChart"></canvas> <canvas id="revenueVsCostChart"></canvas>
@ -156,7 +297,16 @@
</flux:card> </flux:card>
<!-- 13. Customer Retention --> <!-- 13. Customer Retention -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Analisa persentase pelanggan setia (repeat order) dibandingkan pelanggan baru.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Analisa Pelanggan (Baru vs Lama)</flux:heading> <flux:heading class="mb-4">Analisa Pelanggan (Baru vs Lama)</flux:heading>
<div class="h-80 flex items-center justify-center"> <div class="h-80 flex items-center justify-center">
<canvas id="customerRetentionChart"></canvas> <canvas id="customerRetentionChart"></canvas>
@ -164,7 +314,16 @@
</flux:card> </flux:card>
<!-- 14. Voucher Activity --> <!-- 14. Voucher Activity -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Grafik efektivitas penggunaan voucher diskon dari waktu ke waktu.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Efektivitas Voucher</flux:heading> <flux:heading class="mb-4">Efektivitas Voucher</flux:heading>
<div class="h-80"><canvas id="voucherUsageTrendChart"></canvas></div> <div class="h-80"><canvas id="voucherUsageTrendChart"></canvas></div>
</flux:card> </flux:card>
@ -173,7 +332,8 @@
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6 mt-2"> <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6 mt-2">
<div> <div>
<flux:heading>Top 5 Parfum</flux:heading> <flux:heading>Top 5 Parfum</flux:heading>
<flux:card class="mb-6 mt-2 bg-zinc-200 dark:bg-zinc-700"> <flux:card
class="mb-6 mt-2 bg-zinc-200 dark:bg-zinc-700 transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50">
<flux:table> <flux:table>
<flux:table.columns> <flux:table.columns>
<flux:table.column>No</flux:table.column> <flux:table.column>No</flux:table.column>
@ -201,7 +361,8 @@
<div> <div>
<flux:heading>Top 5 Produk</flux:heading> <flux:heading>Top 5 Produk</flux:heading>
<flux:card class="mb-6 mt-2 bg-zinc-200 dark:bg-zinc-700"> <flux:card
class="mb-6 mt-2 bg-zinc-200 dark:bg-zinc-700 transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50">
<flux:table> <flux:table>
<flux:table.columns> <flux:table.columns>
<flux:table.column>No</flux:table.column> <flux:table.column>No</flux:table.column>
@ -229,7 +390,8 @@
<div> <div>
<flux:heading>Top 5 Botol</flux:heading> <flux:heading>Top 5 Botol</flux:heading>
<flux:card class="mb-6 mt-2 bg-zinc-200 dark:bg-zinc-700"> <flux:card
class="mb-6 mt-2 bg-zinc-200 dark:bg-zinc-700 transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50">
<flux:table> <flux:table>
<flux:table.columns> <flux:table.columns>
<flux:table.column>No</flux:table.column> <flux:table.column>No</flux:table.column>
@ -246,7 +408,8 @@
<flux:table.cell>{{ $topBottle['name'] }}</flux:table.cell> <flux:table.cell>{{ $topBottle['name'] }}</flux:table.cell>
<flux:table.cell>{{ $topBottle['sale_price'] }}</flux:table.cell> <flux:table.cell>{{ $topBottle['sale_price'] }}</flux:table.cell>
<flux:table.cell>{{ $topBottle['total_sold'] }}</flux:table.cell> <flux:table.cell>{{ $topBottle['total_sold'] }}</flux:table.cell>
<flux:table.cell variant="strong">{{ $topBottle['total_sales'] }}</flux:table.cell> <flux:table.cell variant="strong">{{ $topBottle['total_sales'] }}
</flux:table.cell>
</flux:table.row> </flux:table.row>
@endforeach @endforeach
</flux:table.rows> </flux:table.rows>
@ -274,7 +437,10 @@
return JSON.parse(JSON.stringify(data)); return JSON.parse(JSON.stringify(data));
} catch (e) { } catch (e) {
// Fallback for safety // Fallback for safety
return { labels: [], datasets: [] }; return {
labels: [],
datasets: []
};
} }
}, },
@ -291,19 +457,31 @@
// Watchers // Watchers
this.$watch('$wire.revenueTrendChart', () => this.updateChart('revenue', this.getRevenueData)); this.$watch('$wire.revenueTrendChart', () => this.updateChart('revenue', this.getRevenueData));
this.$watch('$wire.memberGrowthChart', () => this.updateChart('memberGrowth', this.getMemberGrowthData)); this.$watch('$wire.memberGrowthChart', () => this.updateChart('memberGrowth', this
this.$watch('$wire.hourlySalesChart', () => this.updateChart('hourlySales', this.getHourlySalesData)); .getMemberGrowthData));
this.$watch('$wire.outletPerformanceChart', () => this.updateChart('outletPerformance', this.getOutletPerformanceData)); this.$watch('$wire.hourlySalesChart', () => this.updateChart('hourlySales', this
this.$watch('$wire.categoryDistributionChart', () => this.updateChart('categoryDist', this.getCategoryDistributionData)); .getHourlySalesData));
this.$watch('$wire.paymentMethodChart', () => this.updateChart('paymentMethod', this.getPaymentMethodData)); this.$watch('$wire.outletPerformanceChart', () => this.updateChart('outletPerformance', this
this.$watch('$wire.dayOfWeekSalesChart', () => this.updateChart('dayOfWeekSales', this.getDayOfWeekData)); .getOutletPerformanceData));
this.$watch('$wire.customerTypeChart', () => this.updateChart('customerType', this.getCustomerTypeData)); this.$watch('$wire.categoryDistributionChart', () => this.updateChart('categoryDist', this
this.$watch('$wire.transactionTrendChart', () => this.updateChart('transactionTrend', this.getTransactionTrendData)); .getCategoryDistributionData));
this.$watch('$wire.paymentMethodChart', () => this.updateChart('paymentMethod', this
.getPaymentMethodData));
this.$watch('$wire.dayOfWeekSalesChart', () => this.updateChart('dayOfWeekSales', this
.getDayOfWeekData));
this.$watch('$wire.customerTypeChart', () => this.updateChart('customerType', this
.getCustomerTypeData));
this.$watch('$wire.transactionTrendChart', () => this.updateChart('transactionTrend', this
.getTransactionTrendData));
this.$watch('$wire.aovTrendChart', () => this.updateChart('aovTrend', this.getAovTrendData)); this.$watch('$wire.aovTrendChart', () => this.updateChart('aovTrend', this.getAovTrendData));
this.$watch('$wire.expenseBreakdownChart', () => this.updateChart('expenseBreakdown', this.getExpenseBreakdownData)); this.$watch('$wire.expenseBreakdownChart', () => this.updateChart('expenseBreakdown', this
this.$watch('$wire.revenueVsCostChart', () => this.updateChart('revenueVsCost', this.getRevenueVsCostData)); .getExpenseBreakdownData));
this.$watch('$wire.customerRetentionChart', () => this.updateChart('customerRetention', this.getCustomerRetentionData)); this.$watch('$wire.revenueVsCostChart', () => this.updateChart('revenueVsCost', this
this.$watch('$wire.voucherUsageTrendChart', () => this.updateChart('voucherUsage', this.getVoucherUsageData)); .getRevenueVsCostData));
this.$watch('$wire.customerRetentionChart', () => this.updateChart('customerRetention', this
.getCustomerRetentionData));
this.$watch('$wire.voucherUsageTrendChart', () => this.updateChart('voucherUsage', this
.getVoucherUsageData));
}, },
updateChart(key, getDataFn) { updateChart(key, getDataFn) {
@ -311,7 +489,7 @@
try { try {
charts[key].data = getDataFn.call(this); charts[key].data = getDataFn.call(this);
charts[key].update(); charts[key].update();
} catch(e) { } catch (e) {
console.error(`Error updating chart ${key}`, e); console.error(`Error updating chart ${key}`, e);
} }
} else { } else {
@ -360,16 +538,23 @@
initRevenueTrend() { initRevenueTrend() {
this.createChart('revenueTrendChart', 'revenue', 'line', this.getRevenueData(), { this.createChart('revenueTrendChart', 'revenue', 'line', this.getRevenueData(), {
plugins: { legend: { position: 'top' } }, plugins: {
scales: { y: { beginAtZero: true } } legend: {
position: 'top'
}
},
scales: {
y: {
beginAtZero: true
}
}
}); });
}, },
getRevenueData() { getRevenueData() {
const data = this.unwrap($wire.revenueTrendChart); const data = this.unwrap($wire.revenueTrendChart);
return { return {
labels: data.labels || [], labels: data.labels || [],
datasets: [ datasets: [{
{
label: 'Pendapatan', label: 'Pendapatan',
data: data.revenue || [], data: data.revenue || [],
borderColor: '#10b981', borderColor: '#10b981',
@ -391,7 +576,11 @@
initMemberGrowth() { initMemberGrowth() {
this.createChart('memberGrowthChart', 'memberGrowth', 'line', this.getMemberGrowthData(), { this.createChart('memberGrowthChart', 'memberGrowth', 'line', this.getMemberGrowthData(), {
scales: { y: { beginAtZero: true } } scales: {
y: {
beginAtZero: true
}
}
}); });
}, },
getMemberGrowthData() { getMemberGrowthData() {
@ -425,7 +614,8 @@
}, },
initOutletPerformance() { initOutletPerformance() {
this.createChart('outletPerformanceChart', 'outletPerformance', 'bar', this.getOutletPerformanceData(), { this.createChart('outletPerformanceChart', 'outletPerformance', 'bar', this
.getOutletPerformanceData(), {
indexAxis: 'y' indexAxis: 'y'
}); });
}, },
@ -442,7 +632,8 @@
}, },
initCategoryDistribution() { initCategoryDistribution() {
this.createChart('categoryDistributionChart', 'categoryDist', 'doughnut', this.getCategoryDistributionData()); this.createChart('categoryDistributionChart', 'categoryDist', 'doughnut', this
.getCategoryDistributionData());
}, },
getCategoryDistributionData() { getCategoryDistributionData() {
const data = this.unwrap($wire.categoryDistributionChart); const data = this.unwrap($wire.categoryDistributionChart);
@ -499,7 +690,8 @@
}, },
initTransactionTrend() { initTransactionTrend() {
this.createChart('transactionTrendChart', 'transactionTrend', 'line', this.getTransactionTrendData()); this.createChart('transactionTrendChart', 'transactionTrend', 'line', this
.getTransactionTrendData());
}, },
getTransactionTrendData() { getTransactionTrendData() {
const data = this.unwrap($wire.transactionTrendChart); const data = this.unwrap($wire.transactionTrendChart);
@ -535,7 +727,8 @@
}, },
initExpenseBreakdown() { initExpenseBreakdown() {
this.createChart('expenseBreakdownChart', 'expenseBreakdown', 'doughnut', this.getExpenseBreakdownData()); this.createChart('expenseBreakdownChart', 'expenseBreakdown', 'doughnut', this
.getExpenseBreakdownData());
}, },
getExpenseBreakdownData() { getExpenseBreakdownData() {
const data = this.unwrap($wire.expenseBreakdownChart); const data = this.unwrap($wire.expenseBreakdownChart);
@ -564,7 +757,8 @@
}, },
initCustomerRetention() { initCustomerRetention() {
this.createChart('customerRetentionChart', 'customerRetention', 'bar', this.getCustomerRetentionData()); this.createChart('customerRetentionChart', 'customerRetention', 'bar', this
.getCustomerRetentionData());
}, },
getCustomerRetentionData() { getCustomerRetentionData() {
const data = this.unwrap($wire.customerRetentionChart); const data = this.unwrap($wire.customerRetentionChart);

View File

@ -18,9 +18,20 @@
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 mb-6 mt-2"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 mb-6 mt-2">
@foreach ($stats as $stat) @foreach ($stats as $stat)
<div <div
class="@if ($stat['title'] === 'Top Parfum') md:col-span-2 xl:col-span-2 @endif rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700"> class="relative @if ($stat['title'] === 'Top Parfum') md:col-span-2 xl:col-span-2 @endif rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700 transition-all duration-300 hover:scale-[1.02] hover:shadow-lg dark:hover:shadow-zinc-900/50">
<div class="flex items-start justify-between">
<flux:subheading>{{ $stat['title'] }}</flux:subheading> <flux:subheading>{{ $stat['title'] }}</flux:subheading>
<div class="flex items-center justify-between">
@if (isset($stat['description']))
<div class="absolute top-2 right-2">
<flux:tooltip toggleable content="{{ $stat['description'] }}">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!px-1 border-none bg-transparent hover:bg-zinc-300 dark:hover:bg-zinc-600" />
</flux:tooltip>
</div>
@endif
</div>
<div class="flex items-center justify-between mt-1">
<flux:heading size="xl">{{ $stat['value'] }}</flux:heading> <flux:heading size="xl">{{ $stat['value'] }}</flux:heading>
<flux:subheading size="xs">{{ $stat['previous'] }}</flux:subheading> <flux:subheading size="xs">{{ $stat['previous'] }}</flux:subheading>
</div> </div>
@ -35,7 +46,16 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mt-8"> <div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mt-8">
<!-- 1. Today vs Yesterday Hourly Comparison --> <!-- 1. Today vs Yesterday Hourly Comparison -->
<flux:card class="md:col-span-2 xl:col-span-2" wire:ignore> <flux:card
class="relative md:col-span-2 xl:col-span-2 transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Grafik perbandingan jumlah transaksi tiap jam antara hari ini dan kemarin.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Perbandingan Order per Jam (Hari Ini vs Kemarin)</flux:heading> <flux:heading class="mb-4">Perbandingan Order per Jam (Hari Ini vs Kemarin)</flux:heading>
<div class="h-80"> <div class="h-80">
<canvas id="hourlyComparisonChart"></canvas> <canvas id="hourlyComparisonChart"></canvas>
@ -43,7 +63,16 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g
</flux:card> </flux:card>
<!-- 2. Today Category Distribution --> <!-- 2. Today Category Distribution -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Pembagian pendapatan berdasarkan kategori produk (Parfum, Produk, Botol).">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Distribusi Penjualan</flux:heading> <flux:heading class="mb-4">Distribusi Penjualan</flux:heading>
<div class="h-80 flex items-center justify-center"> <div class="h-80 flex items-center justify-center">
<canvas id="todayCategoryChart"></canvas> <canvas id="todayCategoryChart"></canvas>
@ -51,7 +80,15 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g
</flux:card> </flux:card>
<!-- 3. Today Payment Methods --> <!-- 3. Today Payment Methods -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable content="Persentase penggunaan berbagai metode pembayaran oleh pelanggan.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Metode Pembayaran</flux:heading> <flux:heading class="mb-4">Metode Pembayaran</flux:heading>
<div class="h-80 flex items-center justify-center"> <div class="h-80 flex items-center justify-center">
<canvas id="todayPaymentChart"></canvas> <canvas id="todayPaymentChart"></canvas>
@ -59,7 +96,16 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g
</flux:card> </flux:card>
<!-- 4. Today Customer Type --> <!-- 4. Today Customer Type -->
<flux:card wire:ignore> <flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Perbandingan jumlah transaksi antara pelanggan terdaftar (Member) dan pelanggan umum (Guest).">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Member vs Guest</flux:heading> <flux:heading class="mb-4">Member vs Guest</flux:heading>
<div class="h-80 flex items-center justify-center"> <div class="h-80 flex items-center justify-center">
<canvas id="todayCustomerTypeChart"></canvas> <canvas id="todayCustomerTypeChart"></canvas>
@ -67,7 +113,14 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g
</flux:card> </flux:card>
<!-- 5. Today Top Selling Items --> <!-- 5. Today Top Selling Items -->
<flux:card wire:ignore> <flux:card class="relative" wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Lima produk dengan jumlah unit terjual terbanyak selama periode ini.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">5 Item Terlaris</flux:heading> <flux:heading class="mb-4">5 Item Terlaris</flux:heading>
<div class="h-80 flex items-center justify-center"> <div class="h-80 flex items-center justify-center">
<canvas id="todayTopSellingChart"></canvas> <canvas id="todayTopSellingChart"></canvas>
@ -75,7 +128,14 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g
</flux:card> </flux:card>
<!-- 6. Today Top Customers --> <!-- 6. Today Top Customers -->
<flux:card wire:ignore> <flux:card class="relative" wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Lima pelanggan dengan total nominal belanja tertinggi selama periode ini.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Top 5 Pelanggan</flux:heading> <flux:heading class="mb-4">Top 5 Pelanggan</flux:heading>
<div class="h-80 flex items-center justify-center"> <div class="h-80 flex items-center justify-center">
<canvas id="todayTopCustomersChart"></canvas> <canvas id="todayTopCustomersChart"></canvas>
@ -83,7 +143,14 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g
</flux:card> </flux:card>
<!-- 7. Today Discount Distribution --> <!-- 7. Today Discount Distribution -->
<flux:card wire:ignore> <flux:card class="relative" wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Perbandingan jumlah transaksi antara pelanggan terdaftar (Member) dan pelanggan umum (Guest).">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Normal vs Diskon</flux:heading> <flux:heading class="mb-4">Normal vs Diskon</flux:heading>
<div class="h-80 flex items-center justify-center"> <div class="h-80 flex items-center justify-center">
<canvas id="todayDiscountChart"></canvas> <canvas id="todayDiscountChart"></canvas>