add helloword.html, improved UI for analytics
This commit is contained in:
1 parent
f15f7d0e60
commit
75bdfe843e
4 files changed
+334
-126
No files matched your search
+225
-30
@@ -27,6 +27,7 @@ $(document).ready(function () {
|
||||
$("#statTotalSpend").text("₱" + parseFloat(data.total_spend).toLocaleString("en-US", { minimumFractionDigits: 2 }));
|
||||
$("#statTotalItems").text(data.total_items);
|
||||
$("#statTurnover").text(data.turnover_ratio + "x");
|
||||
$("#statIssuedItems").text(data.issued_30_days || "0");
|
||||
|
||||
// Predictive List
|
||||
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');
|
||||
}
|
||||
|
||||
// Charts
|
||||
let valLabels = []; let valData = [];
|
||||
data.valuation_data.forEach(row => { valLabels.push(row.supplier_name); valData.push(row.supplier_value); });
|
||||
// Charts Data Preparation
|
||||
let valLabels = [];
|
||||
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 = [];
|
||||
data.spend_by_category.forEach(row => { spendLabels.push(row.category_name); spendData.push(row.category_spend); });
|
||||
let spendLabels = [];
|
||||
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.spendChartInstance) window.spendChartInstance.destroy();
|
||||
@@ -67,20 +78,86 @@ $(document).ready(function () {
|
||||
if(ctxVal) {
|
||||
window.valuationChartInstance = new Chart(ctxVal.getContext("2d"), {
|
||||
type: "bar",
|
||||
data: { labels: valLabels, datasets: [{ label: "Valuation (₱)", data: valData, backgroundColor: "rgba(13, 110, 253, 0.8)", borderRadius: 4 }] },
|
||||
plugins: [ChartDataLabels],
|
||||
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") } } }
|
||||
data: {
|
||||
labels: valLabels,
|
||||
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");
|
||||
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"), {
|
||||
type: "doughnut",
|
||||
data: { labels: spendLabels, datasets: [{ data: spendData, backgroundColor: ["#198754", "#ffc107", "#0dcaf0", "#d63384", "#6f42c1"], borderWidth: 2, borderColor: '#ffffff' }] },
|
||||
plugins: [ChartDataLabels],
|
||||
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") } } }
|
||||
data: {
|
||||
labels: spendLabels,
|
||||
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");
|
||||
@@ -117,13 +194,75 @@ $(document).ready(function () {
|
||||
|
||||
// --- 3. EXPORT TO EXCEL ---
|
||||
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 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");
|
||||
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 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");
|
||||
XLSX.writeFile(wb, `Inventory_Analytics_${new Date().toISOString().split('T')[0]}.xlsx`);
|
||||
|
||||
// SHEET 1: KPI Summary
|
||||
const kpiData = [
|
||||
["System Analytics", "Value"],
|
||||
["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 ---
|
||||
@@ -132,20 +271,76 @@ $(document).ready(function () {
|
||||
const element = document.getElementById('analyticsContainer');
|
||||
const buttons = document.getElementById('analyticsButtons');
|
||||
|
||||
buttons.style.display = 'none';
|
||||
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';
|
||||
if (!element) return; // Failsafe
|
||||
|
||||
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 => {
|
||||
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");
|
||||
});
|
||||
// 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");
|
||||
|
||||
}).catch(err => {
|
||||
// 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");
|
||||
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);
|
||||
|
||||
$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
|
||||
foreach($predictive_data as &$pred) {
|
||||
$pred['days_left'] = floor($pred['quantity'] / $pred['daily_burn']);
|
||||
@@ -792,6 +799,7 @@ switch ($action) {
|
||||
'low_stock' => $shortages['low_stock'] ?? 0,
|
||||
'total_spend' => $total_spend,
|
||||
'turnover_ratio' => round($turnover_ratio, 2),
|
||||
'issued_30_days' => $issued_30_days,
|
||||
'spend_by_category' => $spend_by_category,
|
||||
'valuation_data' => $valuation_data,
|
||||
'predictions' => $predictive_data
|
||||
|
||||
+90
-96
@@ -1,102 +1,96 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 header-actions" id="analyticsButtons">
|
||||
<h4 class="mb-0 fw-bold">System Overview</h4>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-outline-success bg-white shadow-sm" onclick="exportExcel()">
|
||||
<i class="bi bi-file-earmark-excel me-1"></i> Excel
|
||||
</button>
|
||||
<button class="btn btn-outline-danger bg-white shadow-sm" onclick="exportPDF()">
|
||||
<i class="bi bi-file-earmark-pdf me-1"></i> PDF
|
||||
</button>
|
||||
<button class="btn btn-primary shadow-sm" onclick="loadAnalytics(this)">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i> Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="analyticsContainer">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 header-actions">
|
||||
<h4 class="mb-0 fw-bold">Dashboard Analytics</h4>
|
||||
<div class="d-flex gap-2" id="analyticsButtons">
|
||||
<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
|
||||
</button>
|
||||
<button class="btn btn-outline-danger shadow-sm rounded-pill px-3 bg-white" onclick="exportPDF()">
|
||||
<i class="bi bi-file-earmark-pdf me-1"></i> PDF
|
||||
</button>
|
||||
<button class="btn btn-outline-primary shadow-sm rounded-pill px-3 bg-white" onclick="loadAnalytics(this)">
|
||||
<i class="bi bi-arrow-clockwise me-1"></i> Refresh
|
||||
</button>
|
||||
<div class="row g-3 mb-4">
|
||||
<div class="col-md-3">
|
||||
<div class="card border-0 shadow-sm h-100 bg-primary text-white">
|
||||
<div class="card-body p-4">
|
||||
<div class="text-white-50 small fw-bold text-uppercase mb-1">Current Stock Value</div>
|
||||
<h3 class="fw-bold mb-0" id="statTotalValue">₱0.00</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<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="col-12 col-md-4">
|
||||
<div class="card card-custom kpi-card p-3 text-center h-100 bg-light">
|
||||
<div class="text-muted small fw-bold mb-1">INVENTORY TURNOVER</div>
|
||||
<h3 id="statTurnover" class="fw-bold text-info mb-1">0.00x</h3>
|
||||
<span class="small text-muted">Spend vs. Current Stock</span>
|
||||
</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 class="col-12">
|
||||
<div class="card card-custom p-3">
|
||||
<div class="text-muted small fw-bold mb-2 text-center">VALUATION BY SUPPLIER</div>
|
||||
<div style="position: relative; height: 200px; width: 100%">
|
||||
<canvas id="valuationChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</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 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>
|
||||
|
||||
<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="card card-custom bg-success bg-opacity-10 border-0 p-4 text-center">
|
||||
<div class="mb-3">
|
||||
<i class="bi bi-robot text-success" style="font-size: 2.5rem;"></i>
|
||||
</div>
|
||||
<h5 class="fw-bold text-dark mb-2">System Recommendation</h5>
|
||||
<p class="text-muted small mb-4">
|
||||
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.
|
||||
</p>
|
||||
<button class="btn btn-success rounded-pill fw-bold shadow-sm w-100 py-2" id="prescriptiveAutoRestockBtn">
|
||||
<i class="bi bi-magic me-2"></i> Execute Auto-Restock
|
||||
</button>
|
||||
</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">Items Borrowed/Issued (30 Days)</div>
|
||||
<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 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>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm h-100">
|
||||
<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>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="card border-0 shadow-sm h-100 bg-light border border-success border-opacity-25">
|
||||
<div class="card-body p-4 text-center d-flex flex-column justify-content-center">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in new issue
Block a user