Files
icis-inventory/assets/js/app.js
T
2026-04-24 11:00:14 +08:00

1138 lines
49 KiB
JavaScript

$(document).ready(function () {
// ==========================================
// 1. GLOBAL VARIABLES & STATE
// ==========================================
let currentUserRole = '';
let allInventoryData = [];
let hasCheckedLowStock = false;
let filteredInventoryData = [];
let inventoryCurrentPage = 1;
const inventoryItemsPerPage = 5;
let allPOData = [];
let filteredPOData = [];
let poCurrentPage = 1;
const poItemsPerPage = 5;
let currentAnalyticsData = null;
let valuationChartInstance = null;
let spendChartInstance = null;
// ==========================================
// 2. AUTHENTICATION LOGIC
// ==========================================
checkAuth();
function checkAuth() {
$.ajax({
url: "includes/api/api.php?action=check_auth",
type: "GET",
dataType: "json",
success: function (res) {
if (res.status === "success") {
currentUserRole = res.role;
$("#currentUser").text(res.full_name);
$("#loginSection").removeClass("d-flex").hide();
$("#topNavbar, #dashboardSection").fadeIn();
// SPA Initial Load
if (!window.currentTab) {
switchTab('inventory');
}
} else {
$("#topNavbar, #dashboardSection").hide();
$("#loginSection").addClass("d-flex").fadeIn();
}
},
error: function() {
$("#topNavbar, #dashboardSection").hide();
$("#loginSection").addClass("d-flex").fadeIn();
}
});
}
let loginCooldownInterval;
$(document).on("submit", "#loginForm", function (e) {
e.preventDefault();
let btn = $(this).find('button[type="submit"]');
let originalText = 'Sign In <i class="bi bi-arrow-right ms-2"></i>';
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm"></span> Authenticating...');
$.ajax({
url: "includes/api/api.php?action=login",
type: "POST",
data: $(this).serialize(),
dataType: "json",
success: function (res) {
if (res.status === "success") {
btn.prop('disabled', false).html(originalText);
$("#loginForm")[0].reset();
$("#loginAlert").empty();
checkAuth();
} else if (res.status === "locked") {
let timeLeft = res.remaining;
clearInterval(loginCooldownInterval);
loginCooldownInterval = setInterval(function() {
if (timeLeft <= 0) {
clearInterval(loginCooldownInterval);
btn.prop('disabled', false).html(originalText);
$("#loginAlert").html(`<div class="alert alert-info py-2 small border-0 shadow-sm"><i class="bi bi-info-circle-fill me-1"></i> Cooldown finished. You may try again.</div>`);
} else {
btn.prop('disabled', true).html(`<i class="bi bi-lock-fill me-1"></i> Locked (${timeLeft}s)`);
$("#loginAlert").html(`<div class="alert alert-warning py-2 small border-0 shadow-sm text-dark">
<i class="bi bi-clock-history me-1"></i> Too many failed attempts. Try again in <strong>${timeLeft}</strong> seconds.
</div>`);
timeLeft--;
}
}, 1000);
} else {
btn.prop('disabled', false).html(originalText);
$("#loginAlert").html(`<div class="alert alert-danger py-2 small border-0 shadow-sm"><i class="bi bi-exclamation-circle-fill me-1"></i> ${res.message}</div>`);
}
},
error: function() {
btn.prop('disabled', false).html(originalText);
$("#loginAlert").html(`<div class="alert alert-danger py-2 small border-0 shadow-sm">Network error connecting to server.</div>`);
}
});
});
$(document).on("click", "#logoutBtn", function () {
let btn = $(this);
btn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm"></span>');
$.ajax({
url: "includes/api/api.php?action=logout",
type: "GET",
dataType: "json",
success: function () {
btn.prop('disabled', false).html('<i class="bi bi-box-arrow-right me-1"></i> Logout');
allInventoryData = [];
allPOData = [];
hasCheckedLowStock = false;
window.currentTab = null;
checkAuth();
}
});
});
// ==========================================
// 3. SPA LAZY LOADING ENGINE
// ==========================================
function initPageScripts(page) {
if (page === "inventory") {
loadInventory();
loadSuppliers();
} else if (page === "po") {
loadPOs();
loadSuppliers();
populatePODropdowns();
} else if (page === "analytics") {
loadAnalytics();
} else if (page === "vendors") {
loadVendors();
}
}
function loadPage(page) {
$("#pageContainer").html(`
<div class="text-center py-5">
<div class="spinner-border text-primary"></div>
<div class="mt-2 text-muted">Loading ${page}...</div>
</div>
`);
$("#pageContainer").load("pages/" + page + ".php", function (response, status) {
if (status === "error") {
$("#pageContainer").html(`
<div class="alert alert-danger shadow-sm border-0">
<i class="bi bi-exclamation-triangle-fill me-2"></i> Failed to load ${page} module. Ensure pages/${page}.php exists.
</div>
`);
} else {
initPageScripts(page);
}
});
}
window.switchTab = function (tab) {
window.currentTab = tab;
$(".nav-link").removeClass("active");
$("#tab-" + tab).addClass("active");
loadPage(tab);
};
// ==========================================
// 4. INVENTORY & FILTER LOGIC
// ==========================================
function loadInventory() {
$.ajax({
url: "includes/api/api.php?action=get_inventory",
type: "GET",
cache: false,
dataType: "json",
success: function (res) {
if (res.status === "success") {
allInventoryData = res.data;
applyFilters();
if (!hasCheckedLowStock) {
checkAndShowLowStockAlert(res.data);
hasCheckedLowStock = true;
}
}
},
});
}
function applyFilters() {
// Failsafe in case elements don't exist yet
if ($("#searchInput").length === 0) return;
let searchTerm = $("#searchInput").val().toLowerCase();
let supplierFilter = $("#filterSupplier").val();
let stockFilter = $("#filterStock").val();
filteredInventoryData = allInventoryData.filter(function (item) {
let matchesSearch = item.name.toLowerCase().includes(searchTerm) ||
item.inventory_id.toString().includes(searchTerm) ||
(item.description && item.description.toLowerCase().includes(searchTerm));
let matchesSupplier = supplierFilter === "" || item.supplier_id.toString() === supplierFilter;
let isLowStock = parseInt(item.quantity) <= parseInt(item.reorder_level);
let matchesStock = stockFilter === "" || (stockFilter === "low" && isLowStock) || (stockFilter === "healthy" && !isLowStock);
return matchesSearch && matchesSupplier && matchesStock;
});
inventoryCurrentPage = 1;
renderInventoryTable();
}
// 🔥 DELEGATED BINDINGS FOR LAZY LOADED INPUTS
$(document).on("keyup", "#searchInput", applyFilters);
$(document).on("change", "#filterSupplier, #filterStock", applyFilters);
function renderInventoryTable() {
let rows = "";
let totalItems = filteredInventoryData.length;
let totalPages = Math.ceil(totalItems / inventoryItemsPerPage) || 1;
let startIndex = (inventoryCurrentPage - 1) * inventoryItemsPerPage;
let endIndex = Math.min(startIndex + inventoryItemsPerPage, totalItems);
let pageData = filteredInventoryData.slice(startIndex, endIndex);
if (pageData.length === 0) {
rows = `<tr><td colspan="7" class="text-center py-4 text-muted">No items found matching your filters.</td></tr>`;
} else {
pageData.forEach(function (item) {
let isLowStock = parseInt(item.quantity) <= parseInt(item.reorder_level);
let statusBadge = isLowStock
? '<span class="badge bg-danger bg-opacity-10 text-danger border border-danger border-opacity-25 rounded-pill px-3 py-2"><i class="bi bi-exclamation-triangle-fill me-1"></i> Low Stock</span>'
: '<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25 rounded-pill px-3 py-2"><i class="bi bi-check-circle-fill me-1"></i> Healthy</span>';
let forecastHtml = "";
let daysOut = parseInt(item.days_to_stockout);
let deleteItemBtn = (currentUserRole === 'admin')
? `<button class="btn btn-sm btn-light text-danger delete-btn" data-id="${item.inventory_id}"><i class="bi bi-trash"></i></button>`
: '';
if (daysOut === 999 || item.quantity == 0) forecastHtml = `<span class="text-muted small"><i class="bi bi-dash"></i> No recent usage</span>`;
else if (daysOut <= 7) forecastHtml = `<span class="text-danger fw-bold small"><i class="bi bi-graph-down-arrow"></i> Out in ~${daysOut} days</span>`;
else if (daysOut <= 21) forecastHtml = `<span class="text-warning fw-bold small text-dark"><i class="bi bi-graph-down"></i> Out in ~${daysOut} days</span>`;
else forecastHtml = `<span class="text-success fw-bold small"><i class="bi bi-shield-check"></i> Safe (>3 wks)</span>`;
if (item.daily_burn_rate > 0) forecastHtml += `<br><span class="text-muted" style="font-size: 0.7rem;">Issues ${item.daily_burn_rate}/day</span>`;
rows += `<tr>
<td class="ps-4">
<div class="fw-bold text-dark">${item.name}</div>
<div class="text-muted small">${item.description || "No description"}</div>
<div class="text-muted small">ID: #${item.inventory_id}</div>
</td>
<td class="hide-on-mobile"><span class="badge bg-light text-dark border">${item.supplier_name || "N/A"}</span></td>
<td class="hide-on-mobile fw-medium">₱${parseFloat(item.unit_price).toFixed(2)}</td>
<td>
<div class="fw-bold ${isLowStock ? "text-danger" : ""}">${item.quantity} units</div>
<div class="text-muted small">Reorder at ${item.reorder_level}</div>
</td>
<td class="hide-on-mobile">${forecastHtml}</td>
<td class="hide-on-mobile text-end">${statusBadge}</td>
<td class="pe-4 text-end">
<button class="btn btn-sm btn-light text-warning me-1 issue-btn" data-id="${item.inventory_id}" data-name="${item.name}"><i class="bi bi-box-arrow-up"></i></button>
<button class="btn btn-sm btn-light text-primary me-1 edit-btn"
data-id="${item.inventory_id}" data-name="${item.name}" data-desc="${item.description}"
data-qty="${item.quantity}" data-price="${item.unit_price}" data-reorder="${item.reorder_level}"
data-supplier="${item.supplier_id}"><i class="bi bi-pencil"></i></button>
${deleteItemBtn}
</td>
</tr>`;
});
}
$("#inventoryTableBody").html(rows);
if (totalItems > 0) $("#inventoryPaginationInfo").text(`Showing ${startIndex + 1} to ${endIndex} of ${totalItems} entries`);
else $("#inventoryPaginationInfo").text("Showing 0 entries");
renderInventoryPagination(totalPages);
}
function renderInventoryPagination(totalPages) {
let paginationHtml = `<li class="page-item ${inventoryCurrentPage === 1 ? "disabled" : ""}"><a class="page-link" href="#" onclick="changeInventoryPage(${inventoryCurrentPage - 1}); return false;">Previous</a></li>`;
for (let i = 1; i <= totalPages; i++) {
paginationHtml += `<li class="page-item ${inventoryCurrentPage === i ? "active" : ""}"><a class="page-link" href="#" onclick="changeInventoryPage(${i}); return false;">${i}</a></li>`;
}
paginationHtml += `<li class="page-item ${inventoryCurrentPage === totalPages ? "disabled" : ""}"><a class="page-link" href="#" onclick="changeInventoryPage(${inventoryCurrentPage + 1}); return false;">Next</a></li>`;
$("#inventoryPaginationControls").html(paginationHtml);
}
window.changeInventoryPage = function(newPage) {
inventoryCurrentPage = newPage;
renderInventoryTable();
};
// ==========================================
// 5. ITEM CRUD MODALS
// ==========================================
$(document).on("submit", "#addItemForm", function (e) {
e.preventDefault();
let submitBtn = $(this).find('button[type="submit"]');
let originalText = submitBtn.html();
submitBtn.prop("disabled", true).html('<span class="spinner-border spinner-border-sm"></span> Saving...');
$.ajax({
url: "includes/api/api.php?action=add_item",
type: "POST",
data: $(this).serialize(),
dataType: "json",
success: function (res) {
submitBtn.prop("disabled", false).html(originalText);
if (res.status === "success") {
$("#addItemModal").modal("hide");
$("#addItemForm")[0].reset();
showAlert(res.message, "success");
if (window.currentTab === "inventory") loadInventory();
} else {
showAlert(res.message, "danger");
}
},
error: function () {
submitBtn.prop("disabled", false).html(originalText);
showAlert("A network error occurred.", "danger");
},
});
});
$(document).on("click", ".edit-btn", function () {
$("#edit_supplier_id").html($("#supplierSelect").html());
let btn = $(this);
$("#edit_inventory_id").val(btn.data("id"));
$("#edit_name").val(btn.data("name"));
$("#edit_description").val(btn.data("desc"));
$("#edit_quantity").val(btn.data("qty"));
$("#edit_unit_price").val(btn.data("price"));
$("#edit_reorder_level").val(btn.data("reorder"));
$("#edit_supplier_id").val(btn.data("supplier"));
$("#editItemModal").modal("show");
});
$(document).on("submit", "#editItemForm", function (e) {
e.preventDefault();
let submitBtn = $(this).find('button[type="submit"]');
let originalText = submitBtn.html();
submitBtn.prop("disabled", true).html('<span class="spinner-border spinner-border-sm"></span> Updating...');
$.ajax({
url: "includes/api/api.php?action=update_item",
type: "POST",
data: $(this).serialize(),
dataType: "json",
success: function (res) {
submitBtn.prop("disabled", false).html(originalText);
if (res.status === "success") {
$("#editItemModal").modal("hide");
showAlert(res.message, "success");
if (window.currentTab === "inventory") loadInventory();
} else {
showAlert(res.message, "danger");
}
},
});
});
$(document).on("click", ".delete-btn", function () {
let itemId = $(this).data("id");
if (confirm("Are you sure you want to delete this item? This cannot be undone.")) {
$.ajax({
url: "includes/api/api.php?action=delete_item",
type: "POST",
data: { inventory_id: itemId },
dataType: "json",
success: function (res) {
if (res.status === "success") {
showAlert(res.message, "success");
if (window.currentTab === "inventory") loadInventory();
} else {
showAlert(res.message, "danger");
}
},
});
}
});
$(document).on("click", ".issue-btn", function () {
let btn = $(this);
$("#issue_inventory_id").val(btn.data("id"));
$("#issue_item_name").text(btn.data("name"));
$("#issue_qty").val(1);
$("#issueItemModal").modal("show");
});
$(document).on("submit", "#issueItemForm", function (e) {
e.preventDefault();
let submitBtn = $(this).find('button[type="submit"]');
let originalText = submitBtn.html();
submitBtn.prop("disabled", true).html('<span class="spinner-border spinner-border-sm"></span> Processing...');
$.ajax({
url: "includes/api/api.php?action=issue_item",
type: "POST",
data: $(this).serialize(),
dataType: "json",
success: function (res) {
submitBtn.prop("disabled", false).html(originalText);
if (res.status === "success") {
$("#issueItemModal").modal("hide");
showAlert(res.message, "success");
if (window.currentTab === "inventory") loadInventory();
} else {
showAlert(res.message, "danger");
}
},
});
});
// ==========================================
// 6. LOW STOCK NOTIFICATION ENGINE
// ==========================================
function checkAndShowLowStockAlert(inventoryData) {
let lowStockItems = inventoryData.filter(item => parseInt(item.quantity) <= parseInt(item.reorder_level));
if (lowStockItems.length > 0) {
let rows = "";
lowStockItems.forEach(item => {
rows += `<tr>
<td class="ps-4 fw-bold text-dark">${item.name}</td>
<td class="text-danger fw-bold">${item.quantity}</td>
<td class="text-muted">${item.reorder_level}</td>
<td><span class="badge bg-light text-dark border">${item.supplier_name || "No Supplier"}</span></td>
</tr>`;
});
$("#lowStockTableBody").html(rows);
setTimeout(() => { $("#lowStockModal").modal("show"); }, 500);
}
}
$(document).on("click", "#manualRestockBtn", function() {
$("#lowStockModal").modal("hide");
switchTab('po');
});
$(document).on("click", "#autoRestockBtn", function() {
let btn = $(this);
let originalText = btn.html();
btn.prop("disabled", true).html('<span class="spinner-border spinner-border-sm"></span> Processing...');
$.ajax({
url: "includes/api/api.php?action=auto_restock",
type: "POST",
dataType: "json",
success: function (res) {
btn.prop("disabled", false).html(originalText);
$("#lowStockModal").modal("hide");
if (res.status === "success") {
showAlert(res.message, "success");
switchTab('po');
} else {
showAlert(res.message, "danger");
}
},
error: function() {
btn.prop("disabled", false).html(originalText);
showAlert("Network error during auto-restock.", "danger");
}
});
});
// ==========================================
// 7. VENDORS LOGIC
// ==========================================
function loadSuppliers() {
$.ajax({
url: "includes/api/api.php?action=get_suppliers",
type: "GET",
dataType: "json",
success: function (res) {
if (res.status === "success") {
let options = '<option value="">Select...</option>';
let filterOptions = '<option value="">All Suppliers</option>';
res.data.forEach(function (sup) {
options += `<option value="${sup.supplier_id}">${sup.name}</option>`;
filterOptions += `<option value="${sup.supplier_id}">${sup.name}</option>`;
});
$("#supplierSelect").html(options);
$("#filterSupplier").html(filterOptions);
$("#filterPoSupplier").html(filterOptions);
if ($("#poSupplierSelect").length) {
$("#poSupplierSelect").html(options);
$("#poItemSelect").html('<option value="">Select a Supplier first...</option>');
}
}
},
});
}
function loadVendors() {
$.ajax({
url: "includes/api/api.php?action=get_suppliers",
type: "GET",
dataType: "json",
success: function (res) {
if (res.status === "success") {
let rows = "";
res.data.forEach(function (vendor) {
let deleteVendorBtn = (currentUserRole === 'admin')
? `<button class="btn btn-sm btn-light text-danger me-2 delete-vendor-btn" data-id="${vendor.supplier_id}" title="Delete"><i class="bi bi-trash"></i></button>`
: '';
rows += `<tr>
<td class="ps-4">
<div class="fw-bold text-dark">${vendor.name}</div>
<div class="text-muted small"><i class="bi bi-person me-1"></i>${vendor.contact_person || "N/A"}</div>
</td>
<td>
<div class="small"><i class="bi bi-envelope me-1"></i>${vendor.email || "N/A"}</div>
<div class="text-muted small"><i class="bi bi-telephone me-1"></i>${vendor.phone || "N/A"}</div>
</td>
<td class="pe-4 text-end">
<button class="btn btn-sm btn-light text-primary me-1 edit-vendor-btn"
data-id="${vendor.supplier_id}" data-name="${vendor.name}" data-person="${vendor.contact_person || ''}"
data-phone="${vendor.phone || ''}" data-email="${vendor.email || ''}" data-address="${vendor.address || ''}" title="Edit">
<i class="bi bi-pencil"></i>
</button>
${deleteVendorBtn}
<button class="btn btn-sm btn-outline-primary rounded-pill view-history-btn" data-id="${vendor.supplier_id}" data-name="${vendor.name}">
<i class="bi bi-clock-history"></i> History
</button>
</td>
</tr>`;
});
$("#vendorTableBody").html(rows);
}
},
});
}
$(document).on("click", ".edit-vendor-btn", function () {
let btn = $(this);
$("#edit_vendor_id").val(btn.data("id"));
$("#edit_vendor_name").val(btn.data("name"));
$("#edit_vendor_contact_person").val(btn.data("person"));
$("#edit_vendor_phone").val(btn.data("phone"));
$("#edit_vendor_email").val(btn.data("email"));
$("#edit_vendor_address").val(btn.data("address"));
$("#editVendorModal").modal("show");
});
$(document).on("submit", "#addVendorForm", function (e) {
e.preventDefault();
let submitBtn = $(this).find('button[type="submit"]');
let originalText = submitBtn.html();
submitBtn.prop("disabled", true).html('<span class="spinner-border spinner-border-sm"></span> Saving...');
$.ajax({
url: "includes/api/api.php?action=add_supplier",
type: "POST",
data: $(this).serialize(),
dataType: "json",
success: function (res) {
submitBtn.prop("disabled", false).html(originalText);
if (res.status === "success") {
$("#addVendorModal").modal("hide");
$("#addVendorForm")[0].reset();
showAlert(res.message, "success");
if (window.currentTab === "vendors") loadVendors();
loadSuppliers();
} else {
showAlert(res.message, "danger");
}
}
});
});
$(document).on("submit", "#editVendorForm", function (e) {
e.preventDefault();
let submitBtn = $(this).find('button[type="submit"]');
let originalText = submitBtn.html();
submitBtn.prop("disabled", true).html('<span class="spinner-border spinner-border-sm"></span> Updating...');
$.ajax({
url: "includes/api/api.php?action=update_supplier",
type: "POST",
data: $(this).serialize(),
dataType: "json",
success: function (res) {
submitBtn.prop("disabled", false).html(originalText);
if (res.status === "success") {
$("#editVendorModal").modal("hide");
showAlert(res.message, "success");
if (window.currentTab === "vendors") loadVendors();
loadSuppliers();
} else {
showAlert(res.message, "danger");
}
}
});
});
$(document).on("click", ".delete-vendor-btn", function () {
let supplierId = $(this).data("id");
if (confirm("Are you sure you want to delete this vendor? This will fail if they have active inventory items or purchase orders.")) {
$.ajax({
url: "includes/api/api.php?action=delete_supplier",
type: "POST",
data: { supplier_id: supplierId },
dataType: "json",
success: function (res) {
if (res.status === "success") {
showAlert(res.message, "success");
if (window.currentTab === "vendors") loadVendors();
loadSuppliers();
} else {
showAlert(res.message, "danger");
}
}
});
}
});
$(document).on("click", ".view-history-btn", function () {
let supplierId = $(this).data("id");
let supplierName = $(this).data("name");
$("#historyVendorName").text(supplierName);
$("#vendorHistoryTableBody").html('<tr><td colspan="5" class="text-center text-muted py-3"><span class="spinner-border spinner-border-sm"></span> Loading history...</td></tr>');
$("#vendorHistoryModal").modal("show");
$.ajax({
url: "includes/api/api.php?action=get_vendor_history&supplier_id=" + supplierId,
type: "GET",
dataType: "json",
success: function (res) {
if (res.status === "success") {
let rows = "";
if (res.data.length === 0) {
rows = `<tr><td colspan="5" class="text-center py-4 text-muted">No procurement history found for this vendor.</td></tr>`;
} else {
res.data.forEach(function (po) {
let statusClass = po.status === "Received" ? "text-success" : (po.status === "Cancelled" ? "text-danger" : "text-warning");
rows += `<tr>
<td class="ps-4 fw-bold">#PO-${po.po_id}</td>
<td>${po.order_date}</td>
<td class="fw-medium">₱${parseFloat(po.total_amount).toFixed(2)}</td>
<td class="${statusClass} fw-bold">${po.status}</td>
<td class="pe-4 text-end">
<button class="btn btn-sm btn-outline-secondary rounded-pill view-po-details-btn" data-id="${po.po_id}">
<i class="bi bi-list-ul"></i> Items
</button>
</td>
</tr>`;
});
}
$("#vendorHistoryTableBody").html(rows);
} else {
$("#vendorHistoryTableBody").html(`<tr><td colspan="5" class="text-center text-danger py-4">Error loading data.</td></tr>`);
}
}
});
});
// ==========================================
// 8. PURCHASE ORDERS LOGIC
// ==========================================
function loadPOs() {
$.ajax({
url: "includes/api/api.php?action=get_pos",
type: "GET",
cache: false,
dataType: "json",
success: function (res) {
if (res.status === "success") {
allPOData = res.data;
applyPOFilters();
}
}
});
}
function applyPOFilters() {
// Failsafe in case elements don't exist yet
if ($("#filterPoDateFrom").length === 0) return;
let dateFrom = $("#filterPoDateFrom").val();
let dateTo = $("#filterPoDateTo").val();
let supplierFilter = $("#filterPoSupplier").val();
filteredPOData = allPOData.filter(function (po) {
let matchesDateFrom = dateFrom === "" || po.order_date >= dateFrom;
let matchesDateTo = dateTo === "" || po.order_date <= dateTo;
let matchesSupplier = supplierFilter === "" || po.supplier_id.toString() === supplierFilter;
return matchesDateFrom && matchesDateTo && matchesSupplier;
});
poCurrentPage = 1;
renderPOTable();
}
// 🔥 DELEGATED BINDINGS FOR LAZY LOADED INPUTS
$(document).on("change", "#filterPoDateFrom, #filterPoDateTo, #filterPoSupplier", applyPOFilters);
function renderPOTable() {
let rows = "";
let totalItems = filteredPOData.length;
let totalPages = Math.ceil(totalItems / poItemsPerPage) || 1;
let startIndex = (poCurrentPage - 1) * poItemsPerPage;
let endIndex = Math.min(startIndex + poItemsPerPage, totalItems);
let pageData = filteredPOData.slice(startIndex, endIndex);
if (pageData.length === 0) {
rows = `<tr><td colspan="6" class="text-center py-4 text-muted">No purchase orders found matching your filters.</td></tr>`;
} else {
pageData.forEach(function (po) {
let statusDisplay = "";
let role = currentUserRole.toLowerCase();
if (po.status === "Pending") {
// 1. PENDING: Only Admins can approve or cancel
if (role === 'admin') {
statusDisplay = `
<select class="form-select form-select-sm border border-warning shadow-sm status-dropdown fw-bold text-warning"
data-id="${po.po_id}" data-current="${po.status}" style="width: 150px; cursor: pointer;">
<option value="Pending" selected disabled>Pending (Action...)</option>
<option value="Approved" class="text-primary">↳ Approve PO</option>
<option value="Cancelled" class="text-danger">↳ Cancel PO</option>
</select>`;
} else {
statusDisplay = '<span class="badge bg-warning bg-opacity-10 text-warning rounded-pill px-3 py-2"><i class="bi bi-hourglass-split me-1"></i> Pending Finance</span>';
}
}
else if (po.status === "Approved") {
// 2. APPROVED: ANY user (Admin or Employee) can Receive or report an Issue
statusDisplay = `
<select class="form-select form-select-sm border border-success text-success shadow-sm status-dropdown fw-bold"
data-id="${po.po_id}" data-current="${po.status}" style="width: 150px; cursor: pointer;">
<option value="Approved" selected disabled>Approved (Action...)</option>
<option value="Received" class="text-success">↳ Receive Items</option>
<option value="Missing" class="text-danger">↳ Report Missing</option>
<option value="Issues" class="text-warning">↳ Report Issues</option>
</select>`;
}
else if (po.status === "Missing") {
// 3. MISSING: Locked! Static badge, no longer a dropdown.
statusDisplay = '<span class="badge bg-danger bg-opacity-10 text-danger rounded-pill px-3 py-2"><i class="bi bi-exclamation-triangle-fill me-1"></i> Missing</span>';
}
else if (po.status === "Issues") {
// 4. ISSUES: Locked! Static badge, no longer a dropdown.
statusDisplay = '<span class="badge bg-warning bg-opacity-10 text-dark border border-warning rounded-pill px-3 py-2"><i class="bi bi-exclamation-circle-fill me-1"></i> Issues Reported</span>';
}
else if (po.status === "Received") {
// 5. RECEIVED: Locked!
statusDisplay = '<span class="badge bg-success bg-opacity-10 text-success rounded-pill px-3 py-2"><i class="bi bi-check-circle-fill me-1"></i> Received</span>';
}
else {
// 6. CANCELLED / OTHER: Locked!
statusDisplay = `<span class="badge bg-secondary bg-opacity-10 text-secondary rounded-pill px-3 py-2"><i class="bi bi-x-circle-fill me-1"></i> ${po.status}</span>`;
}
let editBtn = po.status === "Pending" ? `<br><button class="btn btn-link btn-sm p-0 text-decoration-none small edit-po-qty-btn mt-1" data-id="${po.po_id}" data-qty="${po.item_qty}" data-price="${po.unit_price}">Edit Qty</button>` : "";
rows += `<tr>
<td class="ps-4 fw-bold">#PO-${po.po_id}</td>
<td class="hide-on-mobile">${po.order_date}</td>
<td>${po.supplier_name}</td>
<td>
<div class="fw-bold text-dark">${po.item_name || "No Item Data"}</div>
<div class="text-muted small">Qty: ${po.item_qty || 0} @ ₱${parseFloat(po.unit_price).toFixed(2)} ${editBtn}</div>
</td>
<td class="hide-on-mobile fw-medium text-primary">₱${parseFloat(po.total_amount).toFixed(2)}</td>
<td>${statusDisplay}</td>
</tr>`;
});
}
$("#poTableBody").html(rows);
if (totalItems > 0) $("#poPaginationInfo").text(`Showing ${startIndex + 1} to ${endIndex} of ${totalItems} entries`);
else $("#poPaginationInfo").text("Showing 0 entries");
renderPOPagination(totalPages);
}
function renderPOPagination(totalPages) {
let paginationHtml = `<li class="page-item ${poCurrentPage === 1 ? "disabled" : ""}"><a class="page-link" href="#" onclick="changePOPage(${poCurrentPage - 1}); return false;">Previous</a></li>`;
for (let i = 1; i <= totalPages; i++) {
paginationHtml += `<li class="page-item ${poCurrentPage === i ? "active" : ""}"><a class="page-link" href="#" onclick="changePOPage(${i}); return false;">${i}</a></li>`;
}
paginationHtml += `<li class="page-item ${poCurrentPage === totalPages ? "disabled" : ""}"><a class="page-link" href="#" onclick="changePOPage(${poCurrentPage + 1}); return false;">Next</a></li>`;
$("#poPaginationControls").html(paginationHtml);
}
window.changePOPage = function(newPage) {
poCurrentPage = newPage;
renderPOTable();
};
$(document).on("click", ".edit-po-qty-btn", function () {
let poId = $(this).data("id");
let currentQty = $(this).data("qty");
let unitPrice = parseFloat($(this).data("price"));
$("#edit_po_id").val(poId);
$("#edit_po_unit_price").val(unitPrice);
$("#edit_po_unit_price_display").text("₱" + unitPrice.toFixed(2));
$("#edit_po_qty").val(currentQty);
$("#edit_po_total_display").text("₱" + (currentQty * unitPrice).toFixed(2));
$("#editPoQtyModal").modal("show");
});
$(document).on("input", "#edit_po_qty", function() {
let qty = parseInt($(this).val()) || 0;
let price = parseFloat($("#edit_po_unit_price").val());
$("#edit_po_total_display").text("₱" + (qty * price).toFixed(2));
});
$(document).on("submit", "#editPoQtyForm", function (e) {
e.preventDefault();
let submitBtn = $(this).find('button[type="submit"]');
let originalText = submitBtn.html();
submitBtn.prop("disabled", true).html('<span class="spinner-border spinner-border-sm"></span> Updating...');
$.ajax({
url: "includes/api/api.php?action=update_po_qty",
type: "POST",
data: $(this).serialize(),
dataType: "json",
success: function (res) {
submitBtn.prop("disabled", false).html(originalText);
if (res.status === "success") {
$("#editPoQtyModal").modal("hide");
showAlert(res.message, "success");
if (window.currentTab === "po") loadPOs();
} else {
showAlert(res.message, "danger");
}
}
});
});
function populatePODropdowns() {
$("#poSupplierSelect").html($("#supplierSelect").html());
$("#poItemSelect").html('<option value="">Select a Supplier first...</option>');
}
$(document).on("change", "#poItemSelect", function () {
let selectedOption = $(this).find("option:selected");
let itemPrice = selectedOption.data("price");
let priceInput = $('#createPOForm input[name="unit_price"]');
if (itemPrice !== undefined) priceInput.val(itemPrice);
else priceInput.val("");
});
$(document).on("change", "#poSupplierSelect", function () {
let supplier_id = $(this).val();
let itemSelect = $("#poItemSelect");
if (!supplier_id) {
itemSelect.html('<option value="">Select a Supplier first...</option>');
return;
}
itemSelect.html('<option value="">Loading items...</option>');
$.ajax({
url: "includes/api/api.php?action=get_supplier_items&supplier_id=" + supplier_id,
type: "GET",
dataType: "json",
success: function (res) {
if (res.status === "success") {
if (res.data.length === 0) {
itemSelect.html('<option value="">No items registered for this supplier</option>');
} else {
let options = '<option value="">Select an Item...</option>';
res.data.forEach(function (item) {
options += `<option value="${item.inventory_id}" data-price="${item.unit_price}">${item.name} (Current Stock: ${item.quantity})</option>`;
});
itemSelect.html(options);
}
}
},
});
});
$(document).on("submit", "#createPOForm", function (e) {
e.preventDefault();
$.ajax({
url: "includes/api/api.php?action=create_po",
type: "POST",
data: $(this).serialize(),
dataType: "json",
success: function (res) {
if (res.status === "success") {
$("#createPOModal").modal("hide");
$("#createPOForm")[0].reset();
showAlert(res.message, "success");
if (window.currentTab === "po") loadPOs();
} else {
showAlert(res.message, "danger");
}
},
});
});
$(document).on("change", ".status-dropdown", function () {
let selectElement = $(this);
let po_id = selectElement.data("id");
let currentStatus = selectElement.data("current");
let newStatus = selectElement.val();
if (newStatus === "Received") {
if (confirm("Are you sure you want to mark this as RECEIVED? This will officially add stock to your inventory and generate a Finance invoice. This cannot be undone.")) {
selectElement.prop("disabled", true);
$.ajax({
url: "includes/api/api.php?action=receive_po",
type: "POST",
dataType: "json",
data: { po_id: po_id },
success: function (res) {
showAlert(res.message, res.status === "success" ? "success" : "danger");
if (window.currentTab === "po") loadPOs();
}
});
} else selectElement.val(currentStatus);
} else if (newStatus === "Cancelled") {
if (confirm("Are you sure you want to CANCEL this Purchase Order?")) {
$.ajax({
url: "includes/api/api.php?action=cancel_po",
type: "POST",
dataType: "json",
data: { po_id: po_id },
success: function (res) {
showAlert(res.message, res.status === "success" ? "success" : "danger");
if (window.currentTab === "po") loadPOs();
}
});
} else selectElement.val(currentStatus);
} else {
$.ajax({
url: "includes/api/api.php?action=update_po_status",
type: "POST",
dataType: "json",
data: { po_id: po_id, status: newStatus },
success: function (res) {
showAlert(res.message, res.status === "success" ? "success" : "danger");
if (res.status === "success") {
if (window.currentTab === "po") loadPOs();
} else {
selectElement.val(currentStatus);
}
},
});
}
});
$(document).on("click", ".view-po-details-btn", function () {
let poId = $(this).data("id");
$("#detailsPoId").text("#PO-" + poId);
$("#poDetailsTableBody").html('<tr><td colspan="4" class="text-center text-muted py-3"><span class="spinner-border spinner-border-sm"></span> Loading items...</td></tr>');
$("#vendorHistoryModal").modal("hide");
$("#poDetailsModal").modal("show");
$.ajax({
url: "includes/api/api.php?action=get_po_details&po_id=" + poId,
type: "GET",
dataType: "json",
success: function (res) {
if (res.status === "success") {
let rows = "";
if (res.data.length === 0) {
rows = `<tr><td colspan="4" class="text-center py-4 text-muted">No items found for this PO.</td></tr>`;
} else {
res.data.forEach(function (item) {
let subtotal = item.quantity * item.unit_price;
rows += `<tr>
<td class="ps-4 fw-bold text-dark">${item.name}</td>
<td>${item.quantity}</td>
<td>₱${parseFloat(item.unit_price).toFixed(2)}</td>
<td class="pe-4 text-end fw-medium text-success">₱${subtotal.toFixed(2)}</td>
</tr>`;
});
}
$("#poDetailsTableBody").html(rows);
} else {
$("#poDetailsTableBody").html(`<tr><td colspan="4" class="text-center text-danger py-3">Error loading details.</td></tr>`);
}
},
error: function() {
$("#poDetailsTableBody").html(`<tr><td colspan="4" class="text-center text-danger py-3">Network error. Check connection.</td></tr>`);
}
});
});
// ==========================================
// 9. ANALYTICS LOGIC
// ==========================================
window.loadAnalytics = function (btn = null) {
let originalText = "";
if (btn) {
originalText = $(btn).html();
$(btn).prop("disabled", true).html('<span class="spinner-border spinner-border-sm"></span> 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;
$("#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");
let predHtml = "";
if (data.predictions && data.predictions.length > 0) {
data.predictions.forEach(item => {
predHtml += `<li class="list-group-item d-flex justify-content-between align-items-center py-3">
<div>
<div class="fw-bold text-dark">${item.name}</div>
<div class="small text-muted">Stock: ${item.quantity} | Burns: ${parseFloat(item.daily_burn).toFixed(1)}/day</div>
</div>
<span class="badge bg-danger rounded-pill px-3 py-2">Out in ~${item.days_left} days</span>
</li>`;
});
} else {
predHtml = `<li class="list-group-item text-center py-4 text-success small fw-bold"><i class="bi bi-shield-check me-2"></i>No imminent stockouts predicted!</li>`;
}
$("#predictiveList").html(predHtml);
$("#prescriptiveLowCount").text(data.low_stock);
if (data.low_stock > 0) {
$("#prescriptiveAutoRestockBtn").prop("disabled", false).removeClass("btn-secondary").addClass("btn-success");
} else {
$("#prescriptiveAutoRestockBtn").prop("disabled", true).removeClass("btn-success").addClass("btn-secondary").html('<i class="bi bi-check-all me-2"></i> Stock Optimal');
}
let valLabels = []; let valData = [];
data.valuation_data.forEach(row => { valLabels.push(row.supplier_name); 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 }] },
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") } } }
});
}
let ctxSpend = document.getElementById("spendChart");
if (ctxSpend) {
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") } } }
});
}
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");
}
});
};
$(document).on("click", "#prescriptiveAutoRestockBtn", function() {
let btn = $(this);
let originalText = btn.html();
btn.prop("disabled", true).html('<span class="spinner-border spinner-border-sm"></span> 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");
}
}
});
});
// ==========================================
// 10. HELPER EXPORTS & ALERTS
// ==========================================
function showAlert(msg, type) {
$("#alertBox").html(`<div class="alert alert-${type} alert-dismissible fade show border-0 shadow-sm">
${msg}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>`);
}
window.exportExcel = function() {
if (!currentAnalyticsData) { showAlert("Please wait for analytics data to load.", "warning"); return; }
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)], ["Overstock Alerts", parseInt(currentAnalyticsData.overstock)] ];
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`);
};
window.exportPDF = function() {
const { jsPDF } = window.jspdf; 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';
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");
});
};
});