|
|
<x-app-layout>
|
|
|
<x-slot name="header">
|
|
|
<div class="flex items-center justify-between">
|
|
|
<div>
|
|
|
<h1 class="heading-2 mb-2">Analytics Dashboard</h1>
|
|
|
<p class="text-muted">Real-time insights and performance metrics</p>
|
|
|
</div>
|
|
|
<div class="text-right">
|
|
|
<div class="text-sm text-muted">{{ date('l, F j, Y') }}</div>
|
|
|
<div class="text-lg font-semibold text-white">{{ date('g:i A') }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</x-slot>
|
|
|
|
|
|
<div class="py-8">
|
|
|
<div class="container-custom space-y-8">
|
|
|
<!-- Time Period Filter -->
|
|
|
<div class="flex items-center justify-between mb-8">
|
|
|
<div>
|
|
|
<h2 class="text-2xl font-bold text-white mb-2">Performance Overview</h2>
|
|
|
<p class="text-white/60">Track your store's key metrics and trends</p>
|
|
|
</div>
|
|
|
<div class="flex items-center space-x-4">
|
|
|
<div class="flex items-center space-x-2 text-white/60">
|
|
|
<div class="w-2 h-2 bg-emerald-400 rounded-full animate-pulse"></div>
|
|
|
<span class="text-sm">Real-time</span>
|
|
|
</div>
|
|
|
<!-- Simple Time Filter -->
|
|
|
<div class="bg-white/5 backdrop-blur-2xl border border-white/10 rounded-2xl p-2 shadow-2xl">
|
|
|
<div class="flex space-x-1">
|
|
|
<button onclick="switchPeriod('daily')" id="btn-daily" data-period="daily" class="time-filter-btn active px-4 py-2 rounded-xl text-sm font-semibold">
|
|
|
Daily
|
|
|
</button>
|
|
|
<button onclick="switchPeriod('weekly')" id="btn-weekly" data-period="weekly" class="time-filter-btn px-4 py-2 rounded-xl text-sm font-semibold">
|
|
|
Weekly
|
|
|
</button>
|
|
|
<button onclick="switchPeriod('monthly')" id="btn-monthly" data-period="monthly" class="time-filter-btn px-4 py-2 rounded-xl text-sm font-semibold">
|
|
|
Monthly
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Metric Cards Grid -->
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
|
|
<!-- Revenue Card -->
|
|
|
<div class="bg-white/5 backdrop-blur-2xl border border-white/10 rounded-2xl p-6 shadow-2xl">
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
<div class="flex items-center space-x-3">
|
|
|
<div class="w-10 h-10 bg-gradient-to-br from-emerald-400/20 to-emerald-600/10 rounded-xl flex items-center justify-center border border-white/20">
|
|
|
<i class="fas fa-dollar-sign text-emerald-400 text-lg"></i>
|
|
|
</div>
|
|
|
<h3 class="text-white text-sm font-semibold uppercase tracking-wider" id="revenue-title">Today's Revenue</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="text-3xl font-bold text-emerald-400 mb-1" id="revenue-value">฿0</div>
|
|
|
<p class="text-white/60 text-xs" id="revenue-previous">Previous: ฿0</p>
|
|
|
</div>
|
|
|
|
|
|
<!-- Sales Card -->
|
|
|
<div class="bg-white/5 backdrop-blur-2xl border border-white/10 rounded-2xl p-6 shadow-2xl">
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
<div class="flex items-center space-x-3">
|
|
|
<div class="w-10 h-10 bg-gradient-to-br from-blue-400/20 to-blue-600/10 rounded-xl flex items-center justify-center border border-white/20">
|
|
|
<i class="fas fa-shopping-bag text-blue-400 text-lg"></i>
|
|
|
</div>
|
|
|
<h3 class="text-white text-sm font-semibold uppercase tracking-wider" id="sales-title">Orders Today</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="text-3xl font-bold text-blue-400 mb-1" id="sales-value">0</div>
|
|
|
<p class="text-white/60 text-xs" id="sales-previous">Previous: 0</p>
|
|
|
</div>
|
|
|
|
|
|
<!-- Customers Card -->
|
|
|
<div class="bg-white/5 backdrop-blur-2xl border border-white/10 rounded-2xl p-6 shadow-2xl">
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
<div class="flex items-center space-x-3">
|
|
|
<div class="w-10 h-10 bg-gradient-to-br from-orange-400/20 to-orange-600/10 rounded-xl flex items-center justify-center border border-white/20">
|
|
|
<i class="fas fa-users text-orange-400 text-lg"></i>
|
|
|
</div>
|
|
|
<h3 class="text-white text-sm font-semibold uppercase tracking-wider" id="customers-title">Customers Today</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="text-3xl font-bold text-orange-400 mb-1" id="customers-value">0</div>
|
|
|
<p class="text-white/60 text-xs" id="customers-previous">Previous: 0</p>
|
|
|
</div>
|
|
|
|
|
|
<!-- Products Card -->
|
|
|
<div class="bg-white/5 backdrop-blur-2xl border border-white/10 rounded-2xl p-6 shadow-2xl">
|
|
|
<div class="flex items-center justify-between mb-4">
|
|
|
<div class="flex items-center space-x-3">
|
|
|
<div class="w-10 h-10 bg-gradient-to-br from-purple-400/20 to-purple-600/10 rounded-xl flex items-center justify-center border border-white/20">
|
|
|
<i class="fas fa-box text-purple-400 text-lg"></i>
|
|
|
</div>
|
|
|
<h3 class="text-white text-sm font-semibold uppercase tracking-wider">Total Products</h3>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="text-3xl font-bold text-purple-400 mb-1" id="products-value">0</div>
|
|
|
<p class="text-white/60 text-xs">Active products</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Charts Grid -->
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
|
|
<!-- Revenue Chart -->
|
|
|
<div class="bg-white/5 backdrop-blur-2xl border border-white/10 rounded-2xl p-6 shadow-2xl hover:bg-white/10 transition-all duration-300 cursor-pointer" onclick="openRevenueDetails()">
|
|
|
<div class="flex items-center justify-between mb-6">
|
|
|
<h3 class="text-xl font-bold text-white">Revenue Trend</h3>
|
|
|
<div class="flex items-center space-x-2">
|
|
|
<div class="w-3 h-3 bg-emerald-400 rounded-full"></div>
|
|
|
<span class="text-sm text-white/60">Revenue</span>
|
|
|
<i class="fas fa-expand-alt text-white/40 ml-2"></i>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="relative h-80">
|
|
|
<canvas id="revenueChart"></canvas>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Top Products Chart -->
|
|
|
<div class="bg-white/5 backdrop-blur-2xl border border-white/10 rounded-2xl p-6 shadow-2xl hover:bg-white/10 transition-all duration-300 cursor-pointer" onclick="openProductsDetails()">
|
|
|
<div class="flex items-center justify-between mb-6">
|
|
|
<h3 class="text-xl font-bold text-white">Top Products</h3>
|
|
|
<div class="flex items-center space-x-2">
|
|
|
<span class="text-sm text-white/60">By Revenue</span>
|
|
|
<i class="fas fa-expand-alt text-white/40 ml-2"></i>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="relative h-80">
|
|
|
<canvas id="productsChart"></canvas>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Modal for Chart Details -->
|
|
|
<div id="chartModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden opacity-0 transition-all duration-500">
|
|
|
<div class="flex items-center justify-center min-h-screen p-4">
|
|
|
<div id="modalContainer" class="bg-gray-900/95 backdrop-blur-2xl border border-white/10 rounded-2xl p-6 max-w-6xl w-full max-h-[90vh] shadow-2xl flex flex-col transform scale-75 transition-all duration-500">
|
|
|
<div class="flex items-center justify-between mb-6 flex-shrink-0">
|
|
|
<h2 id="modalTitle" class="text-2xl font-bold text-white flex items-center">
|
|
|
<i id="modalIcon" class="fas fa-chart-line mr-3 text-emerald-400"></i>
|
|
|
<span id="modalTitleText">Chart Details</span>
|
|
|
</h2>
|
|
|
<button onclick="closeModal()" class="text-white/60 hover:text-white text-2xl hover:rotate-90 transition-all duration-300">
|
|
|
<i class="fas fa-times"></i>
|
|
|
</button>
|
|
|
</div>
|
|
|
<div id="modalContent" class="text-white overflow-y-auto flex-1 space-y-6">
|
|
|
<!-- Dynamic content will be loaded here -->
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Quick Actions -->
|
|
|
<div class="bg-white/5 backdrop-blur-2xl border border-white/10 rounded-2xl p-6 shadow-2xl">
|
|
|
<h3 class="text-xl font-bold text-white mb-6">
|
|
|
<i class="fas fa-rocket mr-3 text-purple-400"></i>Quick Actions
|
|
|
</h3>
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
|
<a href="{{ route('products.listOFproduct') }}" class="bg-gradient-to-r from-blue-600 to-purple-600 hover:from-blue-700 hover:to-purple-700 text-white font-semibold py-3 px-6 rounded-xl transition-all duration-200 flex items-center justify-center space-x-2 shadow-lg hover:shadow-xl transform hover:scale-105">
|
|
|
<i class="fas fa-cogs"></i>
|
|
|
<span>Manage Products</span>
|
|
|
</a>
|
|
|
<a href="{{ route('orders.index') }}" class="bg-gradient-to-r from-emerald-600 to-teal-600 hover:from-emerald-700 hover:to-teal-700 text-white font-semibold py-3 px-6 rounded-xl transition-all duration-200 flex items-center justify-center space-x-2 shadow-lg hover:shadow-xl transform hover:scale-105">
|
|
|
<i class="fas fa-shopping-bag"></i>
|
|
|
<span>View Orders</span>
|
|
|
</a>
|
|
|
<a href="{{ route('categories') }}" class="bg-gradient-to-r from-orange-600 to-red-600 hover:from-orange-700 hover:to-red-700 text-white font-semibold py-3 px-6 rounded-xl transition-all duration-200 flex items-center justify-center space-x-2 shadow-lg hover:shadow-xl transform hover:scale-105">
|
|
|
<i class="fas fa-store"></i>
|
|
|
<span>Visit Store</span>
|
|
|
</a>
|
|
|
<a href="{{ route('profile.edit') }}" class="bg-gradient-to-r from-purple-600 to-pink-600 hover:from-purple-700 hover:to-pink-700 text-white font-semibold py-3 px-6 rounded-xl transition-all duration-200 flex items-center justify-center space-x-2 shadow-lg hover:shadow-xl transform hover:scale-105">
|
|
|
<i class="fas fa-user-edit"></i>
|
|
|
<span>Edit Profile</span>
|
|
|
</a>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<script src="https:
|
|
|
|
|
|
<style>
|
|
|
.time-filter-btn {
|
|
|
background: rgba(255, 255, 255, 0.05);
|
|
|
color: rgba(255, 255, 255, 0.7);
|
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
|
transition: all 0.3s ease;
|
|
|
}
|
|
|
|
|
|
.time-filter-btn:hover {
|
|
|
background: rgba(255, 255, 255, 0.1);
|
|
|
color: rgba(255, 255, 255, 0.9);
|
|
|
border-color: rgba(255, 255, 255, 0.2);
|
|
|
box-shadow: 0 0 20px rgba(255, 255, 255, 0.1);
|
|
|
}
|
|
|
|
|
|
.time-filter-btn.active {
|
|
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.3), rgba(147, 51, 234, 0.3));
|
|
|
color: white;
|
|
|
border-color: rgba(59, 130, 246, 0.4);
|
|
|
box-shadow: 0 0 25px rgba(59, 130, 246, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
|
}
|
|
|
</style>
|
|
|
|
|
|
<script>
|
|
|
let currentPeriod = 'daily';
|
|
|
let revenueChart = null;
|
|
|
let productsChart = null;
|
|
|
|
|
|
|
|
|
function switchPeriod(period) {
|
|
|
currentPeriod = period;
|
|
|
|
|
|
|
|
|
document.querySelectorAll('.time-filter-btn').forEach(btn => btn.classList.remove('active'));
|
|
|
document.getElementById(`btn-${period}`).classList.add('active');
|
|
|
|
|
|
|
|
|
updateTitles(period);
|
|
|
|
|
|
|
|
|
loadDashboardData();
|
|
|
}
|
|
|
|
|
|
function updateTitles(period) {
|
|
|
const titles = {
|
|
|
daily: {
|
|
|
revenue: "Today's Revenue",
|
|
|
sales: "Orders Today",
|
|
|
customers: "Customers Today"
|
|
|
},
|
|
|
weekly: {
|
|
|
revenue: "This Week's Revenue",
|
|
|
sales: "Orders This Week",
|
|
|
customers: "Customers This Week"
|
|
|
},
|
|
|
monthly: {
|
|
|
revenue: "This Month's Revenue",
|
|
|
sales: "Orders This Month",
|
|
|
customers: "Customers This Month"
|
|
|
}
|
|
|
};
|
|
|
|
|
|
document.getElementById('revenue-title').textContent = titles[period].revenue;
|
|
|
document.getElementById('sales-title').textContent = titles[period].sales;
|
|
|
document.getElementById('customers-title').textContent = titles[period].customers;
|
|
|
}
|
|
|
|
|
|
async function loadDashboardData() {
|
|
|
try {
|
|
|
|
|
|
const response = await fetch(`/api/dashboard/summary?period=${currentPeriod}`);
|
|
|
const data = await response.json();
|
|
|
|
|
|
if (data.success) {
|
|
|
updateMetricCards(data.data);
|
|
|
}
|
|
|
|
|
|
|
|
|
await loadCharts();
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('Error loading dashboard data:', error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function updateMetricCards(data) {
|
|
|
|
|
|
document.getElementById('revenue-value').textContent = `฿${data.revenue.current.toLocaleString()}`;
|
|
|
document.getElementById('revenue-previous').textContent = `Previous: ฿${data.revenue.previous.toLocaleString()}`;
|
|
|
|
|
|
|
|
|
document.getElementById('sales-value').textContent = data.orders.current.toLocaleString();
|
|
|
document.getElementById('sales-previous').textContent = `Previous: ${data.orders.previous.toLocaleString()}`;
|
|
|
|
|
|
|
|
|
document.getElementById('customers-value').textContent = data.customers.total_customers.toLocaleString();
|
|
|
document.getElementById('customers-previous').textContent = `Active: ${data.customers.active_today || 0}`;
|
|
|
|
|
|
|
|
|
document.getElementById('products-value').textContent = data.products.total_products.toLocaleString();
|
|
|
}
|
|
|
|
|
|
async function loadCharts() {
|
|
|
await Promise.all([
|
|
|
loadRevenueChart(),
|
|
|
loadProductsChart()
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
async function loadRevenueChart() {
|
|
|
try {
|
|
|
const response = await fetch(`/api/dashboard/revenue?period=${currentPeriod}`);
|
|
|
const result = await response.json();
|
|
|
|
|
|
const ctx = document.getElementById('revenueChart');
|
|
|
|
|
|
|
|
|
if (revenueChart) {
|
|
|
revenueChart.destroy();
|
|
|
}
|
|
|
|
|
|
if (result.success && result.data.chart_data) {
|
|
|
const chartData = result.data.chart_data;
|
|
|
const labels = chartData.map(item => {
|
|
|
if (item.hour !== undefined) return `${item.hour}:00`;
|
|
|
if (item.day_name) return item.day_name;
|
|
|
return item.date;
|
|
|
});
|
|
|
const data = chartData.map(item => item.revenue);
|
|
|
|
|
|
revenueChart = new Chart(ctx, {
|
|
|
type: 'line',
|
|
|
data: {
|
|
|
labels: labels,
|
|
|
datasets: [{
|
|
|
label: 'Revenue (฿)',
|
|
|
data: data,
|
|
|
borderColor: 'rgba(52, 211, 153, 0.8)',
|
|
|
backgroundColor: 'rgba(52, 211, 153, 0.1)',
|
|
|
borderWidth: 3,
|
|
|
fill: true,
|
|
|
tension: 0.4
|
|
|
}]
|
|
|
},
|
|
|
options: {
|
|
|
responsive: true,
|
|
|
maintainAspectRatio: false,
|
|
|
plugins: {
|
|
|
legend: {
|
|
|
labels: {
|
|
|
color: 'rgba(255, 255, 255, 0.8)'
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
scales: {
|
|
|
x: {
|
|
|
ticks: {
|
|
|
color: 'rgba(255, 255, 255, 0.6)'
|
|
|
},
|
|
|
grid: {
|
|
|
color: 'rgba(255, 255, 255, 0.1)'
|
|
|
}
|
|
|
},
|
|
|
y: {
|
|
|
ticks: {
|
|
|
color: 'rgba(255, 255, 255, 0.6)'
|
|
|
},
|
|
|
grid: {
|
|
|
color: 'rgba(255, 255, 255, 0.1)'
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error loading revenue chart:', error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function loadProductsChart() {
|
|
|
try {
|
|
|
const response = await fetch('/api/dashboard/sales');
|
|
|
const result = await response.json();
|
|
|
|
|
|
const ctx = document.getElementById('productsChart');
|
|
|
|
|
|
|
|
|
if (productsChart) {
|
|
|
productsChart.destroy();
|
|
|
}
|
|
|
|
|
|
if (result.success && result.data.top_products) {
|
|
|
const products = result.data.top_products;
|
|
|
const labels = products.map(p => p.product_name);
|
|
|
const data = products.map(p => parseFloat(p.total_revenue));
|
|
|
|
|
|
productsChart = new Chart(ctx, {
|
|
|
type: 'doughnut',
|
|
|
data: {
|
|
|
labels: labels,
|
|
|
datasets: [{
|
|
|
data: data,
|
|
|
backgroundColor: [
|
|
|
'rgba(52, 211, 153, 0.8)',
|
|
|
'rgba(168, 85, 247, 0.8)',
|
|
|
'rgba(59, 130, 246, 0.8)',
|
|
|
'rgba(251, 146, 60, 0.8)',
|
|
|
'rgba(236, 72, 153, 0.8)'
|
|
|
],
|
|
|
borderWidth: 2,
|
|
|
borderColor: 'rgba(255, 255, 255, 0.1)'
|
|
|
}]
|
|
|
},
|
|
|
options: {
|
|
|
responsive: true,
|
|
|
maintainAspectRatio: false,
|
|
|
plugins: {
|
|
|
legend: {
|
|
|
position: 'right',
|
|
|
labels: {
|
|
|
color: 'rgba(255, 255, 255, 0.8)',
|
|
|
usePointStyle: true,
|
|
|
padding: 20
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error loading products chart:', error);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
async function openRevenueDetails() {
|
|
|
showModal('Revenue Analytics', 'fas fa-chart-line', 'emerald');
|
|
|
|
|
|
try {
|
|
|
const response = await fetch(`/api/dashboard/revenue?period=${currentPeriod}&detailed=true`);
|
|
|
const result = await response.json();
|
|
|
|
|
|
if (result.success) {
|
|
|
const data = result.data;
|
|
|
const peak = data.chart_data.reduce((max, item) => item.revenue > max.revenue ? item : max, data.chart_data[0]);
|
|
|
const peakLabel = peak.hour !== undefined ? `${peak.hour}:00` : (peak.day_name || peak.date);
|
|
|
const avgRevenue = data.summary.current / data.chart_data.length;
|
|
|
|
|
|
const content = `
|
|
|
<!-- Key Metrics Cards -->
|
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
|
<div class="bg-gradient-to-br from-emerald-500/20 to-emerald-600/10 rounded-xl p-4 border border-emerald-400/20">
|
|
|
<div class="flex items-center justify-between mb-2">
|
|
|
<i class="fas fa-dollar-sign text-emerald-400 text-xl"></i>
|
|
|
<span class="text-xs text-emerald-400 font-semibold">TOTAL</span>
|
|
|
</div>
|
|
|
<div class="text-2xl font-bold text-white">฿${data.summary.current.toLocaleString()}</div>
|
|
|
<div class="text-sm text-white/60">Revenue</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="bg-gradient-to-br from-purple-500/20 to-purple-600/10 rounded-xl p-4 border border-purple-400/20">
|
|
|
<div class="flex items-center justify-between mb-2">
|
|
|
<i class="fas fa-trending-up text-purple-400 text-xl"></i>
|
|
|
<span class="text-xs text-purple-400 font-semibold">GROWTH</span>
|
|
|
</div>
|
|
|
<div class="text-2xl font-bold ${data.summary.percentage_change >= 0 ? 'text-emerald-400' : 'text-red-400'}">
|
|
|
${data.summary.percentage_change >= 0 ? '+' : ''}${data.summary.percentage_change.toFixed(1)}%
|
|
|
</div>
|
|
|
<div class="text-sm text-white/60">vs Previous</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="bg-gradient-to-br from-blue-500/20 to-blue-600/10 rounded-xl p-4 border border-blue-400/20">
|
|
|
<div class="flex items-center justify-between mb-2">
|
|
|
<i class="fas fa-chart-bar text-blue-400 text-xl"></i>
|
|
|
<span class="text-xs text-blue-400 font-semibold">AVERAGE</span>
|
|
|
</div>
|
|
|
<div class="text-2xl font-bold text-white">฿${avgRevenue.toFixed(0)}</div>
|
|
|
<div class="text-sm text-white/60">Per ${currentPeriod === 'daily' ? 'Hour' : 'Day'}</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="bg-gradient-to-br from-orange-500/20 to-orange-600/10 rounded-xl p-4 border border-orange-400/20">
|
|
|
<div class="flex items-center justify-between mb-2">
|
|
|
<i class="fas fa-crown text-orange-400 text-xl"></i>
|
|
|
<span class="text-xs text-orange-400 font-semibold">PEAK</span>
|
|
|
</div>
|
|
|
<div class="text-2xl font-bold text-white">฿${peak.revenue.toLocaleString()}</div>
|
|
|
<div class="text-sm text-white/60">${peakLabel}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Chart and Top Periods -->
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
|
<div class="bg-white/5 rounded-xl p-6">
|
|
|
<h3 class="text-lg font-semibold mb-4 flex items-center">
|
|
|
<i class="fas fa-chart-area text-emerald-400 mr-2"></i>
|
|
|
Revenue Trend
|
|
|
</h3>
|
|
|
<div class="relative h-64">
|
|
|
<canvas id="detailedRevenueChart"></canvas>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="bg-white/5 rounded-xl p-6">
|
|
|
<h3 class="text-lg font-semibold mb-4 flex items-center">
|
|
|
<i class="fas fa-trophy text-orange-400 mr-2"></i>
|
|
|
Top 5 Periods
|
|
|
</h3>
|
|
|
<div class="space-y-3">
|
|
|
${generateTopRevenuePeriods(data.chart_data, data.summary.current)}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Detailed Revenue Table -->
|
|
|
<div class="bg-white/5 rounded-xl p-6">
|
|
|
<h3 class="text-lg font-semibold mb-4 flex items-center">
|
|
|
<i class="fas fa-table text-blue-400 mr-2"></i>
|
|
|
Detailed Breakdown
|
|
|
</h3>
|
|
|
<div class="overflow-x-auto">
|
|
|
<table class="w-full text-sm">
|
|
|
<thead>
|
|
|
<tr class="border-b border-white/20">
|
|
|
<th class="text-left py-3 px-2 text-white/80 font-semibold">Period</th>
|
|
|
<th class="text-right py-3 px-2 text-white/80 font-semibold">Revenue</th>
|
|
|
<th class="text-right py-3 px-2 text-white/80 font-semibold">Orders</th>
|
|
|
<th class="text-right py-3 px-2 text-white/80 font-semibold">Avg Order</th>
|
|
|
<th class="text-right py-3 px-2 text-white/80 font-semibold">% of Total</th>
|
|
|
<th class="text-center py-3 px-2 text-white/80 font-semibold">Performance</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
${generateRevenueTable(data.chart_data, data.summary.current, avgRevenue)}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
document.getElementById('modalContent').innerHTML = content;
|
|
|
|
|
|
// Create detailed chart with animation
|
|
|
setTimeout(() => {
|
|
|
createDetailedRevenueChart(data.chart_data);
|
|
|
}, 300);
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error loading revenue details:', error);
|
|
|
document.getElementById('modalContent').innerHTML = '<p class="text-red-400">Error loading revenue details</p>';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function openProductsDetails() {
|
|
|
showModal('Product Performance', 'fas fa-box', 'purple');
|
|
|
|
|
|
try {
|
|
|
const response = await fetch('/api/dashboard/sales?detailed=true');
|
|
|
const result = await response.json();
|
|
|
|
|
|
if (result.success && result.data.top_products) {
|
|
|
const products = result.data.top_products.slice(0, 8); // Show top 8 products
|
|
|
const totalRevenue = products.reduce((sum, p) => sum + parseFloat(p.total_revenue), 0);
|
|
|
const totalQuantity = products.reduce((sum, p) => sum + parseInt(p.total_quantity), 0);
|
|
|
const avgPrice = totalRevenue / totalQuantity;
|
|
|
|
|
|
const content = `
|
|
|
<!-- Summary Cards -->
|
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
|
<div class="bg-gradient-to-br from-purple-500/20 to-purple-600/10 rounded-xl p-4 border border-purple-400/20">
|
|
|
<div class="flex items-center justify-between mb-2">
|
|
|
<i class="fas fa-box text-purple-400 text-xl"></i>
|
|
|
<span class="text-xs text-purple-400 font-semibold">PRODUCTS</span>
|
|
|
</div>
|
|
|
<div class="text-2xl font-bold text-white">${products.length}</div>
|
|
|
<div class="text-sm text-white/60">Top Sellers</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="bg-gradient-to-br from-emerald-500/20 to-emerald-600/10 rounded-xl p-4 border border-emerald-400/20">
|
|
|
<div class="flex items-center justify-between mb-2">
|
|
|
<i class="fas fa-dollar-sign text-emerald-400 text-xl"></i>
|
|
|
<span class="text-xs text-emerald-400 font-semibold">REVENUE</span>
|
|
|
</div>
|
|
|
<div class="text-2xl font-bold text-white">฿${totalRevenue.toLocaleString()}</div>
|
|
|
<div class="text-sm text-white/60">Total Sales</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="bg-gradient-to-br from-blue-500/20 to-blue-600/10 rounded-xl p-4 border border-blue-400/20">
|
|
|
<div class="flex items-center justify-between mb-2">
|
|
|
<i class="fas fa-shopping-cart text-blue-400 text-xl"></i>
|
|
|
<span class="text-xs text-blue-400 font-semibold">QUANTITY</span>
|
|
|
</div>
|
|
|
<div class="text-2xl font-bold text-white">${totalQuantity.toLocaleString()}</div>
|
|
|
<div class="text-sm text-white/60">Units Sold</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="bg-gradient-to-br from-orange-500/20 to-orange-600/10 rounded-xl p-4 border border-orange-400/20">
|
|
|
<div class="flex items-center justify-between mb-2">
|
|
|
<i class="fas fa-tag text-orange-400 text-xl"></i>
|
|
|
<span class="text-xs text-orange-400 font-semibold">AVG PRICE</span>
|
|
|
</div>
|
|
|
<div class="text-2xl font-bold text-white">฿${avgPrice.toFixed(0)}</div>
|
|
|
<div class="text-sm text-white/60">Per Unit</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Chart and Top Products -->
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
|
<div class="bg-white/5 rounded-xl p-6">
|
|
|
<h3 class="text-lg font-semibold mb-4 flex items-center">
|
|
|
<i class="fas fa-chart-pie text-purple-400 mr-2"></i>
|
|
|
Market Share
|
|
|
</h3>
|
|
|
<div class="relative h-64">
|
|
|
<canvas id="detailedProductsChart"></canvas>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="bg-white/5 rounded-xl p-6">
|
|
|
<h3 class="text-lg font-semibold mb-4 flex items-center">
|
|
|
<i class="fas fa-medal text-orange-400 mr-2"></i>
|
|
|
Top Performers
|
|
|
</h3>
|
|
|
<div class="space-y-3 max-h-64 overflow-y-auto">
|
|
|
${generateProductRankings(products, totalRevenue)}
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<!-- Detailed Product Table -->
|
|
|
<div class="bg-white/5 rounded-xl p-6">
|
|
|
<h3 class="text-lg font-semibold mb-4 flex items-center">
|
|
|
<i class="fas fa-table text-blue-400 mr-2"></i>
|
|
|
Product Analysis
|
|
|
</h3>
|
|
|
<div class="overflow-x-auto">
|
|
|
<table class="w-full text-sm">
|
|
|
<thead>
|
|
|
<tr class="border-b border-white/20">
|
|
|
<th class="text-left py-3 px-2 text-white/80 font-semibold">Rank</th>
|
|
|
<th class="text-left py-3 px-2 text-white/80 font-semibold">Product</th>
|
|
|
<th class="text-right py-3 px-2 text-white/80 font-semibold">Revenue</th>
|
|
|
<th class="text-right py-3 px-2 text-white/80 font-semibold">Quantity</th>
|
|
|
<th class="text-right py-3 px-2 text-white/80 font-semibold">Avg Price</th>
|
|
|
<th class="text-right py-3 px-2 text-white/80 font-semibold">Market Share</th>
|
|
|
<th class="text-center py-3 px-2 text-white/80 font-semibold">Performance</th>
|
|
|
</tr>
|
|
|
</thead>
|
|
|
<tbody>
|
|
|
${generateProductTable(products, totalRevenue)}
|
|
|
</tbody>
|
|
|
</table>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
|
|
|
document.getElementById('modalContent').innerHTML = content;
|
|
|
|
|
|
// Create detailed chart with animation
|
|
|
setTimeout(() => {
|
|
|
createDetailedProductsChart(products);
|
|
|
}, 300);
|
|
|
}
|
|
|
} catch (error) {
|
|
|
console.error('Error loading product details:', error);
|
|
|
document.getElementById('modalContent').innerHTML = '<p class="text-red-400">Error loading product details</p>';
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function showModal(title, icon, color) {
|
|
|
document.getElementById('modalTitleText').textContent = title;
|
|
|
document.getElementById('modalIcon').className = `${icon} mr-3 text-${color}-400`;
|
|
|
|
|
|
const modal = document.getElementById('chartModal');
|
|
|
const container = document.getElementById('modalContainer');
|
|
|
|
|
|
modal.classList.remove('hidden');
|
|
|
|
|
|
// Animate modal appearance
|
|
|
setTimeout(() => {
|
|
|
modal.classList.remove('opacity-0');
|
|
|
container.classList.remove('scale-75');
|
|
|
container.classList.add('scale-100');
|
|
|
}, 10);
|
|
|
}
|
|
|
|
|
|
function closeModal() {
|
|
|
const modal = document.getElementById('chartModal');
|
|
|
const container = document.getElementById('modalContainer');
|
|
|
|
|
|
// Animate modal disappearance
|
|
|
modal.classList.add('opacity-0');
|
|
|
container.classList.remove('scale-100');
|
|
|
container.classList.add('scale-75');
|
|
|
|
|
|
setTimeout(() => {
|
|
|
modal.classList.add('hidden');
|
|
|
}, 500);
|
|
|
}
|
|
|
|
|
|
// Helper functions for generating content
|
|
|
function getPeakPeriod(chartData) {
|
|
|
const peak = chartData.reduce((max, item) => item.revenue > max.revenue ? item : max, chartData[0]);
|
|
|
if (peak.hour !== undefined) return `${peak.hour}:00`;
|
|
|
if (peak.day_name) return peak.day_name;
|
|
|
return peak.date;
|
|
|
}
|
|
|
|
|
|
function generateRevenueDistribution(chartData) {
|
|
|
const total = chartData.reduce((sum, item) => sum + item.revenue, 0);
|
|
|
const sorted = chartData.sort((a, b) => b.revenue - a.revenue).slice(0, 5);
|
|
|
|
|
|
return sorted.map(item => {
|
|
|
const percentage = ((item.revenue / total) * 100).toFixed(1);
|
|
|
const label = item.hour !== undefined ? `${item.hour}:00` : (item.day_name || item.date);
|
|
|
return `
|
|
|
<div class="flex items-center justify-between">
|
|
|
<span class="text-white/70">${label}:</span>
|
|
|
<div class="flex items-center space-x-2">
|
|
|
<div class="w-20 bg-white/10 rounded-full h-2">
|
|
|
<div class="bg-emerald-400 h-2 rounded-full" style="width: ${percentage}%"></div>
|
|
|
</div>
|
|
|
<span class="text-sm font-medium">${percentage}%</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
function generateRevenueTable(chartData, totalRevenue) {
|
|
|
return chartData.map(item => {
|
|
|
const percentage = ((item.revenue / totalRevenue) * 100).toFixed(1);
|
|
|
const label = item.hour !== undefined ? `${item.hour}:00` : (item.day_name || item.date);
|
|
|
const avgOrder = item.orders > 0 ? (item.revenue / item.orders).toFixed(2) : '0.00';
|
|
|
|
|
|
return `
|
|
|
<tr class="border-b border-white/5">
|
|
|
<td class="py-2">${label}</td>
|
|
|
<td class="py-2 text-right font-medium">฿${item.revenue.toLocaleString()}</td>
|
|
|
<td class="py-2 text-right">${item.orders}</td>
|
|
|
<td class="py-2 text-right">฿${avgOrder}</td>
|
|
|
<td class="py-2 text-right">${percentage}%</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
function generateProductCards(products, totalRevenue) {
|
|
|
return products.slice(0, 5).map((product, index) => {
|
|
|
const percentage = ((parseFloat(product.total_revenue) / totalRevenue) * 100).toFixed(1);
|
|
|
const colors = ['emerald', 'purple', 'blue', 'orange', 'pink'];
|
|
|
const color = colors[index] || 'gray';
|
|
|
|
|
|
return `
|
|
|
<div class="flex items-center space-x-3 p-3 bg-white/5 rounded-lg">
|
|
|
<div class="w-10 h-10 bg-${color}-400/20 rounded-lg flex items-center justify-center">
|
|
|
<span class="text-${color}-400 font-bold">#${index + 1}</span>
|
|
|
</div>
|
|
|
<div class="flex-1">
|
|
|
<div class="font-medium">${product.product_name}</div>
|
|
|
<div class="text-sm text-white/60">฿${parseFloat(product.total_revenue).toLocaleString()} (${percentage}%)</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
function generateDetailedProductCards(products, totalRevenue) {
|
|
|
return products.map((product, index) => {
|
|
|
const percentage = ((parseFloat(product.total_revenue) / totalRevenue) * 100).toFixed(1);
|
|
|
const avgPrice = product.total_quantity > 0 ? (parseFloat(product.total_revenue) / product.total_quantity).toFixed(2) : '0.00';
|
|
|
|
|
|
return `
|
|
|
<div class="bg-white/5 rounded-xl p-6">
|
|
|
<div class="flex items-center space-x-3 mb-4">
|
|
|
<div class="w-12 h-12 bg-gradient-to-br from-purple-400/20 to-purple-600/10 rounded-xl flex items-center justify-center">
|
|
|
<i class="fas fa-box text-purple-400"></i>
|
|
|
</div>
|
|
|
<div>
|
|
|
<h4 class="font-semibold">${product.product_name}</h4>
|
|
|
<p class="text-sm text-white/60">Rank #${index + 1}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="space-y-3">
|
|
|
<div class="flex justify-between">
|
|
|
<span class="text-white/70">Revenue:</span>
|
|
|
<span class="font-bold text-emerald-400">฿${parseFloat(product.total_revenue).toLocaleString()}</span>
|
|
|
</div>
|
|
|
<div class="flex justify-between">
|
|
|
<span class="text-white/70">Quantity:</span>
|
|
|
<span class="font-medium">${product.total_quantity}</span>
|
|
|
</div>
|
|
|
<div class="flex justify-between">
|
|
|
<span class="text-white/70">Avg Price:</span>
|
|
|
<span class="font-medium">฿${avgPrice}</span>
|
|
|
</div>
|
|
|
<div class="flex justify-between">
|
|
|
<span class="text-white/70">Market Share:</span>
|
|
|
<span class="font-medium">${percentage}%</span>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="mt-4">
|
|
|
<div class="w-full bg-white/10 rounded-full h-2">
|
|
|
<div class="bg-gradient-to-r from-purple-400 to-purple-600 h-2 rounded-full" style="width: ${percentage}%"></div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
function generateProductTable(products, totalRevenue) {
|
|
|
return products.map((product, index) => {
|
|
|
const percentage = ((parseFloat(product.total_revenue) / totalRevenue) * 100).toFixed(1);
|
|
|
const avgPrice = product.total_quantity > 0 ? (parseFloat(product.total_revenue) / product.total_quantity).toFixed(2) : '0.00';
|
|
|
const performance = parseFloat(percentage) > 20 ? 'Excellent' : parseFloat(percentage) > 10 ? 'Good' : 'Average';
|
|
|
const performanceColor = parseFloat(percentage) > 20 ? 'text-emerald-400' : parseFloat(percentage) > 10 ? 'text-blue-400' : 'text-orange-400';
|
|
|
|
|
|
return `
|
|
|
<tr class="border-b border-white/5">
|
|
|
<td class="py-3">
|
|
|
<div class="flex items-center space-x-2">
|
|
|
<span class="w-6 h-6 bg-purple-400/20 rounded flex items-center justify-center text-xs">${index + 1}</span>
|
|
|
<span>${product.product_name}</span>
|
|
|
</div>
|
|
|
</td>
|
|
|
<td class="py-3 text-right font-medium">฿${parseFloat(product.total_revenue).toLocaleString()}</td>
|
|
|
<td class="py-3 text-right">${product.total_quantity}</td>
|
|
|
<td class="py-3 text-right">฿${avgPrice}</td>
|
|
|
<td class="py-3 text-right">${percentage}%</td>
|
|
|
<td class="py-3 text-right ${performanceColor}">${performance}</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
function createDetailedRevenueChart(chartData) {
|
|
|
const ctx = document.getElementById('detailedRevenueChart');
|
|
|
if (!ctx) return;
|
|
|
|
|
|
const labels = chartData.map(item => {
|
|
|
if (item.hour !== undefined) return `${item.hour}:00`;
|
|
|
if (item.day_name) return item.day_name;
|
|
|
return item.date;
|
|
|
});
|
|
|
const data = chartData.map(item => item.revenue);
|
|
|
|
|
|
new Chart(ctx, {
|
|
|
type: 'bar',
|
|
|
data: {
|
|
|
labels: labels,
|
|
|
datasets: [{
|
|
|
label: 'Revenue (฿)',
|
|
|
data: data,
|
|
|
backgroundColor: 'rgba(52, 211, 153, 0.6)',
|
|
|
borderColor: 'rgba(52, 211, 153, 0.8)',
|
|
|
borderWidth: 2
|
|
|
}]
|
|
|
},
|
|
|
options: {
|
|
|
responsive: true,
|
|
|
maintainAspectRatio: false,
|
|
|
plugins: {
|
|
|
legend: {
|
|
|
labels: {
|
|
|
color: 'rgba(255, 255, 255, 0.8)'
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
scales: {
|
|
|
x: {
|
|
|
ticks: {
|
|
|
color: 'rgba(255, 255, 255, 0.6)'
|
|
|
},
|
|
|
grid: {
|
|
|
color: 'rgba(255, 255, 255, 0.1)'
|
|
|
}
|
|
|
},
|
|
|
y: {
|
|
|
ticks: {
|
|
|
color: 'rgba(255, 255, 255, 0.6)'
|
|
|
},
|
|
|
grid: {
|
|
|
color: 'rgba(255, 255, 255, 0.1)'
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
function createDetailedProductsChart(products) {
|
|
|
const ctx = document.getElementById('detailedProductsChart');
|
|
|
if (!ctx) return;
|
|
|
|
|
|
const labels = products.map(p => p.product_name);
|
|
|
const data = products.map(p => parseFloat(p.total_revenue));
|
|
|
|
|
|
new Chart(ctx, {
|
|
|
type: 'polarArea',
|
|
|
data: {
|
|
|
labels: labels,
|
|
|
datasets: [{
|
|
|
data: data,
|
|
|
backgroundColor: [
|
|
|
'rgba(52, 211, 153, 0.6)',
|
|
|
'rgba(168, 85, 247, 0.6)',
|
|
|
'rgba(59, 130, 246, 0.6)',
|
|
|
'rgba(251, 146, 60, 0.6)',
|
|
|
'rgba(236, 72, 153, 0.6)'
|
|
|
],
|
|
|
borderColor: [
|
|
|
'rgba(52, 211, 153, 0.8)',
|
|
|
'rgba(168, 85, 247, 0.8)',
|
|
|
'rgba(59, 130, 246, 0.8)',
|
|
|
'rgba(251, 146, 60, 0.8)',
|
|
|
'rgba(236, 72, 153, 0.8)'
|
|
|
],
|
|
|
borderWidth: 2
|
|
|
}]
|
|
|
},
|
|
|
options: {
|
|
|
responsive: true,
|
|
|
maintainAspectRatio: false,
|
|
|
plugins: {
|
|
|
legend: {
|
|
|
position: 'bottom',
|
|
|
labels: {
|
|
|
color: 'rgba(255, 255, 255, 0.8)',
|
|
|
usePointStyle: true,
|
|
|
padding: 20
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
scales: {
|
|
|
r: {
|
|
|
ticks: {
|
|
|
color: 'rgba(255, 255, 255, 0.6)'
|
|
|
},
|
|
|
grid: {
|
|
|
color: 'rgba(255, 255, 255, 0.1)'
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// Close modal when clicking outside
|
|
|
document.addEventListener('click', function(event) {
|
|
|
const modal = document.getElementById('chartModal');
|
|
|
if (event.target === modal) {
|
|
|
closeModal();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// Helper functions for generating content
|
|
|
function generateTopRevenuePeriods(chartData, totalRevenue) {
|
|
|
const sorted = [...chartData].sort((a, b) => b.revenue - a.revenue).slice(0, 5);
|
|
|
|
|
|
return sorted.map((item, index) => {
|
|
|
const percentage = ((item.revenue / totalRevenue) * 100).toFixed(1);
|
|
|
const label = item.hour !== undefined ? `${item.hour}:00` : (item.day_name || item.date);
|
|
|
const colors = ['emerald', 'purple', 'blue', 'orange', 'pink'];
|
|
|
const color = colors[index];
|
|
|
|
|
|
return `
|
|
|
<div class="flex items-center justify-between p-3 bg-gradient-to-r from-${color}-500/10 to-${color}-600/5 rounded-lg border border-${color}-400/20 hover:border-${color}-400/40 transition-all duration-300">
|
|
|
<div class="flex items-center space-x-3">
|
|
|
<div class="w-8 h-8 bg-${color}-400/20 rounded-lg flex items-center justify-center">
|
|
|
<span class="text-sm font-bold text-${color}-400">${index + 1}</span>
|
|
|
</div>
|
|
|
<div>
|
|
|
<div class="font-medium text-white">${label}</div>
|
|
|
<div class="text-xs text-white/60">${item.orders || 0} orders</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="text-right">
|
|
|
<div class="font-bold text-emerald-400">฿${item.revenue.toLocaleString()}</div>
|
|
|
<div class="text-xs text-white/60">${percentage}%</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
function generateRevenueTable(chartData, totalRevenue, avgRevenue) {
|
|
|
return chartData.map((item, index) => {
|
|
|
const percentage = ((item.revenue / totalRevenue) * 100).toFixed(1);
|
|
|
const label = item.hour !== undefined ? `${item.hour}:00` : (item.day_name || item.date);
|
|
|
const avgOrder = item.orders > 0 ? (item.revenue / item.orders).toFixed(0) : '0';
|
|
|
const performance = item.revenue > avgRevenue * 1.2 ? 'Excellent' : item.revenue > avgRevenue ? 'Good' : 'Average';
|
|
|
const performanceColor = item.revenue > avgRevenue * 1.2 ? 'text-emerald-400' : item.revenue > avgRevenue ? 'text-blue-400' : 'text-orange-400';
|
|
|
const performanceIcon = item.revenue > avgRevenue * 1.2 ? 'fas fa-star' : item.revenue > avgRevenue ? 'fas fa-thumbs-up' : 'fas fa-minus';
|
|
|
|
|
|
return `
|
|
|
<tr class="border-b border-white/10 hover:bg-white/5 transition-colors duration-200">
|
|
|
<td class="py-3 px-2 font-medium">${label}</td>
|
|
|
<td class="py-3 px-2 text-right font-bold text-emerald-400">฿${item.revenue.toLocaleString()}</td>
|
|
|
<td class="py-3 px-2 text-right">${item.orders || 0}</td>
|
|
|
<td class="py-3 px-2 text-right">฿${avgOrder}</td>
|
|
|
<td class="py-3 px-2 text-right font-medium">${percentage}%</td>
|
|
|
<td class="py-3 px-2 text-center">
|
|
|
<span class="${performanceColor} flex items-center justify-center">
|
|
|
<i class="${performanceIcon} mr-1 text-xs"></i>
|
|
|
<span class="text-xs font-medium">${performance}</span>
|
|
|
</span>
|
|
|
</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
function generateProductRankings(products, totalRevenue) {
|
|
|
return products.map((product, index) => {
|
|
|
const percentage = ((parseFloat(product.total_revenue) / totalRevenue) * 100).toFixed(1);
|
|
|
const colors = ['emerald', 'purple', 'blue', 'orange', 'pink', 'indigo', 'teal', 'rose'];
|
|
|
const color = colors[index] || 'gray';
|
|
|
const avgPrice = product.total_quantity > 0 ? (parseFloat(product.total_revenue) / product.total_quantity).toFixed(0) : '0';
|
|
|
|
|
|
return `
|
|
|
<div class="flex items-center justify-between p-3 bg-gradient-to-r from-${color}-500/10 to-${color}-600/5 rounded-lg border border-${color}-400/20 hover:border-${color}-400/40 transition-all duration-300">
|
|
|
<div class="flex items-center space-x-3">
|
|
|
<div class="w-8 h-8 bg-${color}-400/20 rounded-lg flex items-center justify-center">
|
|
|
<span class="text-sm font-bold text-${color}-400">${index + 1}</span>
|
|
|
</div>
|
|
|
<div>
|
|
|
<div class="font-medium text-white">${product.product_name}</div>
|
|
|
<div class="text-xs text-white/60">${product.total_quantity} units • ฿${avgPrice} avg</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="text-right">
|
|
|
<div class="font-bold text-emerald-400">฿${parseFloat(product.total_revenue).toLocaleString()}</div>
|
|
|
<div class="text-xs text-white/60">${percentage}%</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
`;
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
function generateProductTable(products, totalRevenue) {
|
|
|
return products.map((product, index) => {
|
|
|
const percentage = ((parseFloat(product.total_revenue) / totalRevenue) * 100).toFixed(1);
|
|
|
const avgPrice = product.total_quantity > 0 ? (parseFloat(product.total_revenue) / product.total_quantity).toFixed(0) : '0';
|
|
|
const performance = parseFloat(percentage) > 20 ? 'Excellent' : parseFloat(percentage) > 10 ? 'Good' : parseFloat(percentage) > 5 ? 'Average' : 'Low';
|
|
|
const performanceColor = parseFloat(percentage) > 20 ? 'text-emerald-400' : parseFloat(percentage) > 10 ? 'text-blue-400' : parseFloat(percentage) > 5 ? 'text-orange-400' : 'text-red-400';
|
|
|
const performanceIcon = parseFloat(percentage) > 20 ? 'fas fa-star' : parseFloat(percentage) > 10 ? 'fas fa-thumbs-up' : parseFloat(percentage) > 5 ? 'fas fa-minus' : 'fas fa-arrow-down';
|
|
|
|
|
|
return `
|
|
|
<tr class="border-b border-white/10 hover:bg-white/5 transition-colors duration-200">
|
|
|
<td class="py-3 px-2">
|
|
|
<div class="w-6 h-6 bg-purple-400/20 rounded flex items-center justify-center">
|
|
|
<span class="text-xs font-bold text-purple-400">${index + 1}</span>
|
|
|
</div>
|
|
|
</td>
|
|
|
<td class="py-3 px-2 font-medium">${product.product_name}</td>
|
|
|
<td class="py-3 px-2 text-right font-bold text-emerald-400">฿${parseFloat(product.total_revenue).toLocaleString()}</td>
|
|
|
<td class="py-3 px-2 text-right">${product.total_quantity}</td>
|
|
|
<td class="py-3 px-2 text-right">฿${avgPrice}</td>
|
|
|
<td class="py-3 px-2 text-right font-medium">${percentage}%</td>
|
|
|
<td class="py-3 px-2 text-center">
|
|
|
<span class="${performanceColor} flex items-center justify-center">
|
|
|
<i class="${performanceIcon} mr-1 text-xs"></i>
|
|
|
<span class="text-xs font-medium">${performance}</span>
|
|
|
</span>
|
|
|
</td>
|
|
|
</tr>
|
|
|
`;
|
|
|
}).join('');
|
|
|
}
|
|
|
|
|
|
// Initialize dashboard
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
|
loadDashboardData();
|
|
|
|
|
|
// Auto-refresh every 30 seconds
|
|
|
setInterval(loadDashboardData, 30000);
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
<style>
|
|
|
.time-filter-btn {
|
|
|
@apply text-white/60 hover:text-white/90 transition-all duration-300;
|
|
|
background: rgba(255, 255, 255, 0.02);
|
|
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
|
}
|
|
|
|
|
|
.time-filter-btn:hover {
|
|
|
background: rgba(255, 255, 255, 0.08);
|
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
}
|
|
|
|
|
|
.time-filter-btn.active {
|
|
|
@apply text-white;
|
|
|
background: linear-gradient(135deg, rgba(59, 130, 246, 0.15), rgba(99, 102, 241, 0.10));
|
|
|
border: 1px solid rgba(59, 130, 246, 0.25);
|
|
|
box-shadow:
|
|
|
0 0 25px rgba(59, 130, 246, 0.15),
|
|
|
0 4px 15px rgba(0, 0, 0, 0.2),
|
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
|
}
|
|
|
|
|
|
/* Different colors for each button when active */
|
|
|
.time-filter-btn.active[data-period="daily"] {
|
|
|
background: linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(5, 150, 105, 0.10));
|
|
|
border: 1px solid rgba(16, 185, 129, 0.25);
|
|
|
box-shadow:
|
|
|
0 0 25px rgba(16, 185, 129, 0.15),
|
|
|
0 4px 15px rgba(0, 0, 0, 0.2),
|
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
|
}
|
|
|
|
|
|
.time-filter-btn.active[data-period="weekly"] {
|
|
|
background: linear-gradient(135deg, rgba(168, 85, 247, 0.15), rgba(147, 51, 234, 0.10));
|
|
|
border: 1px solid rgba(168, 85, 247, 0.25);
|
|
|
box-shadow:
|
|
|
0 0 25px rgba(168, 85, 247, 0.15),
|
|
|
0 4px 15px rgba(0, 0, 0, 0.2),
|
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
|
}
|
|
|
|
|
|
.time-filter-btn.active[data-period="monthly"] {
|
|
|
background: linear-gradient(135deg, rgba(245, 158, 11, 0.15), rgba(217, 119, 6, 0.10));
|
|
|
border: 1px solid rgba(245, 158, 11, 0.25);
|
|
|
box-shadow:
|
|
|
0 0 25px rgba(245, 158, 11, 0.15),
|
|
|
0 4px 15px rgba(0, 0, 0, 0.2),
|
|
|
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
|
|
}
|
|
|
</style>
|
|
|
</x-app-layout> |