Files
wms-app/app/po/manage_po.php
T

1012 lines
38 KiB
PHP

<?php
session_start();
require '../config.php';
require '../include_header.php';
$po_id = (int)($_GET['id'] ?? 0);
?>
<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">
<div class="col-12">
<div class="mb-6 d-flex justify-content-between align-items-center">
<div>
<h1 class="fs-3 mb-1" id="page_title">
<?php echo $po_id ? 'Purchase Order Detail' : 'New Purchase Order'; ?>
</h1>
<p class="mb-0 text-muted" id="page_subtitle">
<?php echo $po_id ? 'View or manage this purchase order' : 'Create a new purchase order'; ?>
</p>
</div>
<div class="d-flex gap-2">
<a href="<?php echo $server_url?>po/po.php" class="btn btn-light">
<i class="ti ti-arrow-left me-1"></i>Back
</a>
</div>
</div>
</div>
</div>
<div class="row g-5">
<!-- Left: PO form -->
<div class="col-lg-8">
<!-- PO header card -->
<div class="card mb-5">
<div class="card-body p-5">
<h2 class="fs-5 mb-4">Order Information</h2>
<div class="row gx-5">
<div class="mb-3 col-lg-6">
<label class="form-label">Supplier</label>
<input type="text" id="contact" class="form-control contact_search"
placeholder="Search supplier..." autocomplete="off">
<input type="hidden" id="contact_id">
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">PO Date</label>
<input type="text" id="po_date" class="form-control"
placeholder="DD/MM/YYYY" autocomplete="off">
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">Expected Delivery <span class="text-muted fw-normal">(optional)</span></label>
<input type="text" id="expected_date" class="form-control"
placeholder="DD/MM/YYYY" autocomplete="off">
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">Default Warehouse</label>
<select id="warehouse_id" class="form-select">
<option value="">Select warehouse...</option>
</select>
</div>
<div class="mb-3 col-lg-12">
<label class="form-label">Notes</label>
<textarea id="notes" class="form-control" rows="2"
placeholder="Internal notes..."></textarea>
</div>
</div>
</div>
</div>
<!-- PO items card -->
<div class="card mb-5">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fs-5 mb-0">Items to Order</h2>
<button class="btn btn-sm btn-outline-primary" id="btn_add_item"
onclick="add_item_row()">
<i class="ti ti-plus me-1"></i>Add Item
</button>
</div>
<div class="table-responsive">
<table class="table mb-0" id="item_table">
<thead class="table-light">
<tr>
<th>Product</th>
<th style="width:120px;">Qty</th>
<th style="width:130px;">Unit Price</th>
<th style="width:130px;">Total</th>
<th style="width:40px;"></th>
</tr>
</thead>
<tbody id="item_tbody"></tbody>
</table>
</div>
</div>
</div>
<!-- Receive goods card (confirmed / partial only) -->
<div class="card mb-5 d-none" id="receive_card">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-start mb-4">
<div>
<h2 class="fs-5 mb-1">Receive Goods</h2>
<p class="mb-0 text-muted small">Enter quantities received and assign rack locations for each line</p>
</div>
<button class="btn btn-success" id="btn_receive" onclick="receive_goods()">
<i class="ti ti-package-import me-1"></i>Confirm Receipt
</button>
</div>
<div id="receive_rows_container"></div>
</div>
</div>
</div>
<!-- Right: Summary + actions -->
<div class="col-lg-4">
<!-- Status card (view mode only) -->
<div class="card mb-5 d-none" id="status_card">
<div class="card-body p-4">
<h2 class="fs-5 mb-3">Status</h2>
<div class="d-flex gap-2 mb-2">
<span id="badge_status"></span>
<span id="badge_payment"></span>
</div>
<p class="mb-0 small text-muted" id="po_number_display"></p>
<p class="mb-0 small text-muted d-none" id="expected_date_display"></p>
</div>
</div>
<!-- Totals card -->
<div class="card mb-5">
<div class="card-body p-4">
<h2 class="fs-5 mb-3">Summary</h2>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Subtotal</span>
<span id="summary_subtotal">0.00</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Discount</span>
<input type="number" id="discount" class="form-control form-control-sm text-end"
style="width:110px;" value="0" min="0" oninput="recalc_totals()">
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Tax</span>
<input type="number" id="tax" class="form-control form-control-sm text-end"
style="width:110px;" value="0" min="0" oninput="recalc_totals()">
</div>
<div class="d-flex justify-content-between mb-3">
<span class="text-muted">Shipping</span>
<input type="number" id="shipping_fee" class="form-control form-control-sm text-end"
style="width:110px;" value="0" min="0" oninput="recalc_totals()">
</div>
<hr>
<div class="d-flex justify-content-between fw-bold fs-5">
<span>Grand Total</span>
<span id="summary_grand_total">0.00</span>
</div>
</div>
</div>
<!-- Action buttons -->
<div class="card">
<div class="card-body p-4 d-flex flex-column gap-2">
<button class="btn btn-primary w-100" id="btn_save" onclick="save_po()">
<i class="ti ti-device-floppy me-1"></i>Save Draft
</button>
<?php if ($po_id): ?>
<button class="btn btn-success w-100" id="btn_confirm" onclick="confirm_po()">
<i class="ti ti-check me-1"></i>Confirm PO
</button>
<?php endif; ?>
<div id="payment_status_group" class="d-none">
<label class="form-label small text-muted mb-1">Payment Status</label>
<div class="d-flex gap-2">
<select id="payment_status_select" class="form-select form-select-sm">
<option value="0">Unpaid</option>
<option value="1">Paid</option>
<option value="2">Partial</option>
</select>
<button class="btn btn-sm btn-outline-primary text-nowrap"
onclick="update_payment_status()">
<i class="ti ti-check"></i>
</button>
</div>
</div>
<button class="btn btn-outline-danger w-100" id="btn_cancel" onclick="cancel_po()">
<i class="ti ti-x me-1"></i>Cancel PO
</button>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
var po_id = <?php echo $po_id; ?>;
var po_data = null;
var warehouses = [];
var receive_warehouses = [];
var item_index = 0;
var recv_expiry_pickers = {};
// ── Location config (loaded from company settings) ────────────────────────
var advanced = false;
var label_rack = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
// ── Status helpers ────────────────────────────────────────────────────────
function po_status_badge(status) {
const map = {
'-1': '<span class="badge bg-secondary">Cancelled</span>',
'0': '<span class="badge bg-warning text-dark">Draft</span>',
'1': '<span class="badge bg-info text-white">Confirmed</span>',
'2': '<span class="badge bg-primary">Partial</span>',
'3': '<span class="badge bg-success">Completed</span>',
};
return map[String(status)] ?? '—';
}
function po_payment_badge(status) {
const map = {
'0': '<span class="badge bg-warning text-dark">Unpaid</span>',
'1': '<span class="badge bg-success">Paid</span>',
'2': '<span class="badge bg-info text-white">Partial</span>',
};
return map[String(status)] ?? '—';
}
// ── Recalculate totals ────────────────────────────────────────────────────
function recalc_totals() {
var subtotal = 0;
$('#item_tbody tr').each(function() {
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
var price = parseFloat($(this).find('.item_price').val()) || 0;
var total = qty * price;
$(this).find('.item_total').text(format_number(total, 2));
subtotal += total;
});
var discount = parseFloat($('#discount').val()) || 0;
var tax = parseFloat($('#tax').val()) || 0;
var shipping_fee = parseFloat($('#shipping_fee').val()) || 0;
$('#summary_subtotal').text(format_number(subtotal, 2));
$('#summary_grand_total').text(format_number(subtotal - discount + tax + shipping_fee, 2));
}
// ── Add item row ──────────────────────────────────────────────────────────
function add_item_row(item) {
item = item || {};
var idx = item_index++;
var row = `<tr id="item_row_${idx}">
<td>
<input type="text" class="form-control form-control-sm item_sku product_search"
placeholder="SKU..." value="${item.product_sku || ''}" autocomplete="off" data-idx="${idx}">
<input type="hidden" class="item_name" value="${item.product_name || ''}">
<input type="hidden" class="item_id" value="${item.item_id ?? idx}">
<small class="text-muted item_name_display">${item.product_name || ''}</small>
</td>
<td>
<input type="number" class="form-control form-control-sm item_qty"
value="${item.quantity || 1}" min="0.0001" step="any" oninput="recalc_totals()">
</td>
<td>
<input type="number" class="form-control form-control-sm item_price"
value="${item.unit_price || 0}" min="0" step="any" oninput="recalc_totals()">
</td>
<td class="item_total text-end">${format_number((item.quantity || 1) * (item.unit_price || 0), 2)}</td>
<td>
<a href="javascript:void(0);" class="link-danger remove_item_btn" onclick="remove_item(${idx})">
<i class="ti ti-trash fs-5"></i>
</a>
</td>
</tr>`;
$('#item_tbody').append(row);
recalc_totals();
}
function remove_item(idx) {
$(`#item_row_${idx}`).remove();
recalc_totals();
}
// ── Collect items from DOM ────────────────────────────────────────────────
function collect_items() {
var items = [];
var idx = 0;
$('#item_tbody tr').each(function() {
var sku = $(this).find('.item_sku').val().trim();
var name = $(this).find('.item_name').val().trim();
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
var price = parseFloat($(this).find('.item_price').val()) || 0;
if (!sku || qty <= 0) { idx++; return; }
items.push({
item_id: idx,
product_sku: sku,
product_name: name,
quantity: qty,
unit_price: price,
total_price: qty * price,
received_qty: 0,
stock_in_id: 0,
});
idx++;
});
return items;
}
// ── Location config loader ────────────────────────────────────────────────
function load_location_config() {
return ajax_request({
url: '<?php echo $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';
}
});
}
// ── Cascade helpers (scoped per receive row by rowId) ─────────────────────
function recv_load_zones(rowId, callback) {
var wh_id = $(`#recv_wh_${rowId}`).val();
if (!wh_id) return;
ajax_request({
url: '<?php echo $server_url?>ics/api/engine/retrieve_zone.php',
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false,
action: 'read',
data: { type: 'to', warehouse_to: wh_id },
onSuccess: function(res) {
var opts = `<option value="">Select ${label_zone.toLowerCase()}...</option>`;
(res.output || []).forEach(function(z) {
opts += `<option value="${z.zone}">${z.zone}</option>`;
});
$(`#recv_zone_${rowId}`).html(opts);
$(`#recv_aisle_${rowId}`).html(`<option value="">Select ${label_aisle.toLowerCase()}...</option>`);
$(`#recv_rack_${rowId}`).html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`);
if (callback) callback();
}
});
}
function recv_load_aisles(rowId, callback) {
var wh_id = $(`#recv_wh_${rowId}`).val();
var zone = $(`#recv_zone_${rowId}`).val();
if (!wh_id || !zone) return;
ajax_request({
url: '<?php echo $server_url?>ics/api/engine/retrieve_aisle.php',
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false,
action: 'read',
data: { type: 'to', warehouse_to: wh_id, zone_to: zone },
onSuccess: function(res) {
var opts = `<option value="">Select ${label_aisle.toLowerCase()}...</option>`;
(res.output || []).forEach(function(a) {
opts += `<option value="${a}">${a}</option>`;
});
$(`#recv_aisle_${rowId}`).html(opts);
$(`#recv_rack_${rowId}`).html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`);
if (callback) callback();
}
});
}
function recv_load_racks(rowId, callback) {
var wh_id = $(`#recv_wh_${rowId}`).val();
var zone = advanced ? ($(`#recv_zone_${rowId}`).val() || '') : '';
var aisle = advanced ? ($(`#recv_aisle_${rowId}`).val() || '') : '';
if (!wh_id) return;
ajax_request({
url: '<?php echo $server_url?>ics/api/engine/retrieve_rack.php',
autoPrepare: true, checkRequired: 0, noLoading: true, queueLock: false,
action: 'read',
data: { type: 'to', warehouse_to: wh_id, zone_to: zone, aisle_to: aisle, id: 0 },
onSuccess: function(res) {
var opts = `<option value="">Select ${label_rack.toLowerCase()}...</option>`;
(res.output || []).forEach(function(r) {
opts += `<option value="${r}">${r}</option>`;
});
$(`#recv_rack_${rowId}`).html(opts);
if (callback) callback();
}
});
}
function lock_recv_expiry(rowId, expiry_date) {
var picker = recv_expiry_pickers[rowId];
if (picker) {
picker.setDate(expiry_date || '', true, 'Y-m-d');
if (picker.altInput) picker.altInput.disabled = true;
}
$(`#recv_expiry_${rowId}`)
.prop('disabled', true)
.attr('title', 'Expiry date is locked for existing lots');
}
function unlock_recv_expiry(rowId) {
var picker = recv_expiry_pickers[rowId];
$(`#recv_expiry_${rowId}`).prop('disabled', false).removeAttr('title');
if (picker) {
picker.clear();
if (picker.altInput) picker.altInput.disabled = false;
}
}
function init_receive_lot_search(rowId) {
var $lot = $(`#recv_lot_${rowId}`);
$lot.autocomplete({
source: function(request, response) {
if ($lot.data('xhr')) {
$lot.data('xhr').abort();
}
var $card = $lot.closest('[data-row]');
var q = prepare_form_data(0, true);
q.keyword = request.term;
q.product_sku = $card.data('sku') || '';
var xhr = $.ajax({
url: server_url + 'ics/api/engine/retrieve_lot.php',
type: 'POST',
dataType: 'json',
data: { json: JSON.stringify(q) },
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
},
success: function(res) {
response((res.result || []).map(function(item) {
var label = `
<div class="lot-result-item" style="font-family:'Poppins',sans-serif;padding:4px 0;">
<div style="font-size:10px;text-transform:uppercase;color:#E66239;font-weight:600;line-height:1.2;">
Lot: ${item.lot_number}
</div>
<div style="font-size:13px;font-weight:500;color:#171717;margin-top:2px;">
Expiry: ${item.expiry_date || 'N/A'}
</div>
</div>`;
return {
label: label,
value: item.lot_number,
data: item
};
}));
}
});
$lot.data('xhr', xhr);
},
minLength: 0,
delay: 300,
select: function(e, ui) {
if (!ui.item || !ui.item.data) return;
var lot = ui.item.data;
$lot
.val(lot.lot_number)
.attr('secondary', lot.lot_number)
.attr('data-existing', '1');
lock_recv_expiry(rowId, lot.expiry_date || '');
return false;
}
}).data('ui-autocomplete')._renderItem = function(ul, item) {
return $('<li>').append($('<div>').html(item.label)).appendTo(ul);
};
$lot.on('blur', function() {
var secondary = $lot.attr('secondary');
if (secondary) $lot.val(secondary);
});
$lot.on('focus', function() {
$lot.autocomplete('search', $lot.val());
});
$lot.on('input', function() {
$lot.removeAttr('data-existing secondary');
unlock_recv_expiry(rowId);
});
}
// Called when warehouse select changes on a receive row
function on_recv_wh_change(rowId) {
if (advanced) {
recv_load_zones(rowId);
} else {
recv_load_racks(rowId);
}
}
// ── Build receive rows (one card per receivable item) ─────────────────────
function build_receive_rows(receivable) {
var wh_options = `<option value="">Select warehouse...</option>`;
receive_warehouses.forEach(function(w) {
wh_options += `<option value="${w.id}">${w.warehouse_name}</option>`;
});
var default_wh = po_data ? (po_data.warehouse_id || '') : '';
var html = '';
$.each(receivable, function(i, item) {
var rowId = 'r' + i; // short safe id, no special chars
var zone_aisle_html = '';
if (advanced) {
zone_aisle_html = `
<div class="col-lg-4">
<label class="form-label small text-muted">${label_zone}</label>
<select id="recv_zone_${rowId}" class="form-select form-select-sm"
onchange="recv_load_aisles('${rowId}')">
<option value="">Select ${label_zone.toLowerCase()}...</option>
</select>
</div>
<div class="col-lg-4">
<label class="form-label small text-muted">${label_aisle}</label>
<select id="recv_aisle_${rowId}" class="form-select form-select-sm"
onchange="recv_load_racks('${rowId}')">
<option value="">Select ${label_aisle.toLowerCase()}...</option>
</select>
</div>`;
}
var rack_col_class = advanced ? 'col-lg-4' : 'col-lg-6';
html += `
<div class="border rounded p-4 mb-4"
data-item-id="${item.item_id}"
data-sku="${item.product_sku}"
data-row="${rowId}">
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<span class="fw-semibold">${item.product_sku}</span>
<span class="text-muted ms-2 small">${item.product_name || ''}</span>
</div>
<div class="d-flex gap-3 text-muted small">
<span>Ordered: <strong>${format_number(item.ordered_qty, 0)}</strong></span>
<span>Received: <strong>${format_number(item.received_qty, 0)}</strong></span>
<span>Remaining: <strong class="text-primary">${format_number(item.remaining_qty, 0)}</strong></span>
</div>
</div>
<div class="row g-3">
<div class="col-lg-3">
<label class="form-label small text-muted">Warehouse <span class="text-danger">*</span></label>
<select id="recv_wh_${rowId}" class="form-select form-select-sm"
onchange="on_recv_wh_change('${rowId}')">
${wh_options}
</select>
</div>
<div class="col-lg-3">
<label class="form-label small text-muted">Receiving Qty <span class="text-danger">*</span></label>
<input type="number" id="recv_qty_${rowId}" class="form-control form-control-sm"
value="${item.remaining_qty}" min="0.0001" max="${item.remaining_qty}" step="any">
</div>
<div class="col-lg-3">
<label class="form-label small text-muted">Lot Number</label>
<input type="text" id="recv_lot_${rowId}" class="form-control form-control-sm recv_lot_search"
placeholder="Type to search or enter new lot..." autocomplete="off">
</div>
<div class="col-lg-3">
<label class="form-label small text-muted">Expiry Date</label>
<input type="text" id="recv_expiry_${rowId}" class="form-control form-control-sm"
placeholder="DD/MM/YYYY">
</div>
<div class="col-lg-3">
<label class="form-label small text-muted">Serial Number</label>
<input type="text" id="recv_serial_${rowId}" class="form-control form-control-sm"
placeholder="Optional...">
</div>
${zone_aisle_html}
<div class="${rack_col_class}">
<label class="form-label small text-muted">${label_rack} <span class="text-danger">*</span></label>
<select id="recv_rack_${rowId}" class="form-select form-select-sm">
<option value="">Select ${label_rack.toLowerCase()}...</option>
</select>
</div>
</div>
</div>`;
});
$('#receive_rows_container').html(html);
// Init flatpickr on each expiry field with ISO value, alt dd/mm/yyyy display
$.each(receivable, function(i) {
var rowId = 'r' + i;
recv_expiry_pickers[rowId] = flatpickr(`#recv_expiry_${rowId}`, {
dateFormat: 'Y-m-d',
altInput: true,
altFormat: 'd/m/Y',
allowInput: true,
onReady: function(_, __, fp) {
fp.altInput.classList.add('form-control', 'form-control-sm');
}
});
init_receive_lot_search(rowId);
});
// Pre-select default warehouse and trigger cascade for each row
$.each(receivable, function(i) {
var rowId = 'r' + i;
if (default_wh) {
$(`#recv_wh_${rowId}`).val(default_wh);
on_recv_wh_change(rowId);
}
});
}
// ── Set view/edit mode ────────────────────────────────────────────────────
function set_mode(status, receivable_count) {
status = parseInt(status);
receivable_count = receivable_count ?? 0;
var is_draft = status === 0;
var can_confirm = status === 0;
var can_cancel = status >= 0 && status <= 1;
var can_receive = (status === 1 || status === 2) && receivable_count > 0;
var is_cancelled = status === -1;
$('#btn_add_item, #contact, #po_date, #expected_date, #warehouse_id, #notes, #discount, #tax, #shipping_fee')
.prop('disabled', !is_draft);
$('.item_sku, .item_qty, .item_price').prop('disabled', !is_draft);
$('.remove_item_btn').toggleClass('d-none', !is_draft);
set_btn_state('#btn_save', is_draft, 'Only draft POs can be edited');
set_btn_state('#btn_confirm', can_confirm, 'PO must be in draft to confirm');
set_btn_state('#btn_cancel', can_cancel && !is_cancelled,
is_cancelled ? 'PO is already cancelled' : 'Only draft or confirmed POs can be cancelled');
$('#payment_status_group').toggleClass('d-none', status < 1 || is_cancelled);
$('#receive_card').toggleClass('d-none', !can_receive);
}
// ── Load existing PO ──────────────────────────────────────────────────────
function retrieve_po() {
if (!po_id) return Promise.resolve();
return ajax_request({
url: '<?php echo $server_url?>po/api/engine/retrieve_po.php',
autoPrepare: true, checkRequired: 0,
action: 'read',
data: { id: po_id },
onSuccess: function(res) {
po_data = res.output;
var o = po_data;
$('#page_title').text(o.po_number);
$('#page_subtitle').text('');
$('#status_card').removeClass('d-none');
$('#badge_status').html(po_status_badge(o.status));
$('#badge_payment').html(po_payment_badge(o.payment_status));
$('#po_number_display').text(o.po_number);
$('#expected_date_display')
.toggleClass('d-none', !o.expected_date)
.text(o.expected_date ? 'Expected: ' + format_date(o.expected_date) : '');
$('#contact').val(o.contact_name || '');
$('#contact_id').val(o.contact_id || '');
$('#po_date').val(o.po_date ? format_date(o.po_date) : '');
$('#expected_date').val(o.expected_date ? format_date(o.expected_date) : '');
$('#warehouse_id').val(o.warehouse_id || '');
$('#notes').val(o.notes || '');
$('#discount').val(o.discount || 0);
$('#tax').val(o.tax || 0);
$('#shipping_fee').val(o.shipping_fee || 0);
$('#payment_status_select').val(o.payment_status ?? 0);
$('#item_tbody').empty();
(o.items || []).forEach(function(item) { add_item_row(item); });
recalc_totals();
var receivable = res.receivable_items || [];
set_mode(o.status, receivable.length);
if (receivable.length > 0) {
build_receive_rows(receivable);
}
}
});
}
// ── Save draft ────────────────────────────────────────────────────────────
function save_po() {
var items = collect_items();
if (!items.length) { bootbox.alert('Please add at least one item.'); return; }
ajax_request({
url: '<?php echo $server_url?>po/api/engine/manage_po.php',
autoPrepare: true, checkRequired: 0,
action: po_id ? 'update' : 'create',
data: {
id: po_id,
contact_id: $('#contact_id').val() || 0,
po_date: to_iso_date($('#po_date').val()),
expected_date: to_iso_date($('#expected_date').val()) || '',
warehouse_id: $('#warehouse_id').val() || 0,
items: JSON.stringify(items),
discount: $('#discount').val() || 0,
tax: $('#tax').val() || 0,
shipping_fee: $('#shipping_fee').val() || 0,
notes: $('#notes').val(),
},
onSuccess: function(res) {
if (!po_id && res.new_id) {
window.location.href = '<?php echo $server_url?>po/manage_po.php?id=' + res.new_id;
} else {
retrieve_po();
}
}
});
}
// ── Confirm PO ────────────────────────────────────────────────────────────
function confirm_po() {
bootbox.confirm({
message: 'Confirm this purchase order? It will be locked for editing.',
buttons: {
confirm: { label: 'Confirm PO', className: 'btn-success' },
cancel: { label: 'Back', className: 'btn-secondary' }
},
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>po/api/engine/confirm_po.php',
autoPrepare: true, checkRequired: 0,
action: 'update',
data: { id: po_id },
onSuccess: function() { retrieve_po(); }
});
}
});
}
// ── Receive goods ─────────────────────────────────────────────────────────
function receive_goods() {
var receive_items = [];
var errors = [];
$('#receive_rows_container [data-row]').each(function() {
var $card = $(this);
var rowId = $card.data('row');
var qty = parseFloat($(`#recv_qty_${rowId}`).val()) || 0;
if (qty <= 0) return;
var wh_id = parseInt($(`#recv_wh_${rowId}`).val()) || 0;
var rack = $(`#recv_rack_${rowId}`).val() || '';
if (!wh_id) {
errors.push(`${$card.data('sku')}: please select a warehouse.`);
return;
}
if (!rack) {
errors.push(`${$card.data('sku')}: please select a ${label_rack.toLowerCase()}.`);
return;
}
var zone = advanced ? ($(`#recv_zone_${rowId}`).val() || '') : '';
var aisle = advanced ? ($(`#recv_aisle_${rowId}`).val() || '') : '';
if (!advanced) {
zone = rack;
aisle = rack;
}
// flatpickr with altInput: the real (hidden) input holds the ISO value
var expiry_val = $(`#recv_expiry_${rowId}`).val() || '';
receive_items.push({
item_id: parseInt($card.data('item-id')),
product_sku: $card.data('sku'),
warehouse_id: wh_id,
quantity: qty,
lot_number: $(`#recv_lot_${rowId}`).val().trim(),
expiry_date: expiry_val,
serial_number: $(`#recv_serial_${rowId}`).val().trim(),
zone: zone,
aisle: aisle,
rack: rack,
});
});
if (errors.length) {
bootbox.alert('<ul class="mb-0 ps-3">' + errors.map(e => `<li>${e}</li>`).join('') + '</ul>');
return;
}
if (!receive_items.length) {
bootbox.alert('Please enter at least one quantity to receive.');
return;
}
bootbox.confirm({
message: `Receive ${receive_items.length} item line(s)? Stock-in rows will be created.`,
buttons: {
confirm: { label: 'Receive Goods', className: 'btn-success' },
cancel: { label: 'Back', className: 'btn-secondary' }
},
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>po/api/engine/receive_po.php',
autoPrepare: true, checkRequired: 0,
action: 'create',
data: {
id: po_id,
receive_items: JSON.stringify(receive_items),
},
onSuccess: function(res) {
var msg = res.auto_approved
? 'Goods received and stock approved automatically.'
: 'Goods received. Stock-in rows are pending warehouse approval.';
bootbox.alert(msg, function() { retrieve_po(); });
}
});
}
});
}
// ── Update payment status ─────────────────────────────────────────────────
function update_payment_status() {
ajax_request({
url: '<?php echo $server_url?>po/api/engine/update_po_payment.php',
autoPrepare: true, checkRequired: 0,
action: 'update',
data: { id: po_id, payment_status: $('#payment_status_select').val() },
onSuccess: function() { retrieve_po(); }
});
}
// ── Cancel PO ────────────────────────────────────────────────────────────
function cancel_po() {
bootbox.confirm({
message: 'Cancel this purchase order? Any pending (unapproved) stock-in rows will be cancelled.',
buttons: {
confirm: { label: 'Yes, Cancel PO', className: 'btn-danger' },
cancel: { label: 'Back', className: 'btn-secondary' }
},
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>po/api/engine/cancel_po.php',
autoPrepare: true, checkRequired: 0,
action: 'update',
data: { id: po_id },
onSuccess: function() { retrieve_po(); }
});
}
});
}
// ── Retrieve warehouses ───────────────────────────────────────────────────
function retrieve_warehouses() {
var all_warehouses = ajax_request({
url: '<?php echo $server_url?>inventory/api/engine/warehouse.php',
autoPrepare: true, checkRequired: 0,
queueLock: false,
action: 'read',
onSuccess: function(res) {
warehouses = res.output || [];
var opts = '<option value="">Select warehouse...</option>';
warehouses.forEach(function(w) {
opts += `<option value="${w.id}">${w.warehouse_name}</option>`;
});
$('#warehouse_id').html(opts);
}
});
var receivable_warehouses = ajax_request({
url: '<?php echo $server_url?>ics/api/engine/retrieve_warehouse.php',
autoPrepare: true, checkRequired: 0,
queueLock: false,
action: 'read',
data: { type: 'to' },
onSuccess: function(res) {
receive_warehouses = res.output || [];
}
});
return $.when(all_warehouses, receivable_warehouses);
}
// ── Contact autocomplete ──────────────────────────────────────────────────
function init_contact_search() {
$('#contact').autocomplete({
source: function(req, res) {
ajax_request({
url: '<?php echo $server_url?>contact/api/engine/contact.php',
autoPrepare: true, checkRequired: 0,
action: 'read',
data: { keyword: req.term },
onSuccess: function(data) {
res(data.output.map(c => ({ label: c.contact_name, value: c.contact_name, id: c.id })));
}
});
},
select: function(e, ui) { $('#contact_id').val(ui.item.id); }
});
}
// ── Product autocomplete ──────────────────────────────────────────────────
function init_product_search() {
$(document).on('input', '.item_sku', function() {
var $input = $(this);
var $row = $input.closest('tr');
$input.autocomplete({
source: function(req, res) {
ajax_request({
url: '<?php echo $server_url?>inventory/api/engine/product.php',
autoPrepare: true, checkRequired: 0,
action: 'read',
data: { keyword: req.term },
onSuccess: function(data) {
res((data.output || []).map(p => ({
label: p.sku + ' — ' + p.product_name,
value: p.sku,
name: p.product_name,
})));
}
});
},
select: function(e, ui) {
$row.find('.item_name').val(ui.item.name);
$row.find('.item_name_display').text(ui.item.name);
recalc_totals();
}
});
});
}
// ── Boot ─────────────────────────────────────────────────────────────────
$(async function() {
try {
await load_location_config();
await retrieve_warehouses();
if (po_id) {
await retrieve_po();
} else {
$('#po_date').val(flatpickr.formatDate(new Date(), 'd/m/Y'));
add_item_row();
set_mode(0);
}
flatpickr('#po_date', { dateFormat: 'd/m/Y', allowInput: true });
flatpickr('#expected_date', { dateFormat: 'd/m/Y', allowInput: true });
init_contact_search();
init_product_search();
} catch(e) {
console.log(e);
}
});
</script>
</body>
</html>