From 3b289588f2a9adff206dd10ba8a4c5f008f43c89 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Thu, 12 Feb 2026 15:11:48 +0700 Subject: [PATCH] feat: Add tooltips and hover effects to dashboard cards and stats for improved user experience, and include a description for the outlet statistic. --- app/Livewire/Studio/Dashboard/Analysis.php | 142 ++++---- app/Livewire/Studio/Dashboard/Overview.php | 146 +++++---- .../studio/dashboard/analysis.blade.php | 306 ++++++++++++++---- .../studio/dashboard/overview.blade.php | 91 +++++- 4 files changed, 498 insertions(+), 187 deletions(-) diff --git a/app/Livewire/Studio/Dashboard/Analysis.php b/app/Livewire/Studio/Dashboard/Analysis.php index 68f6743..9a90015 100644 --- a/app/Livewire/Studio/Dashboard/Analysis.php +++ b/app/Livewire/Studio/Dashboard/Analysis.php @@ -83,11 +83,15 @@ class Analysis extends Component 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 { + /** @var \App\Models\User $user */ + $user = auth()->user(); $outletIds = $this->selectedOutletIds; $period = $this->period; $range = $this->range; @@ -119,79 +123,90 @@ public function render(): View [ 'title' => 'Outlet', 'value' => Outlet::when($dateQuery || ($startDate && $endDate), $filterDate)->count(), + 'description' => 'Total jumlah outlet atau cabang toko yang terdaftar.', ], [ 'title' => 'Pegawai', '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) ->withoutDeveloper() ->count(), + 'description' => 'Jumlah karyawan aktif yang terdaftar di outlet terpilih.', ], [ 'title' => 'Tier', 'value' => Tier::when($dateQuery || ($startDate && $endDate), $filterDate)->count(), + 'description' => 'Jumlah tingkatan loyalitas member (seperti Bronze, Silver, Gold).', ], [ '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) ->active() ->count(), + 'description' => 'Jumlah kode promo atau voucher yang masih berlaku saat ini.', ], [ 'title' => 'Member', '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) ->count(), + 'description' => 'Total pelanggan yang sudah mendaftar sebagai member resmi.', ], [ 'title' => 'Customer', 'value' => Customer::whereDoesntHave('user') ->when($dateQuery || ($startDate && $endDate), $filterDate) ->count(), + 'description' => 'Jumlah pelanggan umum (guest) yang pernah melakukan transaksi.', ], [ 'title' => 'Kategori', 'value' => Category::when($dateQuery || ($startDate && $endDate), $filterDate)->count(), + 'description' => 'Jumlah kategori barang (seperti Parfum, Sabun, dll).', ], [ 'title' => 'Merek', 'value' => Brand::when($dateQuery || ($startDate && $endDate), $filterDate)->count(), + 'description' => 'Jumlah merek parfum yang terdaftar di sistem.', ], [ '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) ->count(), + 'description' => 'Jumlah varian bibit parfum yang tersedia dalam katalog.', ], [ '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) ->count(), + 'description' => 'Jumlah item produk jadi (seperti aksesoris) yang terdaftar.', ], [ '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) ->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); $totalIncome = (clone $orderQuery)->sum('total'); $totalCogs = (clone $orderQuery)->sum('cogs'); $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); $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) ->paid(); $totalPayroll = $payrollQuery->sum('total_salary'); @@ -200,7 +215,7 @@ public function render(): View $netProfit = $grossProfit - $totalExpense - $totalPayroll; $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; $profitMargin = $totalIncome > 0 ? ($netProfit / $totalIncome) * 100 : 0; @@ -210,7 +225,7 @@ public function render(): View ->whereNotNull('customer_id') ->groupBy('customer_id') ->get(); - $repeatCustomers = $customerOrders->filter(fn ($c) => $c->count > 1)->count(); + $repeatCustomers = $customerOrders->filter(fn($c) => $c->count > 1)->count(); $totalCustomers = $customerOrders->count(); $loyaltyRate = $totalCustomers > 0 ? ($repeatCustomers / $totalCustomers) * 100 : 0; @@ -218,76 +233,90 @@ public function render(): View [ 'title' => 'Average Order Value (AOV)', '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', - 'value' => round($profitMargin, 1).'%', + 'value' => round($profitMargin, 1) . '%', + 'description' => 'Persentase keuntungan bersih dibandingkan dengan total pendapatan.', ] : null, [ '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', 'value' => round($itemsPerOrder, 1), + 'description' => 'Rata-rata jumlah barang yang dibeli dalam satu transaksi.', ], [ 'title' => 'Total Order', 'value' => formatCurrencyNumber($totalOrdersCount), + 'description' => 'Total frekuensi transaksi atau pesanan yang masuk selama periode ini.', ], [ 'title' => 'Pendapatan', 'value' => formatCurrencyNumber($totalIncome, 'Rp'), + 'description' => 'Total nominal uang yang masuk dari penjualan (Omzet).', ], - auth()->user()->hasRole(['Developer', 'Owner']) ? [ + $user->hasRole(['Developer', 'Owner']) ? [ 'title' => 'HPP', 'value' => formatCurrencyNumber($totalCogs, 'Rp'), + 'description' => 'Total modal barang atau Harga Pokok Penjualan (HPP).', ] : null, - auth()->user()->hasRole(['Developer', 'Owner']) ? [ + $user->hasRole(['Developer', 'Owner']) ? [ 'title' => 'Laba Kotor', 'value' => formatCurrencyNumber($grossProfit, 'Rp'), + 'description' => 'Pendapatan dikurangi dengan HPP dan Diskon.', ] : null, - auth()->user()->hasRole(['Developer', 'Owner']) ? [ + $user->hasRole(['Developer', 'Owner']) ? [ 'title' => 'Laba Bersih', 'value' => formatCurrencyNumber($netProfit, 'Rp'), + 'description' => 'Laba kotor dikurangi dengan beban operasional dan gaji.', ] : null, [ 'title' => 'Diskon', 'value' => formatCurrencyNumber($totalDiscount, 'Rp'), + 'description' => 'Total nilai potongan harga (diskon) yang diberikan kepada pelanggan.', ], [ 'title' => 'Voucher Terpakai', 'value' => formatCurrencyNumber( (clone $orderQuery)->whereNotNull('voucher_id')->count() ), + 'description' => 'Jumlah transaksi yang menggunakan voucher diskon selama periode ini.', ], [ 'title' => 'Parfum Terjual', 'value' => formatCurrencyNumber( 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') - ).' ml', + ) . ' ml', + 'description' => 'Total volume cairan parfum yang terjual dalam satuan mililiter (ml).', ], [ 'title' => 'Produk Terjual', 'value' => formatCurrencyNumber( 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') - ).' pcs', + ) . ' pcs', + 'description' => 'Total jumlah unit produk yang terjual dalam satuan pcs.', ], [ 'title' => 'Botol Terjual', 'value' => formatCurrencyNumber( 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') - ).' pcs', + ) . ' pcs', + 'description' => 'Total jumlah unit botol yang terjual dalam satuan pcs.', ], ]); @@ -295,37 +324,40 @@ public function render(): View [ 'title' => 'Beban Toko', '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) ->sum('amount'), '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', 'value' => formatCurrencyNumber( Purchase::when($dateQuery || ($startDate && $endDate), $filterDate) ->sum('total'), 'Rp' ), + 'description' => 'Total pengeluaran untuk pembelian stok barang atau inventaris selama periode ini.', ] : null, [ 'title' => 'Penggajian', '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) ->paid() ->sum('total_salary'), 'Rp' ), + 'description' => 'Total pengeluaran untuk gaji karyawan.', ], ]); $topPerfumes = OrderItem::where('orderable_type', Perfume::class) ->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); }) ->with('orderable') @@ -334,7 +366,7 @@ public function render(): View ->orderByDesc('total_sales_value') ->take(5) ->get() - ->map(fn ($item) => [ + ->map(fn($item) => [ 'name' => $item->orderable?->name ?? 'Unknown', 'sale_price' => formatCurrencyNumber($item->orderable?->sale_price ?? 0, 'Rp'), 'total_sold' => formatCurrencyNumber($item->total_sold), @@ -344,7 +376,7 @@ public function render(): View $topProducts = OrderItem::where('orderable_type', Product::class) ->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); }) ->with('orderable') @@ -353,7 +385,7 @@ public function render(): View ->orderByDesc('total_sales_value') ->take(5) ->get() - ->map(fn ($item) => [ + ->map(fn($item) => [ 'name' => $item->orderable?->name ?? 'Unknown', 'sale_price' => formatCurrencyNumber($item->orderable?->sale_price ?? 0, 'Rp'), 'total_sold' => formatCurrencyNumber($item->total_sold), @@ -363,7 +395,7 @@ public function render(): View $topBottles = OrderItem::where('orderable_type', Bottle::class) ->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); }) ->with('orderable') @@ -372,7 +404,7 @@ public function render(): View ->orderByDesc('total_sales_value') ->take(5) ->get() - ->map(fn ($item) => [ + ->map(fn($item) => [ 'name' => $item->orderable?->name ?? 'Unknown', 'sale_price' => formatCurrencyNumber($item->orderable?->sale_price ?? 0, 'Rp'), 'total_sold' => formatCurrencyNumber($item->total_sold), @@ -383,9 +415,9 @@ public function render(): View // --- Chart Data Logic --- // 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() - ->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()) ->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') @@ -393,7 +425,7 @@ public function render(): View ->keyBy('date'); $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()) ->select(DB::raw('DATE(created_at) as date'), DB::raw('SUM(amount) as total_expense')) ->groupBy('date') @@ -401,9 +433,9 @@ public function render(): View ->keyBy('date'); $this->revenueTrendChart = [ - 'labels' => $trendDates->map(fn ($d) => Carbon::parse($d)->format('d M'))->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(), + 'labels' => $trendDates->map(fn($d) => Carbon::parse($d)->format('d M'))->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(), ]; // 2. busiest hour @@ -416,26 +448,26 @@ public function render(): View ->keyBy('hour'); $this->hourlySalesChart = [ - '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(), + '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(), ]; // 3. Product Category Distribution $perfumeSales = OrderItem::where('orderable_type', Perfume::class) ->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); })->sum(DB::raw('unit_price * quantity')); $productSales = OrderItem::where('orderable_type', Product::class) ->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); })->sum(DB::raw('unit_price * quantity')); $bottleSales = OrderItem::where('orderable_type', Bottle::class) ->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); })->sum(DB::raw('unit_price * quantity')); @@ -446,7 +478,7 @@ public function render(): View // 4. Payment Method $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); }) ->select('method', DB::raw('SUM(amount) as total')) @@ -454,13 +486,13 @@ public function render(): View ->get(); $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(), ]; // 5. Outlet Performance $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) ->select('outlet_id', DB::raw('SUM(total) as revenue')) ->groupBy('outlet_id') @@ -468,7 +500,7 @@ public function render(): View ->get(); $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(), ]; @@ -514,7 +546,7 @@ public function render(): View $this->dayOfWeekSalesChart = [ '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) @@ -528,13 +560,13 @@ public function render(): View // 9. Transaction Trend (Daily Count) $this->transactionTrendChart = [ - 'labels' => $trendDates->map(fn ($d) => Carbon::parse($d)->format('d M'))->toArray(), - 'data' => $trendDates->map(fn ($d) => (int) ($ordersTrend->get($d)?->count ?? 0))->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(), ]; // 10. AOV Trend (Daily Revenue / Daily Count) $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) { $rev = $ordersTrend->get($d)?->revenue ?? 0; $count = $ordersTrend->get($d)?->count ?? 0; @@ -563,7 +595,7 @@ public function render(): View // 14. Voucher Usage Trend $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()) ->whereNotNull('voucher_id') ->select(DB::raw('DATE(created_at) as date'), DB::raw('count(*) as count')) @@ -572,8 +604,8 @@ public function render(): View ->keyBy('date'); $this->voucherUsageTrendChart = [ - 'labels' => $trendDates->map(fn ($d) => Carbon::parse($d)->format('d M'))->toArray(), - 'data' => $trendDates->map(fn ($d) => (int) ($vouchersTrend->get($d)?->count ?? 0))->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(), ]; $this->countingData = $countingData; diff --git a/app/Livewire/Studio/Dashboard/Overview.php b/app/Livewire/Studio/Dashboard/Overview.php index d7076c9..0db838e 100644 --- a/app/Livewire/Studio/Dashboard/Overview.php +++ b/app/Livewire/Studio/Dashboard/Overview.php @@ -44,64 +44,68 @@ class Overview extends Component 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 { + /** @var \App\Models\User $user */ + $user = auth()->user(); $outletIds = $this->selectedOutletIds; 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() ->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() ->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() ->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() ->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() ->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() ->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() ->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() ->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() ->whereNotNull('voucher_id') ->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() ->whereNotNull('voucher_id') ->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() ->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() ->sum('amount'); @@ -118,21 +122,21 @@ public function render(): View $todaySoldPerfume = OrderItem::where('orderable_type', Perfume::class) ->whereHas( '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'); $yesterdaySoldPerfume = OrderItem::where('orderable_type', Perfume::class) ->whereHas( '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'); $todayTopPerfume = OrderItem::where('orderable_type', Perfume::class) ->whereHas( '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')) ->groupBy('orderable_id', 'orderable_type') @@ -143,7 +147,7 @@ public function render(): View $yesterdayTopPerfume = OrderItem::where('orderable_type', Perfume::class) ->whereHas( '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')) ->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)'; $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')) ->groupBy('hour') ->get()->keyBy('hour'); $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')) ->groupBy('hour') ->get()->keyBy('hour'); $this->hourlyComparisonChart = [ - '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(), - 'yesterday' => collect(range(0, 23))->map(fn ($h) => $yesterdayHourly->get($h)?->count ?? 0)->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(), + 'yesterday' => collect(range(0, 23))->map(fn($h) => $yesterdayHourly->get($h)?->count ?? 0)->toArray(), ]; // 2. Today's Distribution (Category) $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')) ->groupBy('orderable_type') @@ -204,25 +208,25 @@ public function render(): View // 3. Today's Payment Methods $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')) ->groupBy('method') ->get(); $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(), ]; // 4. Today's Customer Type $todayMemberOrders = Order::query() ->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(); $todayGuestOrders = Order::query() ->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(); $this->todayCustomerTypeChart = [ @@ -232,7 +236,7 @@ public function render(): View // 5. Today's Top 5 Selling Items $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') ->select('orderable_id', 'orderable_type', DB::raw('SUM(quantity) as total_sold')) @@ -242,14 +246,14 @@ public function render(): View ->get(); $this->todayTopSellingChart = [ - 'labels' => $todayTopItems->map(fn ($item) => $item->orderable?->name ?? 'Unknown')->toArray(), - 'data' => $todayTopItems->pluck('total_sold')->map(fn ($val) => (int) $val)->toArray(), + 'labels' => $todayTopItems->map(fn($item) => $item->orderable?->name ?? 'Unknown')->toArray(), + 'data' => $todayTopItems->pluck('total_sold')->map(fn($val) => (int) $val)->toArray(), ]; // 6. Today's Top 5 Customers $todayTopCustomers = Order::query() ->today() - ->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) + ->when(! empty($outletIds), fn($q) => $q->whereIn('outlet_id', $outletIds)) ->whereNotNull('customer_id') ->select('customer_id', DB::raw('SUM(total) as total_spent')) ->groupBy('customer_id') @@ -259,18 +263,18 @@ public function render(): View ->get(); $this->todayTopCustomersChart = [ - '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(), + '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(), ]; // 7. Today's Discount Distribution (With Discount vs Regular) $withDiscount = Order::query() ->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(); $noDiscount = Order::query() ->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(); $this->todayDiscountChart = [ @@ -279,24 +283,26 @@ public function render(): View ]; $this->stats = array_values(array_filter([ - auth()->user()->hasRole(['Developer', 'Owner']) ? [ + $user->hasRole(['Developer', 'Owner']) ? [ 'title' => 'Average Order Value (AOV)', 'value' => formatCurrencyNumber($todayAov, 'Rp'), 'previous' => formatCurrencyNumber($yesterdayAov, 'Rp'), 'trend' => $yesterdayAov > 0 - ? round((($todayAov - $yesterdayAov) / $yesterdayAov) * 100, 1).'%' + ? round((($todayAov - $yesterdayAov) / $yesterdayAov) * 100, 1) . '%' : '∞%', 'trendUp' => $todayAov > $yesterdayAov, + 'description' => 'Rata-rata nilai belanja pelanggan dalam satu kali checkout.', ] : null, - auth()->user()->hasRole(['Developer', 'Owner']) ? [ + $user->hasRole(['Developer', 'Owner']) ? [ 'title' => 'Profit Margin', - 'value' => round($todayProfitMargin, 1).'%', - 'previous' => round($yesterdayProfitMargin, 1).'%', + 'value' => round($todayProfitMargin, 1) . '%', + 'previous' => round($yesterdayProfitMargin, 1) . '%', 'trend' => $yesterdayProfitMargin > 0 - ? round($todayProfitMargin - $yesterdayProfitMargin, 1).'%' + ? round($todayProfitMargin - $yesterdayProfitMargin, 1) . '%' : '∞%', 'trendUp' => $todayProfitMargin > $yesterdayProfitMargin, + 'description' => 'Persentase keuntungan bersih dibandingkan dengan total pendapatan.', ] : null, [ @@ -304,9 +310,10 @@ public function render(): View 'value' => $todayOrder, 'previous' => $yesterdayOrder, 'trend' => $yesterdayOrder > 0 - ? round((($todayOrder - $yesterdayOrder) / $yesterdayOrder) * 100, 1).'%' + ? round((($todayOrder - $yesterdayOrder) / $yesterdayOrder) * 100, 1) . '%' : '∞%', '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'), 'previous' => formatCurrencyNumber($yesterdayIncome, 'Rp'), 'trend' => $yesterdayIncome > 0 - ? round((($todayIncome - $yesterdayIncome) / $yesterdayIncome) * 100, 1).'%' + ? round((($todayIncome - $yesterdayIncome) / $yesterdayIncome) * 100, 1) . '%' : '∞%', 'trendUp' => $todayIncome > $yesterdayIncome, 'formatted' => formatCurrencyNumber($todayIncome, 'Rp'), + 'description' => 'Total nominal uang yang masuk dari penjualan (Omzet).', ], - auth()->user()->hasRole(['Developer', 'Owner']) ? [ + $user->hasRole(['Developer', 'Owner']) ? [ 'title' => 'HPP', 'value' => formatCurrencyNumber($todayCogs, 'Rp'), 'previous' => formatCurrencyNumber($yesterdayCogs, 'Rp'), 'trend' => $yesterdayCogs > 0 - ? round((($todayCogs - $yesterdayCogs) / $yesterdayCogs) * 100, 1).'%' + ? round((($todayCogs - $yesterdayCogs) / $yesterdayCogs) * 100, 1) . '%' : '∞%', 'trendUp' => $todayCogs > $yesterdayCogs, 'formatted' => formatCurrencyNumber($todayCogs, 'Rp'), + 'description' => 'Harga Pokok Penjualan, yaitu total modal dari barang-barang yang terjual.', ] : null, [ 'title' => 'Member Baru', - 'value' => $todayNewMembers.' Orang', - 'previous' => $yesterdayNewMembers.' Orang', + 'value' => $todayNewMembers . ' Orang', + 'previous' => $yesterdayNewMembers . ' Orang', 'trend' => $yesterdayNewMembers > 0 - ? round((($todayNewMembers - $yesterdayNewMembers) / $yesterdayNewMembers) * 100, 1).'%' + ? round((($todayNewMembers - $yesterdayNewMembers) / $yesterdayNewMembers) * 100, 1) . '%' : '∞%', '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'), 'previous' => formatCurrencyNumber($yesterdayDiscount, 'Rp'), 'trend' => $yesterdayDiscount > 0 - ? round((($todayDiscount - $yesterdayDiscount) / $yesterdayDiscount) * 100, 1).'%' + ? round((($todayDiscount - $yesterdayDiscount) / $yesterdayDiscount) * 100, 1) . '%' : '∞%', 'trendUp' => $todayDiscount > $yesterdayDiscount, 'formatted' => formatCurrencyNumber($todayDiscount, 'Rp'), + 'description' => 'Total nilai potongan harga (diskon) yang diberikan kepada pelanggan.', ], [ @@ -357,9 +368,10 @@ public function render(): View 'value' => $todayVoucher, 'previous' => $yesterdayVoucher, 'trend' => $yesterdayVoucher > 0 - ? round((($todayVoucher - $yesterdayVoucher) / $yesterdayVoucher) * 100, 1).'%' + ? round((($todayVoucher - $yesterdayVoucher) / $yesterdayVoucher) * 100, 1) . '%' : '∞%', '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'), 'previous' => formatCurrencyNumber($yesterdayExpense, 'Rp'), 'trend' => $yesterdayExpense > 0 - ? round((($todayExpense - $yesterdayExpense) / $yesterdayExpense) * 100, 1).'%' + ? round((($todayExpense - $yesterdayExpense) / $yesterdayExpense) * 100, 1) . '%' : '∞%', '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', 'value' => formatCurrencyNumber($todayPurchase, 'Rp'), 'previous' => formatCurrencyNumber($yesterdayPurchase, 'Rp'), 'trend' => $yesterdayPurchase > 0 - ? round((($todayPurchase - $yesterdayPurchase) / $yesterdayPurchase) * 100, 1).'%' + ? round((($todayPurchase - $yesterdayPurchase) / $yesterdayPurchase) * 100, 1) . '%' : '∞%', 'trendUp' => $todayPurchase > $yesterdayPurchase, + 'description' => 'Total pengeluaran untuk pembelian stok barang atau inventaris selama periode ini.', ] : null, - auth()->user()->hasRole(['Developer', 'Owner']) ? [ + $user->hasRole(['Developer', 'Owner']) ? [ 'title' => 'Laba Kotor', 'value' => formatCurrencyNumber($todayGrossProfit, 'Rp'), 'previous' => formatCurrencyNumber($yesterdayGrossProfit, 'Rp'), 'trend' => $yesterdayGrossProfit > 0 - ? round((($todayGrossProfit - $yesterdayGrossProfit) / $yesterdayGrossProfit) * 100, 1).'%' + ? round((($todayGrossProfit - $yesterdayGrossProfit) / $yesterdayGrossProfit) * 100, 1) . '%' : '∞%', 'trendUp' => $todayGrossProfit > $yesterdayGrossProfit, + 'description' => 'Pendapatan dikurangi dengan HPP dan Diskon.', ] : null, - auth()->user()->hasRole(['Developer', 'Owner']) ? [ + $user->hasRole(['Developer', 'Owner']) ? [ 'title' => 'Laba Bersih', 'value' => formatCurrencyNumber($todayNetProfit, 'Rp'), 'previous' => formatCurrencyNumber($yesterdayNetProfit, 'Rp'), 'trend' => $yesterdayNetProfit > 0 - ? round((($todayNetProfit - $yesterdayNetProfit) / $yesterdayNetProfit) * 100, 1).'%' + ? round((($todayNetProfit - $yesterdayNetProfit) / $yesterdayNetProfit) * 100, 1) . '%' : '∞%', 'trendUp' => $todayNetProfit > $yesterdayNetProfit, + 'description' => 'Laba kotor dikurangi dengan beban operasional dan gaji.', ] : null, [ 'title' => 'Parfum Terjual', - 'value' => $todaySoldPerfume.' ml', - 'previous' => $yesterdaySoldPerfume.' ml', + 'value' => $todaySoldPerfume . ' ml', + 'previous' => $yesterdaySoldPerfume . ' ml', 'trend' => $yesterdaySoldPerfume > 0 - ? round((($todaySoldPerfume - $yesterdaySoldPerfume) / $yesterdaySoldPerfume) * 100, 1).'%' + ? round((($todaySoldPerfume - $yesterdaySoldPerfume) / $yesterdaySoldPerfume) * 100, 1) . '%' : '∞%', 'trendUp' => $todaySoldPerfume > $yesterdaySoldPerfume, + 'description' => 'Total volume cairan parfum yang terjual dalam satuan mililiter (ml).', ], [ 'title' => 'Top Parfum', - 'value' => optional($todayTopPerfume?->orderable)->name.' ('.$todayTopPerfume?->total_sold.'ml)' ?? '-', - 'previous' => optional($yesterdayTopPerfume?->orderable)->name.'('.$yesterdayTopPerfume?->total_sold.'ml)' ?? '-', + 'value' => optional($todayTopPerfume?->orderable)->name . ' (' . $todayTopPerfume?->total_sold . 'ml)' ?? '-', + 'previous' => optional($yesterdayTopPerfume?->orderable)->name . '(' . $yesterdayTopPerfume?->total_sold . 'ml)' ?? '-', '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), + 'description' => 'Nama varian parfum yang paling banyak terjual hari ini.', ], ])); diff --git a/resources/views/livewire/studio/dashboard/analysis.blade.php b/resources/views/livewire/studio/dashboard/analysis.blade.php index 1130cad..43c0c08 100644 --- a/resources/views/livewire/studio/dashboard/analysis.blade.php +++ b/resources/views/livewire/studio/dashboard/analysis.blade.php @@ -45,8 +45,20 @@ Counting Data
@foreach ($countingData as $item) -
- {{ $item['title'] }} +
+
+ {{ $item['title'] }} + + @if (isset($item['description'])) +
+ + + +
+ @endif +
{{ $item['value'] }}
@endforeach @@ -55,8 +67,20 @@ Order
@foreach ($orders as $order) -
- {{ $order['title'] }} +
+
+ {{ $order['title'] }} + + @if (isset($order['description'])) +
+ + + +
+ @endif +
{{ $order['value'] }}
@endforeach @@ -65,8 +89,20 @@ Pengeluaran
@foreach ($expenses as $expense) -
- {{ $expense['title'] }} +
+
+ {{ $expense['title'] }} + + @if (isset($expense['description'])) +
+ + + +
+ @endif +
{{ $expense['value'] }}
@endforeach @@ -74,31 +110,75 @@
- + +
+ + + +
Trend Pendapatan & Laba (30 Hari Terakhir)
- + +
+ + + +
Pertumbuhan Member (Akumulasi)
- + +
+ + + +
Jam Sibuk (Total Order per Jam)
- + +
+ + + +
Performa Outlet (Pendapatan)
- + +
+ + + +
Distribusi Penjualan
@@ -106,7 +186,16 @@ - + +
+ + + +
Metode Pembayaran
@@ -114,13 +203,31 @@ - + +
+ + + +
Performa Penjualan per Hari
- + +
+ + + +
Segmentasi Pelanggan (Member vs Guest)
@@ -128,19 +235,44 @@ - + +
+ + + +
Trend Volume Transaksi (30 Hari)
- + +
+ + + +
Trend Rata-rata Belanja (AOV)
- + +
+ + + +
Komposisi Biaya Operasional
@@ -148,7 +280,16 @@ - + +
+ + + +
Omzet vs Total Biaya
@@ -156,7 +297,16 @@ - + +
+ + + +
Analisa Pelanggan (Baru vs Lama)
@@ -164,7 +314,16 @@ - + +
+ + + +
Efektivitas Voucher
@@ -173,7 +332,8 @@
Top 5 Parfum - + No @@ -201,7 +361,8 @@
Top 5 Produk - + No @@ -229,7 +390,8 @@
Top 5 Botol - + No @@ -246,7 +408,8 @@ {{ $topBottle['name'] }} {{ $topBottle['sale_price'] }} {{ $topBottle['total_sold'] }} - {{ $topBottle['total_sales'] }} + {{ $topBottle['total_sales'] }} + @endforeach @@ -273,8 +436,11 @@ try { return JSON.parse(JSON.stringify(data)); } catch (e) { - // Fallback for safety - return { labels: [], datasets: [] }; + // Fallback for safety + return { + labels: [], + datasets: [] + }; } }, @@ -291,19 +457,31 @@ // Watchers this.$watch('$wire.revenueTrendChart', () => this.updateChart('revenue', this.getRevenueData)); - this.$watch('$wire.memberGrowthChart', () => this.updateChart('memberGrowth', this.getMemberGrowthData)); - this.$watch('$wire.hourlySalesChart', () => this.updateChart('hourlySales', this.getHourlySalesData)); - this.$watch('$wire.outletPerformanceChart', () => this.updateChart('outletPerformance', this.getOutletPerformanceData)); - this.$watch('$wire.categoryDistributionChart', () => this.updateChart('categoryDist', this.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.memberGrowthChart', () => this.updateChart('memberGrowth', this + .getMemberGrowthData)); + this.$watch('$wire.hourlySalesChart', () => this.updateChart('hourlySales', this + .getHourlySalesData)); + this.$watch('$wire.outletPerformanceChart', () => this.updateChart('outletPerformance', this + .getOutletPerformanceData)); + this.$watch('$wire.categoryDistributionChart', () => this.updateChart('categoryDist', this + .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.expenseBreakdownChart', () => this.updateChart('expenseBreakdown', this.getExpenseBreakdownData)); - this.$watch('$wire.revenueVsCostChart', () => this.updateChart('revenueVsCost', this.getRevenueVsCostData)); - this.$watch('$wire.customerRetentionChart', () => this.updateChart('customerRetention', this.getCustomerRetentionData)); - this.$watch('$wire.voucherUsageTrendChart', () => this.updateChart('voucherUsage', this.getVoucherUsageData)); + this.$watch('$wire.expenseBreakdownChart', () => this.updateChart('expenseBreakdown', this + .getExpenseBreakdownData)); + this.$watch('$wire.revenueVsCostChart', () => this.updateChart('revenueVsCost', this + .getRevenueVsCostData)); + this.$watch('$wire.customerRetentionChart', () => this.updateChart('customerRetention', this + .getCustomerRetentionData)); + this.$watch('$wire.voucherUsageTrendChart', () => this.updateChart('voucherUsage', this + .getVoucherUsageData)); }, updateChart(key, getDataFn) { @@ -311,7 +489,7 @@ try { charts[key].data = getDataFn.call(this); charts[key].update(); - } catch(e) { + } catch (e) { console.error(`Error updating chart ${key}`, e); } } else { @@ -332,10 +510,10 @@ 'customerRetention': 'initCustomerRetention', 'voucherUsage': 'initVoucherUsage', }; - const fnName = initMap[key]; - if (fnName && typeof this[fnName] === 'function') { - this[fnName](); - } + const fnName = initMap[key]; + if (fnName && typeof this[fnName] === 'function') { + this[fnName](); + } } }, @@ -360,16 +538,23 @@ initRevenueTrend() { this.createChart('revenueTrendChart', 'revenue', 'line', this.getRevenueData(), { - plugins: { legend: { position: 'top' } }, - scales: { y: { beginAtZero: true } } + plugins: { + legend: { + position: 'top' + } + }, + scales: { + y: { + beginAtZero: true + } + } }); }, getRevenueData() { const data = this.unwrap($wire.revenueTrendChart); return { labels: data.labels || [], - datasets: [ - { + datasets: [{ label: 'Pendapatan', data: data.revenue || [], borderColor: '#10b981', @@ -391,7 +576,11 @@ initMemberGrowth() { this.createChart('memberGrowthChart', 'memberGrowth', 'line', this.getMemberGrowthData(), { - scales: { y: { beginAtZero: true } } + scales: { + y: { + beginAtZero: true + } + } }); }, getMemberGrowthData() { @@ -425,9 +614,10 @@ }, initOutletPerformance() { - this.createChart('outletPerformanceChart', 'outletPerformance', 'bar', this.getOutletPerformanceData(), { - indexAxis: 'y' - }); + this.createChart('outletPerformanceChart', 'outletPerformance', 'bar', this + .getOutletPerformanceData(), { + indexAxis: 'y' + }); }, getOutletPerformanceData() { const data = this.unwrap($wire.outletPerformanceChart); @@ -442,7 +632,8 @@ }, initCategoryDistribution() { - this.createChart('categoryDistributionChart', 'categoryDist', 'doughnut', this.getCategoryDistributionData()); + this.createChart('categoryDistributionChart', 'categoryDist', 'doughnut', this + .getCategoryDistributionData()); }, getCategoryDistributionData() { const data = this.unwrap($wire.categoryDistributionChart); @@ -499,7 +690,8 @@ }, initTransactionTrend() { - this.createChart('transactionTrendChart', 'transactionTrend', 'line', this.getTransactionTrendData()); + this.createChart('transactionTrendChart', 'transactionTrend', 'line', this + .getTransactionTrendData()); }, getTransactionTrendData() { const data = this.unwrap($wire.transactionTrendChart); @@ -535,7 +727,8 @@ }, initExpenseBreakdown() { - this.createChart('expenseBreakdownChart', 'expenseBreakdown', 'doughnut', this.getExpenseBreakdownData()); + this.createChart('expenseBreakdownChart', 'expenseBreakdown', 'doughnut', this + .getExpenseBreakdownData()); }, getExpenseBreakdownData() { const data = this.unwrap($wire.expenseBreakdownChart); @@ -564,7 +757,8 @@ }, initCustomerRetention() { - this.createChart('customerRetentionChart', 'customerRetention', 'bar', this.getCustomerRetentionData()); + this.createChart('customerRetentionChart', 'customerRetention', 'bar', this + .getCustomerRetentionData()); }, getCustomerRetentionData() { const data = this.unwrap($wire.customerRetentionChart); @@ -599,7 +793,7 @@ // Helper to create chart efficiently createChart(elementId, chartKey, type, data, extraOptions = {}) { if (!data || !data.labels) return; - + const ctx = document.getElementById(elementId); if (!ctx) return; diff --git a/resources/views/livewire/studio/dashboard/overview.blade.php b/resources/views/livewire/studio/dashboard/overview.blade.php index 10fcc9c..8b9c26c 100644 --- a/resources/views/livewire/studio/dashboard/overview.blade.php +++ b/resources/views/livewire/studio/dashboard/overview.blade.php @@ -18,9 +18,20 @@
@foreach ($stats as $stat)
- {{ $stat['title'] }} -
+ 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"> +
+ {{ $stat['title'] }} + + @if (isset($stat['description'])) +
+ + + +
+ @endif +
+
{{ $stat['value'] }} {{ $stat['previous'] }}
@@ -35,7 +46,16 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g
- + +
+ + + +
Perbandingan Order per Jam (Hari Ini vs Kemarin)
@@ -43,7 +63,16 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g - + +
+ + + +
Distribusi Penjualan
@@ -51,7 +80,15 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g - + +
+ + + +
Metode Pembayaran
@@ -59,7 +96,16 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g - + +
+ + + +
Member vs Guest
@@ -67,7 +113,14 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g - + +
+ + + +
5 Item Terlaris
@@ -75,7 +128,14 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g - + +
+ + + +
Top 5 Pelanggan
@@ -83,7 +143,14 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g - + +
+ + + +
Normal vs Diskon
@@ -130,7 +197,7 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g this.$nextTick(() => start()); this.$watch('$wire.hourlyComparisonChart', () => this.updateChart('hourly', this - .getHourlyData)); + .getHourlyData)); this.$watch('$wire.todayCategoryChart', () => this.updateChart('category', this .getCategoryData)); this.$watch('$wire.todayPaymentChart', () => this.updateChart('payment', this.getPaymentData)); @@ -275,7 +342,7 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g initCustomerType() { this.createChart('todayCustomerTypeChart', 'customerType', 'doughnut', this - .getCustomerTypeData()); + .getCustomerTypeData()); }, getCustomerTypeData() { const data = this.unwrap($wire.todayCustomerTypeChart);