Files
wms-app/app/dashboard/low_stock_products.php
T
2026-05-06 09:37:28 +07:00

317 lines
12 KiB
PHP

<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php';?>
<?php require '../include_sidebar.php';?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="mb-6 d-flex justify-content-between align-items-center">
<div>
<h1 class="fs-3 mb-1">Low Stock Products</h1>
<p class="mb-0 text-muted">Products whose current balance has fallen below minimum stock level</p>
</div>
</div>
</div>
</div>
<!-- Summary cards -->
<div class="row mb-5 g-5">
<div class="col-lg-4 col-12">
<div class="card border border-warning border-opacity-25">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center">
<span>Total Low Stock</span>
<span><i class="ti ti-alert-triangle fs-3 text-warning"></i></span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-3 fw-bold mb-0 d-block text-warning" id="stat_total">—</span>
<small class="text-muted">Products need attention</small>
</div>
<div class="card-loader-overlay" id="loader_total">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
</div>
<div class="col-lg-4 col-12">
<div class="card border border-danger border-opacity-25">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center">
<span>Critical Level</span>
<span><i class="ti ti-activity-heartbeat fs-3 text-danger"></i></span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-3 fw-bold mb-0 d-block text-danger" id="stat_critical">—</span>
<small class="text-muted">Immediate restock needed</small>
</div>
<div class="card-loader-overlay" id="loader_critical">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
</div>
<div class="col-lg-4 col-12">
<div class="card border border-secondary border-opacity-25">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center">
<span>Warning Level</span>
<span><i class="ti ti-box-seam fs-3 text-secondary"></i></span>
</div>
<div class="d-flex flex-column mt-3">
<span class="fs-3 fw-bold mb-0 d-block text-secondary" id="stat_warning">—</span>
<small class="text-muted">Restock soon</small>
</div>
<div class="card-loader-overlay" id="loader_warning">
<div class="spinner-border spinner-border-sm text-primary"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Filter bar -->
<div class="row mb-4 g-3 align-items-end">
<div class="col-md-4 col-12">
<label class="form-label mb-1">Filter by Status</label>
<select id="filter_status" class="form-select">
<option value="">All</option>
<option value="critical">Critical</option>
<option value="warning">Warning</option>
</select>
</div>
<div class="col-md-4 col-12">
<label class="form-label mb-1">Filter by Warehouse</label>
<select id="filter_warehouse" class="form-select">
<option value="">All Warehouses</option>
</select>
</div>
<div class="col-md-4 col-12">
<label class="form-label mb-1">Search Product</label>
<input type="text" id="filter_search" class="form-control" placeholder="Name or SKU…">
</div>
</div>
<!-- Table -->
<div class="row g-5">
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fs-5 mb-0">Low Stock Items</h2>
<small class="text-muted" id="result_count"></small>
</div>
<div class="table-responsive">
<table class="table table-hover mb-0 table-centered" id="low_stock">
<thead class="table-light">
<tr>
<th>Product</th>
<th>SKU</th>
<th>Warehouse</th>
<th>Balance</th>
<th>Min Stock</th>
<th>Reorder Point</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr>
<td colspan="8" class="text-center py-5 text-muted">
<div class="spinner-border spinner-border-sm me-2"></div>Loading…
</td>
</tr>
</tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require "../include_ending.php";?>
<script>
// ── In-memory dataset ────────────────────────────────────────────────
var all_items = [];
// ── Fetch low-stock data from the report engine ──────────────────────
function retrieve_low_stock() {
return ajax_request({
url: "<?php echo $server_url?>dashboard/api/engine_report/low_stock.php",
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var out = res.output || {};
all_items = out.items || [];
// Summary cards
$('#stat_total').text(out.total_low ?? 0);
$('#stat_critical').text(out.total_critical ?? 0);
$('#stat_warning').text(out.total_warning ?? 0);
$('#loader_total, #loader_critical, #loader_warning').hide();
// Warehouse filter options (derived from data)
var warehouses = {};
$.each(all_items, function(i, item) {
warehouses[item.warehouse_id] = item.warehouse_name;
});
var wh_options = '<option value="">All Warehouses</option>';
$.each(warehouses, function(id, name) {
wh_options += `<option value="${id}">${name}</option>`;
});
$('#filter_warehouse').html(wh_options);
// Register dataset in alasql for paginated rendering
alasql('CREATE TABLE IF NOT EXISTS low_stock');
alasql.tables.low_stock.data = all_items;
change_page_low_stock(1);
}
});
}
// ── Apply filters, then paginate ─────────────────────────────────────
function change_page_low_stock(page_num) {
var offset = (page_num - 1) * prop_limit;
var status = $('#filter_status').val().trim().toLowerCase();
var warehouse = $('#filter_warehouse').val();
var search = $('#filter_search').val().trim().toLowerCase();
// Filter first — we need the full filtered set for correct pagination
var filtered = all_items.filter(function(item) {
if (status && item.status !== status) return false;
if (warehouse && String(item.warehouse_id) !== String(warehouse)) return false;
if (search) {
var hay = (item.product_name + ' ' + item.product_sku).toLowerCase();
if (hay.indexOf(search) === -1) return false;
}
return true;
});
// Pagination footer reflects filtered count
var page = generate_pagination('low_stock', filtered.length);
$(`table#low_stock tfoot`).html(page);
$('#result_count').text(filtered.length + ' item(s)');
if (filtered.length === 0) {
$('table#low_stock > tbody').html(
`<tr><td colspan="8" class="text-center py-5 text-muted">
<i class="ti ti-mood-smile fs-3 d-block mb-2"></i>
No low-stock products found.
</td></tr>`
);
return;
}
// Render only the current page slice
var page_data = filtered.slice(offset, offset + prop_limit);
var body = '';
$.each(page_data, function(i, item) {
var pct = item.reorder_point > 0 ? Math.round((item.balance / item.reorder_point) * 100) : 0;
var bar_cls = item.status === 'critical' ? 'bg-danger' : 'bg-warning';
var badge = item.status === 'critical' ?
'<span class="badge bg-danger rounded-pill">Critical</span>' :
'<span class="badge bg-secondary bg-opacity-10 text-dark rounded-pill">Warning</span>';
var balance_cls = item.status === 'critical' ? 'text-danger fw-semibold' : 'text-warning fw-semibold';
body += `<tr>
<td class="py-3">
<div class="d-flex flex-column">
<span>${item.product_name}</span>
<div class="progress mt-1" style="height:4px;width:80px;" title="${pct}% of reorder point">
<div class="progress-bar ${bar_cls}" style="width:${Math.min(pct,100)}%"></div>
</div>
</div>
</td>
<td class="py-3 text-secondary">${item.product_sku}</td>
<td class="py-3">${item.warehouse_name}</td>
<td class="py-3 ${balance_cls}">
${item.balance} <span class="text-muted fw-normal small">${item.uom || 'pcs'}</span>
${item.cost_price > 0
? `<div class="text-muted small fw-normal">Value: ${format_number(item.balance * item.cost_price, 2)}</div>`
: ''}
</td>
<td class="py-3">${item.min_stock}</td>
<td class="py-3">${item.reorder_point}</td>
<td class="py-3">${badge}</td>
<td class="py-3">
<a href="<?php echo $server_url?>ics/manage_stock_in.php?sku=${encodeURIComponent(item.product_sku)}&name=${encodeURIComponent(item.product_name)}&qty=${Math.max(0, item.reorder_point - item.balance)}&wh=${item.warehouse_id}"
class="btn btn-sm btn-outline-primary">
<i class="ti ti-plus me-1"></i>Restock
</a>
</td>
</tr>`;
});
$('table#low_stock > tbody').html(body);
}
// ── Filter listeners ─────────────────────────────────────────────────
function debounce(fn, ms) {
var t;
return function() {
var args = arguments,
ctx = this;
clearTimeout(t);
t = setTimeout(function() {
fn.apply(ctx, args);
}, ms);
};
}
$('#filter_status, #filter_warehouse').on('change', function() {
change_page_low_stock(1); // reset to page 1 on filter change
});
$('#filter_search').on('input', debounce(function() {
change_page_low_stock(1);
}, 200));
// ── Boot ─────────────────────────────────────────────────────────────
$(async function() {
try {
await retrieve_low_stock();
} catch (e) {
console.error(e);
$('table#low_stock > tbody').html(
'<tr><td colspan="8" class="text-center py-5 text-danger">Failed to load data.</td></tr>'
);
}
});
</script>
</body>
</html>