Reports
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
|
||||
$warehouse_id = (int)($data['warehouse_id'] ?? 0);
|
||||
|
||||
$items = $report->getExpiredStock($warehouse_id);
|
||||
|
||||
$expired = 0;
|
||||
$critical = 0;
|
||||
$warning = 0;
|
||||
$caution = 0;
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item['status'] === 'expired') { $expired++; }
|
||||
if ($item['status'] === 'critical') { $critical++; }
|
||||
if ($item['status'] === 'warning') { $warning++; }
|
||||
if ($item['status'] === 'caution') { $caution++; }
|
||||
}
|
||||
|
||||
$answer['output'] = [
|
||||
'items' => $items,
|
||||
'expired' => $expired,
|
||||
'critical' => $critical,
|
||||
'warning' => $warning,
|
||||
'caution' => $caution,
|
||||
];
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
$answer['output'] = $report->getLotStockLog(
|
||||
$data['product_sku'] ?? '',
|
||||
$data['lot_number'] ?? ''
|
||||
);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
$result = $report->getProductLots();
|
||||
|
||||
$answer['output'] = $result['rows'];
|
||||
$answer['total'] = $result['total'];
|
||||
$answer['active'] = $result['active'];
|
||||
$answer['expired'] = $result['expired'];
|
||||
$answer['near'] = $result['near'];
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
$rack_id = (int)($data['rack_id'] ?? 0);
|
||||
|
||||
if (!$rack_id) {
|
||||
$answer['output'] = [];
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
$answer['output'] = $report->getRackLog($rack_id);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
$total = $report->getTotalCapacity();
|
||||
$occupied = $report->getSpaceUsed();
|
||||
|
||||
$answer['output'] = $report->getRackOccupancy();
|
||||
$answer['total'] = $total;
|
||||
$answer['occupied'] = $occupied;
|
||||
$answer['empty'] = $total - $occupied;
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
$data = json_decode($_POST['json'] ?? '{}', true) ?: [];
|
||||
$warehouse_id = (int)($data['warehouse_id'] ?? 0);
|
||||
$from_month = $data['from_month'] ?? '';
|
||||
$to_month = $data['to_month'] ?? '';
|
||||
|
||||
if (!$warehouse_id || !$from_month || !$to_month) {
|
||||
$answer['message'] = 'warehouse_id, from_month, and to_month are required.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
if ($from_month > $to_month) [$from_month, $to_month] = [$to_month, $from_month];
|
||||
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
|
||||
$answer['output'] = $report->getStockMovementSummary($warehouse_id, $from_month, $to_month);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../../../assets/utils/db_auth.php';
|
||||
require '../../../assets/utils/classes/ReportManager.php';
|
||||
|
||||
$data = json_decode($_POST['json'] ?? '{}', true) ?: [];
|
||||
$warehouse_id = (int)($data['warehouse_id'] ?? 0);
|
||||
$product_sku = trim($data['product_sku'] ?? '');
|
||||
$from_month = $data['from_month'] ?? '';
|
||||
$to_month = $data['to_month'] ?? '';
|
||||
|
||||
if (!$warehouse_id || !$product_sku || !$from_month || !$to_month) {
|
||||
$answer['message'] = 'warehouse_id, product_sku, from_month, and to_month are required.';
|
||||
exit(json_encode($answer));
|
||||
}
|
||||
|
||||
if ($from_month > $to_month) [$from_month, $to_month] = [$to_month, $from_month];
|
||||
|
||||
$report = new ReportManager($pdo2, $company_id);
|
||||
|
||||
$answer['output'] = $report->getStockMovementBySku($warehouse_id, $product_sku, $from_month, $to_month);
|
||||
$answer['success'] = 1;
|
||||
exit(json_encode($answer));
|
||||
?>
|
||||
@@ -0,0 +1,365 @@
|
||||
<?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="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-6 gap-3">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1">Expired / Near Expiry Products</h1>
|
||||
<p class="mb-0">Monitor and manage products approaching or past expiry dates</p>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<label class="mb-0 text-nowrap fw-semibold">Warehouse</label>
|
||||
<select id="warehouse" class="form-select" style="min-width:200px;" required>
|
||||
<option value="">Select warehouse...</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<div id="overview_content">
|
||||
|
||||
<!-- Summary stats -->
|
||||
<div class="row mb-5 g-5">
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card bg-danger bg-opacity-10 border-danger border-opacity-25">
|
||||
<div class="card-body p-5">
|
||||
<span class="icon-shape icon-md bg-danger text-white rounded-2">
|
||||
<i class="ti ti-trash fs-4"></i>
|
||||
</span>
|
||||
<div class="d-flex flex-column mt-3">
|
||||
<span class="fs-2 fw-bold mb-0 d-block" id="stat_expired">—</span>
|
||||
<span class="fs-5">Expired Products</span>
|
||||
<small class="text-muted">Requires immediate action</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card bg-danger bg-opacity-10 border-danger border-opacity-25">
|
||||
<div class="card-body p-5">
|
||||
<span class="icon-shape icon-md bg-danger text-white rounded-2">
|
||||
<i class="ti ti-alert-triangle fs-4"></i>
|
||||
</span>
|
||||
<div class="d-flex flex-column mt-3">
|
||||
<span class="fs-2 fw-bold mb-0 d-block" id="stat_critical">—</span>
|
||||
<span class="fs-5">Critical (≤7 days)</span>
|
||||
<small class="text-muted">Expiring within a week</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card bg-warning bg-opacity-10 border-warning border-opacity-25">
|
||||
<div class="card-body p-5">
|
||||
<span class="icon-shape icon-md bg-warning text-white rounded-2">
|
||||
<i class="ti ti-clock fs-4"></i>
|
||||
</span>
|
||||
<div class="d-flex flex-column mt-3">
|
||||
<span class="fs-2 fw-bold mb-0 d-block" id="stat_warning">—</span>
|
||||
<span class="fs-5">Warning (8–14 days)</span>
|
||||
<small class="text-muted">Expiring in 2 weeks</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card bg-warning bg-opacity-10 border-warning border-opacity-25">
|
||||
<div class="card-body p-5">
|
||||
<span class="icon-shape icon-md bg-warning text-white rounded-2">
|
||||
<i class="ti ti-calendar-clock fs-4"></i>
|
||||
</span>
|
||||
<div class="d-flex flex-column mt-3">
|
||||
<span class="fs-2 fw-bold mb-0 d-block" id="stat_caution">—</span>
|
||||
<span class="fs-5">Caution (15–30 days)</span>
|
||||
<small class="text-muted">Monitor closely</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter -->
|
||||
<div class="row g-5 mb-5">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fs-4 mb-4">Filter Products</h2>
|
||||
<div class="row g-5">
|
||||
<div class="col-lg-7">
|
||||
<div class="position-relative">
|
||||
<input type="search" id="search_expired" class="form-control ps-9"
|
||||
placeholder="Search by product name, SKU, or lot number…">
|
||||
<span class="position-absolute top-25 ms-4">
|
||||
<i class="ti ti-search text-muted"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-5">
|
||||
<select class="form-select" id="filter_status">
|
||||
<option value="">All Status</option>
|
||||
<option value="expired">Expired</option>
|
||||
<option value="critical">Critical</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="caution">Caution</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Expired table -->
|
||||
<div class="row g-5 mb-5">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fs-4 mb-2">Expired Products</h2>
|
||||
<p class="mb-4">Products that have passed their expiry date and need to be removed</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0 text-nowrap table-hover table-centered" id="tbl_expired">
|
||||
<thead class="table-danger border-light">
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>SKU</th>
|
||||
<th>Lot Number</th>
|
||||
<th>Location</th>
|
||||
<th>Expiry Date</th>
|
||||
<th>Days Overdue</th>
|
||||
<th>Quantity</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Near expiry table -->
|
||||
<div class="row g-5 mb-5">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-5">
|
||||
<h2 class="fs-4 mb-2">Near Expiry Products</h2>
|
||||
<p class="mb-4">Products approaching their expiry date that may need attention</p>
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0 text-nowrap table-hover table-centered" id="tbl_near_expiry">
|
||||
<thead class="table-warning border-light">
|
||||
<tr>
|
||||
<th>Product</th>
|
||||
<th>SKU</th>
|
||||
<th>Lot Number</th>
|
||||
<th>Location</th>
|
||||
<th>Expiry Date</th>
|
||||
<th>Days Remaining</th>
|
||||
<th>Quantity</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div><!-- /overview_content -->
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php require "../include_ending.php";?>
|
||||
|
||||
<script>
|
||||
var all_items = [];
|
||||
var search_term = '';
|
||||
var filter_status = '';
|
||||
|
||||
|
||||
// ── Fetch + populate ─────────────────────────────────────────────────
|
||||
function retrieve_expired_stock() {
|
||||
|
||||
var warehouse_id = $('#warehouse').val();
|
||||
if (!warehouse_id) return;
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>reports/api/engine_report/expired_stock.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
data: {
|
||||
warehouse_id: warehouse_id
|
||||
},
|
||||
action: 'read',
|
||||
debugMode: 0,
|
||||
onSuccess: function(res) {
|
||||
|
||||
var data = res.output;
|
||||
|
||||
$('#stat_expired').text(data.expired);
|
||||
$('#stat_critical').text(data.critical);
|
||||
$('#stat_warning').text(data.warning);
|
||||
$('#stat_caution').text(data.caution);
|
||||
|
||||
all_items = data.items || [];
|
||||
render_tables();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Render both tables from filtered data ────────────────────────────
|
||||
function render_tables() {
|
||||
|
||||
var kw = search_term.toLowerCase();
|
||||
var fs = filter_status;
|
||||
|
||||
var filtered = all_items.filter(function(item) {
|
||||
var match_search = !kw ||
|
||||
item.product_name.toLowerCase().includes(kw) ||
|
||||
item.product_sku.toLowerCase().includes(kw) ||
|
||||
(item.lot_number || '').toLowerCase().includes(kw);
|
||||
var match_status = !fs || item.status === fs;
|
||||
return match_search && match_status;
|
||||
});
|
||||
|
||||
render_expired(filtered.filter(function(i) { return i.status === 'expired'; }));
|
||||
render_near_expiry(filtered.filter(function(i) { return i.status !== 'expired'; }));
|
||||
}
|
||||
|
||||
|
||||
function status_badge(status) {
|
||||
var map = {
|
||||
expired: '<span class="badge bg-danger">Expired</span>',
|
||||
critical: '<span class="badge bg-danger">Critical (≤7 days)</span>',
|
||||
warning: '<span class="badge bg-warning text-dark">Warning (8–14 days)</span>',
|
||||
caution: '<span class="badge bg-warning text-dark">Caution (15–30 days)</span>',
|
||||
};
|
||||
return map[status] || '';
|
||||
}
|
||||
|
||||
|
||||
function render_expired(items) {
|
||||
if (items.length === 0) {
|
||||
$('table#tbl_expired tbody').html(
|
||||
'<tr><td colspan="8" class="text-center py-5 text-muted">No expired products.</td></tr>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
var body = '';
|
||||
$.each(items, function(i, item) {
|
||||
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 || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${item.zone}-${item.aisle}-${item.rack}</td>
|
||||
<td class="py-3">${item.expiry_date}</td>
|
||||
<td class="py-3 text-danger fw-semibold">${Math.abs(item.days_remaining)} days</td>
|
||||
<td class="py-3">${item.quantity}</td>
|
||||
<td class="py-3">${status_badge(item.status)}</td>
|
||||
</tr>`;
|
||||
});
|
||||
$('table#tbl_expired tbody').html(body);
|
||||
}
|
||||
|
||||
|
||||
function render_near_expiry(items) {
|
||||
if (items.length === 0) {
|
||||
$('table#tbl_near_expiry tbody').html(
|
||||
'<tr><td colspan="8" class="text-center py-5 text-muted">No near-expiry products.</td></tr>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
var body = '';
|
||||
$.each(items, function(i, item) {
|
||||
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 || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${item.zone}-${item.aisle}-${item.rack}</td>
|
||||
<td class="py-3">${item.expiry_date}</td>
|
||||
<td class="py-3 text-warning fw-semibold">${item.days_remaining} days</td>
|
||||
<td class="py-3">${item.quantity}</td>
|
||||
<td class="py-3">${status_badge(item.status)}</td>
|
||||
</tr>`;
|
||||
});
|
||||
$('table#tbl_near_expiry tbody').html(body);
|
||||
}
|
||||
|
||||
|
||||
// ── Search + filter ──────────────────────────────────────────────────
|
||||
function debounce(fn, ms) {
|
||||
var t;
|
||||
return function() { clearTimeout(t); t = setTimeout(fn, ms); };
|
||||
}
|
||||
|
||||
$('#search_expired').on('input', debounce(function() {
|
||||
search_term = $(this).val().trim();
|
||||
render_tables();
|
||||
}, 200));
|
||||
|
||||
$('#filter_status').on('change', function() {
|
||||
filter_status = $(this).val();
|
||||
render_tables();
|
||||
});
|
||||
|
||||
|
||||
// ── Load warehouse dropdown ───────────────────────────────────────────
|
||||
function retrieve_warehouse() {
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/retrieve_warehouse.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var option = '';
|
||||
$.each(res.output, function(key, item) {
|
||||
option += `<option value='${item.id}'>${item.warehouse_name}</option>`;
|
||||
});
|
||||
$('#warehouse').html(option);
|
||||
|
||||
// Auto-select first warehouse on page load
|
||||
if (res.output && res.output.length > 0) {
|
||||
$('#warehouse').val(res.output[0].id);
|
||||
retrieve_expired_stock();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Boot ─────────────────────────────────────────────────────────────
|
||||
$(async function() {
|
||||
try {
|
||||
await retrieve_warehouse();
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
|
||||
$('#warehouse').on('change', function() {
|
||||
retrieve_expired_stock();
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,525 @@
|
||||
<?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="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5 gap-3">
|
||||
<div>
|
||||
<h1 class="fs-3 mb-1">Rack Occupancy</h1>
|
||||
<p class="mb-0">View occupied and available rack positions across all warehouses</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary stats -->
|
||||
<div class="row g-5 mb-5">
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-6">
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<div class="icon-shape icon-lg bg-primary bg-opacity-10 text-primary rounded-2">
|
||||
<i class="ti ti-box-seam fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span>Total Racks</span>
|
||||
<h2 class="fw-bold mb-0" id="stat_total">—</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-6">
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<div class="icon-shape icon-lg bg-danger bg-opacity-10 text-danger rounded-2">
|
||||
<i class="ti ti-lock fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span>Occupied</span>
|
||||
<h2 class="fw-bold mb-0" id="stat_occupied">—</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-6">
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<div class="icon-shape icon-lg bg-success bg-opacity-10 text-success rounded-2">
|
||||
<i class="ti ti-lock-open fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span>Available</span>
|
||||
<h2 class="fw-bold mb-0" id="stat_empty">—</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-6">
|
||||
<div class="d-flex align-items-start gap-3">
|
||||
<div class="icon-shape icon-lg bg-info bg-opacity-10 text-info rounded-2">
|
||||
<i class="ti ti-chart-pie fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span>Occupancy Rate</span>
|
||||
<h2 class="fw-bold mb-0" id="stat_rate">—</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<ul class="nav nav-pills mb-3 bg-light p-2 rounded-2 d-inline-flex" id="pills-tab" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="pills-all-tab" data-bs-toggle="pill"
|
||||
data-bs-target="#pills-all" type="button" role="tab">All</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="pills-occupied-tab" data-bs-toggle="pill"
|
||||
data-bs-target="#pills-occupied" type="button" role="tab">Occupied</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="pills-empty-tab" data-bs-toggle="pill"
|
||||
data-bs-target="#pills-empty" type="button" role="tab">Available</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- All tab -->
|
||||
<div class="row g-5 mb-5 tab-pane fade show active" id="pills-all" role="tabpanel">
|
||||
<div class="col-12">
|
||||
<div class="card overflow-hidden">
|
||||
<div class="card-body p-6">
|
||||
<div class="mb-6 d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h3 class="mb-1 fs-4">All Racks</h3>
|
||||
<p class="mb-0">Complete rack list across all warehouses</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 mb-5">
|
||||
<div class="position-relative">
|
||||
<input type="search" id="search_all" class="form-control ps-9"
|
||||
placeholder="Search by warehouse, zone, aisle, rack, or SKU…">
|
||||
<span class="position-absolute top-25 ms-4">
|
||||
<i class="ti ti-search text-muted"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0 text-nowrap table-hover table-centered" id="tbl_all">
|
||||
<thead class="table-primary border-light">
|
||||
<tr>
|
||||
<th>Warehouse</th>
|
||||
<th>Zone</th>
|
||||
<th>Aisle</th>
|
||||
<th>Rack</th>
|
||||
<th>SKU</th>
|
||||
<th>Product</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
<tfoot></tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Occupied tab -->
|
||||
<div class="row g-5 mb-5 tab-pane fade" id="pills-occupied" role="tabpanel">
|
||||
<div class="col-12">
|
||||
<div class="card overflow-hidden">
|
||||
<div class="card-body p-6">
|
||||
<div class="mb-6 d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h3 class="mb-1 fs-4">Occupied Racks</h3>
|
||||
<p class="mb-0">Racks currently holding stock</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 mb-5">
|
||||
<div class="position-relative">
|
||||
<input type="search" id="search_occupied" class="form-control ps-9"
|
||||
placeholder="Search by warehouse, zone, aisle, rack, or SKU…">
|
||||
<span class="position-absolute top-25 ms-4">
|
||||
<i class="ti ti-search text-muted"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0 text-nowrap table-hover table-centered" id="tbl_occupied">
|
||||
<thead class="table-danger border-light">
|
||||
<tr>
|
||||
<th>Warehouse</th>
|
||||
<th>Zone</th>
|
||||
<th>Aisle</th>
|
||||
<th>Rack</th>
|
||||
<th>SKU</th>
|
||||
<th>Product</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
<tfoot></tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Available tab -->
|
||||
<div class="row g-5 mb-5 tab-pane fade" id="pills-empty" role="tabpanel">
|
||||
<div class="col-12">
|
||||
<div class="card overflow-hidden">
|
||||
<div class="card-body p-6">
|
||||
<div class="mb-6 d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<h3 class="mb-1 fs-4">Available Racks</h3>
|
||||
<p class="mb-0">Empty racks ready to receive stock</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-12 mb-5">
|
||||
<div class="position-relative">
|
||||
<input type="search" id="search_empty" class="form-control ps-9"
|
||||
placeholder="Search by warehouse, zone, aisle, or rack…">
|
||||
<span class="position-absolute top-25 ms-4">
|
||||
<i class="ti ti-search text-muted"></i>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0 text-nowrap table-hover table-centered" id="tbl_empty">
|
||||
<thead class="table-success border-light">
|
||||
<tr>
|
||||
<th>Warehouse</th>
|
||||
<th>Zone</th>
|
||||
<th>Aisle</th>
|
||||
<th>Rack</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
<tfoot></tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Rack Log Modal -->
|
||||
<div class="modal fade" id="rackLogModal" tabindex="-1" aria-labelledby="rackLogModalLabel" 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="rackLogModalLabel">Rack 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 / Time</th>
|
||||
<th>Action</th>
|
||||
<th>SKU</th>
|
||||
<th>User</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="rack_log_body">
|
||||
<tr><td colspan="4" class="text-center py-4 text-muted">Loading...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<?php require "../include_ending.php";?>
|
||||
|
||||
<script>
|
||||
var search_all = '';
|
||||
var search_occupied = '';
|
||||
var search_empty = '';
|
||||
|
||||
|
||||
// ── Fetch ─────────────────────────────────────────────────────────────
|
||||
function retrieve_rack_occupancy() {
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>reports/api/engine_report/rack_occupancy.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
debugMode: 0,
|
||||
onSuccess: function(res) {
|
||||
|
||||
// Stats
|
||||
var total = res.total || 0;
|
||||
var occupied = res.occupied || 0;
|
||||
var empty = res.empty || 0;
|
||||
var rate = total > 0 ? Math.round((occupied / total) * 100) : 0;
|
||||
|
||||
$('#stat_total').text(format_number(total));
|
||||
$('#stat_occupied').text(format_number(occupied));
|
||||
$('#stat_empty').text(format_number(empty));
|
||||
$('#stat_rate').text(rate + '%');
|
||||
|
||||
// Store in alasql
|
||||
alasql('CREATE TABLE IF NOT EXISTS rack_all');
|
||||
alasql.tables.rack_all.data = res.output || [];
|
||||
|
||||
change_page_all(1);
|
||||
change_page_occupied(1);
|
||||
change_page_empty(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Render: All ───────────────────────────────────────────────────────
|
||||
function change_page_all(page_num) {
|
||||
|
||||
var offset = (page_num - 1) * prop_limit;
|
||||
var kw = search_all.toLowerCase();
|
||||
|
||||
var filtered = kw ? alasql(
|
||||
`SELECT * FROM rack_all
|
||||
WHERE LOWER(warehouse_name) LIKE '%${kw}%'
|
||||
OR LOWER(zone) LIKE '%${kw}%'
|
||||
OR LOWER(aisle) LIKE '%${kw}%'
|
||||
OR LOWER(rack) LIKE '%${kw}%'
|
||||
OR LOWER(product_sku) LIKE '%${kw}%'
|
||||
OR LOWER(product_name) LIKE '%${kw}%'`
|
||||
) : alasql('SELECT * FROM rack_all');
|
||||
|
||||
$('table#tbl_all tfoot').html(generate_pagination('tbl_all', filtered.length));
|
||||
|
||||
if (filtered.length === 0) {
|
||||
$('table#tbl_all > tbody').html(
|
||||
'<tr><td colspan="7" class="text-center py-5 text-muted">No racks match your search.</td></tr>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var body = '';
|
||||
$.each(filtered.slice(offset, offset + prop_limit), function(i, item) {
|
||||
var badge = item.status === 'occupied'
|
||||
? '<span class="badge bg-danger">Occupied</span>'
|
||||
: '<span class="badge bg-success">Available</span>';
|
||||
body += `<tr>
|
||||
<td class="py-3">${item.warehouse_name}</td>
|
||||
<td class="py-3">${item.zone}</td>
|
||||
<td class="py-3">${item.aisle}</td>
|
||||
<td class="py-3">${item.rack}</td>
|
||||
<td class="py-3">${item.product_sku || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${item.product_name || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${badge}</td>
|
||||
<td class="py-3"><a href="javascript:;" onclick="view_rack_log(${item.id}, '${item.warehouse_name} ${item.zone}-${item.aisle}-${item.rack}')"><i class="ti ti-history fs-5"></i></a></td>
|
||||
</tr>`;
|
||||
});
|
||||
$('table#tbl_all > tbody').html(body);
|
||||
}
|
||||
|
||||
|
||||
// ── Render: Occupied ─────────────────────────────────────────────────
|
||||
function change_page_occupied(page_num) {
|
||||
|
||||
var offset = (page_num - 1) * prop_limit;
|
||||
var kw = search_occupied.toLowerCase();
|
||||
|
||||
var base = alasql("SELECT * FROM rack_all WHERE status = 'occupied'");
|
||||
|
||||
var filtered = kw ? base.filter(function(item) {
|
||||
return (item.warehouse_name || '').toLowerCase().includes(kw)
|
||||
|| (item.zone || '').toLowerCase().includes(kw)
|
||||
|| (item.aisle || '').toLowerCase().includes(kw)
|
||||
|| (item.rack || '').toLowerCase().includes(kw)
|
||||
|| (item.product_sku || '').toLowerCase().includes(kw)
|
||||
|| (item.product_name || '').toLowerCase().includes(kw);
|
||||
}) : base;
|
||||
|
||||
$('table#tbl_occupied tfoot').html(generate_pagination('tbl_occupied', filtered.length));
|
||||
|
||||
if (filtered.length === 0) {
|
||||
$('table#tbl_occupied > tbody').html(
|
||||
'<tr><td colspan="6" class="text-center py-5 text-muted">No occupied racks found.</td></tr>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var body = '';
|
||||
$.each(filtered.slice(offset, offset + prop_limit), function(i, item) {
|
||||
body += `<tr>
|
||||
<td class="py-3">${item.warehouse_name}</td>
|
||||
<td class="py-3">${item.zone}</td>
|
||||
<td class="py-3">${item.aisle}</td>
|
||||
<td class="py-3">${item.rack}</td>
|
||||
<td class="py-3">${item.product_sku}</td>
|
||||
<td class="py-3">${item.product_name || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3"><a href="javascript:;" onclick="view_rack_log(${item.id}, '${item.warehouse_name} ${item.zone}-${item.aisle}-${item.rack}')"><i class="ti ti-history fs-5"></i></a></td>
|
||||
</tr>`;
|
||||
});
|
||||
$('table#tbl_occupied > tbody').html(body);
|
||||
}
|
||||
|
||||
|
||||
// ── Render: Available ─────────────────────────────────────────────────
|
||||
function change_page_empty(page_num) {
|
||||
|
||||
var offset = (page_num - 1) * prop_limit;
|
||||
var kw = search_empty.toLowerCase();
|
||||
|
||||
var base = alasql("SELECT * FROM rack_all WHERE status = 'empty'");
|
||||
|
||||
var filtered = kw ? base.filter(function(item) {
|
||||
return (item.warehouse_name || '').toLowerCase().includes(kw)
|
||||
|| (item.zone || '').toLowerCase().includes(kw)
|
||||
|| (item.aisle || '').toLowerCase().includes(kw)
|
||||
|| (item.rack || '').toLowerCase().includes(kw);
|
||||
}) : base;
|
||||
|
||||
$('table#tbl_empty tfoot').html(generate_pagination('tbl_empty', filtered.length));
|
||||
|
||||
if (filtered.length === 0) {
|
||||
$('table#tbl_empty > tbody').html(
|
||||
'<tr><td colspan="4" class="text-center py-5 text-muted">No available racks found.</td></tr>'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
var body = '';
|
||||
$.each(filtered.slice(offset, offset + prop_limit), function(i, item) {
|
||||
body += `<tr>
|
||||
<td class="py-3">${item.warehouse_name}</td>
|
||||
<td class="py-3">${item.zone}</td>
|
||||
<td class="py-3">${item.aisle}</td>
|
||||
<td class="py-3">${item.rack}</td>
|
||||
<td class="py-3"><a href="javascript:;" onclick="view_rack_log(${item.id}, '${item.warehouse_name} ${item.zone}-${item.aisle}-${item.rack}')"><i class="ti ti-history fs-5"></i></a></td>
|
||||
</tr>`;
|
||||
});
|
||||
$('table#tbl_empty > tbody').html(body);
|
||||
}
|
||||
|
||||
|
||||
// ── Search ────────────────────────────────────────────────────────────
|
||||
function debounce(fn, ms) {
|
||||
var t;
|
||||
return function() {
|
||||
clearTimeout(t);
|
||||
t = setTimeout(fn, ms);
|
||||
};
|
||||
}
|
||||
|
||||
$('#search_all').on('input', debounce(function() {
|
||||
search_all = $(this).val().trim();
|
||||
change_page_all(1);
|
||||
}, 200));
|
||||
|
||||
$('#search_occupied').on('input', debounce(function() {
|
||||
search_occupied = $(this).val().trim();
|
||||
change_page_occupied(1);
|
||||
}, 200));
|
||||
|
||||
$('#search_empty').on('input', debounce(function() {
|
||||
search_empty = $(this).val().trim();
|
||||
change_page_empty(1);
|
||||
}, 200));
|
||||
|
||||
|
||||
// ── Re-render on tab switch ───────────────────────────────────────────
|
||||
$('#pills-occupied-tab').on('shown.bs.tab', function() { change_page_occupied(1); });
|
||||
$('#pills-empty-tab').on('shown.bs.tab', function() { change_page_empty(1); });
|
||||
$('#pills-all-tab').on('shown.bs.tab', function() { change_page_all(1); });
|
||||
|
||||
// Remember last active tab
|
||||
$(document).ready(function() {
|
||||
$('button.nav-link').on('click', function() {
|
||||
sessionStorage.setItem('rack_nav_id', $(this).attr('id'));
|
||||
});
|
||||
var nav_id = sessionStorage.getItem('rack_nav_id');
|
||||
if (nav_id) $(`button#${nav_id}`).trigger('click');
|
||||
});
|
||||
|
||||
|
||||
// ── Rack Log ─────────────────────────────────────────────────────────
|
||||
function view_rack_log(rack_id, label) {
|
||||
|
||||
$('#rackLogModalLabel').text('Rack Log — ' + label);
|
||||
$('#rack_log_body').html('<tr><td colspan="4" class="text-center py-4 text-muted">Loading...</td></tr>');
|
||||
var modal = new bootstrap.Modal(document.getElementById('rackLogModal'));
|
||||
modal.show();
|
||||
|
||||
ajax_request({
|
||||
url: "<?php echo $server_url?>reports/api/engine_report/rack_log.php",
|
||||
autoPrepare: true,
|
||||
checkRequired: 0,
|
||||
action: 'read',
|
||||
data: { "rack_id": rack_id },
|
||||
onSuccess: function(res) {
|
||||
|
||||
if (!res.output || res.output.length === 0) {
|
||||
$('#rack_log_body').html('<tr><td colspan="4" class="text-center py-4 text-muted">No log entries.</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
var action_badge = {
|
||||
'occupy': '<span class="badge bg-danger">Occupy</span>',
|
||||
'release': '<span class="badge bg-success">Release</span>',
|
||||
'transfer_in': '<span class="badge bg-primary">Transfer In</span>',
|
||||
'transfer_out': '<span class="badge bg-warning text-dark">Transfer Out</span>',
|
||||
};
|
||||
|
||||
var body = '';
|
||||
$.each(res.output, function(i, row) {
|
||||
body += `<tr>
|
||||
<td class="py-3">${format_date(row.dt)}</td>
|
||||
<td class="py-3">${action_badge[row.action] || row.action}</td>
|
||||
<td class="py-3">${row.product_sku || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3">${row.user_name || '<span class="text-muted">—</span>'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
$('#rack_log_body').html(body);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Boot ─────────────────────────────────────────────────────────────
|
||||
$(async function() {
|
||||
try {
|
||||
await retrieve_rack_occupancy();
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,364 @@
|
||||
<?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 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>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>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 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>',
|
||||
};
|
||||
|
||||
|
||||
// ── 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">${Number(item.balance).toLocaleString()}</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">${row.zone}-${row.aisle}-${row.rack}</td>
|
||||
<td class="py-3 text-success">${parseFloat(row.stock_in) > 0 ? '+' + Number(row.stock_in).toLocaleString() : '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-3 text-danger">${parseFloat(row.stock_out) > 0 ? '-' + Number(row.stock_out).toLocaleString() : '<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 retrieve_product_lot();
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,374 @@
|
||||
<?php
|
||||
session_start();
|
||||
require '../config.php';
|
||||
require '../include_header.php';
|
||||
?>
|
||||
|
||||
<body>
|
||||
<?php require '../include_topbar.php';?>
|
||||
<?php require '../include_sidebar.php';?>
|
||||
|
||||
<main id="content" class="content py-15">
|
||||
<div class="container-fluid">
|
||||
|
||||
<!-- Page header -->
|
||||
<div class="row mb-5">
|
||||
<div class="col-12">
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-3">
|
||||
<div>
|
||||
<h1 class="fs-4 mb-0">Stock Movement</h1>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2 flex-nowrap">
|
||||
<select id="warehouse" class="form-select form-select-sm" style="min-width:140px;max-width:180px;">
|
||||
<option value="">Warehouse...</option>
|
||||
</select>
|
||||
<input type="month" id="from_month" class="form-control form-control-sm" style="width:150px;">
|
||||
<span class="text-muted small text-nowrap">to</span>
|
||||
<input type="month" id="to_month" class="form-control form-control-sm" style="width:150px;">
|
||||
<button class="btn btn-primary btn-sm text-nowrap" onclick="do_search();">
|
||||
<i class="ti ti-search"></i> Search
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary stats -->
|
||||
<div class="row mb-4 g-3">
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-4 d-flex align-items-center gap-3">
|
||||
<span class="icon-shape icon-sm bg-primary text-white rounded-2">
|
||||
<i class="ti ti-list fs-5"></i>
|
||||
</span>
|
||||
<div>
|
||||
<div class="fs-3 fw-bold mb-0" id="stat_transactions">—</div>
|
||||
<div class="text-muted small">Total Transactions</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-4 d-flex align-items-center gap-3">
|
||||
<span class="icon-shape icon-sm bg-success text-white rounded-2">
|
||||
<i class="ti ti-package-import fs-5"></i>
|
||||
</span>
|
||||
<div>
|
||||
<div class="fs-3 fw-bold mb-0 text-success" id="stat_in">—</div>
|
||||
<div class="text-muted small">Total Stock In</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-4 d-flex align-items-center gap-3">
|
||||
<span class="icon-shape icon-sm bg-danger text-white rounded-2">
|
||||
<i class="ti ti-package-export fs-5"></i>
|
||||
</span>
|
||||
<div>
|
||||
<div class="fs-3 fw-bold mb-0 text-danger" id="stat_out">—</div>
|
||||
<div class="text-muted small">Total Stock Out</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-3 col-md-6 col-12">
|
||||
<div class="card">
|
||||
<div class="card-body p-4 d-flex align-items-center gap-3">
|
||||
<span class="icon-shape icon-sm bg-warning text-white rounded-2">
|
||||
<i class="ti ti-arrows-diff fs-5"></i>
|
||||
</span>
|
||||
<div>
|
||||
<div class="fs-3 fw-bold mb-0" id="stat_net">—</div>
|
||||
<div class="text-muted small">Net Movement</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="row">
|
||||
<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-4 mb-0">Movement Detail</h2>
|
||||
<span id="result_summary" class="text-muted small"></span>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table mb-0 text-nowrap table-hover table-centered table-sm" id="tbl_movement">
|
||||
<thead class="table-light border-light">
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Type</th>
|
||||
<th>SKU</th>
|
||||
<th>Lot</th>
|
||||
<th>Location</th>
|
||||
<th>Contact</th>
|
||||
<th class="text-end">Stock In</th>
|
||||
<th class="text-end">Stock Out</th>
|
||||
<th class="text-end">Running Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="tbl_body">
|
||||
<tr>
|
||||
<td colspan="9" class="text-center py-5 text-muted">
|
||||
Select a warehouse and date range, then click Search.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Lazy load sentinel + loader -->
|
||||
<div id="lazy_sentinel" class="py-3 text-center text-muted small" style="display:none;">
|
||||
<div id="lazy_loader"><i class="ti ti-loader-2 me-1"></i>Loading...</div>
|
||||
<div id="lazy_done" style="display:none;">All products loaded.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<?php require "../include_ending.php";?>
|
||||
|
||||
<script>
|
||||
// ── State ─────────────────────────────────────────────────────────────
|
||||
var sku_queue = []; // ordered list of SKUs returned by summary call
|
||||
var sku_index = 0; // next SKU to load
|
||||
var is_loading = false;
|
||||
var search_params = {}; // warehouse_id, from_month, to_month — fixed per search
|
||||
|
||||
var type_badge = {
|
||||
'in': '<span class="badge bg-success">In</span>',
|
||||
'out': '<span class="badge bg-danger">Out</span>',
|
||||
'transfer': '<span class="badge bg-info text-dark">Transfer</span>',
|
||||
};
|
||||
|
||||
// ── Default months ────────────────────────────────────────────────────
|
||||
(function() {
|
||||
var now = new Date();
|
||||
var month = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0');
|
||||
$('#from_month').val(month);
|
||||
$('#to_month').val(month);
|
||||
})();
|
||||
|
||||
|
||||
// ── Warehouse dropdown ────────────────────────────────────────────────
|
||||
function retrieve_warehouse() {
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>ics/api/engine/retrieve_warehouse.php",
|
||||
autoPrepare: true, checkRequired: 0, action: 'read',
|
||||
onSuccess: function(res) {
|
||||
var options = '';
|
||||
$.each(res.output, function(i, item) {
|
||||
options += `<option value="${item.id}">${item.warehouse_name}</option>`;
|
||||
});
|
||||
$('#warehouse').html(options);
|
||||
if (res.output && res.output.length > 0) {
|
||||
$('#warehouse').val(res.output[0].id);
|
||||
do_search();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Search — fetch summary, reset state, start lazy load ─────────────
|
||||
function do_search() {
|
||||
var warehouse_id = $('#warehouse').val();
|
||||
var from_month = $('#from_month').val();
|
||||
var to_month = $('#to_month').val();
|
||||
if (!warehouse_id || !from_month || !to_month) return;
|
||||
|
||||
// Reset state
|
||||
sku_queue = [];
|
||||
sku_index = 0;
|
||||
is_loading = false;
|
||||
search_params = { warehouse_id: warehouse_id, from_month: from_month, to_month: to_month };
|
||||
|
||||
// Reset UI
|
||||
$('#tbl_body').html('<tr><td colspan="9" class="text-center py-4 text-muted">Loading…</td></tr>');
|
||||
$('#lazy_sentinel').hide();
|
||||
$('#lazy_done').hide();
|
||||
$('#lazy_loader').show();
|
||||
$('#result_summary').text('');
|
||||
|
||||
// Reset stats
|
||||
$('#stat_transactions, #stat_in, #stat_out, #stat_net').text('—');
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>reports/api/engine_report/stock_movement.php",
|
||||
autoPrepare: true, checkRequired: 0, action: 'read',
|
||||
data: { warehouse_id: warehouse_id, from_month: from_month, to_month: to_month },
|
||||
onSuccess: function(res) {
|
||||
var d = res.output;
|
||||
|
||||
// Populate stats
|
||||
$('#stat_transactions').text(d.total_transactions);
|
||||
$('#stat_in').text(fmt(d.total_in));
|
||||
$('#stat_out').text(fmt(d.total_out));
|
||||
var net = d.net;
|
||||
$('#stat_net')
|
||||
.text((net >= 0 ? '+' : '') + fmt(net))
|
||||
.removeClass('text-success text-danger')
|
||||
.addClass(net >= 0 ? 'text-success' : 'text-danger');
|
||||
|
||||
if (!d.skus || d.skus.length === 0) {
|
||||
$('#tbl_body').html('<tr><td colspan="9" class="text-center py-5 text-muted">No movement data found.</td></tr>');
|
||||
return;
|
||||
}
|
||||
|
||||
sku_queue = d.skus;
|
||||
sku_index = 0;
|
||||
$('#tbl_body').html('');
|
||||
$('#result_summary').text(d.skus.length + ' product' + (d.skus.length > 1 ? 's' : ''));
|
||||
$('#lazy_sentinel').show();
|
||||
|
||||
// Start loading — sentinel_is_visible() will keep pulling until window is filled
|
||||
load_next_sku();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Load one SKU group, then keep going if sentinel still visible ─────
|
||||
function load_next_sku() {
|
||||
if (is_loading || sku_index >= sku_queue.length) {
|
||||
if (sku_index >= sku_queue.length) {
|
||||
$('#lazy_loader').hide();
|
||||
$('#lazy_done').show();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
is_loading = true;
|
||||
var sku = sku_queue[sku_index];
|
||||
|
||||
return ajax_request({
|
||||
url: "<?php echo $server_url?>reports/api/engine_report/stock_movement_sku.php",
|
||||
autoPrepare: true, checkRequired: 0, action: 'read',
|
||||
queueLock: false,
|
||||
data: {
|
||||
warehouse_id : search_params.warehouse_id,
|
||||
product_sku : sku,
|
||||
from_month : search_params.from_month,
|
||||
to_month : search_params.to_month,
|
||||
},
|
||||
onSuccess: function(res) {
|
||||
append_sku_group(res.output);
|
||||
sku_index++;
|
||||
is_loading = false;
|
||||
|
||||
if (sku_index >= sku_queue.length) {
|
||||
$('#lazy_loader').hide();
|
||||
$('#lazy_done').show();
|
||||
return;
|
||||
}
|
||||
|
||||
// If sentinel is still within the viewport (or above it),
|
||||
// the table hasn't filled the window yet — keep loading immediately.
|
||||
if (sentinel_is_visible()) {
|
||||
load_next_sku();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Returns true if the sentinel is visible in the current viewport ───
|
||||
function sentinel_is_visible() {
|
||||
var el = document.getElementById('lazy_sentinel');
|
||||
var rect = el.getBoundingClientRect();
|
||||
return rect.top <= (window.innerHeight || document.documentElement.clientHeight);
|
||||
}
|
||||
|
||||
|
||||
// ── Render one SKU group into the table ───────────────────────────────
|
||||
function append_sku_group(d) {
|
||||
var bf = d.brought_forward;
|
||||
var rows = d.rows;
|
||||
|
||||
// SKU header row
|
||||
var header = `<tr class="table-secondary">
|
||||
<td colspan="9" class="py-2 fw-semibold">
|
||||
<code>${d.sku}</code>
|
||||
<span class="text-muted fw-normal ms-2">${d.product_name}</span>
|
||||
</td>
|
||||
</tr>`;
|
||||
|
||||
// Brought-forward row
|
||||
var bf_in_val = bf.in > 0 ? fmt(bf.in) : '<span class="text-muted">—</span>';
|
||||
var bf_out_val = bf.out > 0 ? fmt(bf.out) : '<span class="text-muted">—</span>';
|
||||
var bf_bal_cls = bf.balance >= 0 ? 'text-success' : 'text-danger';
|
||||
var bf_row = `<tr class="table-light text-muted fst-italic">
|
||||
<td class="py-2" colspan="6">Brought Forward</td>
|
||||
<td class="py-2 text-end text-success">${bf_in_val}</td>
|
||||
<td class="py-2 text-end text-danger">${bf_out_val}</td>
|
||||
<td class="py-2 text-end fw-semibold ${bf_bal_cls}">${fmt(bf.balance)}</td>
|
||||
</tr>`;
|
||||
|
||||
// Transaction rows
|
||||
var body = '';
|
||||
if (rows.length === 0) {
|
||||
body = `<tr>
|
||||
<td colspan="9" class="text-center py-2 text-muted fst-italic">No transactions in range.</td>
|
||||
</tr>`;
|
||||
} else {
|
||||
$.each(rows, function(i, row) {
|
||||
var in_val = row.stock_in > 0 ? fmt(row.stock_in) : '<span class="text-muted">—</span>';
|
||||
var out_val = row.stock_out > 0 ? fmt(row.stock_out) : '<span class="text-muted">—</span>';
|
||||
var rt_cls = row.running_total >= 0 ? 'text-success' : 'text-danger';
|
||||
body += `<tr>
|
||||
<td class="py-2">${row.date}</td>
|
||||
<td class="py-2">${type_badge[row.type] || row.type}</td>
|
||||
<td class="py-2"><code>${row.product_sku}</code></td>
|
||||
<td class="py-2">${row.lot_number || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-2">${row.zone}-${row.aisle}-${row.rack}</td>
|
||||
<td class="py-2">${row.contact_name || '<span class="text-muted">—</span>'}</td>
|
||||
<td class="py-2 text-end text-success">${in_val}</td>
|
||||
<td class="py-2 text-end text-danger">${out_val}</td>
|
||||
<td class="py-2 text-end fw-semibold ${rt_cls}">${fmt(row.running_total)}</td>
|
||||
</tr>`;
|
||||
});
|
||||
}
|
||||
|
||||
$('#tbl_body').append(header + bf_row + body);
|
||||
}
|
||||
|
||||
|
||||
// ── Intersection Observer for lazy load ───────────────────────────────
|
||||
var observer = new IntersectionObserver(function(entries) {
|
||||
if (entries[0].isIntersecting && !is_loading && sku_index < sku_queue.length) {
|
||||
load_next_sku();
|
||||
}
|
||||
}, { rootMargin: '200px' });
|
||||
|
||||
observer.observe(document.getElementById('lazy_sentinel'));
|
||||
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────
|
||||
function fmt(n) {
|
||||
return parseFloat(n || 0).toLocaleString(undefined, {
|
||||
minimumFractionDigits: 2, maximumFractionDigits: 2
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// ── Boot ──────────────────────────────────────────────────────────────
|
||||
$(async function() {
|
||||
try { await retrieve_warehouse(); } catch(e) { console.log(e); }
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user