feat: Add multiple new charts to the dashboard overview for enhanced sales and customer analytics.

This commit is contained in:
Yoga Pangestu 2026-01-03 01:17:35 +07:00
parent 4cb94e3ed5
commit 3de603f4c2
4 changed files with 1511 additions and 53 deletions

View File

@ -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,
]);
}
}

View File

@ -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,
]);
}
}

View File

@ -72,6 +72,104 @@
@endforeach
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8" x-data="analysisCharts">
<!-- 1. Revenue & Profit Trend -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Trend Pendapatan & Laba (30 Hari Terakhir)</flux:heading>
<div class="h-80"><canvas id="revenueTrendChart"></canvas></div>
</flux:card>
<!-- 6. Member Growth (Area Chart) - Cumulative -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Pertumbuhan Member (Akumulasi)</flux:heading>
<div class="h-80"><canvas id="memberGrowthChart"></canvas></div>
</flux:card>
<!-- 2. busiest hour -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Jam Sibuk (Total Order per Jam)</flux:heading>
<div class="h-80"><canvas id="hourlySalesChart"></canvas></div>
</flux:card>
<!-- 5. Outlet Performance -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Performa Outlet (Pendapatan)</flux:heading>
<div class="h-80"><canvas id="outletPerformanceChart"></canvas></div>
</flux:card>
<!-- 3. Product Category Distribution -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Distribusi Penjualan</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="categoryDistributionChart"></canvas>
</div>
</flux:card>
<!-- 4. Payment Method -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Metode Pembayaran</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="paymentMethodChart"></canvas>
</div>
</flux:card>
<!-- 7. Day of Week Sales -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Performa Penjualan per Hari</flux:heading>
<div class="h-80"><canvas id="dayOfWeekSalesChart"></canvas></div>
</flux:card>
<!-- 8. Customer Type Distribution -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Segmentasi Pelanggan (Member vs Guest)</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="customerTypeChart"></canvas>
</div>
</flux:card>
<!-- 9. Transaction Trend -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Trend Volume Transaksi (30 Hari)</flux:heading>
<div class="h-80"><canvas id="transactionTrendChart"></canvas></div>
</flux:card>
<!-- 10. AOV Trend -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Trend Rata-rata Belanja (AOV)</flux:heading>
<div class="h-80"><canvas id="aovTrendChart"></canvas></div>
</flux:card>
<!-- 11. Expense Breakdown -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Komposisi Biaya Operasional</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="expenseBreakdownChart"></canvas>
</div>
</flux:card>
<!-- 12. Revenue vs Cost -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Omzet vs Total Biaya</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="revenueVsCostChart"></canvas>
</div>
</flux:card>
<!-- 13. Customer Retention -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Analisa Pelanggan (Baru vs Lama)</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="customerRetentionChart"></canvas>
</div>
</flux:card>
<!-- 14. Voucher Activity -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Efektivitas Voucher</flux:heading>
<div class="h-80"><canvas id="voucherUsageTrendChart"></canvas></div>
</flux:card>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6 mt-2">
<div>
<flux:heading>Top 5 Parfum</flux:heading>
@ -158,3 +256,591 @@
</div>
</div>
</flux:main>
@assets
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@endassets
@script
<script>
Alpine.data('analysisCharts', () => ({
charts: {},
init() {
// Memastikan DOM siap dan library tersedia sebelum inisialisasi awal
const start = () => {
if (typeof Chart !== 'undefined') {
this.initAll();
} else {
setTimeout(start, 50);
}
};
this.$nextTick(() => start());
// Watchers untuk pembaruan data reaktif
this.$watch('$wire.revenueTrendChart', () => this.updateRevenueTrend());
this.$watch('$wire.memberGrowthChart', () => this.updateMemberGrowth());
this.$watch('$wire.hourlySalesChart', () => this.updateHourlySales());
this.$watch('$wire.outletPerformanceChart', () => this.updateOutletPerformance());
this.$watch('$wire.categoryDistributionChart', () => this.updateCategoryDistribution());
this.$watch('$wire.paymentMethodChart', () => this.updatePaymentMethod());
this.$watch('$wire.dayOfWeekSalesChart', () => this.updateDayOfWeekSales());
this.$watch('$wire.customerTypeChart', () => this.updateCustomerType());
this.$watch('$wire.transactionTrendChart', () => this.updateTransactionTrend());
this.$watch('$wire.aovTrendChart', () => this.updateAovTrend());
this.$watch('$wire.expenseBreakdownChart', () => this.updateExpenseBreakdown());
this.$watch('$wire.revenueVsCostChart', () => this.updateRevenueVsCost());
this.$watch('$wire.customerRetentionChart', () => this.updateCustomerRetention());
this.$watch('$wire.voucherUsageTrendChart', () => this.updateVoucherUsage());
},
initAll() {
console.log('Rendering Analysis Charts...');
this.initRevenueTrend();
this.initMemberGrowth();
this.initHourlySales();
this.initOutletPerformance();
this.initCategoryDistribution();
this.initPaymentMethod();
this.initDayOfWeekSales();
this.initCustomerType();
this.initTransactionTrend();
this.initAovTrend();
this.initExpenseBreakdown();
this.initRevenueVsCost();
this.initCustomerRetention();
this.initVoucherUsage();
this.isInitialized = true;
},
initRevenueTrend() {
const ctx = document.getElementById('revenueTrendChart');
if (!ctx) return;
const data = $wire.revenueTrendChart;
if (!data || !data.labels) return;
this.charts.revenue = new Chart(ctx, {
type: 'line',
data: this.getRevenueData(),
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top'
}
},
scales: {
y: {
beginAtZero: true
}
}
}
});
},
getRevenueData() {
const data = $wire.revenueTrendChart;
return {
labels: data.labels || [],
datasets: [{
label: 'Pendapatan',
data: data.revenue || [],
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
tension: 0.4,
fill: true
},
{
label: 'Laba Bersih',
data: data.profit || [],
borderColor: '#3b82f6',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4,
fill: true
}
]
};
},
updateRevenueTrend() {
if (this.charts.revenue) {
this.charts.revenue.data = this.getRevenueData();
this.charts.revenue.update();
} else {
this.initRevenueTrend();
}
},
initMemberGrowth() {
const ctx = document.getElementById('memberGrowthChart');
if (!ctx) return;
const data = $wire.memberGrowthChart;
if (!data || !data.labels) return;
this.charts.memberGrowth = new Chart(ctx, {
type: 'line',
data: this.getMemberGrowthData(),
options: {
responsive: true,
maintainAspectRatio: false,
scales: {
y: {
beginAtZero: true
}
}
}
});
},
getMemberGrowthData() {
const data = $wire.memberGrowthChart;
return {
labels: data.labels || [],
datasets: [{
label: 'Total Member',
data: data.data || [],
borderColor: '#8b5cf6',
backgroundColor: 'rgba(139, 92, 246, 0.1)',
fill: true,
tension: 0.2
}]
};
},
updateMemberGrowth() {
if (this.charts.memberGrowth) {
this.charts.memberGrowth.data = this.getMemberGrowthData();
this.charts.memberGrowth.update();
} else {
this.initMemberGrowth();
}
},
initHourlySales() {
const ctx = document.getElementById('hourlySalesChart');
if (!ctx) return;
const data = $wire.hourlySalesChart;
if (!data || !data.labels) return;
this.charts.hourlySales = new Chart(ctx, {
type: 'bar',
data: this.getHourlySalesData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getHourlySalesData() {
const data = $wire.hourlySalesChart;
return {
labels: data.labels || [],
datasets: [{
label: 'Jumlah Order',
data: data.data || [],
backgroundColor: '#f59e0b'
}]
};
},
updateHourlySales() {
if (this.charts.hourlySales) {
this.charts.hourlySales.data = this.getHourlySalesData();
this.charts.hourlySales.update();
} else {
this.initHourlySales();
}
},
initOutletPerformance() {
const ctx = document.getElementById('outletPerformanceChart');
if (!ctx) return;
const data = $wire.outletPerformanceChart;
if (!data || !data.labels) return;
this.charts.outletPerformance = new Chart(ctx, {
type: 'bar',
data: this.getOutletPerformanceData(),
options: {
indexAxis: 'y',
responsive: true,
maintainAspectRatio: false
}
});
},
getOutletPerformanceData() {
const data = $wire.outletPerformanceChart;
return {
labels: data.labels || [],
datasets: [{
label: 'Pendapatan',
data: data.data || [],
backgroundColor: '#ef4444'
}]
};
},
updateOutletPerformance() {
if (this.charts.outletPerformance) {
this.charts.outletPerformance.data = this.getOutletPerformanceData();
this.charts.outletPerformance.update();
} else {
this.initOutletPerformance();
}
},
initCategoryDistribution() {
const ctx = document.getElementById('categoryDistributionChart');
if (!ctx) return;
const data = $wire.categoryDistributionChart;
if (!data || !data.labels) return;
this.charts.categoryDist = new Chart(ctx, {
type: 'doughnut',
data: this.getCategoryDistributionData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getCategoryDistributionData() {
const data = $wire.categoryDistributionChart;
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#10b981', '#3b82f6', '#f59e0b']
}]
};
},
updateCategoryDistribution() {
if (this.charts.categoryDist) {
this.charts.categoryDist.data = this.getCategoryDistributionData();
this.charts.categoryDist.update();
} else {
this.initCategoryDistribution();
}
},
initPaymentMethod() {
const ctx = document.getElementById('paymentMethodChart');
if (!ctx) return;
const data = $wire.paymentMethodChart;
if (!data || !data.labels) return;
this.charts.paymentMethod = new Chart(ctx, {
type: 'pie',
data: this.getPaymentMethodData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getPaymentMethodData() {
const data = $wire.paymentMethodChart;
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#10b981', '#3b82f6', '#8b5cf6', '#6b7280']
}]
};
},
updatePaymentMethod() {
if (this.charts.paymentMethod) {
this.charts.paymentMethod.data = this.getPaymentMethodData();
this.charts.paymentMethod.update();
} else {
this.initPaymentMethod();
}
},
initDayOfWeekSales() {
const ctx = document.getElementById('dayOfWeekSalesChart');
if (!ctx) return;
const data = $wire.dayOfWeekSalesChart;
if (!data || !data.labels) return;
this.charts.dayOfWeek = new Chart(ctx, {
type: 'bar',
data: this.getDayOfWeekData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getDayOfWeekData() {
const data = $wire.dayOfWeekSalesChart;
return {
labels: data.labels || [],
datasets: [{
label: 'Pendapatan',
data: data.data || [],
backgroundColor: '#6366f1'
}]
};
},
updateDayOfWeekSales() {
if (this.charts.dayOfWeek) {
this.charts.dayOfWeek.data = this.getDayOfWeekData();
this.charts.dayOfWeek.update();
} else {
this.initDayOfWeekSales();
}
},
initCustomerType() {
const ctx = document.getElementById('customerTypeChart');
if (!ctx) return;
const data = $wire.customerTypeChart;
if (!data || !data.labels) return;
this.charts.customerType = new Chart(ctx, {
type: 'doughnut',
data: this.getCustomerTypeData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getCustomerTypeData() {
const data = $wire.customerTypeChart;
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#8b5cf6', '#94a3b8']
}]
};
},
updateCustomerType() {
if (this.charts.customerType) {
this.charts.customerType.data = this.getCustomerTypeData();
this.charts.customerType.update();
} else {
this.initCustomerType();
}
},
initTransactionTrend() {
const ctx = document.getElementById('transactionTrendChart');
if (!ctx) return;
const data = $wire.transactionTrendChart;
if (!data || !data.labels) return;
this.charts.transactionTrend = new Chart(ctx, {
type: 'line',
data: this.getTransactionTrendData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getTransactionTrendData() {
const data = $wire.transactionTrendChart;
return {
labels: data.labels || [],
datasets: [{
label: 'Jumlah Transaksi',
data: data.data || [],
borderColor: '#f43f5e',
backgroundColor: 'rgba(244, 63, 94, 0.1)',
fill: true,
tension: 0.3
}]
};
},
updateTransactionTrend() {
if (this.charts.transactionTrend) {
this.charts.transactionTrend.data = this.getTransactionTrendData();
this.charts.transactionTrend.update();
} else {
this.initTransactionTrend();
}
},
initAovTrend() {
const ctx = document.getElementById('aovTrendChart');
if (!ctx) return;
const data = $wire.aovTrendChart;
if (!data || !data.labels) return;
this.charts.aovTrend = new Chart(ctx, {
type: 'line',
data: this.getAovTrendData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getAovTrendData() {
const data = $wire.aovTrendChart;
return {
labels: data.labels || [],
datasets: [{
label: 'Rata-rata Keranjang (AOV)',
data: data.data || [],
borderColor: '#0ea5e9',
backgroundColor: 'rgba(14, 165, 233, 0.1)',
fill: true,
tension: 0.3
}]
};
},
updateAovTrend() {
if (this.charts.aovTrend) {
this.charts.aovTrend.data = this.getAovTrendData();
this.charts.aovTrend.update();
} else {
this.initAovTrend();
}
},
initExpenseBreakdown() {
const ctx = document.getElementById('expenseBreakdownChart');
if (!ctx) return;
const data = $wire.expenseBreakdownChart;
if (!data || !data.labels) return;
this.charts.expenseBreakdown = new Chart(ctx, {
type: 'doughnut',
data: this.getExpenseBreakdownData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getExpenseBreakdownData() {
const data = $wire.expenseBreakdownChart;
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#fb923c', '#4ade80']
}]
};
},
updateExpenseBreakdown() {
if (this.charts.expenseBreakdown) {
this.charts.expenseBreakdown.data = this.getExpenseBreakdownData();
this.charts.expenseBreakdown.update();
} else {
this.initExpenseBreakdown();
}
},
initRevenueVsCost() {
const ctx = document.getElementById('revenueVsCostChart');
if (!ctx) return;
const data = $wire.revenueVsCostChart;
if (!data || !data.labels) return;
this.charts.revenueVsCost = new Chart(ctx, {
type: 'bar',
data: this.getRevenueVsCostData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getRevenueVsCostData() {
const data = $wire.revenueVsCostChart;
return {
labels: data.labels || [],
datasets: [{
label: 'Total Nilai',
data: data.data || [],
backgroundColor: ['#10b981', '#ef4444']
}]
};
},
updateRevenueVsCost() {
if (this.charts.revenueVsCost) {
this.charts.revenueVsCost.data = this.getRevenueVsCostData();
this.charts.revenueVsCost.update();
} else {
this.initRevenueVsCost();
}
},
initCustomerRetention() {
const ctx = document.getElementById('customerRetentionChart');
if (!ctx) return;
const data = $wire.customerRetentionChart;
if (!data || !data.labels) return;
this.charts.customerRetention = new Chart(ctx, {
type: 'bar',
data: this.getCustomerRetentionData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getCustomerRetentionData() {
const data = $wire.customerRetentionChart;
return {
labels: data.labels || [],
datasets: [{
label: 'Jumlah Pelanggan',
data: data.data || [],
backgroundColor: ['#8b5cf6', '#ec4899']
}]
};
},
updateCustomerRetention() {
if (this.charts.customerRetention) {
this.charts.customerRetention.data = this.getCustomerRetentionData();
this.charts.customerRetention.update();
} else {
this.initCustomerRetention();
}
},
initVoucherUsage() {
const ctx = document.getElementById('voucherUsageTrendChart');
if (!ctx) return;
const data = $wire.voucherUsageTrendChart;
if (!data || !data.labels) return;
this.charts.voucherUsage = new Chart(ctx, {
type: 'line',
data: this.getVoucherUsageData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getVoucherUsageData() {
const data = $wire.voucherUsageTrendChart;
return {
labels: data.labels || [],
datasets: [{
label: 'Voucher Terpakai',
data: data.data || [],
borderColor: '#f59e0b',
backgroundColor: 'rgba(245, 158, 11, 0.1)',
fill: true,
tension: 0.3
}]
};
},
updateVoucherUsage() {
if (this.charts.voucherUsage) {
this.charts.voucherUsage.data = this.getVoucherUsageData();
this.charts.voucherUsage.update();
} else {
this.initVoucherUsage();
}
}
}));
</script>
@endscript

View File

@ -2,7 +2,7 @@
<flux:heading size="xl" level="1">Haloo, {{ auth()->user()?->employee?->full_name }}</flux:heading>
<flux:text class="mb-6 mt-2 text-base">Selamat datang di Studio Yadi Parfum.</flux:text>
<div class="mt-6">
<div class="mt-6" x-data="overviewCharts">
@if (count($outlets) > 1)
<div class="mb-6 grid grid-cols-1 lg:grid-cols-2 gap-4">
<flux:checkbox.group wire:model.live="selectedOutletIds" variant="buttons">
@ -33,5 +33,371 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g
</div>
@endforeach
</div>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6 mt-8">
<!-- 1. Today vs Yesterday Hourly Comparison -->
<flux:card class="md:col-span-2 xl:col-span-2" wire:ignore>
<flux:heading class="mb-4">Perbandingan Order per Jam (Hari Ini vs Kemarin)</flux:heading>
<div class="h-80">
<canvas id="hourlyComparisonChart"></canvas>
</div>
</flux:card>
<!-- 2. Today Category Distribution -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Distribusi Penjualan</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="todayCategoryChart"></canvas>
</div>
</flux:card>
<!-- 3. Today Payment Methods -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Metode Pembayaran</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="todayPaymentChart"></canvas>
</div>
</flux:card>
<!-- 4. Today Customer Type -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Member vs Guest</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="todayCustomerTypeChart"></canvas>
</div>
</flux:card>
<!-- 5. Today Top Selling Items -->
<flux:card wire:ignore>
<flux:heading class="mb-4">5 Item Terlaris</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="todayTopSellingChart"></canvas>
</div>
</flux:card>
<!-- 6. Today Top Customers -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Top 5 Pelanggan</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="todayTopCustomersChart"></canvas>
</div>
</flux:card>
<!-- 7. Today Discount Distribution -->
<flux:card wire:ignore>
<flux:heading class="mb-4">Normal vs Diskon</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="todayDiscountChart"></canvas>
</div>
</flux:card>
</div>
</div>
</flux:main>
@assets
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
@endassets
@script
<script>
Alpine.data('overviewCharts', () => ({
charts: {},
isInitialized: false,
init() {
const start = () => {
if (typeof Chart !== 'undefined') {
this.initAll();
} else {
setTimeout(start, 50);
}
};
this.$nextTick(() => start());
this.$watch('$wire.hourlyComparisonChart', () => this.updateHourlyComparison());
this.$watch('$wire.todayCategoryChart', () => this.updateCategoryDist());
this.$watch('$wire.todayPaymentChart', () => this.updatePaymentMethod());
this.$watch('$wire.todayCustomerTypeChart', () => this.updateCustomerType());
this.$watch('$wire.todayTopSellingChart', () => this.updateTopSelling());
this.$watch('$wire.todayTopCustomersChart', () => this.updateTopCustomers());
this.$watch('$wire.todayDiscountChart', () => this.updateDiscountDist());
},
initAll() {
this.initHourlyComparison();
this.initCategoryDist();
this.initPaymentMethod();
this.initCustomerType();
this.initTopSelling();
this.initTopCustomers();
this.initDiscountDist();
this.isInitialized = true;
},
initHourlyComparison() {
const ctx = document.getElementById('hourlyComparisonChart');
if (!ctx) return;
const data = $wire.hourlyComparisonChart;
if (!data || !data.labels) return;
this.charts.hourly = new Chart(ctx, {
type: 'line',
data: this.getHourlyData(),
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'top'
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
}
});
},
getHourlyData() {
const data = $wire.hourlyComparisonChart;
return {
labels: data.labels || [],
datasets: [{
label: 'Hari Ini',
data: data.today || [],
borderColor: '#10b981',
backgroundColor: 'rgba(16, 185, 129, 0.1)',
fill: true,
tension: 0.4
},
{
label: 'Kemarin',
data: data.yesterday || [],
borderColor: '#94a3b8',
backgroundColor: 'rgba(148, 163, 184, 0.05)',
borderDash: [5, 5],
fill: false,
tension: 0.4
}
]
};
},
updateHourlyComparison() {
if (this.charts.hourly) {
this.charts.hourly.data = this.getHourlyData();
this.charts.hourly.update();
} else {
this.initHourlyComparison();
}
},
initCategoryDist() {
const ctx = document.getElementById('todayCategoryChart');
if (!ctx) return;
const data = $wire.todayCategoryChart;
if (!data || !data.labels) return;
this.charts.category = new Chart(ctx, {
type: 'doughnut',
data: this.getCategoryData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getCategoryData() {
const data = $wire.todayCategoryChart;
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#10b981', '#3b82f6', '#f59e0b']
}]
};
},
updateCategoryDist() {
if (this.charts.category) {
this.charts.category.data = this.getCategoryData();
this.charts.category.update();
} else {
this.initCategoryDist();
}
},
initPaymentMethod() {
const ctx = document.getElementById('todayPaymentChart');
if (!ctx) return;
const data = $wire.todayPaymentChart;
if (!data || !data.labels) return;
this.charts.payment = new Chart(ctx, {
type: 'pie',
data: this.getPaymentData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getPaymentData() {
const data = $wire.todayPaymentChart;
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#10b981', '#3b82f6', '#8b5cf6', '#6b7280']
}]
};
},
updatePaymentMethod() {
if (this.charts.payment) {
this.charts.payment.data = this.getPaymentData();
this.charts.payment.update();
} else {
this.initPaymentMethod();
}
},
initCustomerType() {
const ctx = document.getElementById('todayCustomerTypeChart');
if (!ctx) return;
const data = $wire.todayCustomerTypeChart;
if (!data || !data.labels) return;
this.charts.customerType = new Chart(ctx, {
type: 'doughnut',
data: this.getCustomerTypeData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getCustomerTypeData() {
const data = $wire.todayCustomerTypeChart;
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#8b5cf6', '#94a3b8']
}]
};
},
updateCustomerType() {
if (this.charts.customerType) {
this.charts.customerType.data = this.getCustomerTypeData();
this.charts.customerType.update();
} else {
this.initCustomerType();
}
},
initTopSelling() {
const ctx = document.getElementById('todayTopSellingChart');
if (!ctx) return;
const data = $wire.todayTopSellingChart;
if (!data || !data.labels) return;
this.charts.topSelling = new Chart(ctx, {
type: 'doughnut',
data: this.getTopSellingData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getTopSellingData() {
const data = $wire.todayTopSellingChart;
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#f43f5e', '#fbbf24', '#10b981', '#3b82f6', '#8b5cf6']
}]
};
},
updateTopSelling() {
if (this.charts.topSelling) {
this.charts.topSelling.data = this.getTopSellingData();
this.charts.topSelling.update();
} else {
this.initTopSelling();
}
},
initTopCustomers() {
const ctx = document.getElementById('todayTopCustomersChart');
if (!ctx) return;
const data = $wire.todayTopCustomersChart;
if (!data || !data.labels) return;
this.charts.topCustomers = new Chart(ctx, {
type: 'pie',
data: this.getTopCustomersData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getTopCustomersData() {
const data = $wire.todayTopCustomersChart;
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#6366f1', '#ec4899', '#f59e0b', '#14b8a6', '#94a3b8']
}]
};
},
updateTopCustomers() {
if (this.charts.topCustomers) {
this.charts.topCustomers.data = this.getTopCustomersData();
this.charts.topCustomers.update();
} else {
this.initTopCustomers();
}
},
initDiscountDist() {
const ctx = document.getElementById('todayDiscountChart');
if (!ctx) return;
const data = $wire.todayDiscountChart;
if (!data || !data.labels) return;
this.charts.discountDist = new Chart(ctx, {
type: 'doughnut',
data: this.getDiscountData(),
options: {
responsive: true,
maintainAspectRatio: false
}
});
},
getDiscountData() {
const data = $wire.todayDiscountChart;
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#f87171', '#4ade80']
}]
};
},
updateDiscountDist() {
if (this.charts.discountDist) {
this.charts.discountDist.data = this.getDiscountData();
this.charts.discountDist.update();
} else {
this.initDiscountDist();
}
}
}));
</script>
@endscript