parfum/resources/views/livewire/studio/dashboard/overview.blade.php

403 lines
18 KiB
PHP

<flux:main>
<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" 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">
@foreach ($outlets as $key => $value)
<flux:checkbox value="{{ $key }}" label="{{ $value }}" />
@endforeach
</flux:checkbox.group>
</div>
<flux:separator class="mb-6" />
@endif
<flux:subheading>Data yang ditampilkan di bawah ini adalah data hari ini dan hari kemarin.</flux:subheading>
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 mb-6 mt-2">
@foreach ($stats as $stat)
<div
class="relative @if ($stat['title'] === 'Top Parfum') md:col-span-2 xl:col-span-2 @endif rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700 transition-all duration-300 hover:scale-[1.02] hover:shadow-lg dark:hover:shadow-zinc-900/50">
<div class="flex items-start justify-between">
<flux:subheading>{{ $stat['title'] }}</flux:subheading>
@if (isset($stat['description']))
<div class="absolute top-2 right-2">
<flux:tooltip toggleable content="{{ $stat['description'] }}">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!px-1 border-none bg-transparent hover:bg-zinc-300 dark:hover:bg-zinc-600" />
</flux:tooltip>
</div>
@endif
</div>
<div class="flex items-center justify-between mt-1">
<flux:heading size="xl">{{ $stat['value'] }}</flux:heading>
<flux:subheading size="xs">{{ $stat['previous'] }}</flux:subheading>
</div>
<div
class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-green-600 dark:text-green-400 @else text-red-500 dark:text-red-400 @endif">
<flux:icon :icon="$stat['trendUp'] ? 'arrow-trending-up' : 'arrow-trending-down'"
variant="micro" />
{{ $stat['trend'] }}
</div>
</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="relative md:col-span-2 xl:col-span-2 transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Grafik perbandingan jumlah transaksi tiap jam antara hari ini dan kemarin.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Perbandingan Order per Jam (Hari Ini vs Kemarin)</flux:heading>
<div class="h-80">
<canvas id="hourlyComparisonChart"></canvas>
</div>
</flux:card>
<!-- 2. Today Category Distribution -->
<flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Pembagian pendapatan berdasarkan kategori produk (Parfum, Produk, Botol).">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Distribusi Penjualan</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="todayCategoryChart"></canvas>
</div>
</flux:card>
<!-- 3. Today Payment Methods -->
<flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable content="Persentase penggunaan berbagai metode pembayaran oleh pelanggan.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Metode Pembayaran</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="todayPaymentChart"></canvas>
</div>
</flux:card>
<!-- 4. Today Customer Type -->
<flux:card
class="relative transition-all duration-300 hover:scale-[1.01] hover:shadow-lg dark:hover:shadow-zinc-900/50"
wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Perbandingan jumlah transaksi antara pelanggan terdaftar (Member) dan pelanggan umum (Guest).">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Member vs Guest</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="todayCustomerTypeChart"></canvas>
</div>
</flux:card>
<!-- 5. Today Top Selling Items -->
<flux:card class="relative" wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Lima produk dengan jumlah unit terjual terbanyak selama periode ini.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">5 Item Terlaris</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="todayTopSellingChart"></canvas>
</div>
</flux:card>
<!-- 6. Today Top Customers -->
<flux:card class="relative" wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Lima pelanggan dengan total nominal belanja tertinggi selama periode ini.">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Top 5 Pelanggan</flux:heading>
<div class="h-80 flex items-center justify-center">
<canvas id="todayTopCustomersChart"></canvas>
</div>
</flux:card>
<!-- 7. Today Discount Distribution -->
<flux:card class="relative" wire:ignore>
<div class="absolute top-3 right-3">
<flux:tooltip toggleable
content="Perbandingan jumlah transaksi antara pelanggan terdaftar (Member) dan pelanggan umum (Guest).">
<flux:button icon="exclamation-circle" variant="subtle" size="sm"
class="!p-1 border-none bg-transparent" />
</flux:tooltip>
</div>
<flux:heading class="mb-4">Normal vs Diskon</flux:heading>
<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', () => {
// Local storage for chart instances to avoid Alpine reactivity proxying
// This prevents "Maximum call stack size exceeded" errors
const charts = {};
return {
isInitialized: false,
unwrap(data) {
try {
return JSON.parse(JSON.stringify(data));
} catch (e) {
return {
labels: [],
datasets: []
};
}
},
init() {
const start = () => {
if (typeof Chart !== 'undefined') {
this.initAll();
} else {
setTimeout(start, 50);
}
};
this.$nextTick(() => start());
this.$watch('$wire.hourlyComparisonChart', () => this.updateChart('hourly', this
.getHourlyData));
this.$watch('$wire.todayCategoryChart', () => this.updateChart('category', this
.getCategoryData));
this.$watch('$wire.todayPaymentChart', () => this.updateChart('payment', this.getPaymentData));
this.$watch('$wire.todayCustomerTypeChart', () => this.updateChart('customerType', this
.getCustomerTypeData));
this.$watch('$wire.todayTopSellingChart', () => this.updateChart('topSelling', this
.getTopSellingData));
this.$watch('$wire.todayTopCustomersChart', () => this.updateChart('topCustomers', this
.getTopCustomersData));
this.$watch('$wire.todayDiscountChart', () => this.updateChart('discountDist', this
.getDiscountData));
},
updateChart(key, getDataFn) {
if (charts[key]) {
try {
charts[key].data = getDataFn.call(this);
charts[key].update();
} catch (e) {
console.error(`Error updating chart ${key}`, e);
}
} else {
// Map key to init function if chart doesn't exist
const initMap = {
'hourly': 'initHourlyComparison',
'category': 'initCategoryDist',
'payment': 'initPaymentMethod',
'customerType': 'initCustomerType',
'topSelling': 'initTopSelling',
'topCustomers': 'initTopCustomers',
'discountDist': 'initDiscountDist',
};
const fnName = initMap[key];
if (fnName && typeof this[fnName] === 'function') {
this[fnName]();
}
}
},
initAll() {
this.initHourlyComparison();
this.initCategoryDist();
this.initPaymentMethod();
this.initCustomerType();
this.initTopSelling();
this.initTopCustomers();
this.initDiscountDist();
this.isInitialized = true;
},
// Helper to create charts consistently
createChart(elementId, key, type, data, options = {}) {
const ctx = document.getElementById(elementId);
if (!ctx) return;
if (!data || !data.labels) return;
if (charts[key]) {
charts[key].destroy();
}
charts[key] = new Chart(ctx, {
type: type,
data: data,
options: {
responsive: true,
maintainAspectRatio: false,
...options
}
});
},
initHourlyComparison() {
this.createChart('hourlyComparisonChart', 'hourly', 'line', this.getHourlyData(), {
plugins: {
legend: {
position: 'top'
}
},
scales: {
y: {
beginAtZero: true,
ticks: {
stepSize: 1
}
}
}
});
},
getHourlyData() {
const data = this.unwrap($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
}
]
};
},
initCategoryDist() {
this.createChart('todayCategoryChart', 'category', 'doughnut', this.getCategoryData());
},
getCategoryData() {
const data = this.unwrap($wire.todayCategoryChart);
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#10b981', '#3b82f6', '#f59e0b']
}]
};
},
initPaymentMethod() {
this.createChart('todayPaymentChart', 'payment', 'pie', this.getPaymentData());
},
getPaymentData() {
const data = this.unwrap($wire.todayPaymentChart);
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#10b981', '#3b82f6', '#8b5cf6', '#6b7280']
}]
};
},
initCustomerType() {
this.createChart('todayCustomerTypeChart', 'customerType', 'doughnut', this
.getCustomerTypeData());
},
getCustomerTypeData() {
const data = this.unwrap($wire.todayCustomerTypeChart);
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#8b5cf6', '#94a3b8']
}]
};
},
initTopSelling() {
this.createChart('todayTopSellingChart', 'topSelling', 'doughnut', this.getTopSellingData());
},
getTopSellingData() {
const data = this.unwrap($wire.todayTopSellingChart);
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#f43f5e', '#fbbf24', '#10b981', '#3b82f6', '#8b5cf6']
}]
};
},
initTopCustomers() {
this.createChart('todayTopCustomersChart', 'topCustomers', 'pie', this.getTopCustomersData());
},
getTopCustomersData() {
const data = this.unwrap($wire.todayTopCustomersChart);
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#6366f1', '#ec4899', '#f59e0b', '#14b8a6', '#94a3b8']
}]
};
},
initDiscountDist() {
this.createChart('todayDiscountChart', 'discountDist', 'doughnut', this.getDiscountData());
},
getDiscountData() {
const data = this.unwrap($wire.todayDiscountChart);
return {
labels: data.labels || [],
datasets: [{
data: data.data || [],
backgroundColor: ['#f87171', '#4ade80']
}]
};
}
};
});
</script>
@endscript