374 lines
14 KiB
PHP
374 lines
14 KiB
PHP
<?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>
|