stockmovement: add summary views

This commit is contained in:
Thanakorn S
2026-05-11 16:55:23 +07:00
parent 7a87909392
commit adfd5ef017
6 changed files with 230 additions and 67 deletions
+52 -52
View File
@@ -138,7 +138,7 @@
<th>Product</th>
<th>SKU</th>
<th>Lot Number</th>
<th class="th_location">Location</th>
<th class="th_location">Location</th>
<th>Expiry Date</th>
<th>Days Overdue</th>
<th>Quantity</th>
@@ -167,7 +167,7 @@
<th>Product</th>
<th>SKU</th>
<th>Lot Number</th>
<th class="th_location">Location</th>
<th class="th_location">Location</th>
<th>Expiry Date</th>
<th>Days Remaining</th>
<th>Quantity</th>
@@ -190,44 +190,44 @@
<?php require "../include_ending.php";?>
<script>
var all_items = [];
var search_term = '';
var filter_status = '';
var advanced = false;
var label_rack = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
function apply_location_labels() {
$('.th_location').text(advanced
? label_zone + ' / ' + label_aisle + ' / ' + label_rack
: label_rack
);
}
function format_location(item) {
if (advanced) {
return [item.zone, item.aisle, item.rack].filter(Boolean).join('-') || '<span class="text-muted">—</span>';
}
return item.rack || '<span class="text-muted">—</span>';
}
function load_company_setting() {
return ajax_request({
url: server_url + 'setting/api/engine/company_setting.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var s = res.output || {};
advanced = s.advanced_location == 1;
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels();
}
});
}
var all_items = [];
var search_term = '';
var filter_status = '';
var advanced = false;
var label_rack = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
function apply_location_labels() {
$('.th_location').text(advanced
? label_zone + ' / ' + label_aisle + ' / ' + label_rack
: label_rack
);
}
function format_location(item) {
if (advanced) {
return [item.zone, item.aisle, item.rack].filter(Boolean).join('-') || '<span class="text-muted">—</span>';
}
return item.rack || '<span class="text-muted">—</span>';
}
function load_company_setting() {
return ajax_request({
url: server_url + 'setting/api/engine/company_setting.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var s = res.output || {};
advanced = s.advanced_location == 1;
label_rack = s.location_label_rack || (advanced ? 'Rack' : 'Location');
label_zone = s.location_label_zone || 'Zone';
label_aisle = s.location_label_aisle || 'Aisle';
apply_location_labels();
}
});
}
// ── Fetch + populate ─────────────────────────────────────────────────
@@ -264,14 +264,14 @@
// ── Render both tables from filtered data ────────────────────────────
function render_tables() {
var kw = search_term;
var kw = search_term.toLowerCase();
var fs = filter_status;
var filtered = all_items.filter(function(item) {
var match_search = !kw ||
item.product_name || ''.includes(kw) ||
item.product_sku || ''.includes(kw) ||
item.lot_number || ''.includes(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;
});
@@ -305,7 +305,7 @@
<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">${format_location(item)}</td>
<td class="py-3">${format_location(item)}</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>
@@ -329,7 +329,7 @@
<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">${format_location(item)}</td>
<td class="py-3">${format_location(item)}</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>
@@ -382,11 +382,11 @@
// ── Boot ─────────────────────────────────────────────────────────────
$(async function() {
try {
await load_company_setting();
await retrieve_warehouse();
} catch(e) {
$(async function() {
try {
await load_company_setting();
await retrieve_warehouse();
} catch(e) {
console.log(e);
}
});
@@ -398,4 +398,4 @@
</body>
</html>
</html>
+143
View File
@@ -115,6 +115,23 @@
</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-movement-tab" data-bs-toggle="pill"
data-bs-target="#pills-movement" type="button" role="tab"
aria-controls="pills-movement" aria-selected="true">Detail</button>
</li>
<li class="nav-item nav-item-secondary" role="presentation">
<button class="nav-link" id="pills-summary-tab" data-bs-toggle="pill"
data-bs-target="#pills-summary" type="button" role="tab"
aria-controls="pills-summary" aria-selected="false">Summary</button>
</li>
</ul>
<!-- Tab 1: Stock Movement -->
<div class="tab-pane fade show active" id="pills-movement" role="tabpanel" aria-labelledby="pills-movement-tab">
<!-- Table -->
<div class="row">
<div class="col-12">
@@ -165,6 +182,65 @@
</div>
</div>
</div><!-- end tab 1 -->
<!-- Tab 2: Movement Summary -->
<div class="tab-pane fade" id="pills-summary" role="tabpanel" aria-labelledby="pills-summary-tab">
<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 Summary</h2>
<span id="summary_note" class="text-muted small">Search to load data.</span>
</div>
<div class="table-responsive">
<table class="table mb-0 text-nowrap table-hover table-centered table-sm">
<thead class="table-light border-light">
<tr>
<th>SKU</th>
<th>Product</th>
<th>UOM</th>
<th class="text-end">Transactions</th>
<th class="text-end">Total In</th>
<th class="text-end">Total Out</th>
<th class="text-end">Revenue</th>
<th class="text-end">COGS</th>
<th class="text-end">Gross Profit</th>
<th class="text-end">Margin</th>
<th class="text-end">Run. Revenue</th>
<th class="text-end">Run. COGS</th>
<th class="text-end">Run. GP</th>
</tr>
</thead>
<tbody id="tbl_summary_body">
<tr>
<td colspan="12" class="text-center py-5 text-muted">
Select a warehouse and date range, then click Search.
</td>
</tr>
</tbody>
<tfoot id="tbl_summary_foot" style="display:none;">
<tr class="table-secondary fw-semibold">
<td colspan="3">Total</td>
<td class="text-end" id="ssum_txn">—</td>
<td class="text-end" id="ssum_total_in">—</td>
<td class="text-end" id="ssum_total_out">—</td>
<td class="text-end" id="ssum_revenue">—</td>
<td class="text-end" id="ssum_cogs">—</td>
<td class="text-end" id="ssum_gp">—</td>
<td class="text-end" id="ssum_margin">—</td>
<td colspan="3"></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div><!-- end tab 2 -->
</div>
</main>
@@ -177,6 +253,9 @@
var is_loading = false;
var search_params = {}; // warehouse_id, from_month, to_month — fixed per search
var advanced = false;
// Movement Summary running totals (reset on each search)
var run_revenue = 0, run_cogs = 0, run_gp = 0, sum_in = 0, sum_out = 0, sum_txn = 0;
var label_rack = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
@@ -263,7 +342,11 @@
sku_index = 0;
is_loading = false;
export_data = [];
run_revenue = run_cogs = run_gp = sum_in = sum_out = sum_txn = 0;
search_params = { warehouse_id: warehouse_id, from_month: from_month, to_month: to_month };
$('#tbl_summary_body').html('<tr><td colspan="13" class="text-center py-4 text-muted">Loading…</td></tr>');
$('#tbl_summary_foot').hide();
$('#summary_note').text('');
// Reset UI
$('#tbl_body').html('<tr><td colspan="9" class="text-center py-4 text-muted">Loading…</td></tr>');
@@ -458,6 +541,66 @@
}
$('#tbl_body').append(header + bf_row + body);
// ── Movement Summary row ──────────────────────────────────────────────
var sku_txn = rows.length;
var sku_in = 0, sku_out = 0, sku_rev = 0, sku_cost = 0;
$.each(rows, function(i, row) {
sku_in += parseFloat(row.stock_in || 0);
if (parseFloat(row.stock_out) > 0) {
sku_out += parseFloat(row.stock_out);
sku_rev += parseFloat(row.stock_out) * parseFloat(row.price || 0);
sku_cost += parseFloat(row.stock_out) * parseFloat(row.unit_cost || 0);
}
});
var sku_gp = sku_rev - sku_cost;
var sku_mgn = sku_rev > 0 ? (sku_gp / sku_rev * 100) : null;
sum_txn += sku_txn;
sum_in += sku_in;
sum_out += sku_out;
run_revenue += sku_rev;
run_cogs += sku_cost;
run_gp += sku_gp;
var mgn_html = sku_mgn !== null
? `<span class="${sku_mgn >= 0 ? 'text-success' : 'text-danger'}">${sku_mgn.toFixed(1)}%</span>`
: '<span class="text-muted">—</span>';
var gp_cls = sku_gp >= 0 ? 'text-success' : 'text-danger';
var run_gp_cls = run_gp >= 0 ? 'text-success' : 'text-danger';
if ($('#tbl_summary_body tr[data-sku]').length === 0) $('#tbl_summary_body').html('');
$('#tbl_summary_body').append(`<tr data-sku="${escape_html(d.sku)}">
<td><code>${escape_html(d.sku)}</code></td>
<td>${escape_html(d.product_name)}</td>
<td class="text-muted small">${escape_html(d.uom || 'pcs')}</td>
<td class="text-end">${sku_txn}</td>
<td class="text-end text-success">${fmt(sku_in)}</td>
<td class="text-end text-danger">${fmt(sku_out)}</td>
<td class="text-end">${sku_rev > 0 ? fmt(sku_rev) : '<span class="text-muted">—</span>'}</td>
<td class="text-end">${sku_cost > 0 ? fmt(sku_cost) : '<span class="text-muted">—</span>'}</td>
<td class="text-end fw-semibold ${gp_cls}">${fmt(sku_gp)}</td>
<td class="text-end">${mgn_html}</td>
<td class="text-end text-muted small">${fmt(run_revenue)}</td>
<td class="text-end text-muted small">${fmt(run_cogs)}</td>
<td class="text-end small fw-semibold ${run_gp_cls}">${fmt(run_gp)}</td>
</tr>`);
// Update totals footer
var tot_mgn = run_revenue > 0 ? (run_gp / run_revenue * 100) : null;
var tot_mgn_html = tot_mgn !== null
? `<span class="${tot_mgn >= 0 ? 'text-success' : 'text-danger'}">${tot_mgn.toFixed(1)}%</span>`
: '—';
$('#ssum_txn').text(sum_txn);
$('#ssum_total_in').text(fmt(sum_in));
$('#ssum_total_out').text(fmt(sum_out));
$('#ssum_revenue').text(fmt(run_revenue));
$('#ssum_cogs').text(fmt(run_cogs));
$('#ssum_gp').html(`<span class="${run_gp >= 0 ? 'text-success' : 'text-danger'}">${fmt(run_gp)}</span>`);
$('#ssum_margin').html(tot_mgn_html);
$('#tbl_summary_foot').show();
$('#summary_note').text((sku_index + 1) + ' of ' + sku_queue.length + ' SKUs loaded');
}