405 lines
15 KiB
PHP
405 lines
15 KiB
PHP
<?php
|
|
require_once __DIR__ . '/../session.php';
|
|
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 align-items-center justify-content-between">
|
|
<div>
|
|
<h1 class="fs-3 mb-1">Product Lots</h1>
|
|
<p class="mb-0">Track lot numbers, expiry dates and stock transactions</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary stats — single card, 4 sections -->
|
|
<div class="row mb-5 g-5">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-body p-0">
|
|
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap">
|
|
|
|
<div class="p-6 flex-grow-1 d-flex justify-content-between" id="stat_total">
|
|
<div class="d-flex flex-column">
|
|
<span class="fs-2 fw-bold mb-3 d-block">—</span>
|
|
<span class="fs-5">Total Lots</span>
|
|
<small class="text-primary">All registered lots</small>
|
|
</div>
|
|
<div>
|
|
<span class="text-primary">
|
|
<i class="ti ti-layers fs-1"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-6 flex-grow-1 d-flex justify-content-between" id="stat_active">
|
|
<div class="d-flex flex-column">
|
|
<span class="fs-2 fw-bold mb-3 d-block">—</span>
|
|
<span class="fs-5">Active Lots</span>
|
|
<small class="text-success">Balance > 0</small>
|
|
</div>
|
|
<div>
|
|
<span class="text-success">
|
|
<i class="ti ti-circle-check fs-1"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-6 flex-grow-1 d-flex justify-content-between" id="stat_expired">
|
|
<div class="d-flex flex-column">
|
|
<span class="fs-2 fw-bold mb-3 d-block">—</span>
|
|
<span class="fs-5">Expired Lots</span>
|
|
<small class="text-danger">Past expiry date</small>
|
|
</div>
|
|
<div>
|
|
<span class="text-danger">
|
|
<i class="ti ti-trash fs-1"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="p-6 flex-grow-1 d-flex justify-content-between" id="stat_near">
|
|
<div class="d-flex flex-column">
|
|
<span class="fs-2 fw-bold mb-3 d-block">—</span>
|
|
<span class="fs-5">Near Expiry</span>
|
|
<small class="text-warning">Within 30 days</small>
|
|
</div>
|
|
<div>
|
|
<span class="text-warning">
|
|
<i class="ti ti-clock fs-1"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lot table -->
|
|
<div class="row g-5 mb-5">
|
|
<div class="col-12">
|
|
<div class="card">
|
|
<div class="card-body p-5">
|
|
|
|
<div class="mb-6 d-flex align-items-center justify-content-between">
|
|
<div>
|
|
<h2 class="fs-4 mb-2">Lot List</h2>
|
|
<p class="mb-0">All lot numbers with expiry and stock balance</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search -->
|
|
<div class="col-lg-12 mb-5">
|
|
<div class="position-relative">
|
|
<input type="search" id="search_lot" class="form-control ps-9"
|
|
placeholder="Search by product, SKU, or lot number…">
|
|
<span class="position-absolute top-25 ms-4">
|
|
<i class="ti ti-search text-muted"></i>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Table -->
|
|
<div class="table-responsive">
|
|
<table class="table mb-0 text-nowrap table-hover table-centered" id="tbl_lot">
|
|
<thead class="table-primary border-light">
|
|
<tr>
|
|
<th>Product</th>
|
|
<th>SKU</th>
|
|
<th>Lot Number</th>
|
|
<th>Expiry Date</th>
|
|
<th>Days Remaining</th>
|
|
<th>Balance</th>
|
|
<th>Active</th>
|
|
<th>Status</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody>
|
|
<tfoot></tfoot>
|
|
</table>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Lot Stock Log Modal -->
|
|
<div class="modal fade" id="lotLogModal" tabindex="-1" aria-labelledby="lotLogModalLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title" id="lotLogModalLabel">Stock Log</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body p-0">
|
|
<table class="table mb-0 table-hover table-centered">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Type</th>
|
|
<th>Warehouse</th>
|
|
<th id="th_location">Location</th>
|
|
<th>In</th>
|
|
<th>Out</th>
|
|
<th>Serial</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="lot_log_body">
|
|
<tr><td colspan="7" class="text-center py-4 text-muted">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="modal-footer py-2">
|
|
<div id="lot_log_pagination" class="w-100"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</main>
|
|
|
|
<?php require "../include_ending.php";?>
|
|
|
|
<script>
|
|
var search_term_lot = '';
|
|
var lot_log_data = [];
|
|
var lot_log_page = 1;
|
|
var lot_log_limit = 10;
|
|
var advanced = false;
|
|
var label_bin = 'Location';
|
|
var label_zone = 'Zone';
|
|
var label_aisle = 'Aisle';
|
|
|
|
var type_badge = {
|
|
'in': '<span class="badge bg-success">Stock In</span>',
|
|
'out': '<span class="badge bg-danger">Stock Out</span>',
|
|
'transfer': '<span class="badge bg-primary">Transfer</span>',
|
|
};
|
|
|
|
var status_badge = {
|
|
'ok': '<span class="badge bg-success">OK</span>',
|
|
'near': '<span class="badge bg-warning text-dark">Near Expiry</span>',
|
|
'critical': '<span class="badge bg-danger">Critical</span>',
|
|
'expired': '<span class="badge bg-secondary">Expired</span>',
|
|
};
|
|
|
|
function apply_location_labels() {
|
|
$('#th_location').text(advanced
|
|
? label_zone + ' / ' + label_aisle + ' / ' + label_bin
|
|
: label_bin
|
|
);
|
|
}
|
|
|
|
function format_location(row) {
|
|
if (advanced) {
|
|
return [row.zone, row.aisle, row.bin].filter(Boolean).join('-') || '<span class="text-muted">—</span>';
|
|
}
|
|
return row.bin || '<span class="text-muted">—</span>';
|
|
}
|
|
|
|
function load_company_setting() {
|
|
return ajax_request({
|
|
url: server_url + 'setting/api/engine/company_setting.php',
|
|
autoPrepare: true,
|
|
checkRequired: 0,
|
|
action: 'read',
|
|
onSuccess: function(res) {
|
|
var s = res.output || {};
|
|
advanced = s.advanced_location == 1;
|
|
label_bin = s.location_label_bin || (advanced ? 'Bin' : 'Location');
|
|
label_zone = s.location_label_zone || 'Zone';
|
|
label_aisle = s.location_label_aisle || 'Aisle';
|
|
apply_location_labels();
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// ── Fetch ─────────────────────────────────────────────────────────────
|
|
function retrieve_product_lot() {
|
|
|
|
return ajax_request({
|
|
url: "<?php echo $server_url?>reports/api/engine_report/product_lot.php",
|
|
autoPrepare: true,
|
|
checkRequired: 0,
|
|
action: 'read',
|
|
debugMode: 0,
|
|
onSuccess: function(res) {
|
|
|
|
// Stats
|
|
$('#stat_total span.fs-2').text(format_number(res.total));
|
|
$('#stat_active span.fs-2').text(format_number(res.active));
|
|
$('#stat_expired span.fs-2').text(format_number(res.expired));
|
|
$('#stat_near span.fs-2').text(format_number(res.near));
|
|
|
|
// Store
|
|
alasql('CREATE TABLE IF NOT EXISTS lot_all');
|
|
alasql.tables.lot_all.data = res.output || [];
|
|
|
|
change_page_lot(1);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// ── Render ────────────────────────────────────────────────────────────
|
|
function change_page_lot(page_num) {
|
|
|
|
var offset = (page_num - 1) * prop_limit;
|
|
var kw = search_term_lot.toLowerCase();
|
|
|
|
var filtered = kw ? alasql(
|
|
`SELECT * FROM lot_all
|
|
WHERE LOWER(product_name) LIKE '%${kw}%'
|
|
OR LOWER(product_sku) LIKE '%${kw}%'
|
|
OR LOWER(lot_number) LIKE '%${kw}%'`
|
|
) : alasql('SELECT * FROM lot_all');
|
|
|
|
$('table#tbl_lot tfoot').html(generate_pagination('tbl_lot', filtered.length));
|
|
|
|
if (filtered.length === 0) {
|
|
$('table#tbl_lot > tbody').html(
|
|
'<tr><td colspan="8" class="text-center py-5 text-muted">No lots match your search.</td></tr>'
|
|
);
|
|
return;
|
|
}
|
|
|
|
var body = '';
|
|
$.each(filtered.slice(offset, offset + prop_limit), function(i, item) {
|
|
var days = parseInt(item.days_remaining);
|
|
var days_lbl = days < 0
|
|
? `<span class="text-danger">${Math.abs(days)} overdue</span>`
|
|
: `<span class="${days <= 7 ? 'text-danger' : days <= 30 ? 'text-warning' : ''}">${days} days</span>`;
|
|
|
|
body += `<tr>
|
|
<td class="py-3">${item.product_name}</td>
|
|
<td class="py-3">${item.product_sku}</td>
|
|
<td class="py-3">${item.lot_number}</td>
|
|
<td class="py-3">${item.expiry_date || '<span class="text-muted">—</span>'}</td>
|
|
<td class="py-3">${days_lbl}</td>
|
|
<td class="py-3">${format_number(item.balance)}</td>
|
|
<td class="py-3">${item.is_active == 1
|
|
? '<span class="badge bg-success">Active</span>'
|
|
: '<span class="badge bg-secondary">Inactive</span>'}</td>
|
|
<td class="py-3">${status_badge[item.status] || ''}</td>
|
|
<td class="py-3">
|
|
<a href="javascript:;" onclick="view_lot_log('${item.product_sku}','${item.lot_number}')">
|
|
<i class="ti ti-history fs-5"></i>
|
|
</a>
|
|
</td>
|
|
</tr>`;
|
|
});
|
|
$('table#tbl_lot > tbody').html(body);
|
|
}
|
|
|
|
|
|
// ── Lot Stock Log modal ───────────────────────────────────────────────
|
|
function render_lot_log(page) {
|
|
|
|
lot_log_page = page;
|
|
var offset = (page - 1) * lot_log_limit;
|
|
var total = lot_log_data.length;
|
|
var page_data = lot_log_data.slice(offset, offset + lot_log_limit);
|
|
var total_pages = Math.ceil(total / lot_log_limit);
|
|
|
|
if (total === 0) {
|
|
$('#lot_log_body').html('<tr><td colspan="7" class="text-center py-4 text-muted">No transactions found.</td></tr>');
|
|
$('#lot_log_pagination').html('');
|
|
return;
|
|
}
|
|
|
|
var body = '';
|
|
$.each(page_data, function(i, row) {
|
|
body += `<tr>
|
|
<td class="py-3">${format_date(row.date)}</td>
|
|
<td class="py-3">${type_badge[row.type] || row.type}</td>
|
|
<td class="py-3">${row.warehouse_name}</td>
|
|
<td class="py-3">${format_location(row)}</td>
|
|
<td class="py-3 text-success">${parseFloat(row.stock_in) > 0 ? '+' + format_number(row.stock_in) : '<span class="text-muted">—</span>'}</td>
|
|
<td class="py-3 text-danger">${parseFloat(row.stock_out) > 0 ? '-' + format_number(row.stock_out) : '<span class="text-muted">—</span>'}</td>
|
|
<td class="py-3">${row.serial_number || '<span class="text-muted">—</span>'}</td>
|
|
</tr>`;
|
|
});
|
|
$('#lot_log_body').html(body);
|
|
|
|
if (total_pages <= 1) { $('#lot_log_pagination').html(''); return; }
|
|
|
|
var pag = '<nav><ul class="pagination pagination-sm mb-0 justify-content-end">';
|
|
pag += `<li class="page-item ${page===1?'disabled':''}"><a class="page-link" href="javascript:;" onclick="render_lot_log(${page-1})">«</a></li>`;
|
|
for (var p = 1; p <= total_pages; p++) {
|
|
pag += `<li class="page-item ${p===page?'active':''}"><a class="page-link" href="javascript:;" onclick="render_lot_log(${p})">${p}</a></li>`;
|
|
}
|
|
pag += `<li class="page-item ${page===total_pages?'disabled':''}"><a class="page-link" href="javascript:;" onclick="render_lot_log(${page+1})">»</a></li>`;
|
|
pag += '</ul></nav>';
|
|
$('#lot_log_pagination').html(pag);
|
|
}
|
|
|
|
function view_lot_log(product_sku, lot_number) {
|
|
|
|
lot_log_data = [];
|
|
lot_log_page = 1;
|
|
$('#lotLogModalLabel').text('Stock Log — Lot: ' + lot_number);
|
|
$('#lot_log_body').html('<tr><td colspan="7" class="text-center py-4 text-muted">Loading...</td></tr>');
|
|
$('#lot_log_pagination').html('');
|
|
|
|
var modal = new bootstrap.Modal(document.getElementById('lotLogModal'));
|
|
modal.show();
|
|
|
|
ajax_request({
|
|
url: "<?php echo $server_url?>reports/api/engine_report/lot_stock_log.php",
|
|
autoPrepare: true,
|
|
checkRequired: 0,
|
|
action: 'read',
|
|
data: { "product_sku": product_sku, "lot_number": lot_number },
|
|
onSuccess: function(res) {
|
|
lot_log_data = res.output || [];
|
|
render_lot_log(1);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
// ── Search ────────────────────────────────────────────────────────────
|
|
function debounce(fn, ms) {
|
|
var t;
|
|
return function() { clearTimeout(t); t = setTimeout(fn, ms); };
|
|
}
|
|
|
|
$('#search_lot').on('input', debounce(function() {
|
|
search_term_lot = $(this).val().trim();
|
|
change_page_lot(1);
|
|
}, 200));
|
|
|
|
|
|
// ── Boot ─────────────────────────────────────────────────────────────
|
|
$(async function() {
|
|
try {
|
|
await load_company_setting();
|
|
await retrieve_product_lot();
|
|
} catch(e) {
|
|
console.log(e);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
|
|
</html>
|