From 3de603f4c2128f3551da02e58547e1d6bef9b3e1 Mon Sep 17 00:00:00 2001 From: Yoga Pangestu Date: Sat, 3 Jan 2026 01:17:35 +0700 Subject: [PATCH] feat: Add multiple new charts to the dashboard overview for enhanced sales and customer analytics. --- app/Livewire/Studio/Dashboard/Analysis.php | 301 +++++++- app/Livewire/Studio/Dashboard/Overview.php | 209 +++++- .../studio/dashboard/analysis.blade.php | 686 ++++++++++++++++++ .../studio/dashboard/overview.blade.php | 368 +++++++++- 4 files changed, 1511 insertions(+), 53 deletions(-) diff --git a/app/Livewire/Studio/Dashboard/Analysis.php b/app/Livewire/Studio/Dashboard/Analysis.php index 6c2fa17..89008bd 100644 --- a/app/Livewire/Studio/Dashboard/Analysis.php +++ b/app/Livewire/Studio/Dashboard/Analysis.php @@ -10,6 +10,7 @@ use App\Models\Order; use App\Models\OrderItem; use App\Models\Outlet; +use App\Models\Payment; use App\Models\Payroll; use App\Models\Perfume; use App\Models\Product; @@ -19,6 +20,7 @@ use App\Models\Voucher; use App\Traits\Notification\WithSubscribeNotification; use Carbon\Carbon; +use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\DB; use Livewire\Attributes\Title; use Livewire\Component; @@ -39,43 +41,42 @@ class Analysis extends Component public array $selectedOutletIds = []; - public array $countingData = []; + public array $revenueTrendChart = []; - public array $orders = []; + public array $hourlySalesChart = []; - public array $expenses = []; + public array $categoryDistributionChart = []; - public array $topPerfumes = []; + public array $paymentMethodChart = []; - public array $topProducts = []; + public array $outletPerformanceChart = []; - public array $topBottles = []; + public array $memberGrowthChart = []; - public function mount() + public array $dayOfWeekSalesChart = []; + + public array $customerTypeChart = []; + + public array $transactionTrendChart = []; + + public array $aovTrendChart = []; + + public array $expenseBreakdownChart = []; + + public array $revenueVsCostChart = []; + + public array $customerRetentionChart = []; + + public array $voucherUsageTrendChart = []; + + public function mount(): void { $this->outlets = auth()->user()->outlets()->get()->pluck('name', 'id')->toArray(); $this->selectedOutletIds = []; - - $this->loadData(); } - public function updatedPeriod(string $period) - { - $this->loadData(); - } - - public function updatedSelectedOutletIds() - { - $this->loadData(); - } - - public function updatedRange($range) - { - $this->loadData(); - } - - protected function loadData() + public function render(): View { $outletIds = $this->selectedOutletIds; $period = $this->period; @@ -104,7 +105,7 @@ protected function loadData() } }; - $this->countingData = [ + $countingData = [ [ 'title' => 'Outlet', 'value' => Outlet::when($dateQuery || ($startDate && $endDate), $filterDate)->count(), @@ -188,12 +189,41 @@ protected function loadData() $grossProfit = $totalIncome - $totalCogs - $totalDiscount; $netProfit = $grossProfit - $totalExpense - $totalPayroll; - $this->orders = array_filter([ + $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'); + + $aov = $totalOrdersCount > 0 ? $totalIncome / $totalOrdersCount : 0; + $profitMargin = $totalIncome > 0 ? ($netProfit / $totalIncome) * 100 : 0; + $itemsPerOrder = $totalOrdersCount > 0 ? $totalItemsCount / $totalOrdersCount : 0; + + $customerOrders = (clone $orderQuery)->select('customer_id', DB::raw('count(*) as count')) + ->whereNotNull('customer_id') + ->groupBy('customer_id') + ->get(); + $repeatCustomers = $customerOrders->filter(fn ($c) => $c->count > 1)->count(); + $totalCustomers = $customerOrders->count(); + $loyaltyRate = $totalCustomers > 0 ? ($repeatCustomers / $totalCustomers) * 100 : 0; + + $orders = array_filter([ + [ + 'title' => 'Average Order Value (AOV)', + 'value' => formatCurrencyNumber($aov, 'Rp'), + ], + auth()->user()->hasRole(['Developer', 'Owner']) ? [ + 'title' => 'Profit Margin', + 'value' => round($profitMargin, 1).'%', + ] : null, + [ + 'title' => 'Loyalty Rate', + 'value' => round($loyaltyRate, 1).'%', + ], + [ + 'title' => 'Item per Transaksi', + 'value' => round($itemsPerOrder, 1), + ], [ 'title' => 'Total Order', - 'value' => formatCurrencyNumber( - (clone $orderQuery)->count() - ), + 'value' => formatCurrencyNumber($totalOrdersCount), ], [ 'title' => 'Pendapatan', @@ -251,7 +281,7 @@ protected function loadData() ], ]); - $this->expenses = array_filter([ + $expenses = array_filter([ [ 'title' => 'Beban Toko', 'value' => formatCurrencyNumber( @@ -283,7 +313,7 @@ protected function loadData() ], ]); - $this->topPerfumes = OrderItem::where('orderable_type', Perfume::class) + $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)) ->when($dateQuery || ($startDate && $endDate), $filterDate); @@ -302,7 +332,7 @@ protected function loadData() ]) ->toArray(); - $this->topProducts = OrderItem::where('orderable_type', Product::class) + $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)) ->when($dateQuery || ($startDate && $endDate), $filterDate); @@ -321,7 +351,7 @@ protected function loadData() ]) ->toArray(); - $this->topBottles = OrderItem::where('orderable_type', Bottle::class) + $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)) ->when($dateQuery || ($startDate && $endDate), $filterDate); @@ -339,12 +369,209 @@ protected function loadData() 'total_sales' => formatCurrencyNumber($item->total_sales_value, 'Rp'), ]) ->toArray(); - } - public function render() - { + // --- Chart Data Logic --- + + // 1. Revenue & Profit Trend + $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)) + ->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 - cogs - discount) as gross_profit'), DB::raw('count(*) as count')) + ->groupBy('date') + ->get() + ->keyBy('date'); + + $expensesTrend = Expense::query() + ->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') + ->get() + ->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(), + ]; + + // 2. busiest hour + $hourlySales = (clone $orderQuery) + ->select(DB::raw('HOUR(created_at) as hour'), DB::raw('count(*) as count')) + ->groupBy('hour') + ->orderBy('hour') + ->get() + ->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(), + ]; + + // 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)) + ->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)) + ->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)) + ->when($dateQuery || ($startDate && $endDate), $filterDate); + })->sum(DB::raw('unit_price * quantity')); + + $this->categoryDistributionChart = [ + 'labels' => ['Parfum', 'Produk', 'Botol'], + 'data' => [(int) $perfumeSales, (int) $productSales, (int) $bottleSales], + ]; + + // 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)) + ->when($dateQuery || ($startDate && $endDate), $filterDate); + }) + ->select('method', DB::raw('SUM(amount) as total')) + ->groupBy('method') + ->get(); + + $this->paymentMethodChart = [ + '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($dateQuery || ($startDate && $endDate), $filterDate) + ->select('outlet_id', DB::raw('SUM(total) as revenue')) + ->groupBy('outlet_id') + ->with('outlet:id,name') + ->get(); + + $this->outletPerformanceChart = [ + 'labels' => $outletPerformance->map(fn ($op) => $op->outlet?->name ?? 'Outlet #'.$op->outlet_id)->toArray(), + 'data' => $outletPerformance->pluck('revenue')->toArray(), + ]; + + // 6. Member Growth (Area Chart) - Cumulative + $memberGrowth = User::whereHas('customer') + ->select(DB::raw('DATE(created_at) as date'), DB::raw('count(*) as count')) + ->groupBy('date') + ->orderBy('date') + ->get(); + + $cumulativeCount = 0; + $memberChartData = $memberGrowth->map(function ($item) use (&$cumulativeCount) { + $cumulativeCount += $item->count; + + return [ + 'date' => Carbon::parse($item->date)->format('d M Y'), + 'count' => $cumulativeCount, + ]; + }); + + $this->memberGrowthChart = [ + 'labels' => $memberChartData->pluck('date')->toArray(), + 'data' => $memberChartData->pluck('count')->toArray(), + ]; + + // 7. Day of Week Sales + $dayOfWeekSales = (clone $orderQuery) + ->select(DB::raw('DAYOFWEEK(created_at) as day'), DB::raw('SUM(total) as revenue')) + ->groupBy('day') + ->get() + ->keyBy('day'); + + $days = [ + 1 => 'Minggu', + 2 => 'Senin', + 3 => 'Selasa', + 4 => 'Rabu', + 5 => 'Kamis', + 6 => 'Jumat', + 7 => 'Sabtu', + ]; + + $this->dayOfWeekSalesChart = [ + 'labels' => array_values($days), + 'data' => collect(range(1, 7))->map(fn ($d) => (int) ($dayOfWeekSales->get($d)?->revenue ?? 0))->toArray(), + ]; + + // 8. Customer Type (Member vs Guest) + $memberOrders = (clone $orderQuery)->whereNotNull('customer_id')->count(); + $guestOrders = (clone $orderQuery)->whereNull('customer_id')->count(); + + $this->customerTypeChart = [ + 'labels' => ['Member', 'Guest (Umum)'], + 'data' => [$memberOrders, $guestOrders], + ]; + + // 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(), + ]; + + // 10. AOV Trend (Daily Revenue / Daily Count) + $this->aovTrendChart = [ + '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; + + return $count > 0 ? (int) ($rev / $count) : 0; + })->toArray(), + ]; + + // 11. Expense Breakdown (Operational vs Payroll) + $this->expenseBreakdownChart = [ + 'labels' => ['Beban Operasional', 'Gaji (Payroll)'], + 'data' => [(int) $totalExpense, (int) $totalPayroll], + ]; + + // 12. Revenue vs Total Cost (COGS + Expense + Payroll) + $this->revenueVsCostChart = [ + 'labels' => ['Pendapatan', 'Total Biaya (HPP + Beban + Gaji)'], + 'data' => [(int) $totalIncome, (int) ($totalCogs + $totalExpense + $totalPayroll)], + ]; + + // 13. Customer Retention (New vs Returning Transactions) + $this->customerRetentionChart = [ + 'labels' => ['Pelanggan Setia (Repeat)', 'Pelanggan Baru/Sekali'], + 'data' => [$repeatCustomers, max(0, $totalCustomers - $repeatCustomers)], + ]; + + // 14. Voucher Usage Trend + $vouchersTrend = Order::query() + ->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')) + ->groupBy('date') + ->get() + ->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(), + ]; + return view('livewire.studio.dashboard.analysis', [ 'pageTitle' => 'Analisa', + 'countingData' => $countingData, + 'orders' => $orders, + 'expenses' => $expenses, + 'topPerfumes' => $topPerfumes, + 'topProducts' => $topProducts, + 'topBottles' => $topBottles, ]); } } diff --git a/app/Livewire/Studio/Dashboard/Overview.php b/app/Livewire/Studio/Dashboard/Overview.php index 69ee4f2..ff70644 100644 --- a/app/Livewire/Studio/Dashboard/Overview.php +++ b/app/Livewire/Studio/Dashboard/Overview.php @@ -2,12 +2,17 @@ namespace App\Livewire\Studio\Dashboard; +use App\Models\Bottle; use App\Models\Expense; use App\Models\Order; use App\Models\OrderItem; +use App\Models\Payment; use App\Models\Perfume; +use App\Models\Product; use App\Models\Purchase; +use App\Models\User; use App\Traits\Notification\WithSubscribeNotification; +use Illuminate\Contracts\View\View; use Illuminate\Support\Facades\DB; use Livewire\Attributes\Title; use Livewire\Component; @@ -21,23 +26,28 @@ class Overview extends Component public array $selectedOutletIds = []; - public array $stats = []; + public array $hourlyComparisonChart = []; - public function mount() + public array $todayCategoryChart = []; + + public array $todayPaymentChart = []; + + public array $todayCustomerTypeChart = []; + + public array $todayTopSellingChart = []; + + public array $todayTopCustomersChart = []; + + public array $todayDiscountChart = []; + + public function mount(): void { $this->outlets = auth()->user()->outlets()->get()->pluck('name', 'id')->toArray(); $this->selectedOutletIds = []; - - $this->loadStats(); } - public function updatedSelectedOutletIds() - { - $this->loadStats(); - } - - protected function loadStats() + public function render(): View { $outletIds = $this->selectedOutletIds; @@ -137,7 +147,160 @@ protected function loadStats() ->with('orderable') ->first(); - $this->stats = array_filter([ + // New Card: Members Joined + $todayNewMembers = User::whereHas('customer')->whereDate('created_at', now())->count(); + $yesterdayNewMembers = User::whereHas('customer')->whereDate('created_at', now()->yesterday())->count(); + + // New Card: Low Stock Alert + $lowStockQuery = function ($query) use ($outletIds) { + return $query->whereHas('outlets', function ($q) use ($outletIds) { + $q->when(! empty($outletIds), fn ($q2) => $q2->whereIn('outlets.id', $outletIds)) + ->where('stock', '<=', 10); + }); + }; + $lowStockCount = Perfume::where($lowStockQuery)->count() + + Product::where($lowStockQuery)->count() + + Bottle::where($lowStockQuery)->count(); + + $todayAov = $todayOrder > 0 ? $todayIncome / $todayOrder : 0; + $yesterdayAov = $yesterdayOrder > 0 ? $yesterdayIncome / $yesterdayOrder : 0; + + $todayProfitMargin = $todayIncome > 0 ? ($todayNetProfit / $todayIncome) * 100 : 0; + $yesterdayProfitMargin = $yesterdayIncome > 0 ? ($yesterdayNetProfit / $yesterdayIncome) * 100 : 0; + + // --- Chart Data Logic --- + + // 1. Hourly Sales Comparison (Today vs Yesterday) + $todayHourly = Order::whereDate('created_at', now()) + ->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) + ->select(DB::raw('HOUR(created_at) 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)) + ->select(DB::raw('HOUR(created_at) 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(), + ]; + + // 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)); + }) + ->select('orderable_type', DB::raw('SUM(unit_price * quantity) as total')) + ->groupBy('orderable_type') + ->get()->keyBy('orderable_type'); + + $this->todayCategoryChart = [ + 'labels' => ['Parfum', 'Produk Jadi', 'Botol'], + 'data' => [ + (int) ($categorySales->get(Perfume::class)?->total ?? 0), + (int) ($categorySales->get(Product::class)?->total ?? 0), + (int) ($categorySales->get(Bottle::class)?->total ?? 0), + ], + ]; + + // 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)); + }) + ->select('method', DB::raw('SUM(amount) as total')) + ->groupBy('method') + ->get(); + + $this->todayPaymentChart = [ + '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)) + ->whereNotNull('customer_id')->count(); + $todayGuestOrders = Order::query() + ->today() + ->when(! empty($outletIds), fn ($q) => $q->whereIn('outlet_id', $outletIds)) + ->whereNull('customer_id')->count(); + + $this->todayCustomerTypeChart = [ + 'labels' => ['Member', 'Guest (Umum)'], + 'data' => [$todayMemberOrders, $todayGuestOrders], + ]; + + // 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)); + }) + ->with('orderable') + ->select('orderable_id', 'orderable_type', DB::raw('SUM(quantity) as total_sold')) + ->groupBy('orderable_id', 'orderable_type') + ->orderByDesc('total_sold') + ->take(5) + ->get(); + + $this->todayTopSellingChart = [ + '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)) + ->whereNotNull('customer_id') + ->select('customer_id', DB::raw('SUM(total) as total_spent')) + ->groupBy('customer_id') + ->orderByDesc('total_spent') + ->take(5) + ->with('customer') + ->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(), + ]; + + // 7. Today's Discount Distribution (With Discount vs Regular) + $withDiscount = Order::query() + ->today() + ->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)) + ->where('discount', '<=', 0)->count(); + + $this->todayDiscountChart = [ + 'labels' => ['Pakai Diskon', 'Harga Normal'], + 'data' => [$withDiscount, $noDiscount], + ]; + + $stats = array_filter([ + [ + 'title' => 'Average Order Value (AOV)', + 'value' => formatCurrencyNumber($todayAov, 'Rp'), + 'previous' => formatCurrencyNumber($yesterdayAov, 'Rp'), + 'trend' => $yesterdayAov > 0 + ? round((($todayAov - $yesterdayAov) / $yesterdayAov) * 100, 1).'%' + : '∞%', + 'trendUp' => $todayAov > $yesterdayAov, + ], + [ + 'title' => 'Profit Margin', + 'value' => round($todayProfitMargin, 1).'%', + 'previous' => round($yesterdayProfitMargin, 1).'%', + 'trend' => $yesterdayProfitMargin > 0 + ? round($todayProfitMargin - $yesterdayProfitMargin, 1).'%' + : '∞%', + 'trendUp' => $todayProfitMargin > $yesterdayProfitMargin, + ], [ 'title' => 'Total Order', 'value' => $todayOrder, @@ -169,6 +332,23 @@ protected function loadStats() 'formatted' => formatCurrencyNumber($todayCogs, 'Rp'), ] : null, + [ + 'title' => 'Member Baru', + 'value' => $todayNewMembers.' Orang', + 'previous' => $yesterdayNewMembers.' Orang', + 'trend' => $yesterdayNewMembers > 0 + ? round((($todayNewMembers - $yesterdayNewMembers) / $yesterdayNewMembers) * 100, 1).'%' + : '∞%', + 'trendUp' => $todayNewMembers > $yesterdayNewMembers, + ], + [ + 'title' => 'Stok Tipis (<= 10)', + 'value' => $lowStockCount.' Item', + 'previous' => '-', + 'trend' => 'Perlu Restock', + 'trendUp' => $lowStockCount === 0, + ], + [ 'title' => 'Diskon', 'value' => formatCurrencyNumber($todayDiscount, 'Rp'), @@ -247,10 +427,9 @@ protected function loadStats() 'trendUp' => ($todayTopPerfume?->total_sold ?? 0) > ($yesterdayTopPerfume?->total_sold ?? 0), ], ]); - } - public function render() - { - return view('livewire.studio.dashboard.overview'); + return view('livewire.studio.dashboard.overview', [ + 'stats' => $stats, + ]); } } diff --git a/resources/views/livewire/studio/dashboard/analysis.blade.php b/resources/views/livewire/studio/dashboard/analysis.blade.php index ad29e98..bc65d6d 100644 --- a/resources/views/livewire/studio/dashboard/analysis.blade.php +++ b/resources/views/livewire/studio/dashboard/analysis.blade.php @@ -72,6 +72,104 @@ @endforeach +
+ + + Trend Pendapatan & Laba (30 Hari Terakhir) +
+
+ + + + Pertumbuhan Member (Akumulasi) +
+
+ + + + Jam Sibuk (Total Order per Jam) +
+
+ + + + Performa Outlet (Pendapatan) +
+
+ + + + Distribusi Penjualan +
+ +
+
+ + + + Metode Pembayaran +
+ +
+
+ + + + Performa Penjualan per Hari +
+
+ + + + Segmentasi Pelanggan (Member vs Guest) +
+ +
+
+ + + + Trend Volume Transaksi (30 Hari) +
+
+ + + + Trend Rata-rata Belanja (AOV) +
+
+ + + + Komposisi Biaya Operasional +
+ +
+
+ + + + Omzet vs Total Biaya +
+ +
+
+ + + + Analisa Pelanggan (Baru vs Lama) +
+ +
+
+ + + + Efektivitas Voucher +
+
+
+
Top 5 Parfum @@ -158,3 +256,591 @@
+ +@assets + +@endassets + +@script + +@endscript diff --git a/resources/views/livewire/studio/dashboard/overview.blade.php b/resources/views/livewire/studio/dashboard/overview.blade.php index d25c6a5..da7a639 100644 --- a/resources/views/livewire/studio/dashboard/overview.blade.php +++ b/resources/views/livewire/studio/dashboard/overview.blade.php @@ -2,7 +2,7 @@ Haloo, {{ auth()->user()?->employee?->full_name }} Selamat datang di Studio Yadi Parfum. -
+
@if (count($outlets) > 1)
@@ -33,5 +33,371 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g
@endforeach
+
+ + + Perbandingan Order per Jam (Hari Ini vs Kemarin) +
+ +
+
+ + + + Distribusi Penjualan +
+ +
+
+ + + + Metode Pembayaran +
+ +
+
+ + + + Member vs Guest +
+ +
+
+ + + + 5 Item Terlaris +
+ +
+
+ + + + Top 5 Pelanggan +
+ +
+
+ + + + Normal vs Diskon +
+ +
+
+
+ +@assets + +@endassets + +@script + +@endscript