refactor: improve Alpine chart instance management to prevent maximum call stack size errors and streamline updates.

This commit is contained in:
Yoga Pangestu 2026-01-05 22:10:00 +07:00
parent ab01645ccd
commit 754b65259e
2 changed files with 548 additions and 839 deletions

View File

@ -263,584 +263,361 @@
@script
<script>
Alpine.data('analysisCharts', () => ({
charts: {},
Alpine.data('analysisCharts', () => {
// Local storage for chart instances to avoid Alpine reactivity proxying
// This prevents "Maximum call stack size exceeded" errors
const charts = {};
init() {
// Memastikan DOM siap dan library tersedia sebelum inisialisasi awal
const start = () => {
if (typeof Chart !== 'undefined') {
this.initAll();
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 {
setTimeout(start, 50);
// 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]();
}
}
};
},
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 {
initAll() {
console.log('Rendering Analysis Charts...');
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();
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

View File

@ -99,53 +99,112 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g
@script
<script>
Alpine.data('overviewCharts', () => ({
charts: {},
isInitialized: false,
Alpine.data('overviewCharts', () => {
// Local storage for chart instances to avoid Alpine reactivity proxying
// This prevents "Maximum call stack size exceeded" errors
const charts = {};
init() {
const start = () => {
if (typeof Chart !== 'undefined') {
this.initAll();
} else {
setTimeout(start, 50);
return {
isInitialized: false,
unwrap(data) {
try {
return JSON.parse(JSON.stringify(data));
} catch (e) {
return {
labels: [],
datasets: []
};
}
};
},
this.$nextTick(() => start());
init() {
const start = () => {
if (typeof Chart !== 'undefined') {
this.initAll();
} else {
setTimeout(start, 50);
}
};
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());
},
this.$nextTick(() => start());
initAll() {
this.initHourlyComparison();
this.initCategoryDist();
this.initPaymentMethod();
this.initCustomerType();
this.initTopSelling();
this.initTopCustomers();
this.initDiscountDist();
this.isInitialized = true;
},
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));
},
initHourlyComparison() {
const ctx = document.getElementById('hourlyComparisonChart');
if (!ctx) return;
const data = $wire.hourlyComparisonChart;
if (!data || !data.labels) return;
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]();
}
}
},
this.charts.hourly = new Chart(ctx, {
type: 'line',
data: this.getHourlyData(),
options: {
responsive: true,
maintainAspectRatio: false,
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'
@ -159,245 +218,118 @@ class="flex items-center gap-1 font-medium text-sm @if ($stat['trendUp']) text-g
}
}
}
}
});
},
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();
});
},
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']
}]
};
}
},
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