Reports
This commit is contained in:
@@ -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