// ========================================== // ANALYTICS & EXPORTS LOGIC (analytics.js) // ========================================== $(document).ready(function () { // --- 1. Load Analytics Dashboard --- window.loadAnalytics = function (btn = null) { let originalText = ""; if (btn) { originalText = $(btn).html(); $(btn).prop("disabled", true).html(' Refreshing...'); } $.ajax({ url: "includes/api/api.php?action=get_analytics", type: "GET", dataType: "json", success: function (res) { if (btn) $(btn).prop("disabled", false).html(originalText); if (res.status === "success") { let data = res.data; currentAnalyticsData = data; // Save globally for exports // KPI Cards $("#statTotalValue").text("₱" + parseFloat(data.total_value).toLocaleString("en-US", { minimumFractionDigits: 2 })); $("#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 = ""; if (data.predictions && data.predictions.length > 0) { data.predictions.forEach(item => { predHtml += `
  • ${item.name}
    Stock: ${item.quantity} | Burns: ${parseFloat(item.daily_burn).toFixed(1)}/day
    Out in ~${item.days_left} days
  • `; }); } else { predHtml = `
  • No imminent stockouts predicted!
  • `; } $("#predictiveList").html(predHtml); // Prescriptive Action Card $("#prescriptiveLowCount").text(data.low_stock); if (data.low_stock > 0) { $("#prescriptiveAutoRestockBtn").prop("disabled", false).removeClass("btn-secondary").addClass("btn-success").html(' Auto-Generate POs'); } else { $("#prescriptiveAutoRestockBtn").prop("disabled", true).removeClass("btn-success").addClass("btn-secondary").html(' Stock Optimal'); } // 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); }); if (window.valuationChartInstance) window.valuationChartInstance.destroy(); if (window.spendChartInstance) window.spendChartInstance.destroy(); let ctxVal = document.getElementById("valuationChart"); 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 }] }, // 🔥 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: 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"); } }, error: function () { if (btn) $(btn).prop("disabled", false).html(originalText); showAlert("Network error while trying to refresh data.", "danger"); } }); }; // --- 2. Prescriptive Auto-Restock --- $(document).on("click", "#prescriptiveAutoRestockBtn", function() { let btn = $(this); let originalText = btn.html(); btn.prop("disabled", true).html(' Generating POs...'); $.ajax({ url: "includes/api/api.php?action=auto_restock", type: "POST", dataType: "json", success: function (res) { if (res.status === "success") { showAlert(res.message, "success"); if (window.currentTab === "analytics") loadAnalytics(); } else { btn.prop("disabled", false).html(originalText); showAlert(res.message, "danger"); } } }); }); // --- 3. EXPORT TO EXCEL --- window.exportExcel = function() { 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(); // 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 --- window.exportPDF = function() { const { jsPDF } = window.jspdf; const element = document.getElementById('analyticsContainer'); const buttons = document.getElementById('analyticsButtons'); if (!element) return; // Failsafe // 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 }; });