diff --git a/assets/js/analytics.js b/assets/js/analytics.js index 8637e6f..7c363e7 100644 --- a/assets/js/analytics.js +++ b/assets/js/analytics.js @@ -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(' 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 = '
'; + + spendLabels.forEach((label, i) => { + let value = '₱' + parseFloat(spendData[i]).toLocaleString("en-US", { minimumFractionDigits: 2 }); + let color = doughnutColors[i % doughnutColors.length]; + + legendHtml += ` +
+
+ + ${label} +
+
${value}
+
`; + }); + legendHtml += '
'; + + // Remove any old custom legend, then inject the new one right below the canvas container + $("#customSpendLegend").remove(); + $(ctxSpend).parent().after(`
${legendHtml}
`); } 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 }; }); \ No newline at end of file diff --git a/helloworld.html b/helloworld.html new file mode 100644 index 0000000..39c0dd9 --- /dev/null +++ b/helloworld.html @@ -0,0 +1,11 @@ + + + + + + Document + + +

Hello, World!

+ + \ No newline at end of file diff --git a/includes/api/api.php b/includes/api/api.php index 38aeeac..ac9bcb4 100644 --- a/includes/api/api.php +++ b/includes/api/api.php @@ -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 diff --git a/pages/analytics.php b/pages/analytics.php index 461d26d..1a748cd 100644 --- a/pages/analytics.php +++ b/pages/analytics.php @@ -1,102 +1,96 @@ +
+

System Overview

+
+ + + +
+
+
-
-

Dashboard Analytics

-
- - - +
+
+
+
+
Current Stock Value
+

₱0.00

+
- -
-
- -
1. Descriptive (Current State)
-
-
-
-
STOCK VALUATION
-

₱0.00

-
-
-
-
-
TOTAL PO SPEND
-

₱0.00

-
-
-
-
-
UNIQUE ITEMS
-

0

-
-
-
- -
2. Diagnostic (Trends & Causes)
-
-
-
-
INVENTORY TURNOVER
-

0.00x

- Spend vs. Current Stock -
-
-
-
-
SPEND BY CATEGORY (SUPPLIER)
-
- -
-
-
-
-
-
VALUATION BY SUPPLIER
-
- -
-
-
-
- -
- -
- -
3. Predictive (Forecast)
-
-
-
High-Risk Stockouts
- Items predicted to run out in < 7 days based on recent burn rates. -
-
-
    -
  • Loading predictions...
  • -
-
-
- -
4. Prescriptive (Action)
-
-
- -
-
System Recommendation
-

- You have 0 items currently below their reorder thresholds. The system can automatically generate optimized Purchase Orders for all affected suppliers right now. -

- -
- +
+
+
+
Items Borrowed/Issued (30 Days)
+

0

+
+
+
+
+
Total Purchases
+

₱0.00

+
+
+
+
+
+
+
Total Unique Items
+

0

+
+
+
+
+ +
+
+
+
+
Stock Value by Supplier
+
+
+
+
+
+
+
+
Spending by Category
+
+
+
+
+
+ +
+
+
+
+
Stock Depletion Forecast
+

Items expected to run out soon based on recent usage.

+
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    Action Items
    +

    You have 0 items currently at or below their minimum reorder level.

    + +
    +
    +
    +
    \ No newline at end of file