This commit is contained in:
Thanakorn S
2026-04-29 17:04:11 +07:00
parent f6dc9a3278
commit db5c47b6ca
15 changed files with 679 additions and 43 deletions
+374
View File
@@ -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>