File: /www/wwwroot/erp.nhatnamsst.com/storage/framework/views/a768b412b4af1942c942e2cafd9d1fef.php
<?php $__env->startPush('libs-css'); ?>
<link href="https://cdn.jsdelivr.net/npm/apexcharts@3.44.0/dist/apexcharts.css" rel="stylesheet">
<?php $__env->stopPush(); ?>
<?php $__env->startPush('css'); ?>
<style>
.statistics-page {
padding: 20px 0;
}
.page-header-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.page-title {
font-size: 24px;
font-weight: 700;
color: #1e293b;
margin: 0;
}
.refresh-btn {
background: #2563eb;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
transition: all 0.2s;
}
.refresh-btn:hover {
background: #1d4ed8;
transform: translateY(-1px);
box-shadow: 0 4px 6px rgba(37, 99, 235, 0.2);
}
.refresh-btn:active {
transform: translateY(0);
}
.refresh-btn i {
font-size: 18px;
}
.chart-container {
background: white;
padding: 0;
overflow: hidden;
border-radius: 4px;
border: 1px solid #e5e7eb;
}
.chart-header {
background: #002b6deb;
color: white;
padding: 10px 15px;
font-weight: 600;
font-size: 14px;
display: flex;
align-items: center;
justify-content: space-between;
}
.chart-title {
display: flex;
align-items: center;
gap: 10px;
}
.chart-title i {
font-size: 20px;
}
.chart-content {
padding: 24px;
}
.date-filter {
margin-bottom: 24px;
}
.date-filter-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 12px;
}
.date-filter label {
font-weight: 600;
color: #374151;
margin: 0;
white-space: nowrap;
}
.date-input-wrapper {
position: relative;
display: inline-block;
}
.date-input-wrapper input {
padding: 4px 8px 4px 34px;
/* border: 1px solid #d1d5db; */
border-radius: 3px;
font-size: 14px;
/* min-width: 160px; */
transition: all 0.2s;
}
.date-input-wrapper input:focus {
outline: none;
border-color: none;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.date-input-wrapper i {
position: absolute;
left: 12px;
top: 50%;
transform: translateY(-50%);
color: #6b7280;
pointer-events: none;
}
.filter-btn {
background: #2563eb;
color: white;
border: none;
padding: 4px 20px;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
transition: all 0.2s;
white-space: nowrap;
}
.filter-btn:active {
transform: translateY(0);
}
.filter-btn i {
font-size: 16px;
}
#chart {
min-height: 500px;
}
.loading-overlay {
display: none;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.9);
z-index: 10;
align-items: center;
justify-content: center;
border-radius: 12px;
}
.loading-overlay.active {
display: flex;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #e5e7eb;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
.page-head {
display: none;
}
.donut-chart-container {
display: flex;
gap: 24px;
margin-bottom: 24px;
flex-wrap: wrap;
}
@media (max-width: 1200px) {
.donut-chart-container {
gap: 16px;
}
}
@media (max-width: 768px) {
.donut-chart-container {
flex-direction: column;
gap: 16px;
}
.donut-chart-wrapper {
width: 100%;
}
}
.donut-chart-wrapper {
flex: 1;
background: white;
border-radius: 4px;
border: 1px solid #e5e7eb;
overflow: hidden;
display: flex;
flex-direction: column;
}
.donut-chart-header {
background: #002b6deb;
color: white;
padding: 10px 15px;
font-weight: 600;
font-size: 14px;
flex-shrink: 0;
}
.donut-chart-content {
padding: 24px 20px;
display: flex;
flex-direction: column;
gap: 16px;
flex: 1;
min-height: 350px;
overflow: hidden;
}
@media (max-width: 768px) {
.donut-chart-content {
padding: 16px 12px;
min-height: auto;
}
}
.donut-chart {
width: 100%;
min-height: 300px;
overflow: hidden;
position: relative;
max-width: 100%;
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
}
.donut-chart svg {
max-width: 100%;
height: auto;
}
.donut-chart .apexcharts-canvas {
max-width: 100%;
}
@media (max-width: 768px) {
.donut-chart {
min-height: 250px;
width: 100%;
}
}
.donut-legend {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
align-items: center;
gap: 16px;
padding-top: 8px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 50%;
}
.no-data {
text-align: center;
padding: 40px;
color: #6b7280;
}
/* Fix dataLabels color for donut charts */
.apexcharts-datalabel-text,
.apexcharts-datalabels text,
.apexcharts-datalabel,
text.apexcharts-datalabel-text {
fill: #ffffff !important;
font-weight: 700 !important;
font-size: 14px !important;
stroke: rgba(0, 0, 0, 0.3) !important;
stroke-width: 0.5px !important;
}
/* Remove gaps between donut segments - use very small stroke to prevent missing segments */
.apexcharts-pie-series path,
.apexcharts-donut-series path {
stroke-width: 1px !important;
stroke: #ffffff !important;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
@media (max-width: 768px) {
.donut-chart-container {
flex-direction: column;
}
.page-header-section {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.date-filter-row {
flex-direction: column;
align-items: stretch;
}
.date-input-wrapper {
width: 100%;
}
.date-input-wrapper input {
width: 100%;
}
.filter-btn {
width: 100%;
justify-content: center;
}
}
</style>
<?php $__env->stopPush(); ?>
<?php $__env->startSection('content'); ?>
<div class="page-body statistics-page">
<div class="container-xl">
<div class="row">
<div class="col-12">
<div class="chart-container">
<div class="chart-header">
<div class="chart-title">
<span>Số lượng yêu cầu báo giá</span>
</div>
</div>
<div class="chart-content">
<div class="date-filter">
<div class="date-filter-row">
<label>Chọn ngày :</label>
<div class="date-input-wrapper">
<i class="ti ti-calendar"></i>
<input type="date" id="fromDate" class="form-control" lang="en">
</div>
<label>Đến</label>
<div class="date-input-wrapper">
<i class="ti ti-calendar"></i>
<input type="date" id="toDate" class="form-control" lang="en">
</div>
<button type="button" id="filterBtn" class="filter-btn">
<span>Lọc</span>
</button>
</div>
</div>
<div style="position: relative;">
<div class="loading-overlay" id="loadingOverlay">
<div class="spinner"></div>
</div>
<div id="chart"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Donut Charts -->
<div class="donut-chart-container mt-3">
<!-- Quote Requests by Group/Role -->
<div class="donut-chart-wrapper">
<div class="donut-chart-header">
Số lượng yêu cầu báo giá theo nhóm
</div>
<div class="donut-chart-content">
<div id="groupCountChart" class="donut-chart"></div>
<div id="groupCountLegend" class="donut-legend"></div>
</div>
</div>
<!-- Customer Count by Type -->
<div class="donut-chart-wrapper">
<div class="donut-chart-header">
Số lượng khách hàng
</div>
<div class="donut-chart-content">
<div id="customerTypeCountChart" class="donut-chart"></div>
<div id="customerTypeCountLegend" class="donut-legend"></div>
</div>
</div>
</div>
</div>
</div>
<?php $__env->stopSection(); ?>
<?php $__env->startPush('libs-js'); ?>
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.44.0/dist/apexcharts.min.js"></script>
<?php $__env->stopPush(); ?>
<?php $__env->startPush('js'); ?>
<script>
let chart;
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '';
function showLoading() {
document.getElementById('loadingOverlay').classList.add('active');
}
function hideLoading() {
document.getElementById('loadingOverlay').classList.remove('active');
}
function loadChart() {
const fromDate = document.getElementById('fromDate').value;
const toDate = document.getElementById('toDate').value;
if (!fromDate || !toDate) {
alert('Vui lòng chọn khoảng thời gian');
return;
}
if (fromDate > toDate) {
alert('Ngày bắt đầu phải nhỏ hơn hoặc bằng ngày kết thúc');
return;
}
showLoading();
fetch('<?php echo e(route('cms.quote_request.get-statistics-data')); ?>', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
from_date: fromDate,
to_date: toDate
})
})
.then(response => response.json())
.then(data => {
hideLoading();
if (data.success) {
updateChart(data.data);
} else {
alert('Có lỗi xảy ra: ' + (data.message || 'Unknown error'));
}
})
.catch(error => {
hideLoading();
console.error('Error:', error);
alert('Có lỗi xảy ra khi tải dữ liệu');
});
}
function updateChart(chartData) {
// Ensure data is valid, use empty arrays if no data
if (!chartData) {
chartData = {
dates: [],
daily_counts: [],
cumulative_counts: []
};
}
// Convert to numbers and filter out invalid values
const dailyCounts = (chartData.daily_counts || []).map(val => parseFloat(val) || 0);
const cumulativeCounts = (chartData.cumulative_counts || []).map(val => parseFloat(val) || 0);
const dates = chartData.dates || [];
// Calculate max values for Y-axis with smart scaling
const actualMaxDaily = Math.max(...dailyCounts, 0);
const actualMaxCumulative = Math.max(...cumulativeCounts, 0);
// Smart scaling for daily counts - ensure visibility even with small data
let maxDaily;
if (actualMaxDaily === 0) {
maxDaily = 10; // Show scale even if no data
} else if (actualMaxDaily <= 5) {
maxDaily = Math.ceil(actualMaxDaily * 1.5); // Add 50% padding for small values
} else if (actualMaxDaily <= 20) {
maxDaily = Math.ceil(actualMaxDaily / 5) * 5 + 5; // Round up to nearest 5 + padding
} else if (actualMaxDaily <= 50) {
maxDaily = Math.ceil(actualMaxDaily / 10) * 10 + 10; // Round up to nearest 10 + padding
} else {
maxDaily = Math.ceil(actualMaxDaily / 20) * 20; // Round up to nearest 20
}
// Smart scaling for cumulative counts
let maxCumulative;
if (actualMaxCumulative === 0) {
maxCumulative = 1000;
} else if (actualMaxCumulative <= 100) {
maxCumulative = Math.ceil(actualMaxCumulative * 1.3);
} else if (actualMaxCumulative <= 1000) {
maxCumulative = Math.ceil(actualMaxCumulative / 100) * 100 + 100;
} else if (actualMaxCumulative <= 10000) {
maxCumulative = Math.ceil(actualMaxCumulative / 1000) * 1000 + 1000;
} else {
maxCumulative = Math.ceil(actualMaxCumulative / 10000) * 10000;
}
const options = {
series: [{
name: 'Số lượng yêu cầu báo giá',
type: 'column',
data: dailyCounts
},
{
name: 'Lũy kế yêu cầu báo giá',
type: 'line',
data: cumulativeCounts
}
],
chart: {
height: 500,
type: 'line',
toolbar: {
show: true,
tools: {
download: true,
selection: false,
zoom: false,
zoomin: false,
zoomout: false,
pan: false,
reset: false
}
},
zoom: {
enabled: false
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800
}
},
stroke: {
width: [0, 3],
curve: 'smooth'
},
markers: {
size: [0, 4],
colors: ['#2563eb', '#10b981'],
strokeWidth: [0, 2],
strokeColors: ['#2563eb', '#10b981'],
hover: {
size: [0, 6]
}
},
plotOptions: {
bar: {
columnWidth: '60%',
dataLabels: {
position: 'top'
},
borderRadius: 4
}
},
dataLabels: {
enabled: true,
enabledOnSeries: [0],
formatter: function(val) {
const numVal = parseFloat(val) || 0;
return numVal > 0 ? Math.round(numVal) : '';
},
offsetY: -5,
style: {
fontSize: '11px',
fontWeight: 600,
colors: ['#2563eb']
}
},
labels: dates,
xaxis: {
type: 'category',
labels: {
rotate: -45,
rotateAlways: true,
style: {
fontSize: '11px'
}
}
},
yaxis: [{
title: {
text: '',
style: {
color: '#2563eb',
fontSize: '12px'
}
},
min: 0,
max: maxDaily,
tickAmount: maxDaily <= 10 ? 5 : (maxDaily <= 50 ? 5 : 5),
forceNiceScale: true,
labels: {
style: {
colors: '#2563eb',
fontSize: '11px'
},
formatter: function(val) {
return Math.round(val);
}
}
},
{
opposite: true,
title: {
text: 'Lũy kế',
style: {
color: '#10b981',
fontSize: '12px'
}
},
min: 0,
max: maxCumulative,
tickAmount: 5,
forceNiceScale: true,
labels: {
style: {
colors: '#10b981',
fontSize: '11px'
},
formatter: function(val) {
return val.toLocaleString('vi-VN');
}
}
}
],
colors: ['#2563eb', '#10b981'],
legend: {
position: 'bottom',
horizontalAlign: 'center',
markers: {
width: 12,
height: 12,
radius: 6
},
itemMargin: {
horizontal: 20
}
},
tooltip: {
shared: true,
intersect: false,
x: {
formatter: function(val, {
dataPointIndex,
w
}) {
// Return the date label instead of index
return w.globals.labels[dataPointIndex] || val;
}
},
y: {
formatter: function(val, {
seriesIndex
}) {
if (seriesIndex === 0) {
return 'Số lượng yêu cầu báo giá: ' + val;
} else {
return 'Lũy kế yêu cầu báo giá: ' + val.toLocaleString('vi-VN');
}
}
},
marker: {
show: true
},
style: {
fontSize: '12px',
fontFamily: 'inherit'
}
},
grid: {
borderColor: '#e5e7eb',
strokeDashArray: 3,
xaxis: {
lines: {
show: false
}
},
yaxis: {
lines: {
show: true
}
},
padding: {
top: 0,
right: 0,
bottom: 0,
left: 0
}
},
annotations: {
points: dates.map((date, index) => {
const cumulativeValue = parseFloat(cumulativeCounts[index]) || 0;
return {
x: date,
y: cumulativeValue,
yAxisIndex: 1,
marker: {
size: 0
},
label: {
text: cumulativeValue.toLocaleString('vi-VN'),
style: {
color: '#10b981',
fontSize: '11px',
fontWeight: 600,
background: 'transparent',
borderWidth: 0
},
offsetY: -30
}
};
}).filter(point => point.y > 0) // Only show annotations for non-zero values
}
};
if (chart) {
chart.updateOptions(options);
} else {
chart = new ApexCharts(document.querySelector("#chart"), options);
chart.render();
}
// Always update donut charts (even if no data)
updateDonutChart('groupCountChart', 'groupCountLegend', chartData.group_count || [], '');
updateDonutChart('customerTypeCountChart', 'customerTypeCountLegend', chartData.customer_type_count || [], '');
}
function updateDonutChart(chartId, legendId, data, labelType) {
// Always show chart, even if no data
const colors = ['#2563eb', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899'];
// If no data, create empty chart
if (!data || data.length === 0) {
const options = {
series: [1],
chart: {
type: 'donut',
height: 300,
events: {
dataPointSelection: function(event, chartContext, config) {
return false; // Disable selection
}
}
},
labels: ['Không có dữ liệu'],
colors: ['#e5e7eb'],
legend: {
show: false
},
dataLabels: {
enabled: false
},
stroke: {
show: true,
width: 1,
colors: ['#ffffff']
},
states: {
hover: {
filter: {
type: 'none'
}
},
active: {
filter: {
type: 'none'
}
}
},
plotOptions: {
pie: {
donut: {
size: '70%',
expandOnClick: false,
labels: {
show: true,
name: {
show: false
},
value: {
show: true,
fontSize: '20px',
fontWeight: 700,
color: '#6b7280',
formatter: function() {
return '0';
}
},
total: {
show: true,
label: labelType || 'Tổng',
fontSize: '14px',
fontWeight: 600,
color: '#9ca3af',
formatter: function() {
return '0';
}
}
}
},
expandOnClick: false
}
},
tooltip: {
enabled: false
}
};
let chart = window[chartId + 'Instance'];
if (chart) {
chart.updateOptions(options);
} else {
const chartEl = document.getElementById(chartId);
if (chartEl) {
chart = new ApexCharts(chartEl, options);
chart.render();
window[chartId + 'Instance'] = chart;
}
}
// Update legend
const legendEl = document.getElementById(legendId);
if (legendEl) {
legendEl.innerHTML = '<div class="legend-item"><div class="legend-color" style="background-color: #e5e7eb"></div><span>Không có dữ liệu: 0</span></div>';
}
return;
}
const total = data.reduce((sum, item) => sum + (parseFloat(item.count || 0)), 0);
const chartData = data.map((item, index) => ({
name: item.name,
value: parseFloat(item.count || 0),
percentage: total > 0 ? ((parseFloat(item.count || 0) / total * 100).toFixed(1)) : '0.0'
}));
const options = {
series: chartData.map(d => parseFloat(d.value) || 0),
chart: {
type: 'donut',
height: 300,
width: '100%',
offsetX: 0,
offsetY: 0,
toolbar: {
show: false
},
animations: {
enabled: true,
easing: 'easeinout',
speed: 800
}
},
labels: chartData.map(d => d.name),
colors: colors.slice(0, chartData.length),
legend: {
show: false
},
dataLabels: {
enabled: true,
formatter: function(val) {
const numVal = parseFloat(val) || 0;
return numVal.toFixed(1) + '%';
},
style: {
fontSize: '14px',
fontWeight: 700,
colors: ['#ffffff']
},
dropShadow: {
enabled: true,
color: '#000',
top: 1,
left: 1,
blur: 2,
opacity: 0.6
}
},
stroke: {
show: true,
width: 1,
colors: ['#ffffff']
},
states: {
hover: {
filter: {
type: 'none'
}
},
active: {
filter: {
type: 'none'
}
}
},
plotOptions: {
pie: {
donut: {
size: '70%',
expandOnClick: false,
labels: {
show: true,
name: {
show: true,
fontSize: '14px',
fontWeight: 600,
color: '#374151'
},
value: {
show: true,
fontSize: '20px',
fontWeight: 700,
color: '#1e293b',
formatter: function(val) {
return val.toLocaleString('vi-VN');
}
},
total: {
show: true,
label: labelType,
fontSize: '14px',
fontWeight: 600,
color: '#6b7280',
formatter: function() {
return total.toLocaleString('vi-VN');
}
}
}
},
expandOnClick: false
}
},
tooltip: {
y: {
formatter: function(val) {
return val.toLocaleString('vi-VN');
}
}
}
};
let chart = window[chartId + 'Instance'];
if (chart) {
chart.updateOptions(options);
// Resize chart to fit container
setTimeout(() => {
chart.resize();
}, 100);
// Force update dataLabels color after update
setTimeout(() => {
const chartEl = document.getElementById(chartId);
if (chartEl) {
const dataLabelTexts = chartEl.querySelectorAll('.apexcharts-datalabel-text, .apexcharts-datalabels text');
dataLabelTexts.forEach(text => {
text.setAttribute('fill', '#ffffff');
text.style.fontWeight = '700';
text.style.fontSize = '14px';
});
}
}, 150);
} else {
const chartEl = document.getElementById(chartId);
if (chartEl) {
chart = new ApexCharts(chartEl, options);
chart.render().then(() => {
// Resize chart to fit container
setTimeout(() => {
chart.resize();
}, 100);
// Force set white color for dataLabels after render
setTimeout(() => {
const dataLabelTexts = chartEl.querySelectorAll('.apexcharts-datalabel-text, .apexcharts-datalabels text');
dataLabelTexts.forEach(text => {
text.setAttribute('fill', '#ffffff');
text.style.fontWeight = '700';
text.style.fontSize = '14px';
});
}, 150);
});
window[chartId + 'Instance'] = chart;
}
}
// Update legend
const legendEl = document.getElementById(legendId);
if (legendEl) {
const legendHtml = chartData.map((item, index) => `
<div class="legend-item">
<div class="legend-color" style="background-color: ${colors[index]}"></div>
<span>${item.name}: ${item.value.toLocaleString('vi-VN')}</span>
</div>
`).join('');
legendEl.innerHTML = legendHtml;
}
}
// Load chart on page load
document.addEventListener('DOMContentLoaded', function() {
// Set default dates (last 30 days)
const today = new Date();
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(today.getDate() - 30);
const fromDateEl = document.getElementById('fromDate');
const toDateEl = document.getElementById('toDate');
if (fromDateEl && !fromDateEl.value) {
fromDateEl.value = thirtyDaysAgo.toISOString().split('T')[0];
}
if (toDateEl && !toDateEl.value) {
toDateEl.value = today.toISOString().split('T')[0];
}
// Filter button click
const filterBtn = document.getElementById('filterBtn');
if (filterBtn) {
filterBtn.addEventListener('click', function() {
loadChart();
});
}
// Refresh button click (if exists)
const refreshBtn = document.getElementById('refreshBtn');
if (refreshBtn) {
refreshBtn.addEventListener('click', function() {
const btn = this;
const icon = btn.querySelector('i');
if (icon) {
icon.style.animation = 'spin 0.8s linear';
setTimeout(() => {
icon.style.animation = '';
}, 800);
}
loadChart();
});
}
// Enter key on date inputs
if (fromDateEl) {
fromDateEl.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
loadChart();
}
});
// Change event on date inputs
fromDateEl.addEventListener('change', function() {
const toDate = toDateEl ? toDateEl.value : null;
if (toDate && this.value > toDate) {
if (toDateEl) {
toDateEl.value = this.value;
}
}
});
}
if (toDateEl) {
toDateEl.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
loadChart();
}
});
// Change event on date inputs
toDateEl.addEventListener('change', function() {
const fromDate = fromDateEl ? fromDateEl.value : null;
if (fromDate && this.value < fromDate) {
if (fromDateEl) {
fromDateEl.value = this.value;
}
}
});
}
loadChart();
// Resize charts on window resize
let resizeTimer;
window.addEventListener('resize', function() {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function() {
// Resize all donut charts
const chartIds = ['groupCountChart', 'customerTypeCountChart'];
chartIds.forEach(chartId => {
const chartInstance = window[chartId + 'Instance'];
if (chartInstance) {
chartInstance.resize();
}
});
// Resize main chart
if (chart) {
chart.resize();
}
}, 250);
});
});
</script>
<?php $__env->stopPush(); ?>
<?php echo $__env->make('cms.layouts.master', array_diff_key(get_defined_vars(), ['__data' => 1, '__path' => 1]))->render(); ?><?php /**PATH /www/wwwroot/erp.nhatnamsst.com/resources/views/cms/quote_requests/statistics.blade.php ENDPATH**/ ?>