[test] contact to setting modules

This commit is contained in:
Thanakorn S
2026-05-06 08:58:09 +07:00
parent 414f1cd81d
commit 6501d326ca
43 changed files with 4295 additions and 581 deletions
+13 -9
View File
@@ -5,15 +5,19 @@
require '../../../assets/utils/classes/WarehouseManager.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
$csm = new CompanySettingManager($pdo1, $company_id);
$auto_approve = (int)$csm->get('default_stock_status') === 1;
$csm = new CompanySettingManager($pdo1, $company_id);
$auto_approve = (int)$csm->get('default_stock_status') === 1;
try {
$new_id = null;
try {
// Ensure the per-warehouse stock table exists (older warehouses may not have it yet)
$whBoot = new WarehouseManager($pdo2, $company_id);
$whBoot->createStockTableForWarehouse((int)$data['warehouse']);
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid, $auto_approve, &$new_id) {
$stock = new StockManager($pdo, $company_id);
$whMgmt = new WarehouseManager($pdo, $company_id);
$new_id = null;
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid, $auto_approve, &$new_id) {
$stock = new StockManager($pdo, $company_id);
$whMgmt = new WarehouseManager($pdo, $company_id);
$new_id = $stock->saveStockIn($data, $logging, $uuid);
if ($auto_approve && $new_id > 0) {
@@ -35,5 +39,5 @@
http_response_code(400);
}
exit(json_encode($answer));
?>
exit(json_encode($answer));
?>
+13 -9
View File
@@ -5,15 +5,19 @@
require '../../../assets/utils/classes/WarehouseManager.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
$csm = new CompanySettingManager($pdo1, $company_id);
$auto_approve = (int)$csm->get('default_stock_status') === 1;
$csm = new CompanySettingManager($pdo1, $company_id);
$auto_approve = (int)$csm->get('default_stock_status') === 1;
try {
$new_id = null;
try {
// Ensure the per-warehouse stock table exists (older warehouses may not have it yet)
$whBoot = new WarehouseManager($pdo2, $company_id);
$whBoot->createStockTableForWarehouse((int)$data['warehouse']);
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid, $auto_approve, &$new_id) {
$stock = new StockManager($pdo, $company_id);
$whMgmt = new WarehouseManager($pdo, $company_id);
$new_id = null;
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid, $auto_approve, &$new_id) {
$stock = new StockManager($pdo, $company_id);
$whMgmt = new WarehouseManager($pdo, $company_id);
$new_id = $stock->saveStockOut($data, $logging, $uuid);
if ($auto_approve && $new_id > 0) {
@@ -35,5 +39,5 @@
http_response_code(400);
}
exit(json_encode($answer));
?>
exit(json_encode($answer));
?>
+13 -8
View File
@@ -5,15 +5,20 @@
require '../../../assets/utils/classes/WarehouseManager.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
$csm = new CompanySettingManager($pdo1, $company_id);
$auto_approve = (int)$csm->get('default_stock_status') === 1;
$csm = new CompanySettingManager($pdo1, $company_id);
$auto_approve = (int)$csm->get('default_stock_status') === 1;
try {
$new_id = null;
try {
// Ensure the per-warehouse stock tables exist (older warehouses may not have them yet)
$whBoot = new WarehouseManager($pdo2, $company_id);
$whBoot->createStockTableForWarehouse((int)$data['warehouse_from']);
$whBoot->createStockTableForWarehouse((int)$data['warehouse_to']);
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid, $auto_approve, &$new_id) {
$stock = new StockManager($pdo, $company_id);
$whMgmt = new WarehouseManager($pdo, $company_id);
$new_id = null;
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, $uuid, $auto_approve, &$new_id) {
$stock = new StockManager($pdo, $company_id);
$whMgmt = new WarehouseManager($pdo, $company_id);
$new_id = $stock->saveStockTransfer($data, $logging, $uuid);
if ($auto_approve && $new_id > 0) {
@@ -36,4 +41,4 @@
}
exit(json_encode($answer));
?>
?>
+44 -6
View File
@@ -104,6 +104,7 @@
<!-- Submit Button -->
<div class="mb-3 col-lg-12">
<button type="submit" class="btn btn-primary" onclick="manage_stock_in();">Add Stock</button>
<button type="button" class="btn btn-success ms-2 d-none" id="approve_stock_btn" onclick="approve_current_stock();">Approve</button>
</div>
</form>
</div>
@@ -161,9 +162,15 @@
function manage_stock_in() {
// In simple mode, mirror rack value to zone and aisle
var rack_val = $('#rack').val();
var zone_val = advanced ? $('#zone').val() : rack_val;
var aisle_val = advanced ? $('#aisle').val() : rack_val;
var warehouse_val = $('#warehouse').val();
var sku_val = $('#product_sku').attr('secondary') || $('#product_sku').val();
var quantity_val = $('#quantity').val();
// Mirror to hidden inputs for autoPrepare consistency (simple mode)
if (!advanced) {
var rack_val = $('#rack').val();
$('#zone').val(rack_val);
$('#aisle').val(rack_val);
}
@@ -178,8 +185,12 @@
'lot_number': $('#lot_number').val(),
'expiry_date': $('#expiry_date').val(),
'serial_number': $('#serial_number').val(),
'zone': $('#zone').val(),
'aisle': $('#aisle').val(),
'zone': zone_val,
'aisle': aisle_val,
'rack': rack_val,
'product_sku': sku_val,
'quantity': quantity_val,
'warehouse': warehouse_val,
},
action: 'manage',
onSuccess: function() {
@@ -189,6 +200,30 @@
}
function approve_current_stock() {
bootbox.confirm({
message: 'Approve this stock-in record?',
buttons: { confirm: { label: 'Approve', className: 'btn-success' }, cancel: { label: 'Cancel', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>ics/api/engine/approve_stock.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: {
id: '<?php echo $_GET["id"] ?? "";?>',
warehouse: '<?php echo $_GET["wh"] ?? "";?>',
type: 'in'
},
onSuccess: function() {
window.location.reload();
}
});
}
});
}
function retrieve_warehouse(type) {
@@ -309,7 +344,7 @@
var data = res.output;
$('#warehouse').val('<?php echo $_GET["wh"];?>').attr('disabled', true);
$('#warehouse').val('<?php echo $_GET["wh"] ?? "";?>').attr('disabled', true);
function populate_fields() {
$.each(data, function(key, item) {
@@ -336,6 +371,9 @@
$('#product_sku, #quantity').prop('disabled', true);
$('button[type=submit]').text('Update');
$('button[type=reset]').hide();
if (data.status == 0) {
$('#approve_stock_btn').removeClass('d-none');
}
}
if (advanced) {
@@ -465,4 +503,4 @@
</body>
</html>
</html>
+50 -6
View File
@@ -99,6 +99,7 @@
<!-- Submit Button -->
<div class="mb-3 col-lg-12">
<button type="submit" class="btn btn-primary" onclick="manage_stock_out();">Remove Stock</button>
<button type="button" class="btn btn-success ms-2 d-none" id="approve_stock_btn" onclick="approve_current_stock();">Approve</button>
</div>
</form>
</div>
@@ -149,8 +150,11 @@
}
function manage_stock_out() {
var rack_val = $('#rack').val();
var sku_val = $('#product_sku').attr('secondary') || $('#product_sku').val();
var wh_val = $('#warehouse').val();
if (!advanced) {
var rack_val = $('#rack').val();
$('#zone').val(rack_val);
$('#aisle').val(rack_val);
}
@@ -161,9 +165,12 @@
checkRequired: 1,
debugMode: 0,
data: {
'contact_id': $('#contact').attr('data-id'),
'zone': $('#zone').val(),
'aisle': $('#aisle').val(),
'contact_id': $('#contact').attr('data-id'),
'zone': advanced ? $('#zone').val() : rack_val,
'aisle': advanced ? $('#aisle').val() : rack_val,
'rack': rack_val,
'product_sku': sku_val,
'warehouse': wh_val,
},
action: 'manage',
onSuccess: function() {
@@ -172,6 +179,30 @@
});
}
function approve_current_stock() {
bootbox.confirm({
message: 'Approve this stock-out record?',
buttons: { confirm: { label: 'Approve', className: 'btn-success' }, cancel: { label: 'Cancel', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>ics/api/engine/approve_stock.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: {
id: '<?php echo $_GET["id"] ?? "";?>',
warehouse: '<?php echo $_GET["wh"] ?? "";?>',
type: 'out'
},
onSuccess: function() {
window.location.reload();
}
});
}
});
}
// ── Step 1: Product selected → load active lots ───────────────────────
function retrieve_active_lot() {
@@ -242,7 +273,11 @@
autoPrepare: true,
checkRequired: 0,
noLoading: true,
data: { "type": 'from', "lot_number": $('#lot_number').val() },
data: {
"type": 'from',
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
"lot_number": $('#lot_number').val()
},
action: 'read',
onSuccess: function(res) {
var option = '<option value="">Select warehouse...</option>';
@@ -264,6 +299,7 @@
checkRequired: 0,
noLoading: true,
data: {
"warehouse": $('#warehouse').val(),
"type": 'from',
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
"lot_number": $('#lot_number').val(),
@@ -289,6 +325,8 @@
checkRequired: 0,
noLoading: true,
data: {
"warehouse": $('#warehouse').val(),
"zone": $('#zone').val(),
"type": 'from',
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
"lot_number": $('#lot_number').val(),
@@ -314,6 +352,9 @@
checkRequired: 0,
noLoading: true,
data: {
"warehouse": $('#warehouse').val(),
"zone": $('#zone').val(),
"aisle": $('#aisle').val(),
"type": 'from',
"product_sku": $('#product_sku').attr('secondary') || $('#product_sku').val(),
"lot_number": $('#lot_number').val(),
@@ -379,6 +420,9 @@
$('#product_sku').attr('secondary', data.product_sku).prop('disabled', true);
$('button[type=submit]').text('Update');
$('button[type=reset]').hide();
if (data.status == 0) {
$('#approve_stock_btn').removeClass('d-none');
}
}
if (advanced) {
@@ -462,4 +506,4 @@
</body>
</html>
</html>
+88 -20
View File
@@ -131,6 +131,7 @@
<!-- Submit -->
<div class="mb-3 col-lg-12">
<button type="submit" class="btn btn-primary" onclick="manage_stock_transfer();">Transfer Stock</button>
<button type="button" class="btn btn-success ms-2 d-none" id="approve_stock_btn" onclick="approve_current_stock();">Approve</button>
</div>
</form>
</div>
@@ -171,6 +172,7 @@
if (advanced) {
$('#group_zone_from, #group_zone_to').show();
$('#group_aisle_from, #group_aisle_to').show();
$('#zone_from, #zone_to, #aisle_from, #aisle_to').prop('required', true);
$('#label_zone_from').text('From ' + label_zone);
$('#label_zone_to').text('To ' + label_zone);
$('#label_aisle_from').text('From ' + label_aisle);
@@ -184,6 +186,7 @@
} else {
$('#group_zone_from, #group_zone_to').hide();
$('#group_aisle_from, #group_aisle_to').hide();
$('#zone_from, #zone_to, #aisle_from, #aisle_to').prop('required', false).removeClass('is-invalid is-valid');
$('#label_rack_from').text('From ' + label_rack);
$('#label_rack_to').text('To ' + label_rack);
$('#rack_from, #rack_to').find('option:first').text('Please select ' + label_rack.toLowerCase());
@@ -194,10 +197,12 @@
// ── Submit ────────────────────────────────────────────────────────────────
function manage_stock_transfer() {
var rack_from_val = $('#rack_from').val();
var rack_to_val = $('#rack_to').val();
var sku_val = $('#product_sku').attr('secondary') || $('#product_sku').val();
// Simple mode — mirror rack value to zone and aisle before submit
if (!advanced) {
var rack_from_val = $('#rack_from').val();
var rack_to_val = $('#rack_to').val();
$('#zone_from').val(rack_from_val);
$('#aisle_from').val(rack_from_val);
$('#zone_to').val(rack_to_val);
@@ -208,7 +213,18 @@
url: '<?php echo $server_url?>ics/api/engine/manage_stock_transfer.php',
autoPrepare: true,
checkRequired: 1,
data: { 'contact_id': $('#contact').attr('data-id') },
data: {
'contact_id': $('#contact').attr('data-id'),
'warehouse_from': $('#warehouse_from').val(),
'warehouse_to': $('#warehouse_to').val(),
'zone_from': advanced ? $('#zone_from').val() : rack_from_val,
'aisle_from': advanced ? $('#aisle_from').val() : rack_from_val,
'rack_from': rack_from_val,
'zone_to': advanced ? $('#zone_to').val() : rack_to_val,
'aisle_to': advanced ? $('#aisle_to').val() : rack_to_val,
'rack_to': rack_to_val,
'product_sku': sku_val,
},
action: 'manage',
onSuccess: function() {
window.location.href = '<?php echo $server_url?>ics/stock_transfer.php';
@@ -216,6 +232,30 @@
});
}
function approve_current_stock() {
bootbox.confirm({
message: 'Approve this stock-transfer record?',
buttons: { confirm: { label: 'Approve', className: 'btn-success' }, cancel: { label: 'Cancel', className: 'btn-secondary' } },
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>ics/api/engine/approve_stock.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: {
id: '<?php echo $_GET["id"] ?? "";?>',
warehouse: '<?php echo $_GET["wh"] ?? "";?>',
type: 'transfer'
},
onSuccess: function() {
window.location.reload();
}
});
}
});
}
// ── Step 1: Product selected → load active lots + from-warehouses ─────────
function retrieve_active_lot() {
@@ -273,7 +313,11 @@
autoPrepare: true,
checkRequired: 0,
noLoading: true,
data: { 'type': type, 'lot_number': type === 'from' ? $('#lot_number').val() : '' },
data: {
'type': type,
'product_sku': type === 'from' ? ($('#product_sku').attr('secondary') || $('#product_sku').val()) : '',
'lot_number': type === 'from' ? $('#lot_number').val() : ''
},
action: 'read',
onSuccess: function(res) {
var option = '<option value="">Select warehouse...</option>';
@@ -288,13 +332,16 @@
// ── Zone / Aisle / Rack loaders ───────────────────────────────────────────
function retrieve_zone(type, callback) {
var extra = {};
var extra = {
'warehouse_from': $('#warehouse_from').val(),
'warehouse_to': $('#warehouse_to').val()
};
if (type === 'from') {
extra = {
extra = Object.assign(extra, {
'product_sku': $('#product_sku').attr('secondary') || $('#product_sku').val(),
'lot_number': $('#lot_number').val(),
'serial_number': $('#serial_number').val()
};
});
}
return ajax_request({
url: '<?php echo $server_url?>ics/api/engine/retrieve_zone.php',
@@ -315,13 +362,18 @@
}
function retrieve_aisle(type, callback) {
var extra = {};
var extra = {
'warehouse_from': $('#warehouse_from').val(),
'warehouse_to': $('#warehouse_to').val(),
'zone_from': $('#zone_from').val(),
'zone_to': $('#zone_to').val()
};
if (type === 'from') {
extra = {
extra = Object.assign(extra, {
'product_sku': $('#product_sku').attr('secondary') || $('#product_sku').val(),
'lot_number': $('#lot_number').val(),
'serial_number': $('#serial_number').val()
};
});
}
return ajax_request({
url: '<?php echo $server_url?>ics/api/engine/retrieve_aisle.php',
@@ -342,13 +394,20 @@
}
function retrieve_rack(type, callback) {
var extra = {};
var extra = {
'warehouse_from': $('#warehouse_from').val(),
'warehouse_to': $('#warehouse_to').val(),
'zone_from': $('#zone_from').val(),
'zone_to': $('#zone_to').val(),
'aisle_from': $('#aisle_from').val(),
'aisle_to': $('#aisle_to').val()
};
if (type === 'from') {
extra = {
extra = Object.assign(extra, {
'product_sku': $('#product_sku').attr('secondary') || $('#product_sku').val(),
'lot_number': $('#lot_number').val(),
'serial_number': $('#serial_number').val()
};
});
}
return ajax_request({
url: '<?php echo $server_url?>ics/api/engine/retrieve_rack.php',
@@ -400,9 +459,17 @@
$('#serial_number').html('<option value="' + (data.serial_number || '') + '">' + (data.serial_number || '—') + '</option>')
.val(data.serial_number || '').prop('disabled', true);
$('#product_name').val(data.product_name);
$('#product_sku').attr('secondary', data.product_sku).prop('disabled', true);
retrieve_warehouse('from').then(function() {
return retrieve_warehouse('to');
}).then(function() {
$('select[name="warehouse"][role="from"]').val(from_wh).prop('disabled', true);
$('select[name="warehouse"][role="to"]').val(to_wh).prop('disabled', true);
if (advanced) {
// Full cascade for From
$('select[name="warehouse"][role="from"]').val(from_wh).prop('disabled', true);
retrieve_zone('from', function() {
$('select[name="zone"][role="from"]').val(data.zone).prop('disabled', true);
retrieve_aisle('from', function() {
@@ -415,7 +482,6 @@
});
} else {
// Simple mode — skip zone/aisle cascade, load rack directly
$('select[name="warehouse"][role="from"]').val(from_wh).prop('disabled', true);
retrieve_rack('from', function() {
$('select[name="rack"][role="from"]').val(data.rack).prop('disabled', true);
populate_to_side_simple(data, to_wh);
@@ -432,16 +498,17 @@
$('#contact').val(data.contact_name)
.attr('secondary', data.contact_name)
.attr('data-id', data.contact_id);
$('#product_name').val(data.product_name);
$('#product_sku').attr('secondary', data.product_sku).prop('disabled', true);
$('button[type=submit]').text('Update');
$('button[type=reset]').hide();
if (data.status == 0) {
$('#approve_stock_btn').removeClass('d-none');
}
});
}
});
}
function populate_to_side(data, to_wh) {
$('select[name="warehouse"][role="to"]').val(to_wh).prop('disabled', true);
retrieve_zone('to', function() {
$('select[name="zone"][role="to"]').val(data.ref.zone).prop('disabled', true);
retrieve_aisle('to', function() {
@@ -454,7 +521,6 @@
}
function populate_to_side_simple(data, to_wh) {
$('select[name="warehouse"][role="to"]').val(to_wh).prop('disabled', true);
retrieve_rack('to', function() {
$('select[name="rack"][role="to"]').val(data.ref.rack).prop('disabled', true);
});
@@ -465,7 +531,9 @@
$(async function() {
try {
await load_location_config(apply_location_labels);
<?php if(empty($_GET['id'])){ ?>
await retrieve_warehouse('to');
<?php } ?>
await retrieve_for_edit();
} catch(e) {
console.log(e);
@@ -526,4 +594,4 @@
</script>
</body>
</html>
</html>
+96 -28
View File
@@ -31,10 +31,18 @@
<div class="mb-3 col-lg-2">
<input class="form-control" value='Warehouse' disabled>
</div>
<div class="mb-3 col-lg-4">
<div class="mb-3 col-lg-2">
<select name="warehouse" id="warehouse" class="form-select" required></select>
</div>
<div class="mb-3 col-lg-6">
<div class="mb-3 col-lg-2">
<input class="form-control" value='Source' disabled>
</div>
<div class="mb-3 col-lg-2">
<select name="source" id="source" class="form-select">
<option value="">All Sources</option>
</select>
</div>
<div class="mb-3 col-lg-2">
<button class='btn btn-secondary' onclick='retrieve_stock_in()'>Search</button>
</div>
@@ -50,7 +58,8 @@
<th>Product</th>
<th>Lot Number</th>
<th>Serial Number</th>
<th>Quantity</th>
<th style="text-align: right;">Quantity</th>
<th>Source</th>
<th class="th_loc_zone">Zone</th>
<th class="th_loc_aisle">Aisle</th>
<th class="th_loc_rack">Rack</th>
@@ -103,6 +112,70 @@
return `<td class="py-3">${item['rack'] || ''}</td>`;
}
function escape_html(value) {
return String(value ?? '').replace(/[&<>"']/g, function(char) {
return {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}[char];
});
}
function source_label(source) {
source = source || '';
if (!source) return 'Manual';
return source.charAt(0).toUpperCase() + source.slice(1).replace(/_/g, ' ');
}
function source_cell(source) {
var label = escape_html(source_label(source));
return source
? `<span class="badge bg-info-subtle text-info">${label}</span>`
: `<span class="badge bg-secondary-subtle text-secondary">${label}</span>`;
}
function populate_source_filter(rows) {
var current = $('#source').val() || '';
var sources = {};
$.each(rows, function(_, item) {
sources[item.source || '__manual__'] = item.source || '';
});
var option = `<option value="">All Sources</option>`;
$.each(Object.keys(sources).sort(), function(_, key) {
var value = sources[key];
var option_value = value || '__manual__';
option += `<option value="${escape_html(option_value)}">${escape_html(source_label(value))}</option>`;
});
$('#source').html(option).val(current);
if ($('#source').val() !== current) $('#source').val('');
}
function apply_source_filter() {
var selected = $('#source').val() || '';
var rows = alasql.tables.stock_in_all ? alasql.tables.stock_in_all.data : [];
if (!selected) {
alasql.tables.stock_in.data = rows;
} else {
alasql.tables.stock_in.data = rows.filter(function(item) {
var source = item.source || '';
return selected === '__manual__' ? !source : source === selected;
});
}
var table_id = 'stock_in';
var total_records = alasql.tables.stock_in.data.length;
var page = generate_pagination(table_id, total_records);
$(`table#${table_id} tfoot`).html(page);
change_page_stock_in(1);
}
function retrieve_stock_in() {
return ajax_request({
@@ -114,22 +187,12 @@
onSuccess: function(res) {
// Register the array as a table name
alasql('CREATE TABLE IF NOT EXISTS stock_in_all');
alasql('CREATE TABLE IF NOT EXISTS stock_in');
alasql.tables.stock_in.data = res.output || [];
alasql.tables.stock_in_all.data = res.output || [];
var total_records = alasql.tables.stock_in.data.length;
var table_id = 'stock_in';
var page = generate_pagination(table_id, total_records);
$(`table#${table_id} tfoot`).html(page);
// dynamic function name
if (typeof window[`change_page_${table_id}`] === "function") {
window[`change_page_${table_id}`](1);
} else {
console.error(`Function change_page_${table_id} does not exist.`);
}
populate_source_filter(alasql.tables.stock_in_all.data);
apply_source_filter();
}
});
@@ -153,19 +216,19 @@
<td class="py-3">${item["product_sku"]}: ${item['product_name']}</td>
<td class="py-3">${item['lot_number'] || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${item['serial_number'] || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${item['quantity']}</td>
<td class="py-3">
<div class="d-flex justify-content-end align-items-baseline gap-1">
<span>${format_number(item['quantity'], 2)}</span>
<span class="text-muted small" style="min-width:28px;">${item['uom'] || ''}</span>
</div>
</td>
<td class="py-3">${source_cell(item['source'])}</td>
${location_cells(item)}
<td class="py-3">
${item['status'] == 1 ? '<span class="badge bg-success">Approved</span>' : '<span class="badge bg-warning text-dark">Draft</span>'}
</td>
<td class="py-3">
<a href="<?php echo $server_url?>ics/manage_stock_in.php?id=${item['id']}&wh=${warehouse}" class=""><i class="ti ti-eye fs-5"></i></a>
${item['status'] == 0
? (auto_approve
? '<button class="btn btn-sm btn-outline-secondary ms-2" disabled title="Auto-approved on save — no manual approval needed"><i class=\"ti ti-check\"></i></button>'
: '<button class="btn btn-sm btn-outline-success ms-2" onclick=\"approve_stock('+item['id']+', '+item['warehouse_id']+', \'in\')\" title="Approve"><i class=\"ti ti-check\"></i></button>'
)
: ''}
<a href="javascript:void(0);" class="link-danger"
onclick="delete_stock_in($(this),${item['id']})">
<i class="ti ti-trash ms-2 fs-5"></i>
@@ -225,6 +288,9 @@
console.log(e);
}
});
$(document).on('change', '#source', apply_source_filter);
$(document).on('change', '#warehouse', retrieve_stock_in);
// ── Stock approval ────────────────────────────────────────────────────────
var auto_approve = false; // set from company_setting on load
@@ -258,11 +324,13 @@
autoPrepare: true,
checkRequired: 0,
action: 'update',
id: id,
warehouse: warehouse_id,
type: type,
data: {
id: id,
warehouse: warehouse_id,
type: type
},
onSuccess: function(res) {
retrieve_stock_list();
retrieve_stock_in();
}
});
}
+96 -28
View File
@@ -31,10 +31,18 @@
<div class="mb-3 col-lg-2">
<input class="form-control" value='Warehouse' disabled>
</div>
<div class="mb-3 col-lg-4">
<div class="mb-3 col-lg-2">
<select name="warehouse" id="warehouse" class="form-select" required></select>
</div>
<div class="mb-3 col-lg-6">
<div class="mb-3 col-lg-2">
<input class="form-control" value='Source' disabled>
</div>
<div class="mb-3 col-lg-2">
<select name="source" id="source" class="form-select">
<option value="">All Sources</option>
</select>
</div>
<div class="mb-3 col-lg-2">
<button class='btn btn-secondary' onclick='retrieve_stock_out()'>Search</button>
</div>
@@ -50,7 +58,8 @@
<th>Product</th>
<th>Lot Number</th>
<th>Serial Number</th>
<th>Quantity</th>
<th style="text-align: right;">Quantity</th>
<th>Source</th>
<th class="th_loc_zone">Zone</th>
<th class="th_loc_aisle">Aisle</th>
<th class="th_loc_rack">Rack</th>
@@ -103,6 +112,70 @@
return `<td class="py-3">${item['rack'] || ''}</td>`;
}
function escape_html(value) {
return String(value ?? '').replace(/[&<>"']/g, function(char) {
return {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}[char];
});
}
function source_label(source) {
source = source || '';
if (!source) return 'Manual';
return source.charAt(0).toUpperCase() + source.slice(1).replace(/_/g, ' ');
}
function source_cell(source) {
var label = escape_html(source_label(source));
return source
? `<span class="badge bg-info-subtle text-info">${label}</span>`
: `<span class="badge bg-secondary-subtle text-secondary">${label}</span>`;
}
function populate_source_filter(rows) {
var current = $('#source').val() || '';
var sources = {};
$.each(rows, function(_, item) {
sources[item.source || '__manual__'] = item.source || '';
});
var option = `<option value="">All Sources</option>`;
$.each(Object.keys(sources).sort(), function(_, key) {
var value = sources[key];
var option_value = value || '__manual__';
option += `<option value="${escape_html(option_value)}">${escape_html(source_label(value))}</option>`;
});
$('#source').html(option).val(current);
if ($('#source').val() !== current) $('#source').val('');
}
function apply_source_filter() {
var selected = $('#source').val() || '';
var rows = alasql.tables.stock_out_all ? alasql.tables.stock_out_all.data : [];
if (!selected) {
alasql.tables.stock_out.data = rows;
} else {
alasql.tables.stock_out.data = rows.filter(function(item) {
var source = item.source || '';
return selected === '__manual__' ? !source : source === selected;
});
}
var table_id = 'stock_out';
var total_records = alasql.tables.stock_out.data.length;
var page = generate_pagination(table_id, total_records);
$(`table#${table_id} tfoot`).html(page);
change_page_stock_out(1);
}
function retrieve_stock_out() {
return ajax_request({
@@ -114,22 +187,12 @@
onSuccess: function(res) {
// Register the array as a table name
alasql('CREATE TABLE IF NOT EXISTS stock_out_all');
alasql('CREATE TABLE IF NOT EXISTS stock_out');
alasql.tables.stock_out.data = res.output || [];
alasql.tables.stock_out_all.data = res.output || [];
var total_records = alasql.tables.stock_out.data.length;
var table_id = 'stock_out';
var page = generate_pagination(table_id, total_records);
$(`table#${table_id} tfoot`).html(page);
// dynamic function name
if (typeof window[`change_page_${table_id}`] === "function") {
window[`change_page_${table_id}`](1);
} else {
console.error(`Function change_page_${table_id} does not exist.`);
}
populate_source_filter(alasql.tables.stock_out_all.data);
apply_source_filter();
}
});
@@ -153,19 +216,19 @@
<td class="py-3">${item["product_sku"]}: ${item['product_name']}</td>
<td class="py-3">${item['lot_number'] || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${item['serial_number'] || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${item['quantity']}</td>
<td class="py-3">
<div class="d-flex justify-content-end align-items-baseline gap-1">
<span>${format_number(item['quantity'], 2)}</span>
<span class="text-muted small" style="min-width:28px;">${item['uom'] || ''}</span>
</div>
</td>
<td class="py-3">${source_cell(item['source'])}</td>
${location_cells(item)}
<td class="py-3">
${item['status'] == 1 ? '<span class="badge bg-success">Approved</span>' : '<span class="badge bg-warning text-dark">Draft</span>'}
</td>
<td class="py-3">
<a href="<?php echo $server_url?>ics/manage_stock_out.php?id=${item['id']}&wh=${warehouse}" class=""><i class="ti ti-eye fs-5"></i></a>
${item['status'] == 0
? (auto_approve
? '<button class="btn btn-sm btn-outline-secondary ms-2" disabled title="Auto-approved on save — no manual approval needed"><i class=\"ti ti-check\"></i></button>'
: '<button class="btn btn-sm btn-outline-success ms-2" onclick=\"approve_stock('+item['id']+', '+item['warehouse_id']+', \'out\')\" title="Approve"><i class=\"ti ti-check\"></i></button>'
)
: ''}
<a href="javascript:void(0);" class="link-danger"
onclick="delete_stock_out($(this),${item['id']})">
<i class="ti ti-trash ms-2 fs-5"></i>
@@ -225,6 +288,9 @@
console.log(e);
}
});
$(document).on('change', '#source', apply_source_filter);
$(document).on('change', '#warehouse', retrieve_stock_out);
// ── Stock approval ────────────────────────────────────────────────────────
var auto_approve = false; // set from company_setting on load
@@ -258,11 +324,13 @@
autoPrepare: true,
checkRequired: 0,
action: 'update',
id: id,
warehouse: warehouse_id,
type: type,
data: {
id: id,
warehouse: warehouse_id,
type: type
},
onSuccess: function(res) {
retrieve_stock_list();
retrieve_stock_out();
}
});
}
+15 -12
View File
@@ -48,7 +48,7 @@
<tr>
<th>Date</th>
<th>Product</th>
<th>Quantity</th>
<th style="text-align: right;">Quantity</th>
<th class="th_loc_zone">Zone</th>
<th class="th_loc_aisle">Aisle</th>
<th class="th_loc_rack">Rack</th>
@@ -149,19 +149,18 @@
body += `<tr>
<td class="py-3">${format_date(item["date"])}</td>
<td class="py-3">${item["product_sku"]}: ${item['product_name']}</td>
<td class="py-3">${item['quantity']}</td>
<td class="py-3">
<div class="d-flex justify-content-end align-items-baseline gap-1">
<span>${format_number(item['quantity'], 2)}</span>
<span class="text-muted small" style="min-width:28px;">${item['uom'] || ''}</span>
</div>
</td>
${location_cells(item)}
<td class="py-3">
${item['status'] == 1 ? '<span class="badge bg-success">Approved</span>' : '<span class="badge bg-warning text-dark">Draft</span>'}
</td>
<td class="py-3">
<a href="<?php echo $server_url?>ics/manage_stock_transfer.php?id=${item['id']}&wh=${warehouse}" class=""><i class="ti ti-eye fs-5"></i></a>
${item['status'] == 0
? (auto_approve
? '<button class="btn btn-sm btn-outline-secondary ms-2" disabled title="Auto-approved on save — no manual approval needed"><i class=\"ti ti-check\"></i></button>'
: '<button class="btn btn-sm btn-outline-success ms-2" onclick=\"approve_stock('+item['id']+', '+item['warehouse_id']+', \'transfer\')\" title="Approve"><i class=\"ti ti-check\"></i></button>'
)
: ''}
<a href="javascript:void(0);" class="link-danger"
onclick="delete_stock_transfer($(this),${item['id']})">
<i class="ti ti-trash ms-2 fs-5"></i>
@@ -223,6 +222,8 @@
console.log(e);
}
});
$(document).on('change', '#warehouse', retrieve_stock_transfer);
// ── Stock approval ────────────────────────────────────────────────────────
var auto_approve = false; // set from company_setting on load
@@ -256,11 +257,13 @@
autoPrepare: true,
checkRequired: 0,
action: 'update',
id: id,
warehouse: warehouse_id,
type: type,
data: {
id: id,
warehouse: warehouse_id,
type: type
},
onSuccess: function(res) {
retrieve_stock_list();
retrieve_stock_transfer();
}
});
}