Files
wms-app/app/expense/manage_purchase_request.php
T

390 lines
19 KiB
PHP

<?php
require_once __DIR__ . '/../session.php';
require '../config.php';
require '../include_header.php';
$request_id = (int)($_GET['id'] ?? 0);
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar_ac.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<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 $request_id ? 'Purchase Request Detail' : 'New Purchase Request'; ?></h1>
<p class="mb-0 text-muted" id="page_subtitle"><?php echo $request_id ? '' : 'Create an internal request before purchase order handoff'; ?></p>
</div>
<a href="<?php echo $server_url?>expense/purchase_request.php" class="btn btn-light">
<i class="ti ti-arrow-left me-1"></i>Back
</a>
</div>
</div>
</div>
<div class="row g-5">
<div class="col-lg-8">
<div class="card mb-5">
<div class="card-body p-5">
<h2 class="fs-5 mb-4">Request Information</h2>
<div class="row gx-5">
<div class="mb-3 col-lg-6">
<label class="form-label">Preferred 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-3">
<label class="form-label">Request Date</label>
<input type="text" id="request_date" class="form-control" placeholder="DD/MM/YYYY">
</div>
<div class="mb-3 col-lg-3">
<label class="form-label">Required Date</label>
<input type="text" id="required_date" class="form-control" placeholder="DD/MM/YYYY">
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">Department</label>
<select id="department_id" class="form-select">
<option value="0">— No Department —</option>
</select>
</div>
<div class="mb-3 col-12">
<label class="form-label">Notes</label>
<textarea id="notes" class="form-control" rows="2"></textarea>
</div>
</div>
</div>
</div>
<div class="card">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fs-5 mb-0">Requested Items</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">
<thead class="table-light">
<tr>
<th style="width:130px;">SKU</th>
<th>Description</th>
<th style="width:90px;">Qty</th>
<th style="width:120px;">Unit Price</th>
<th style="width:80px;">Tax%</th>
<th style="width:120px;">Total</th>
<th style="width:40px;"></th>
</tr>
</thead>
<tbody id="item_tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<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 id="badge_status" class="mb-2"></div>
<p class="mb-0 small fw-semibold text-muted" id="request_number_display"></p>
<p class="mb-0 small text-muted d-none" id="po_display"></p>
</div>
</div>
<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 align-items-center 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" step="any" oninput="recalc_totals()"></div>
<div class="d-flex justify-content-between mb-2"><span class="text-muted">Tax</span><span id="summary_tax" class="fw-semibold">0.00</span></div>
<div class="d-flex justify-content-between align-items-center mb-2"><span class="text-muted small">Tax Adj.</span><input type="number" id="tax_adjustment" class="form-control form-control-sm text-end" style="width:90px;" value="0" min="-0.3" max="0.3" step="0.01" oninput="recalc_totals()"></div>
<div class="d-flex justify-content-between align-items-center 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" step="any" 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>
<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_request()"><i class="ti ti-device-floppy me-1"></i>Save Draft</button>
<button class="btn btn-info text-white w-100 d-none" id="btn_submit" onclick="change_status('submit')"><i class="ti ti-send me-1"></i>Submit Request</button>
<button class="btn btn-success w-100 d-none" id="btn_approve" onclick="change_status('approve')"><i class="ti ti-circle-check me-1"></i>Approve</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_reject" onclick="change_status('reject')"><i class="ti ti-x me-1"></i>Reject</button>
<button class="btn btn-outline-secondary w-100 d-none" id="btn_reopen" onclick="change_status('reopen')"><i class="ti ti-refresh me-1"></i>Reopen as Draft</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_cancel" onclick="change_status('cancel')"><i class="ti ti-ban me-1"></i>Cancel</button>
<button class="btn btn-outline-danger w-100 d-none" id="btn_delete" onclick="delete_request()"><i class="ti ti-trash me-1"></i>Delete Request</button>
<hr class="d-none" id="hr_convert">
<button class="btn btn-success w-100 d-none" id="btn_convert" onclick="convert_to_po()"><i class="ti ti-transfer me-1"></i>Convert to PO</button>
<a href="#" class="btn btn-outline-primary w-100 d-none" id="btn_view_po"><i class="ti ti-clipboard-list me-1"></i>View PO</a>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
const can_delete = ['admin', 'owner'].includes(user_role);
var request_id = <?php echo $request_id; ?>;
var request_data = null;
var item_index = 0;
var status_badge = {
'-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">Submitted</span>',
'2': '<span class="badge bg-success">Approved</span>',
'3': '<span class="badge bg-danger">Rejected</span>',
'5': '<span class="badge bg-primary">Converted</span>',
};
function recalc_totals() {
var subtotal = 0;
var tax_sum = 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 tax_rate = parseFloat($(this).find('.item_tax_rate').val()) || 0;
var total = qty * price;
var tax_amt = round_dp(total * tax_rate / 100, 4);
$(this).find('.item_total').text(format_number(total, 2));
$(this).find('.item_tax_amount').val(tax_amt);
subtotal += total;
tax_sum += tax_amt;
});
var discount = parseFloat($('#discount').val()) || 0;
var tax_adjustment = parseFloat($('#tax_adjustment').val()) || 0;
var shipping = parseFloat($('#shipping_fee').val()) || 0;
var derived_tax = round_dp(tax_sum, 2);
$('#summary_subtotal').text(format_number(subtotal, 2));
$('#summary_tax').text(format_number(derived_tax, 2));
$('#summary_grand_total').text(format_number(subtotal - discount + derived_tax + tax_adjustment + shipping, 2));
}
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" value="${escape_html(item.product_sku || '')}" placeholder="SKU..." autocomplete="off" data-product-name-target=".item_desc"></td>
<td><input type="text" class="form-control form-control-sm item_desc" value="${escape_html(item.product_name || item.description || '')}" placeholder="Description..."></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 || item.price || 0}" min="0" step="any" oninput="recalc_totals()"></td>
<td>
<input type="number" class="form-control form-control-sm item_tax_rate" value="${parseFloat(item.tax_rate || 0).toFixed(2)}" min="0" max="100" step="0.01" oninput="recalc_totals()">
<input type="hidden" class="item_tax_amount" value="${item.tax_amount || 0}">
</td>
<td class="item_total text-end align-middle">${format_number((item.quantity || 1) * (item.unit_price || item.price || 0), 2)}</td>
<td class="align-middle"><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);
if (window.init_product_search_inputs) window.init_product_search_inputs(`#item_row_${idx}`);
recalc_totals();
}
function collect_items() {
var items = [];
$('#item_tbody tr').each(function() {
var sku = $(this).find('.item_sku').val().trim();
var desc = $(this).find('.item_desc').val().trim();
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
var price = parseFloat($(this).find('.item_price').val()) || 0;
var tax_rate = parseFloat($(this).find('.item_tax_rate').val()) || 0;
var tax_amt = parseFloat($(this).find('.item_tax_amount').val()) || 0;
if (!sku && !desc && qty <= 0) return;
items.push({
product_sku: sku,
product_name: desc,
description: desc,
quantity: qty,
unit_price: price,
total_price: round_dp(qty * price, 4),
tax_rate: tax_rate,
tax_amount: tax_amt,
});
});
return items;
}
function set_mode(status) {
status = parseInt(status);
var is_draft = status === 0;
var is_submitted = status === 1;
var is_approved = status === 2;
var is_closed = status === -1 || status === 3 || status === 5;
$('#contact, #request_date, #required_date, #department_id, #notes, #discount, #tax_adjustment, #shipping_fee, #btn_add_item').prop('disabled', !is_draft);
$('.item_sku, .item_desc, .item_qty, .item_price, .item_tax_rate').prop('disabled', !is_draft);
$('.remove_item_btn').toggleClass('d-none', !is_draft);
$('#btn_save').toggleClass('d-none', !is_draft);
$('#btn_submit').toggleClass('d-none', !is_draft || !request_id);
$('#btn_approve, #btn_reject').toggleClass('d-none', !is_submitted);
$('#btn_reopen').toggleClass('d-none', is_draft || is_closed);
$('#btn_cancel').toggleClass('d-none', !(is_draft || is_submitted) || !request_id);
$('#btn_delete').toggleClass('d-none', !can_delete || !request_id);
$('#hr_convert, #btn_convert').toggleClass('d-none', !is_approved);
$('#btn_view_po').toggleClass('d-none', !(request_data && parseInt(request_data.po_id) > 0))
.attr('href', request_data && request_data.po_id ? '<?php echo $server_url?>expense/manage_purchase_order.php?id=' + request_data.po_id : '#');
}
function retrieve_request() {
if (!request_id) return Promise.resolve();
return ajax_request({
url: '<?php echo $server_url?>expense/api/engine/retrieve_purchase_request.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { id: request_id },
onSuccess: function(res) {
request_data = res.output || {};
$('#page_title').text(request_data.request_number || 'Purchase Request Detail');
$('#status_card').removeClass('d-none');
$('#badge_status').html(status_badge[String(request_data.status)] || request_data.status);
$('#request_number_display').text(request_data.request_number || '');
$('#po_display')
.toggleClass('d-none', !(request_data.po_id > 0))
.html(request_data.po_id > 0 ? 'PO: <a href="<?php echo $server_url?>expense/manage_purchase_order.php?id=' + request_data.po_id + '">' + escape_html(request_data.po_number || ('PO #' + request_data.po_id)) + '</a>' : '');
$('#contact').val(request_data.contact_name || '');
$('#contact_id').val(request_data.contact_id || 0);
$('#request_date').val(request_data.request_date ? format_date_input(request_data.request_date) : '');
$('#required_date').val(request_data.required_date ? format_date_input(request_data.required_date) : '');
$('#department_id').val(request_data.department_id || 0);
$('#notes').val(request_data.notes || '');
$('#discount').val(request_data.discount || 0);
$('#tax_adjustment').val(request_data.tax_adjustment || 0);
$('#shipping_fee').val(request_data.shipping_fee || 0);
$('#item_tbody').empty();
item_index = 0;
(request_data.items || []).forEach(function(item) { add_item_row(item); });
recalc_totals();
set_mode(request_data.status);
}
});
}
function save_request() {
var items = collect_items();
if (!items.length) {
bootbox.alert('Please add at least one item.');
return;
}
ajax_request({
url: '<?php echo $server_url?>expense/api/engine/manage_purchase_request.php',
autoPrepare: true,
checkRequired: 0,
action: request_id ? 'update' : 'create',
data: {
id: request_id,
contact_id: $('#contact_id').val() || 0,
department_id: $('#department_id').val() || 0,
request_date: to_iso_date($('#request_date').val()),
required_date: to_iso_date($('#required_date').val()),
items: JSON.stringify(items),
discount: $('#discount').val() || 0,
tax_adjustment: $('#tax_adjustment').val() || 0,
shipping_fee: $('#shipping_fee').val() || 0,
notes: $('#notes').val(),
},
onSuccess: function(res) {
if (!request_id && res.new_id) window.location.href = '<?php echo $server_url?>expense/manage_purchase_request.php?id=' + res.new_id;
else retrieve_request();
}
});
}
function delete_request() {
bootbox.confirm({
message: 'Delete this purchase request? This cannot be undone.',
buttons: { confirm: { label: 'Delete', 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/delete_purchase_request.php',
autoPrepare: false,
checkRequired: 0,
action: 'delete',
data: { id: request_id },
onSuccess: function() { window.location.href = '<?php echo $server_url?>expense/purchase_request.php'; }
});
}
});
}
function change_status(action_type) {
var labels = {
submit: 'Submit this purchase request?',
approve: 'Approve this purchase request?',
reject: 'Reject this purchase request?',
reopen: 'Reopen this purchase request as Draft?',
cancel: 'Cancel this purchase request?',
};
bootbox.confirm(labels[action_type] || 'Confirm?', function(ok) {
if (!ok) return;
ajax_request({
url: '<?php echo $server_url?>expense/api/engine/update_purchase_request_status.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: { id: request_id, action_type: action_type },
onSuccess: function() { retrieve_request(); }
});
});
}
function convert_to_po() {
bootbox.confirm('Convert this approved purchase request to a pending warehouse purchase order?', function(ok) {
if (!ok) return;
ajax_request({
url: '<?php echo $server_url?>expense/api/engine/convert_purchase_request.php',
autoPrepare: true,
checkRequired: 0,
action: 'create',
data: { request_id: request_id },
onSuccess: function(res) {
window.location.href = '<?php echo $server_url?>expense/manage_purchase_order.php?id=' + res.po_id;
}
});
});
}
$(document).on('contact:selected', '#contact', function(e, contact) {
$('#contact_id').val(contact.id);
});
$(document).on('product:selected', '.item_sku', function(e, product) {
var $row = $(this).closest('tr');
var price = parseFloat(product.price || 0);
$row.find('.item_desc').val(product.product_name || '');
if (!(parseFloat($row.find('.item_price').val()) > 0) && price > 0) {
$row.find('.item_price').val(price);
}
$row.find('.item_tax_rate').val(parseFloat(product.tax_rate || 0).toFixed(2));
recalc_totals();
});
$(async function() {
// Options must exist before retrieve_request() selects the saved department
await Promise.resolve(load_departments('department_id')).catch(function() {});
flatpickr('#request_date', { dateFormat: 'd/m/Y', allowInput: true });
flatpickr('#required_date', { dateFormat: 'd/m/Y', allowInput: true });
if (request_id) {
retrieve_request();
} else {
$('#request_date').val(flatpickr.formatDate(new Date(), 'd/m/Y'));
add_item_row();
set_mode(0);
}
});
</script>
</body>
</html>