add helloword.html, improved UI for analytics
This commit is contained in:
1 parent
f15f7d0e60
commit
75bdfe843e
4 files changed
+308
-100
No files matched your search
+222
-27
@@ -27,6 +27,7 @@ $(document).ready(function () {
|
|||||||
$("#statTotalSpend").text("₱" + parseFloat(data.total_spend).toLocaleString("en-US", { minimumFractionDigits: 2 }));
|
$("#statTotalSpend").text("₱" + parseFloat(data.total_spend).toLocaleString("en-US", { minimumFractionDigits: 2 }));
|
||||||
$("#statTotalItems").text(data.total_items);
|
$("#statTotalItems").text(data.total_items);
|
||||||
$("#statTurnover").text(data.turnover_ratio + "x");
|
$("#statTurnover").text(data.turnover_ratio + "x");
|
||||||
|
$("#statIssuedItems").text(data.issued_30_days || "0");
|
||||||
|
|
||||||
// Predictive List
|
// Predictive List
|
||||||
let predHtml = "";
|
let predHtml = "";
|
||||||
@@ -53,12 +54,22 @@ $(document).ready(function () {
|
|||||||
$("#prescriptiveAutoRestockBtn").prop("disabled", true).removeClass("btn-success").addClass("btn-secondary").html('<i class="bi bi-check-all me-2"></i> Stock Optimal');
|
$("#prescriptiveAutoRestockBtn").prop("disabled", true).removeClass("btn-success").addClass("btn-secondary").html('<i class="bi bi-check-all me-2"></i> Stock Optimal');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Charts
|
// Charts Data Preparation
|
||||||
let valLabels = []; let valData = [];
|
let valLabels = [];
|
||||||
data.valuation_data.forEach(row => { valLabels.push(row.supplier_name); valData.push(row.supplier_value); });
|
let valData = [];
|
||||||
|
data.valuation_data.forEach(row => {
|
||||||
|
let formattedValue = '₱' + parseFloat(row.supplier_value).toLocaleString("en-US", { minimumFractionDigits: 2 });
|
||||||
|
// 🔥 Multi-line X-Axis: Puts the Name on top, Number underneath
|
||||||
|
valLabels.push([row.supplier_name, formattedValue]);
|
||||||
|
valData.push(row.supplier_value);
|
||||||
|
});
|
||||||
|
|
||||||
let spendLabels = []; let spendData = [];
|
let spendLabels = [];
|
||||||
data.spend_by_category.forEach(row => { spendLabels.push(row.category_name); spendData.push(row.category_spend); });
|
let spendData = [];
|
||||||
|
data.spend_by_category.forEach(row => {
|
||||||
|
spendLabels.push(row.category_name);
|
||||||
|
spendData.push(row.category_spend);
|
||||||
|
});
|
||||||
|
|
||||||
if (window.valuationChartInstance) window.valuationChartInstance.destroy();
|
if (window.valuationChartInstance) window.valuationChartInstance.destroy();
|
||||||
if (window.spendChartInstance) window.spendChartInstance.destroy();
|
if (window.spendChartInstance) window.spendChartInstance.destroy();
|
||||||
@@ -67,20 +78,86 @@ $(document).ready(function () {
|
|||||||
if(ctxVal) {
|
if(ctxVal) {
|
||||||
window.valuationChartInstance = new Chart(ctxVal.getContext("2d"), {
|
window.valuationChartInstance = new Chart(ctxVal.getContext("2d"), {
|
||||||
type: "bar",
|
type: "bar",
|
||||||
data: { labels: valLabels, datasets: [{ label: "Valuation (₱)", data: valData, backgroundColor: "rgba(13, 110, 253, 0.8)", borderRadius: 4 }] },
|
data: {
|
||||||
plugins: [ChartDataLabels],
|
labels: valLabels,
|
||||||
options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, datalabels: { color: '#ffffff', anchor: 'end', align: 'bottom', font: { weight: 'bold' }, formatter: value => '₱' + parseFloat(value).toLocaleString("en-US") } } }
|
datasets: [{
|
||||||
|
label: "Valuation",
|
||||||
|
data: valData,
|
||||||
|
backgroundColor: "rgba(13, 110, 253, 0.8)",
|
||||||
|
borderRadius: 4
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
// 🔥 Removed ChartDataLabels plugin to clear floating numbers
|
||||||
|
options: {
|
||||||
|
layout: { padding: { top: 20, right: 20, bottom: 10, left: 10 } },
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false }
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
ticks: {
|
||||||
|
// Center aligns the multi-line text beautifully
|
||||||
|
align: 'center',
|
||||||
|
color: '#495057',
|
||||||
|
font: { weight: '500', size: 11 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let ctxSpend = document.getElementById("spendChart");
|
let ctxSpend = document.getElementById("spendChart");
|
||||||
if (ctxSpend) {
|
if (ctxSpend) {
|
||||||
|
// Define colors array so we can share it between the chart and the HTML legend
|
||||||
|
let doughnutColors = ["#198754", "#ffc107", "#0dcaf0", "#d63384", "#6f42c1", "#fd7e14", "#20c997", "#6610f2"];
|
||||||
|
|
||||||
window.spendChartInstance = new Chart(ctxSpend.getContext("2d"), {
|
window.spendChartInstance = new Chart(ctxSpend.getContext("2d"), {
|
||||||
type: "doughnut",
|
type: "doughnut",
|
||||||
data: { labels: spendLabels, datasets: [{ data: spendData, backgroundColor: ["#198754", "#ffc107", "#0dcaf0", "#d63384", "#6f42c1"], borderWidth: 2, borderColor: '#ffffff' }] },
|
data: {
|
||||||
plugins: [ChartDataLabels],
|
labels: spendLabels,
|
||||||
options: { responsive: true, maintainAspectRatio: false, cutout: "60%", plugins: { legend: { position: "right", labels: { padding: 15, generateLabels: function(chart) { const data = chart.data; if (data.labels.length && data.datasets.length) { return data.labels.map(function(label, i) { const meta = chart.getDatasetMeta(0); const style = meta.controller.getStyle(i); const value = data.datasets[0].data[i]; const formattedVal = '₱' + parseFloat(value).toLocaleString("en-US"); return { text: `${label} (${formattedVal})`, fillStyle: style.backgroundColor, strokeStyle: style.borderColor, lineWidth: style.borderWidth, hidden: !chart.getDataVisibility(i), index: i }; }); } return []; } } }, datalabels: { color: '#ffffff', font: { weight: 'bold', size: 11 }, formatter: value => value == 0 ? '' : '₱' + parseFloat(value).toLocaleString("en-US") } } }
|
datasets: [{
|
||||||
|
data: spendData,
|
||||||
|
backgroundColor: doughnutColors,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#ffffff'
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
layout: { padding: 10 },
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
cutout: "65%",
|
||||||
|
plugins: {
|
||||||
|
// 🔥 Turn off the squished built-in canvas legend completely!
|
||||||
|
legend: { display: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 🔥 Generate a beautiful, perfectly formatted HTML Legend
|
||||||
|
let legendHtml = '<div class="row mt-4 text-center justify-content-center">';
|
||||||
|
|
||||||
|
spendLabels.forEach((label, i) => {
|
||||||
|
let value = '₱' + parseFloat(spendData[i]).toLocaleString("en-US", { minimumFractionDigits: 2 });
|
||||||
|
let color = doughnutColors[i % doughnutColors.length];
|
||||||
|
|
||||||
|
legendHtml += `
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<div class="d-flex align-items-center justify-content-center mb-1">
|
||||||
|
<span class="shadow-sm" style="width: 12px; height: 12px; background-color: ${color}; border-radius: 50%; margin-right: 8px;"></span>
|
||||||
|
<span class="text-muted small fw-bold text-truncate" title="${label}">${label}</span>
|
||||||
|
</div>
|
||||||
|
<div class="fw-bold text-dark">${value}</div>
|
||||||
|
</div>`;
|
||||||
|
});
|
||||||
|
legendHtml += '</div>';
|
||||||
|
|
||||||
|
// Remove any old custom legend, then inject the new one right below the canvas container
|
||||||
|
$("#customSpendLegend").remove();
|
||||||
|
$(ctxSpend).parent().after(`<div id="customSpendLegend">${legendHtml}</div>`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (btn) showAlert("Dashboard data refreshed successfully!", "success");
|
if (btn) showAlert("Dashboard data refreshed successfully!", "success");
|
||||||
@@ -117,13 +194,75 @@ $(document).ready(function () {
|
|||||||
|
|
||||||
// --- 3. EXPORT TO EXCEL ---
|
// --- 3. EXPORT TO EXCEL ---
|
||||||
window.exportExcel = function() {
|
window.exportExcel = function() {
|
||||||
if (!currentAnalyticsData) { showAlert("Please wait for analytics data to load.", "warning"); return; }
|
if (!currentAnalyticsData) {
|
||||||
|
showAlert("Please wait for analytics data to load.", "warning");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to cleanly format currency for the spreadsheet
|
||||||
|
const formatCurrency = (val) => '₱' + parseFloat(val).toLocaleString("en-US", { minimumFractionDigits: 2 });
|
||||||
|
|
||||||
const wb = XLSX.utils.book_new();
|
const wb = XLSX.utils.book_new();
|
||||||
const kpiData = [ ["Metric", "Value"], ["Total Stock Valuation", parseFloat(currentAnalyticsData.total_value)], ["Total PO Spend", parseFloat(currentAnalyticsData.total_spend)], ["Inventory Turnover Ratio", parseFloat(currentAnalyticsData.turnover_ratio)], ["Total Unique Items", parseInt(currentAnalyticsData.total_items)], ["Shortages (Low Stock)", parseInt(currentAnalyticsData.low_stock)] ];
|
|
||||||
XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(kpiData), "KPI Summary");
|
// SHEET 1: KPI Summary
|
||||||
const valData = [["Supplier Name", "Stock Valuation (₱)"]]; currentAnalyticsData.valuation_data.forEach(row => { valData.push([row.supplier_name, parseFloat(row.supplier_value)]); }); XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(valData), "Valuation by Supplier");
|
const kpiData = [
|
||||||
const spendData = [["Category/Supplier", "Total Spend (₱)"]]; currentAnalyticsData.spend_by_category.forEach(row => { spendData.push([row.category_name, parseFloat(row.category_spend)]); }); XLSX.utils.book_append_sheet(wb, XLSX.utils.aoa_to_sheet(spendData), "Spend by Category");
|
["System Analytics", "Value"],
|
||||||
XLSX.writeFile(wb, `Inventory_Analytics_${new Date().toISOString().split('T')[0]}.xlsx`);
|
["Current Stock Value", formatCurrency(currentAnalyticsData.total_value)],
|
||||||
|
["Total Purchases (Spend)", formatCurrency(currentAnalyticsData.total_spend)],
|
||||||
|
["Inventory Turnover Ratio", parseFloat(currentAnalyticsData.turnover_ratio) + "x"],
|
||||||
|
["Total Unique Items", parseInt(currentAnalyticsData.total_items)],
|
||||||
|
["Items Issued/Borrowed (30 Days)", parseInt(currentAnalyticsData.issued_30_days || 0)],
|
||||||
|
["Critical Shortages (Action Items)", parseInt(currentAnalyticsData.low_stock)],
|
||||||
|
["Overstock Alerts", parseInt(currentAnalyticsData.overstock)]
|
||||||
|
];
|
||||||
|
const wsKpi = XLSX.utils.aoa_to_sheet(kpiData);
|
||||||
|
// Expand column widths to make it readable
|
||||||
|
wsKpi['!cols'] = [{ wch: 35 }, { wch: 20 }];
|
||||||
|
XLSX.utils.book_append_sheet(wb, wsKpi, "KPI Summary");
|
||||||
|
|
||||||
|
// SHEET 2: Valuation by Supplier
|
||||||
|
const valData = [["Supplier Name", "Stock Valuation"]];
|
||||||
|
if (currentAnalyticsData.valuation_data) {
|
||||||
|
currentAnalyticsData.valuation_data.forEach(row => {
|
||||||
|
valData.push([row.supplier_name, formatCurrency(row.supplier_value)]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const wsVal = XLSX.utils.aoa_to_sheet(valData);
|
||||||
|
wsVal['!cols'] = [{ wch: 30 }, { wch: 20 }];
|
||||||
|
XLSX.utils.book_append_sheet(wb, wsVal, "Valuation by Supplier");
|
||||||
|
|
||||||
|
// SHEET 3: Spend by Category
|
||||||
|
const spendData = [["Category / Supplier", "Total Spend"]];
|
||||||
|
if (currentAnalyticsData.spend_by_category) {
|
||||||
|
currentAnalyticsData.spend_by_category.forEach(row => {
|
||||||
|
spendData.push([row.category_name, formatCurrency(row.category_spend)]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const wsSpend = XLSX.utils.aoa_to_sheet(spendData);
|
||||||
|
wsSpend['!cols'] = [{ wch: 30 }, { wch: 20 }];
|
||||||
|
XLSX.utils.book_append_sheet(wb, wsSpend, "Spend by Category");
|
||||||
|
|
||||||
|
// SHEET 4: Stock Depletion Forecast
|
||||||
|
const forecastData = [["Item Name", "Current Stock", "Daily Burn Rate", "Estimated Days Left"]];
|
||||||
|
if (currentAnalyticsData.predictions && currentAnalyticsData.predictions.length > 0) {
|
||||||
|
currentAnalyticsData.predictions.forEach(item => {
|
||||||
|
forecastData.push([
|
||||||
|
item.name,
|
||||||
|
item.quantity + " units",
|
||||||
|
parseFloat(item.daily_burn).toFixed(2) + " / day",
|
||||||
|
item.days_left + " days"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
forecastData.push(["No imminent stockouts predicted!", "-", "-", "-"]);
|
||||||
|
}
|
||||||
|
const wsForecast = XLSX.utils.aoa_to_sheet(forecastData);
|
||||||
|
wsForecast['!cols'] = [{ wch: 35 }, { wch: 15 }, { wch: 18 }, { wch: 20 }];
|
||||||
|
XLSX.utils.book_append_sheet(wb, wsForecast, "Depletion Forecast");
|
||||||
|
|
||||||
|
// Generate and download the file
|
||||||
|
let dateStr = new Date().toISOString().split('T')[0];
|
||||||
|
XLSX.writeFile(wb, `ICIS_Analytics_Report_${dateStr}.xlsx`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 4. EXPORT TO PDF ---
|
// --- 4. EXPORT TO PDF ---
|
||||||
@@ -132,20 +271,76 @@ $(document).ready(function () {
|
|||||||
const element = document.getElementById('analyticsContainer');
|
const element = document.getElementById('analyticsContainer');
|
||||||
const buttons = document.getElementById('analyticsButtons');
|
const buttons = document.getElementById('analyticsButtons');
|
||||||
|
|
||||||
buttons.style.display = 'none';
|
if (!element) return; // Failsafe
|
||||||
const originalWidth = element.style.width; const originalMaxWidth = element.style.maxWidth; const originalBg = element.style.backgroundColor; const originalPadding = element.style.padding;
|
|
||||||
const targetWidth = 1200; element.style.width = targetWidth + 'px'; element.style.maxWidth = targetWidth + 'px'; element.style.backgroundColor = '#f3f4f6'; element.style.padding = '20px';
|
// 1. Hide buttons and lock layout for snapshot
|
||||||
|
buttons.style.display = 'none';
|
||||||
|
const originalWidth = element.style.width;
|
||||||
|
const originalMaxWidth = element.style.maxWidth;
|
||||||
|
const originalBg = element.style.backgroundColor;
|
||||||
|
const originalPadding = element.style.padding;
|
||||||
|
|
||||||
|
// Force a wide desktop view so charts don't squash together
|
||||||
|
const targetWidth = 1440;
|
||||||
|
element.style.width = targetWidth + 'px';
|
||||||
|
element.style.maxWidth = targetWidth + 'px';
|
||||||
|
element.style.backgroundColor = '#f8f9fa';
|
||||||
|
element.style.padding = '40px';
|
||||||
|
|
||||||
|
// 2. Add a tiny delay to ensure Chart.js animations are 100% finished
|
||||||
|
setTimeout(() => {
|
||||||
|
html2canvas(element, {
|
||||||
|
scale: 2.5, // 🔥 Upped to 3 for ultra-crisp text and charts
|
||||||
|
useCORS: true,
|
||||||
|
logging: false,
|
||||||
|
width: targetWidth,
|
||||||
|
windowWidth: targetWidth
|
||||||
|
}).then((canvas) => {
|
||||||
|
|
||||||
|
// 3. Restore the live UI instantly
|
||||||
|
buttons.style.display = 'flex';
|
||||||
|
element.style.width = originalWidth;
|
||||||
|
element.style.maxWidth = originalMaxWidth;
|
||||||
|
element.style.backgroundColor = originalBg;
|
||||||
|
element.style.padding = originalPadding;
|
||||||
|
|
||||||
|
const h100Cards = element.querySelectorAll('.h-100');
|
||||||
|
h100Cards.forEach(card => card.style.minHeight = '100%');
|
||||||
|
|
||||||
|
// 🔥 Use PNG instead of JPEG to prevent blurry artifacting!
|
||||||
|
const imgData = canvas.toDataURL('image/png');
|
||||||
|
|
||||||
|
// 4. Build the PDF
|
||||||
|
const pdf = new jsPDF('landscape', 'in', 'letter');
|
||||||
|
const pdfWidth = pdf.internal.pageSize.getWidth();
|
||||||
|
const pdfHeight = pdf.internal.pageSize.getHeight();
|
||||||
|
|
||||||
|
const imgProps = pdf.getImageProperties(imgData);
|
||||||
|
const margin = 0.3;
|
||||||
|
const maxImgWidth = pdfWidth - (margin * 2);
|
||||||
|
const maxImgHeight = pdfHeight - (margin * 2);
|
||||||
|
|
||||||
|
const ratio = Math.min(maxImgWidth / imgProps.width, maxImgHeight / imgProps.height);
|
||||||
|
const finalWidth = imgProps.width * ratio;
|
||||||
|
const finalHeight = imgProps.height * ratio;
|
||||||
|
|
||||||
|
// Add the sharp PNG to the PDF and download
|
||||||
|
pdf.addImage(imgData, 'PNG', (pdfWidth - finalWidth) / 2, margin, finalWidth, finalHeight);
|
||||||
|
pdf.save(`ICIS_Analytics_Report_${new Date().toISOString().split('T')[0]}.pdf`);
|
||||||
|
|
||||||
|
showAlert("High-Resolution PDF downloaded successfully!", "success");
|
||||||
|
|
||||||
html2canvas(element, { scale: 2, useCORS: true, logging: false, width: targetWidth, windowWidth: targetWidth }).then((canvas) => {
|
|
||||||
buttons.style.display = 'flex'; element.style.width = originalWidth; element.style.maxWidth = originalMaxWidth; element.style.backgroundColor = originalBg; element.style.padding = originalPadding;
|
|
||||||
const imgData = canvas.toDataURL('image/jpeg', 1.0); const pdf = new jsPDF('landscape', 'in', 'letter'); const pdfWidth = pdf.internal.pageSize.getWidth(); const pdfHeight = pdf.internal.pageSize.getHeight();
|
|
||||||
const imgProps = pdf.getImageProperties(imgData); const margin = 0.3; const maxImgWidth = pdfWidth - (margin * 2); const maxImgHeight = pdfHeight - (margin * 2);
|
|
||||||
const ratio = Math.min(maxImgWidth / imgProps.width, maxImgHeight / imgProps.height); const finalWidth = imgProps.width * ratio; const finalHeight = imgProps.height * ratio;
|
|
||||||
pdf.addImage(imgData, 'JPEG', (pdfWidth - finalWidth) / 2, margin, finalWidth, finalHeight); pdf.save(`Inventory_Report_${new Date().toISOString().split('T')[0]}.pdf`);
|
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
buttons.style.display = 'flex'; element.style.width = originalWidth; element.style.maxWidth = originalMaxWidth; element.style.backgroundColor = originalBg; element.style.padding = originalPadding;
|
// Restore UI if something breaks
|
||||||
|
buttons.style.display = 'flex';
|
||||||
|
element.style.width = originalWidth;
|
||||||
|
element.style.maxWidth = originalMaxWidth;
|
||||||
|
element.style.backgroundColor = originalBg;
|
||||||
|
element.style.padding = originalPadding;
|
||||||
showAlert("Failed to generate PDF.", "danger");
|
showAlert("Failed to generate PDF.", "danger");
|
||||||
|
console.error(err);
|
||||||
});
|
});
|
||||||
|
}, 300); // 300ms pause
|
||||||
};
|
};
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Document</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Hello, World!</h1>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -774,6 +774,13 @@ switch ($action) {
|
|||||||
");
|
");
|
||||||
$predictive_data = $stmtPredict->fetchAll(PDO::FETCH_ASSOC);
|
$predictive_data = $stmtPredict->fetchAll(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$issuedStmt = $pdo->query("
|
||||||
|
SELECT COALESCE(SUM(qty_issued), 0) as total_issued
|
||||||
|
FROM inventory_transactions
|
||||||
|
WHERE transaction_date >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
|
||||||
|
");
|
||||||
|
$issued_30_days = $issuedStmt->fetch(PDO::FETCH_ASSOC)['total_issued'];
|
||||||
|
|
||||||
// Calculate explicit days for the JSON payload
|
// Calculate explicit days for the JSON payload
|
||||||
foreach($predictive_data as &$pred) {
|
foreach($predictive_data as &$pred) {
|
||||||
$pred['days_left'] = floor($pred['quantity'] / $pred['daily_burn']);
|
$pred['days_left'] = floor($pred['quantity'] / $pred['daily_burn']);
|
||||||
@@ -792,6 +799,7 @@ switch ($action) {
|
|||||||
'low_stock' => $shortages['low_stock'] ?? 0,
|
'low_stock' => $shortages['low_stock'] ?? 0,
|
||||||
'total_spend' => $total_spend,
|
'total_spend' => $total_spend,
|
||||||
'turnover_ratio' => round($turnover_ratio, 2),
|
'turnover_ratio' => round($turnover_ratio, 2),
|
||||||
|
'issued_30_days' => $issued_30_days,
|
||||||
'spend_by_category' => $spend_by_category,
|
'spend_by_category' => $spend_by_category,
|
||||||
'valuation_data' => $valuation_data,
|
'valuation_data' => $valuation_data,
|
||||||
'predictions' => $predictive_data
|
'predictions' => $predictive_data
|
||||||
|
|||||||
+67
-73
@@ -1,102 +1,96 @@
|
|||||||
<div id="analyticsContainer">
|
<div class="d-flex justify-content-between align-items-center mb-4 header-actions" id="analyticsButtons">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4 header-actions">
|
<h4 class="mb-0 fw-bold">System Overview</h4>
|
||||||
<h4 class="mb-0 fw-bold">Dashboard Analytics</h4>
|
<div class="d-flex gap-2">
|
||||||
<div class="d-flex gap-2" id="analyticsButtons">
|
<button class="btn btn-outline-success bg-white shadow-sm" onclick="exportExcel()">
|
||||||
<button class="btn btn-outline-success shadow-sm rounded-pill px-3 bg-white" onclick="exportExcel()">
|
|
||||||
<i class="bi bi-file-earmark-excel me-1"></i> Excel
|
<i class="bi bi-file-earmark-excel me-1"></i> Excel
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-danger shadow-sm rounded-pill px-3 bg-white" onclick="exportPDF()">
|
<button class="btn btn-outline-danger bg-white shadow-sm" onclick="exportPDF()">
|
||||||
<i class="bi bi-file-earmark-pdf me-1"></i> PDF
|
<i class="bi bi-file-earmark-pdf me-1"></i> PDF
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-outline-primary shadow-sm rounded-pill px-3 bg-white" onclick="loadAnalytics(this)">
|
<button class="btn btn-primary shadow-sm" onclick="loadAnalytics(this)">
|
||||||
<i class="bi bi-arrow-clockwise me-1"></i> Refresh
|
<i class="bi bi-arrow-clockwise me-1"></i> Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-4">
|
<div id="analyticsContainer">
|
||||||
<div class="col-12 col-lg-8">
|
|
||||||
|
|
||||||
<h6 class="fw-bold text-secondary text-uppercase mb-3" style="letter-spacing: 1px;"><i class="bi bi-eye text-primary me-2"></i>1. Descriptive (Current State)</h6>
|
|
||||||
<div class="row g-3 mb-5">
|
|
||||||
<div class="col-12 col-md-4">
|
|
||||||
<div class="card card-custom kpi-card p-3 text-center h-100 border-top border-primary border-4">
|
|
||||||
<div class="text-muted small fw-bold mb-1">STOCK VALUATION</div>
|
|
||||||
<h4 id="statTotalValue" class="fw-bold text-primary mb-0">₱0.00</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-4">
|
|
||||||
<div class="card card-custom kpi-card p-3 text-center h-100 border-top border-dark border-4">
|
|
||||||
<div class="text-muted small fw-bold mb-1">TOTAL PO SPEND</div>
|
|
||||||
<h4 id="statTotalSpend" class="fw-bold text-dark mb-0">₱0.00</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-4">
|
|
||||||
<div class="card card-custom kpi-card p-3 text-center h-100 border-top border-secondary border-4">
|
|
||||||
<div class="text-muted small fw-bold mb-1">UNIQUE ITEMS</div>
|
|
||||||
<h4 id="statTotalItems" class="fw-bold text-secondary mb-0">0</h4>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h6 class="fw-bold text-secondary text-uppercase mb-3" style="letter-spacing: 1px;"><i class="bi bi-search text-info me-2"></i>2. Diagnostic (Trends & Causes)</h6>
|
|
||||||
<div class="row g-3 mb-4">
|
<div class="row g-3 mb-4">
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-md-3">
|
||||||
<div class="card card-custom kpi-card p-3 text-center h-100 bg-light">
|
<div class="card border-0 shadow-sm h-100 bg-primary text-white">
|
||||||
<div class="text-muted small fw-bold mb-1">INVENTORY TURNOVER</div>
|
<div class="card-body p-4">
|
||||||
<h3 id="statTurnover" class="fw-bold text-info mb-1">0.00x</h3>
|
<div class="text-white-50 small fw-bold text-uppercase mb-1">Current Stock Value</div>
|
||||||
<span class="small text-muted">Spend vs. Current Stock</span>
|
<h3 class="fw-bold mb-0" id="statTotalValue">₱0.00</h3>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-12 col-md-8">
|
|
||||||
<div class="card card-custom p-3 h-100">
|
|
||||||
<div class="text-muted small fw-bold mb-2 text-center">SPEND BY CATEGORY (SUPPLIER)</div>
|
|
||||||
<div style="position: relative; height: 150px; width: 100%">
|
|
||||||
<canvas id="spendChart"></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-md-3">
|
||||||
<div class="card card-custom p-3">
|
<div class="card border-0 shadow-sm h-100">
|
||||||
<div class="text-muted small fw-bold mb-2 text-center">VALUATION BY SUPPLIER</div>
|
<div class="card-body p-4">
|
||||||
<div style="position: relative; height: 200px; width: 100%">
|
<div class="text-muted small fw-bold text-uppercase mb-1">Items Borrowed/Issued (30 Days)</div>
|
||||||
<canvas id="valuationChart"></canvas>
|
<h3 class="fw-bold mb-0 text-dark" id="statIssuedItems">0</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="text-muted small fw-bold text-uppercase mb-1">Total Purchases</div>
|
||||||
|
<h3 class="fw-bold mb-0 text-dark" id="statTotalSpend">₱0.00</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="text-muted small fw-bold text-uppercase mb-1">Total Unique Items</div>
|
||||||
|
<h3 class="fw-bold mb-0 text-dark" id="statTotalItems">0</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-4 mb-4">
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h6 class="fw-bold text-dark mb-4">Stock Value by Supplier</h6>
|
||||||
|
<div style="height: 300px;"><canvas id="valuationChart"></canvas></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h6 class="fw-bold text-dark mb-4">Spending by Category</h6>
|
||||||
|
<div style="height: 300px;"><canvas id="spendChart"></canvas></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-lg-4">
|
|
||||||
|
|
||||||
<h6 class="fw-bold text-secondary text-uppercase mb-3" style="letter-spacing: 1px;"><i class="bi bi-graph-up-arrow text-warning me-2"></i>3. Predictive (Forecast)</h6>
|
|
||||||
<div class="card card-custom mb-5 p-0 overflow-hidden">
|
|
||||||
<div class="bg-warning bg-opacity-10 p-3 border-bottom border-warning border-opacity-25">
|
|
||||||
<h6 class="fw-bold text-dark mb-0">High-Risk Stockouts</h6>
|
|
||||||
<span class="small text-muted">Items predicted to run out in < 7 days based on recent burn rates.</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="p-0">
|
|
||||||
<ul class="list-group list-group-flush" id="predictiveList">
|
|
||||||
<li class="list-group-item text-center py-4 text-muted small">Loading predictions...</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h6 class="fw-bold text-secondary text-uppercase mb-3" style="letter-spacing: 1px;"><i class="bi bi-lightning-charge-fill text-success me-2"></i>4. Prescriptive (Action)</h6>
|
<div class="row g-4">
|
||||||
<div class="card card-custom bg-success bg-opacity-10 border-0 p-4 text-center">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="card border-0 shadow-sm h-100">
|
||||||
<i class="bi bi-robot text-success" style="font-size: 2.5rem;"></i>
|
<div class="card-body p-4">
|
||||||
|
<h6 class="fw-bold text-dark mb-3"><i class="bi bi-graph-down-arrow text-danger me-2"></i>Stock Depletion Forecast</h6>
|
||||||
|
<p class="text-muted small mb-3">Items expected to run out soon based on recent usage.</p>
|
||||||
|
<ul class="list-group list-group-flush border-top" id="predictiveList"></ul>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="fw-bold text-dark mb-2">System Recommendation</h5>
|
</div>
|
||||||
<p class="text-muted small mb-4">
|
</div>
|
||||||
You have <strong class="text-danger" id="prescriptiveLowCount">0</strong> items currently below their reorder thresholds. The system can automatically generate optimized Purchase Orders for all affected suppliers right now.
|
<div class="col-md-6">
|
||||||
</p>
|
<div class="card border-0 shadow-sm h-100 bg-light border border-success border-opacity-25">
|
||||||
<button class="btn btn-success rounded-pill fw-bold shadow-sm w-100 py-2" id="prescriptiveAutoRestockBtn">
|
<div class="card-body p-4 text-center d-flex flex-column justify-content-center">
|
||||||
<i class="bi bi-magic me-2"></i> Execute Auto-Restock
|
<div class="bg-success bg-opacity-10 text-success d-inline-flex p-3 rounded-circle mx-auto mb-3">
|
||||||
|
<i class="bi bi-box-seam fs-2"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="fw-bold text-dark">Action Items</h5>
|
||||||
|
<p class="text-muted mb-4">You have <strong id="prescriptiveLowCount" class="text-danger">0</strong> items currently at or below their minimum reorder level.</p>
|
||||||
|
<button class="btn btn-success rounded-pill px-4 py-2 mx-auto shadow-sm" id="prescriptiveAutoRestockBtn">
|
||||||
|
<i class="bi bi-lightning-charge-fill me-1"></i> Auto-Generate Restock Orders
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
Reference in new issue
Block a user