624 lines
29 KiB
PHP
624 lines
29 KiB
PHP
<flux:main>
|
|
<flux:heading size="xl" level="1" class="mb-6">{{ $pageTitle }}</flux:heading>
|
|
|
|
<div class="mt-6">
|
|
<div class="flex flex-col lg:flex-row lg:justify-between lg:items-center mb-6 gap-4">
|
|
<div class="flex flex-col sm:flex-row flex-wrap items-start sm:items-center gap-4 w-full">
|
|
<div class="flex items-center gap-2 w-full sm:w-auto">
|
|
<flux:select wire:model.live="period" class="w-full sm:w-auto">
|
|
<option value="all">Semua</option>
|
|
<option value="7">7 Hari Terakhir</option>
|
|
<option value="30">30 Hari Terakhir</option>
|
|
<option value="90">90 Hari Terakhir</option>
|
|
<option value="365">1 Tahun Terakhir</option>
|
|
</flux:select>
|
|
</div>
|
|
|
|
<flux:separator vertical class="hidden lg:block mx-2 my-2" />
|
|
|
|
@if (count($outlets) > 1)
|
|
<div class="flex flex-wrap gap-2 w-full sm:w-auto">
|
|
<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 vertical class="hidden lg:block mx-2 my-2" />
|
|
@endif
|
|
|
|
<div class="w-full sm:w-auto">
|
|
<flux:date-picker mode="range" wire:model.live="range">
|
|
<x-slot name="trigger">
|
|
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4 w-full">
|
|
<flux:date-picker.input />
|
|
<flux:date-picker.input />
|
|
</div>
|
|
</x-slot>
|
|
</flux:date-picker>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<flux:separator class="mb-6" />
|
|
|
|
<flux:heading>Counting Data</flux:heading>
|
|
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-6 gap-3 mb-6 mt-2">
|
|
@foreach ($countingData as $item)
|
|
<div class="rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700">
|
|
<flux:subheading>{{ $item['title'] }}</flux:subheading>
|
|
<flux:heading size="xl" class="mb-2">{{ $item['value'] }}</flux:heading>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
<flux:heading>Order</flux:heading>
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 xl:grid-cols-4 gap-4 mb-6 mt-2">
|
|
@foreach ($orders as $order)
|
|
<div class="rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700">
|
|
<flux:subheading>{{ $order['title'] }}</flux:subheading>
|
|
<flux:heading size="xl" class="mb-2">{{ $order['value'] }}</flux:heading>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
|
|
<flux:heading>Pengeluaran</flux:heading>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6 mt-2">
|
|
@foreach ($expenses as $expense)
|
|
<div class="rounded-lg px-6 py-4 bg-zinc-200 dark:bg-zinc-700">
|
|
<flux:subheading>{{ $expense['title'] }}</flux:subheading>
|
|
<flux:heading size="xl" class="mb-2">{{ $expense['value'] }}</flux:heading>
|
|
</div>
|
|
@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>
|
|
<flux:card class="mb-6 mt-2 bg-zinc-200 dark:bg-zinc-700">
|
|
<flux:table>
|
|
<flux:table.columns>
|
|
<flux:table.column>No</flux:table.column>
|
|
<flux:table.column>Nama</flux:table.column>
|
|
<flux:table.column>Harga</flux:table.column>
|
|
<flux:table.column>Terjual</flux:table.column>
|
|
<flux:table.column>Total</flux:table.column>
|
|
</flux:table.columns>
|
|
|
|
<flux:table.rows>
|
|
@foreach ($topPerfumes as $topPerfume)
|
|
<flux:table.row>
|
|
<flux:table.cell>{{ $loop->iteration }}</flux:table.cell>
|
|
<flux:table.cell>{{ $topPerfume['name'] }}</flux:table.cell>
|
|
<flux:table.cell>{{ $topPerfume['sale_price'] }}</flux:table.cell>
|
|
<flux:table.cell>{{ $topPerfume['total_sold'] }}</flux:table.cell>
|
|
<flux:table.cell variant="strong">{{ $topPerfume['total_sales'] }}
|
|
</flux:table.cell>
|
|
</flux:table.row>
|
|
@endforeach
|
|
</flux:table.rows>
|
|
</flux:table>
|
|
</flux:card>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:heading>Top 5 Produk</flux:heading>
|
|
<flux:card class="mb-6 mt-2 bg-zinc-200 dark:bg-zinc-700">
|
|
<flux:table>
|
|
<flux:table.columns>
|
|
<flux:table.column>No</flux:table.column>
|
|
<flux:table.column>Nama</flux:table.column>
|
|
<flux:table.column>Harga</flux:table.column>
|
|
<flux:table.column>Terjual</flux:table.column>
|
|
<flux:table.column>Total</flux:table.column>
|
|
</flux:table.columns>
|
|
|
|
<flux:table.rows>
|
|
@foreach ($topProducts as $topProduct)
|
|
<flux:table.row>
|
|
<flux:table.cell>{{ $loop->iteration }}</flux:table.cell>
|
|
<flux:table.cell>{{ $topProduct['name'] }}</flux:table.cell>
|
|
<flux:table.cell>{{ $topProduct['sale_price'] }}</flux:table.cell>
|
|
<flux:table.cell>{{ $topProduct['total_sold'] }}</flux:table.cell>
|
|
<flux:table.cell variant="strong">{{ $topProduct['total_sales'] }}
|
|
</flux:table.cell>
|
|
</flux:table.row>
|
|
@endforeach
|
|
</flux:table.rows>
|
|
</flux:table>
|
|
</flux:card>
|
|
</div>
|
|
|
|
<div>
|
|
<flux:heading>Top 5 Botol</flux:heading>
|
|
<flux:card class="mb-6 mt-2 bg-zinc-200 dark:bg-zinc-700">
|
|
<flux:table>
|
|
<flux:table.columns>
|
|
<flux:table.column>No</flux:table.column>
|
|
<flux:table.column>Nama</flux:table.column>
|
|
<flux:table.column>Harga</flux:table.column>
|
|
<flux:table.column>Terjual</flux:table.column>
|
|
<flux:table.column>Total</flux:table.column>
|
|
</flux:table.columns>
|
|
|
|
<flux:table.rows>
|
|
@foreach ($topBottles as $topBottle)
|
|
<flux:table.row>
|
|
<flux:table.cell>{{ $loop->iteration }}</flux:table.cell>
|
|
<flux:table.cell>{{ $topBottle['name'] }}</flux:table.cell>
|
|
<flux:table.cell>{{ $topBottle['sale_price'] }}</flux:table.cell>
|
|
<flux:table.cell>{{ $topBottle['total_sold'] }}</flux:table.cell>
|
|
<flux:table.cell variant="strong">{{ $topBottle['total_sales'] }}</flux:table.cell>
|
|
</flux:table.row>
|
|
@endforeach
|
|
</flux:table.rows>
|
|
</flux:table>
|
|
</flux:card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</flux:main>
|
|
|
|
@assets
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
@endassets
|
|
|
|
@script
|
|
<script>
|
|
Alpine.data('analysisCharts', () => {
|
|
// Local storage for chart instances to avoid Alpine reactivity proxying
|
|
// This prevents "Maximum call stack size exceeded" errors
|
|
const charts = {};
|
|
|
|
return {
|
|
unwrap(data) {
|
|
try {
|
|
return JSON.parse(JSON.stringify(data));
|
|
} catch (e) {
|
|
// Fallback for safety
|
|
return { labels: [], datasets: [] };
|
|
}
|
|
},
|
|
|
|
init() {
|
|
const start = () => {
|
|
if (typeof Chart !== 'undefined') {
|
|
this.initAll();
|
|
} else {
|
|
setTimeout(start, 50);
|
|
}
|
|
};
|
|
|
|
this.$nextTick(() => start());
|
|
|
|
// 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.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));
|
|
},
|
|
|
|
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 {
|
|
// init map for manual mapping if needed, or rely on naming convention
|
|
const initMap = {
|
|
'revenue': 'initRevenueTrend',
|
|
'memberGrowth': 'initMemberGrowth',
|
|
'hourlySales': 'initHourlySales',
|
|
'outletPerformance': 'initOutletPerformance',
|
|
'categoryDist': 'initCategoryDistribution',
|
|
'paymentMethod': 'initPaymentMethod',
|
|
'dayOfWeekSales': 'initDayOfWeekSales',
|
|
'customerType': 'initCustomerType',
|
|
'transactionTrend': 'initTransactionTrend',
|
|
'aovTrend': 'initAovTrend',
|
|
'expenseBreakdown': 'initExpenseBreakdown',
|
|
'revenueVsCost': 'initRevenueVsCost',
|
|
'customerRetention': 'initCustomerRetention',
|
|
'voucherUsage': 'initVoucherUsage',
|
|
};
|
|
const fnName = initMap[key];
|
|
if (fnName && typeof this[fnName] === 'function') {
|
|
this[fnName]();
|
|
}
|
|
}
|
|
},
|
|
|
|
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() {
|
|
this.createChart('revenueTrendChart', 'revenue', 'line', this.getRevenueData(), {
|
|
plugins: { legend: { position: 'top' } },
|
|
scales: { y: { beginAtZero: true } }
|
|
});
|
|
},
|
|
getRevenueData() {
|
|
const data = this.unwrap($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
|
|
}
|
|
]
|
|
};
|
|
},
|
|
|
|
initMemberGrowth() {
|
|
this.createChart('memberGrowthChart', 'memberGrowth', 'line', this.getMemberGrowthData(), {
|
|
scales: { y: { beginAtZero: true } }
|
|
});
|
|
},
|
|
getMemberGrowthData() {
|
|
const data = this.unwrap($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
|
|
}]
|
|
};
|
|
},
|
|
|
|
initHourlySales() {
|
|
this.createChart('hourlySalesChart', 'hourlySales', 'bar', this.getHourlySalesData());
|
|
},
|
|
getHourlySalesData() {
|
|
const data = this.unwrap($wire.hourlySalesChart);
|
|
return {
|
|
labels: data.labels || [],
|
|
datasets: [{
|
|
label: 'Jumlah Order',
|
|
data: data.data || [],
|
|
backgroundColor: '#f59e0b'
|
|
}]
|
|
};
|
|
},
|
|
|
|
initOutletPerformance() {
|
|
this.createChart('outletPerformanceChart', 'outletPerformance', 'bar', this.getOutletPerformanceData(), {
|
|
indexAxis: 'y'
|
|
});
|
|
},
|
|
getOutletPerformanceData() {
|
|
const data = this.unwrap($wire.outletPerformanceChart);
|
|
return {
|
|
labels: data.labels || [],
|
|
datasets: [{
|
|
label: 'Pendapatan',
|
|
data: data.data || [],
|
|
backgroundColor: '#ef4444'
|
|
}]
|
|
};
|
|
},
|
|
|
|
initCategoryDistribution() {
|
|
this.createChart('categoryDistributionChart', 'categoryDist', 'doughnut', this.getCategoryDistributionData());
|
|
},
|
|
getCategoryDistributionData() {
|
|
const data = this.unwrap($wire.categoryDistributionChart);
|
|
return {
|
|
labels: data.labels || [],
|
|
datasets: [{
|
|
data: data.data || [],
|
|
backgroundColor: ['#10b981', '#3b82f6', '#f59e0b']
|
|
}]
|
|
};
|
|
},
|
|
|
|
initPaymentMethod() {
|
|
this.createChart('paymentMethodChart', 'paymentMethod', 'pie', this.getPaymentMethodData());
|
|
},
|
|
getPaymentMethodData() {
|
|
const data = this.unwrap($wire.paymentMethodChart);
|
|
return {
|
|
labels: data.labels || [],
|
|
datasets: [{
|
|
data: data.data || [],
|
|
backgroundColor: ['#10b981', '#3b82f6', '#8b5cf6', '#6b7280']
|
|
}]
|
|
};
|
|
},
|
|
|
|
initDayOfWeekSales() {
|
|
this.createChart('dayOfWeekSalesChart', 'dayOfWeekSales', 'bar', this.getDayOfWeekData());
|
|
},
|
|
getDayOfWeekData() {
|
|
const data = this.unwrap($wire.dayOfWeekSalesChart);
|
|
return {
|
|
labels: data.labels || [],
|
|
datasets: [{
|
|
label: 'Pendapatan',
|
|
data: data.data || [],
|
|
backgroundColor: '#6366f1'
|
|
}]
|
|
};
|
|
},
|
|
|
|
initCustomerType() {
|
|
this.createChart('customerTypeChart', 'customerType', 'doughnut', this.getCustomerTypeData());
|
|
},
|
|
getCustomerTypeData() {
|
|
const data = this.unwrap($wire.customerTypeChart);
|
|
return {
|
|
labels: data.labels || [],
|
|
datasets: [{
|
|
data: data.data || [],
|
|
backgroundColor: ['#8b5cf6', '#94a3b8']
|
|
}]
|
|
};
|
|
},
|
|
|
|
initTransactionTrend() {
|
|
this.createChart('transactionTrendChart', 'transactionTrend', 'line', this.getTransactionTrendData());
|
|
},
|
|
getTransactionTrendData() {
|
|
const data = this.unwrap($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
|
|
}]
|
|
};
|
|
},
|
|
|
|
initAovTrend() {
|
|
this.createChart('aovTrendChart', 'aovTrend', 'line', this.getAovTrendData());
|
|
},
|
|
getAovTrendData() {
|
|
const data = this.unwrap($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
|
|
}]
|
|
};
|
|
},
|
|
|
|
initExpenseBreakdown() {
|
|
this.createChart('expenseBreakdownChart', 'expenseBreakdown', 'doughnut', this.getExpenseBreakdownData());
|
|
},
|
|
getExpenseBreakdownData() {
|
|
const data = this.unwrap($wire.expenseBreakdownChart);
|
|
return {
|
|
labels: data.labels || [],
|
|
datasets: [{
|
|
data: data.data || [],
|
|
backgroundColor: ['#fb923c', '#4ade80']
|
|
}]
|
|
};
|
|
},
|
|
|
|
initRevenueVsCost() {
|
|
this.createChart('revenueVsCostChart', 'revenueVsCost', 'bar', this.getRevenueVsCostData());
|
|
},
|
|
getRevenueVsCostData() {
|
|
const data = this.unwrap($wire.revenueVsCostChart);
|
|
return {
|
|
labels: data.labels || [],
|
|
datasets: [{
|
|
label: 'Total Nilai',
|
|
data: data.data || [],
|
|
backgroundColor: ['#10b981', '#ef4444']
|
|
}]
|
|
};
|
|
},
|
|
|
|
initCustomerRetention() {
|
|
this.createChart('customerRetentionChart', 'customerRetention', 'bar', this.getCustomerRetentionData());
|
|
},
|
|
getCustomerRetentionData() {
|
|
const data = this.unwrap($wire.customerRetentionChart);
|
|
return {
|
|
labels: data.labels || [],
|
|
datasets: [{
|
|
label: 'Jumlah Pelanggan',
|
|
data: data.data || [],
|
|
backgroundColor: ['#8b5cf6', '#ec4899']
|
|
}]
|
|
};
|
|
},
|
|
|
|
initVoucherUsage() {
|
|
this.createChart('voucherUsageTrendChart', 'voucherUsage', 'line', this.getVoucherUsageData());
|
|
},
|
|
getVoucherUsageData() {
|
|
const data = this.unwrap($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
|
|
}]
|
|
};
|
|
},
|
|
|
|
// Helper to create chart efficiently
|
|
createChart(elementId, chartKey, type, data, extraOptions = {}) {
|
|
if (!data || !data.labels) return;
|
|
|
|
const ctx = document.getElementById(elementId);
|
|
if (!ctx) return;
|
|
|
|
if (charts[chartKey]) {
|
|
charts[chartKey].destroy();
|
|
}
|
|
|
|
charts[chartKey] = new Chart(ctx, {
|
|
type: type,
|
|
data: data,
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
...extraOptions
|
|
}
|
|
});
|
|
}
|
|
};
|
|
});
|
|
</script>
|
|
@endscript
|