[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
+2 -1
View File
@@ -2,6 +2,7 @@
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/OrderManager.php';
require '../../../assets/utils/classes/WarehouseManager.php';
$id = (int)($data['id'] ?? 0);
@@ -30,4 +31,4 @@
}
exit(json_encode($answer));
?>
?>
+36 -5
View File
@@ -54,11 +54,42 @@
$already_returned = $returned_qty[$i] ?? 0;
$remaining = (float)$item['quantity'] - $already_returned;
if ($remaining > 0) {
$stock_out_warehouse_id = (int)($item['warehouse_id'] ?? 0);
$stock_out_id = (int)($item['stock_out_id'] ?? 0);
if ($stock_out_warehouse_id > 0 && $stock_out_id > 0) {
$table = 'td_stock_' . $stock_out_warehouse_id;
$sth = $pdo2->prepare(
"SELECT id, status, zone, aisle, rack, lot_number, serial_number
FROM `{$table}`
WHERE company_id = :company_id
AND id = :id
AND type = 'out'
LIMIT 1"
);
$sth->execute([
':company_id' => $company_id,
':id' => $stock_out_id,
]);
$stock_out = $sth->fetch(PDO::FETCH_ASSOC);
if ($stock_out) {
$item['zone'] = $stock_out['zone'] ?? ($item['zone'] ?? '');
$item['aisle'] = $stock_out['aisle'] ?? ($item['aisle'] ?? '');
$item['rack'] = $stock_out['rack'] ?? ($item['rack'] ?? '');
$item['lot_number'] = $stock_out['lot_number'] ?? ($item['lot_number'] ?? '');
$item['serial_number'] = $stock_out['serial_number'] ?? ($item['serial_number'] ?? '');
$item['stock_out_status'] = (int)$stock_out['status'];
}
}
$returnable[] = array_merge($item, [
'item_id' => $i,
'original_qty' => (float)$item['quantity'],
'returned_qty' => $already_returned,
'returnable_qty' => $remaining,
'item_id' => $i,
'original_qty' => (float)$item['quantity'],
'returned_qty' => $already_returned,
'returnable_qty' => $remaining,
'stock_out_warehouse_id' => $stock_out_warehouse_id,
'stock_out_status' => (int)($item['stock_out_status'] ?? 0),
]);
}
}
@@ -70,4 +101,4 @@
$answer['success'] = 1;
exit(json_encode($answer));
?>
?>
@@ -11,9 +11,8 @@
exit(json_encode($answer));
}
// Validate payment_status range
// 0=unpaid 1=paid 2=partial 3=refunded
if (!in_array($payment_status, [0, 1, 2, 3], true)) {
// Validate payment_status range: 0=unpaid 1=paid 2=partial
if (!in_array($payment_status, [0, 1, 2], true)) {
$answer['message'] = 'Invalid payment status.';
http_response_code(400);
exit(json_encode($answer));
@@ -66,4 +65,4 @@
}
exit(json_encode($answer));
?>
?>
+11
View File
@@ -123,6 +123,11 @@
<i class="ti ti-send me-1"></i>Issue Invoice
</button>
<button class="btn btn-outline-secondary w-100" id="btn_print"
onclick="print_invoice()">
<i class="ti ti-printer me-1"></i>Print / Save PDF
</button>
<!-- Status selector (issued and above) -->
<div class="" id="invoice_status_group">
<label class="form-label small text-muted mb-1">Invoice Status</label>
@@ -345,6 +350,12 @@
}
function print_invoice() {
if (!invoice_id) return;
window.open('<?php echo $server_url?>order/print_invoice.php?id=' + invoice_id, '_blank');
}
$(function() {
if (invoice_id) {
retrieve_invoice();
+1 -4
View File
@@ -202,7 +202,6 @@
<option value="0">Unpaid</option>
<option value="1">Paid</option>
<option value="2">Partial</option>
<option value="3">Refunded</option>
</select>
<button class="btn btn-sm btn-outline-primary text-nowrap"
onclick="update_payment_status()">
@@ -259,7 +258,6 @@
'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>',
'3': '<span class="badge bg-secondary">Refunded</span>',
};
return map[String(status)] ?? '—';
}
@@ -429,10 +427,9 @@
: '<i class="ti ti-arrow-back-up me-1"></i>Create Return');
// Proceed to Invoice
var can_invoice = status === 1 && !has_active_invoice;
var can_invoice = status >= 1 && !is_cancelled && !has_active_invoice;
var invoice_title = is_cancelled ? 'Order is cancelled'
: status < 1 ? 'Confirm the order before creating an invoice'
: status > 1 ? 'Invoice can only be created for confirmed orders'
: has_active_invoice ? 'An active invoice already exists for this order' : '';
set_btn_state('#btn_invoice', can_invoice, invoice_title);
$('#btn_invoice').html(has_active_invoice
+356 -46
View File
@@ -94,6 +94,10 @@
<th>Product</th>
<th class="text-end" style="width:100px;">Order Qty</th>
<th class="text-end" style="width:100px;">Returned</th>
<th class="th_return_wh" style="width:150px;">Warehouse</th>
<th class="th_return_zone" style="width:120px;">Zone</th>
<th class="th_return_aisle" style="width:120px;">Aisle</th>
<th class="th_return_rack" style="width:120px;">Location</th>
<th class="text-end" style="width:110px;">Return Qty</th>
<th class="text-end" style="width:120px;">Unit Price</th>
<th class="text-end" style="width:120px;">Total</th>
@@ -101,7 +105,7 @@
</thead>
<tbody id="item_tbody">
<tr id="loading_row">
<td colspan="7" class="text-center text-muted py-4">
<td colspan="11" class="text-center text-muted py-4">
<i class="ti ti-loader me-1"></i>Loading items...
</td>
</tr>
@@ -172,6 +176,11 @@
var return_data = null;
var order_data = null;
var returnable = []; // returnable_items from order
var warehouses = [];
var advanced = false;
var label_rack = 'Location';
var label_zone = 'Zone';
var label_aisle = 'Aisle';
// ── Status badge ──────────────────────────────────────────────────────────
@@ -201,6 +210,194 @@
$('#summary_refund').text(format_number(total, 2));
}
function escape_html(value) {
return String(value ?? '').replace(/[&<>"']/g, function(char) {
return {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
}[char];
});
}
function warehouse_options(selected) {
var opts = '<option value="">Select warehouse...</option>';
warehouses.forEach(function(w) {
opts += `<option value="${w.id}" ${String(selected || '') === String(w.id) ? 'selected' : ''}>${escape_html(w.warehouse_name)}</option>`;
});
return opts;
}
function apply_location_headers() {
$('.th_return_wh').text('Warehouse');
$('.th_return_zone').text(label_zone).toggleClass('d-none', !advanced);
$('.th_return_aisle').text(label_aisle).toggleClass('d-none', !advanced);
$('.th_return_rack').text(label_rack);
$('#loading_row td').attr('colspan', 11);
}
function retrieve_return_zones(item_id, selected_zone, selected_aisle, selected_rack) {
var $row = $(`#ret_item_${item_id}`);
var wh = $row.find('.item_return_warehouse').val();
$row.find('.item_return_zone').html(`<option value="">Select ${label_zone.toLowerCase()}...</option>`);
$row.find('.item_return_aisle').html(`<option value="">Select ${label_aisle.toLowerCase()}...</option>`);
$row.find('.item_return_rack').html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`);
if (!wh) return Promise.resolve();
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: wh, id: 0 },
onSuccess: function(res) {
var opts = `<option value="">Select ${label_zone.toLowerCase()}...</option>`;
var has_selected = !selected_zone;
$.each(res.output || [], function(_, item) {
if (String(item.zone) === String(selected_zone)) has_selected = true;
opts += `<option value="${escape_html(item.zone)}">${escape_html(item.zone)}</option>`;
});
if (!has_selected) {
opts += `<option value="${escape_html(selected_zone)}">${escape_html(selected_zone)}</option>`;
}
$row.find('.item_return_zone').html(opts).val(selected_zone || '');
if (selected_zone) retrieve_return_aisles(item_id, selected_aisle, selected_rack);
}
});
}
function change_return_warehouse(item_id) {
var $row = $(`#ret_item_${item_id}`);
$row.find('.item_return_zone').val('');
$row.find('.item_return_aisle').val('');
$row.find('.item_return_rack').val('');
if (!$row.find('.item_check').is(':checked')) return;
if (advanced) {
retrieve_return_zones(item_id);
} else {
retrieve_return_racks(item_id);
}
}
function change_return_zone(item_id) {
var $row = $(`#ret_item_${item_id}`);
$row.find('.item_return_aisle').val('');
$row.find('.item_return_rack').val('');
if ($row.find('.item_check').is(':checked')) retrieve_return_aisles(item_id);
}
function change_return_aisle(item_id) {
var $row = $(`#ret_item_${item_id}`);
$row.find('.item_return_rack').val('');
if ($row.find('.item_check').is(':checked')) retrieve_return_racks(item_id);
}
function retrieve_return_aisles(item_id, selected_aisle, selected_rack) {
var $row = $(`#ret_item_${item_id}`);
var wh = $row.find('.item_return_warehouse').val();
var zone = $row.find('.item_return_zone').val();
$row.find('.item_return_aisle').html(`<option value="">Select ${label_aisle.toLowerCase()}...</option>`);
$row.find('.item_return_rack').html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`);
if (!wh || !zone) return Promise.resolve();
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: wh, zone: zone, id: 0 },
onSuccess: function(res) {
var opts = `<option value="">Select ${label_aisle.toLowerCase()}...</option>`;
var has_selected = !selected_aisle;
$.each(res.output || [], function(_, value) {
if (String(value) === String(selected_aisle)) has_selected = true;
opts += `<option value="${escape_html(value)}">${escape_html(value)}</option>`;
});
if (!has_selected) {
opts += `<option value="${escape_html(selected_aisle)}">${escape_html(selected_aisle)}</option>`;
}
$row.find('.item_return_aisle').html(opts).val(selected_aisle || '');
if (selected_aisle) retrieve_return_racks(item_id, selected_rack);
}
});
}
function retrieve_return_racks(item_id, selected_rack) {
var $row = $(`#ret_item_${item_id}`);
var wh = $row.find('.item_return_warehouse').val();
var zone = advanced ? $row.find('.item_return_zone').val() : '';
var aisle = advanced ? $row.find('.item_return_aisle').val() : '';
$row.find('.item_return_rack').html(`<option value="">Select ${label_rack.toLowerCase()}...</option>`);
if (!wh || (advanced && (!zone || !aisle))) return Promise.resolve();
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: wh, zone: zone, aisle: aisle, id: 0 },
onSuccess: function(res) {
var opts = `<option value="">Select ${label_rack.toLowerCase()}...</option>`;
var has_selected = !selected_rack;
$.each(res.output || [], function(_, value) {
if (String(value) === String(selected_rack)) has_selected = true;
opts += `<option value="${escape_html(value)}">${escape_html(value)}</option>`;
});
if (!has_selected) {
opts += `<option value="${escape_html(selected_rack)}">${escape_html(selected_rack)}</option>`;
}
$row.find('.item_return_rack').html(opts).val(selected_rack || '');
}
});
}
function location_cells(item_id, selected) {
var wh = selected.warehouse_id || '';
var zone = selected.zone || '';
var aisle = selected.aisle || '';
var rack = selected.rack || '';
if (advanced) {
return `<td><select class="form-select form-select-sm item_return_warehouse" onchange="change_return_warehouse(${item_id})">${warehouse_options(wh)}</select></td>
<td><select class="form-select form-select-sm item_return_zone" onchange="change_return_zone(${item_id})"><option value="">Select ${label_zone.toLowerCase()}...</option></select></td>
<td><select class="form-select form-select-sm item_return_aisle" onchange="change_return_aisle(${item_id})"><option value="">Select ${label_aisle.toLowerCase()}...</option></select></td>
<td><select class="form-select form-select-sm item_return_rack"><option value="">Select ${label_rack.toLowerCase()}...</option></select></td>`;
}
return `<td><select class="form-select form-select-sm item_return_warehouse" onchange="change_return_warehouse(${item_id})">${warehouse_options(wh)}</select></td>
<td class="d-none"><select class="form-select form-select-sm item_return_zone"><option value=""></option></select></td>
<td class="d-none"><select class="form-select form-select-sm item_return_aisle"><option value=""></option></select></td>
<td><select class="form-select form-select-sm item_return_rack"><option value="">Select ${label_rack.toLowerCase()}...</option></select></td>`;
}
function hydrate_location_selectors(items, selected_map) {
items.forEach(function(item) {
var selected = selected_map[item.item_id] || {};
var $row = $(`#ret_item_${item.item_id}`);
if (!$row.find('.item_check').is(':checked')) return;
if (!selected.warehouse_id && !selected.zone && !selected.aisle && !selected.rack) {
change_return_warehouse(item.item_id);
return;
}
if (advanced) {
retrieve_return_zones(item.item_id, selected.zone || '', selected.aisle || '', selected.rack || '');
} else if (selected.rack) {
retrieve_return_racks(item.item_id, selected.rack || '');
}
});
}
// ── Render returnable items ───────────────────────────────────────────────
function render_returnable_items(items, selected_items) {
@@ -213,9 +410,36 @@
});
}
if (selected_items && selected_items.length) {
var item_map = {};
items.forEach(function(item) {
item_map[item.item_id] = item;
});
selected_items.forEach(function(si) {
if (item_map[si.item_id]) return;
var base = (order_data.items || [])[si.item_id] || {};
var restored = Object.assign({}, base, si, {
item_id: si.item_id,
original_qty: base.quantity || si.quantity || 0,
returned_qty: si.quantity || 0,
returnable_qty: si.quantity || 0,
price: si.price || base.price || 0,
product_sku: si.product_sku || base.product_sku || '',
product_name: si.product_name || base.product_name || '',
stock_out_warehouse_id: si.stock_out_warehouse_id || base.stock_out_warehouse_id || base.warehouse_id || si.warehouse_id || 0,
stock_out_status: si.stock_out_status || base.stock_out_status || 1,
});
items.push(restored);
item_map[si.item_id] = restored;
});
}
if (!items.length) {
$('#item_tbody').html(
'<tr><td colspan="7" class="text-center text-muted py-4">No returnable items for this order.</td></tr>'
'<tr><td colspan="11" class="text-center text-muted py-4">No returnable items for this order.</td></tr>'
);
return;
}
@@ -224,13 +448,23 @@
items.forEach(function(item) {
var si = selected_map[item.item_id] || null;
var checked = si ? 'checked' : '';
var received = return_data && parseInt(return_data.status) === 1 && si;
var qty = si ? si.quantity : item.returnable_qty;
var price = item.price || 0;
var destination = {
warehouse_id: si ? (si.warehouse_id || item.warehouse_id || '') : (item.warehouse_id || ''),
zone: si ? (si.zone || '') : (item.zone || ''),
aisle: si ? (si.aisle || '') : (item.aisle || ''),
rack: si ? (si.rack || '') : (item.rack || ''),
};
rows += `<tr class="return-item-row" id="ret_item_${item.item_id}">
<td>
<input type="checkbox" class="form-check-input item_check"
onchange="toggle_item_row(${item.item_id}); recalc_totals();" ${checked}>
${received
? `<span class="text-success" title="Received"><i class="ti ti-circle-check-filled"></i></span>
<input type="checkbox" class="d-none item_check" checked>`
: `<input type="checkbox" class="form-check-input item_check"
onchange="toggle_item_row(${item.item_id}); recalc_totals();" ${checked}>`}
<input type="hidden" class="item_id" value="${item.item_id}">
<input type="hidden" class="item_sku" value="${item.product_sku}">
<input type="hidden" class="item_name" value="${item.product_name || ''}">
@@ -241,6 +475,8 @@
<input type="hidden" class="item_lot" value="${item.lot_number || ''}">
<input type="hidden" class="item_serial" value="${item.serial_number || ''}">
<input type="hidden" class="item_stock_out_id" value="${item.stock_out_id || 0}">
<input type="hidden" class="item_stock_out_wh" value="${item.stock_out_warehouse_id || item.warehouse_id || 0}">
<input type="hidden" class="item_stock_out_status" value="${item.stock_out_status || 0}">
</td>
<td>
<div class="fw-semibold">${item.product_sku}</div>
@@ -248,6 +484,7 @@
</td>
<td class="text-end">${format_number(item.original_qty, 2)}</td>
<td class="text-end text-muted">${item.returned_qty > 0 ? format_number(item.returned_qty, 2) : '—'}</td>
${location_cells(item.item_id, destination)}
<td>
<input type="number" class="form-control form-control-sm item_qty text-end"
value="${format_number(qty, 2)}"
@@ -267,14 +504,19 @@
});
$('#item_tbody').html(rows);
hydrate_location_selectors(items, selected_map);
recalc_totals();
}
function toggle_item_row(item_id) {
var $row = $(`#ret_item_${item_id}`);
var checked = $row.find('.item_check').is(':checked');
$row.find('.item_qty, .item_price').prop('disabled', !checked);
if (!checked) $row.find('.item_total').text('—');
$row.find('.item_qty, .item_price, .item_return_warehouse, .item_return_zone, .item_return_aisle, .item_return_rack').prop('disabled', !checked);
if (!checked) {
$row.find('.item_total').text('—');
return;
}
change_return_warehouse(item_id);
}
@@ -311,6 +553,8 @@
url: '<?php echo $server_url?>order/api/engine/retrieve_invoice.php',
autoPrepare: true,
checkRequired: 0,
noLoading: true,
queueLock: false,
action: 'read',
data: { order_id: oid, doc_type: 'invoice' },
onSuccess: function(res) {
@@ -328,6 +572,35 @@
});
}
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_headers();
}
});
}
function retrieve_warehouses() {
return ajax_request({
url: '<?php echo $server_url?>inventory/api/engine/warehouse.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
warehouses = res.output || [];
}
});
}
// ── Collect selected items ────────────────────────────────────────────────
function collect_items() {
@@ -339,18 +612,28 @@
var qty = parseFloat($row.find('.item_qty').val()) || 0;
var price = parseFloat($row.find('.item_price').val()) || 0;
if (qty <= 0) return;
var return_wh = parseInt($row.find('.item_return_warehouse').val()) || 0;
var return_rack = $row.find('.item_return_rack').val() || '';
var return_zone = advanced ? ($row.find('.item_return_zone').val() || '') : return_rack;
var return_aisle = advanced ? ($row.find('.item_return_aisle').val() || '') : return_rack;
if (!return_wh || !return_rack || (advanced && (!return_zone || !return_aisle))) {
throw new Error('Please select return warehouse and ' + label_rack.toLowerCase() + ' for every checked item.');
}
items.push({
item_id: parseInt($row.find('.item_id').val()),
product_sku: $row.find('.item_sku').val(),
product_name: $row.find('.item_name').val(),
warehouse_id: parseInt($row.find('.item_warehouse_id').val()) || 0,
zone: $row.find('.item_zone').val(),
aisle: $row.find('.item_aisle').val(),
rack: $row.find('.item_rack').val(),
warehouse_id: return_wh,
zone: return_zone,
aisle: return_aisle,
rack: return_rack,
lot_number: $row.find('.item_lot').val(),
serial_number: $row.find('.item_serial').val(),
stock_out_id: parseInt($row.find('.item_stock_out_id').val()) || 0,
stock_out_warehouse_id: parseInt($row.find('.item_stock_out_wh').val()) || 0,
stock_out_status: parseInt($row.find('.item_stock_out_status').val()) || 0,
quantity: qty,
price: price,
total_price: qty * price,
@@ -359,6 +642,46 @@
return items;
}
function build_return_payload() {
var items;
try {
items = collect_items();
} catch (e) {
bootbox.alert(e.message || String(e));
return null;
}
if (!items.length) {
bootbox.alert('Please select at least one item to return.');
return null;
}
return {
id: return_id,
order_id: $('#order_id').val() || 0,
invoice_id: $('#invoice_id').val() || 0,
contact_id: $('#contact_id').val() || 0,
return_date: $('#return_date').val().split('/').reverse().join('-'),
reason: $('#reason').val(),
items: JSON.stringify(items),
notes: '',
};
}
function persist_return(onSuccess) {
var payload = build_return_payload();
if (!payload) return false;
return ajax_request({
url: '<?php echo $server_url?>order/api/engine/manage_return.php',
autoPrepare: true,
checkRequired: 0,
action: return_id ? 'update' : 'create',
data: payload,
onSuccess: onSuccess
});
}
// ── Mode ──────────────────────────────────────────────────────────────────
function set_mode(status) {
@@ -369,13 +692,17 @@
$('.item_check').prop('disabled', !is_draft);
$('#item_tbody tr.return-item-row').each(function() {
var checked = $(this).find('.item_check').is(':checked');
$(this).find('.item_qty, .item_price').prop('disabled', !is_draft || !checked);
$(this).find('.item_qty, .item_price, .item_return_warehouse, .item_return_zone, .item_return_aisle, .item_return_rack')
.prop('disabled', !is_draft || !checked);
});
set_btn_state('#btn_save', is_draft,
status === -1 ? 'Return is cancelled' : 'Return has already been confirmed');
set_btn_state('#btn_confirm', is_draft,
status === -1 ? 'Return is cancelled' : 'Return is already confirmed');
!return_id ? 'Save the return before confirming' : (status === -1 ? 'Return is cancelled' : 'Return is already confirmed'));
if (!return_id) {
$('#btn_confirm').prop('disabled', true);
}
set_btn_state('#btn_cancel', is_draft || status === 1,
status === -1 ? 'Return is already cancelled' : 'Return is completed and cannot be cancelled');
}
@@ -413,32 +740,11 @@
// ── Save draft ────────────────────────────────────────────────────────────
function save_return() {
var items = collect_items();
if (!items.length) {
bootbox.alert('Please select at least one item to return.');
return;
}
ajax_request({
url: '<?php echo $server_url?>order/api/engine/manage_return.php',
autoPrepare: true,
checkRequired: 0,
action: return_id ? 'update' : 'create',
data: {
id: return_id,
order_id: $('#order_id').val() || 0,
invoice_id: $('#invoice_id').val() || 0,
contact_id: $('#contact_id').val() || 0,
return_date: $('#return_date').val().split('/').reverse().join('-'),
reason: $('#reason').val(),
items: JSON.stringify(items),
notes: '',
},
onSuccess: function(res) {
if (!return_id && res.new_id) {
window.location.href = '<?php echo $server_url?>order/manage_return.php?id=' + res.new_id;
} else {
retrieve_return();
}
persist_return(function(res) {
if (!return_id && res.new_id) {
window.location.href = '<?php echo $server_url?>order/manage_return.php?id=' + res.new_id;
} else {
retrieve_return();
}
});
}
@@ -454,13 +760,15 @@
},
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/confirm_return.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: { id: return_id },
onSuccess: function() { retrieve_return(); }
persist_return(function() {
ajax_request({
url: '<?php echo $server_url?>order/api/engine/confirm_return.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: { id: return_id },
onSuccess: function() { retrieve_return(); }
});
});
}
});
@@ -494,6 +802,8 @@
$(async function() {
try {
flatpickr('#return_date', { dateFormat: 'd/m/Y', allowInput: true });
await load_company_setting();
await retrieve_warehouses();
if (return_id) {
// Edit/view mode — load existing return
@@ -517,4 +827,4 @@
</script>
</body>
</html>
</html>
-2
View File
@@ -117,7 +117,6 @@
<option value="0">Unpaid</option>
<option value="1">Paid</option>
<option value="2">Partial</option>
<option value="3">Refunded</option>
</select>
</div>
<div class="col-lg-2">
@@ -185,7 +184,6 @@
'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>',
'3': '<span class="badge bg-secondary">Refunded</span>',
};
return map[status] ?? '<span class="badge bg-light text-dark">—</span>';
}
+523
View File
@@ -0,0 +1,523 @@
<?php
session_start();
require '../config.php';
require '../dbconn.php';
require '../assets/utils/db_helpers.php';
require '../assets/utils/classes/InvoiceManager.php';
// ── Session guard ─────────────────────────────────────────────────────────
if (empty($_SESSION['login_company_id'])) {
header('Location: ' . $server_url . 'login/index.php');
exit;
}
$company_id = (int)$_SESSION['login_company_id'];
$invoice_id = (int)($_GET['id'] ?? 0);
if (!$invoice_id) {
http_response_code(400);
exit('Invalid invoice ID.');
}
// ── Load invoice ──────────────────────────────────────────────────────────
$inv_mgr = new InvoiceManager($pdo2, $company_id);
$inv = $inv_mgr->getInvoiceById($invoice_id);
if (!$inv) {
http_response_code(404);
exit('Invoice not found.');
}
// ── Load company profile ──────────────────────────────────────────────────
$sth = $pdo1->prepare(
"SELECT company_name, company_name2, company_logo, address, address2,
tax_id, phone, email, fax, branch, branch_no
FROM company_list
WHERE company_id = :company_id
LIMIT 1"
);
$sth->execute([':company_id' => $company_id]);
$co = $sth->fetch(PDO::FETCH_ASSOC) ?: [];
// ── Helpers ───────────────────────────────────────────────────────────────
function h(mixed $v): string {
return htmlspecialchars((string)($v ?? ''), ENT_QUOTES, 'UTF-8');
}
function fmt(mixed $v, int $dp = 2): string {
return number_format((float)$v, $dp);
}
function fmt_date(string $d): string {
if (!$d) return '—';
$ts = strtotime($d);
return $ts ? date('d/m/Y', $ts) : h($d);
}
$items = $inv['items'] ?? [];
$logo_url = !empty($co['company_logo'])
? $server_url . 'uploads/company/' . $co['company_logo']
: '';
$doc_labels = [
'invoice' => 'INVOICE',
'credit_note' => 'CREDIT NOTE',
'debit_note' => 'DEBIT NOTE',
];
$doc_label = $doc_labels[$inv['doc_type'] ?? 'invoice'] ?? 'INVOICE';
$status_labels = [
'0' => 'Draft',
'1' => 'Issued',
'2' => 'Paid',
'3' => 'Overdue',
'4' => 'Void',
];
$status_label = $status_labels[strval($inv['status'] ?? 0)] ?? '';
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= h($doc_label) ?> — <?= h($inv['invoice_number']) ?></title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Segoe UI', Arial, sans-serif;
font-size: 13px;
color: #1a1a1a;
background: #f0f0f0;
}
/* ── Print action bar ── */
#print_bar {
position: fixed;
top: 0; left: 0; right: 0;
background: #1e293b;
color: #fff;
padding: 10px 24px;
display: flex;
align-items: center;
justify-content: space-between;
z-index: 100;
gap: 12px;
}
#print_bar span { font-size: 13px; opacity: 0.75; }
.btn-print {
background: #3b82f6;
color: #fff;
border: none;
border-radius: 6px;
padding: 8px 20px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
}
.btn-print:hover { background: #2563eb; }
.btn-back {
background: transparent;
color: #94a3b8;
border: 1px solid #334155;
border-radius: 6px;
padding: 7px 14px;
font-size: 13px;
cursor: pointer;
text-decoration: none;
}
.btn-back:hover { color: #fff; border-color: #64748b; }
/* ── Page wrapper ── */
.page-wrap {
padding: 52px 0 32px;
display: flex;
justify-content: center;
}
/* ── A4 sheet ── */
.sheet {
background: #fff;
width: 794px; /* A4 at 96dpi */
min-height: 1123px;
padding: 48px 52px;
box-shadow: 0 4px 24px rgba(0,0,0,0.12);
}
/* ── Header ── */
.inv-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 2px solid #1e293b;
}
.inv-header .co-block img {
max-height: 56px;
max-width: 160px;
object-fit: contain;
margin-bottom: 8px;
display: block;
}
.inv-header .co-block .co-name {
font-size: 16px;
font-weight: 700;
color: #1e293b;
line-height: 1.3;
}
.inv-header .co-block .co-name2 {
font-size: 12px;
color: #64748b;
margin-top: 1px;
}
.inv-header .co-block .co-detail {
margin-top: 6px;
color: #475569;
font-size: 12px;
line-height: 1.6;
}
.inv-header .doc-block {
text-align: right;
}
.inv-header .doc-block .doc-type {
font-size: 26px;
font-weight: 800;
color: #1e293b;
letter-spacing: 1px;
line-height: 1;
}
.inv-header .doc-block .doc-number {
font-size: 15px;
font-weight: 600;
color: #3b82f6;
margin-top: 6px;
}
.inv-header .doc-block .doc-meta {
margin-top: 10px;
font-size: 12px;
color: #475569;
line-height: 1.8;
}
.inv-header .doc-block .doc-meta strong {
color: #1e293b;
font-weight: 600;
display: inline-block;
min-width: 70px;
}
.status-pill {
display: inline-block;
padding: 2px 10px;
border-radius: 20px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
margin-top: 8px;
}
.status-draft { background: #f1f5f9; color: #64748b; }
.status-issued { background: #dbeafe; color: #1d4ed8; }
.status-paid { background: #dcfce7; color: #15803d; }
.status-overdue { background: #fee2e2; color: #b91c1c; }
.status-void { background: #f1f5f9; color: #94a3b8; }
/* ── Bill to ── */
.bill-section {
display: flex;
gap: 32px;
margin-bottom: 28px;
}
.bill-box {
flex: 1;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 14px 18px;
}
.bill-box .bill-label {
font-size: 10px;
font-weight: 700;
color: #94a3b8;
letter-spacing: 1px;
text-transform: uppercase;
margin-bottom: 6px;
}
.bill-box .bill-name {
font-size: 14px;
font-weight: 700;
color: #1e293b;
margin-bottom: 3px;
}
.bill-box .bill-detail {
font-size: 12px;
color: #475569;
line-height: 1.6;
white-space: pre-line;
}
/* ── Items table ── */
.items-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 0;
}
.items-table thead th {
background: #1e293b;
color: #fff;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
padding: 10px 12px;
}
.items-table thead th:first-child { border-radius: 6px 0 0 0; }
.items-table thead th:last-child { border-radius: 0 6px 0 0; }
.items-table thead th.text-end { text-align: right; }
.items-table tbody tr {
border-bottom: 1px solid #e2e8f0;
}
.items-table tbody tr:last-child { border-bottom: 2px solid #e2e8f0; }
.items-table tbody tr:nth-child(even) { background: #f8fafc; }
.items-table tbody td {
padding: 10px 12px;
font-size: 13px;
color: #334155;
vertical-align: top;
}
.items-table tbody td.text-end { text-align: right; }
.items-table tbody td .sku {
font-size: 11px;
color: #94a3b8;
margin-top: 2px;
}
/* ── Totals ── */
.totals-wrap {
display: flex;
justify-content: flex-end;
margin-top: 0;
}
.totals-table {
width: 300px;
border-collapse: collapse;
}
.totals-table td {
padding: 5px 12px;
font-size: 13px;
color: #475569;
}
.totals-table td:last-child { text-align: right; }
.totals-table .total-row td {
border-top: 2px solid #1e293b;
padding-top: 10px;
padding-bottom: 10px;
font-size: 15px;
font-weight: 700;
color: #1e293b;
}
/* ── Notes ── */
.notes-section {
margin-top: 28px;
padding-top: 16px;
border-top: 1px solid #e2e8f0;
}
.notes-section .notes-label {
font-size: 10px;
font-weight: 700;
letter-spacing: 1px;
text-transform: uppercase;
color: #94a3b8;
margin-bottom: 6px;
}
.notes-section .notes-text {
font-size: 12px;
color: #475569;
line-height: 1.7;
white-space: pre-line;
}
/* ── Footer ── */
.inv-footer {
margin-top: 40px;
padding-top: 14px;
border-top: 1px solid #e2e8f0;
text-align: center;
font-size: 11px;
color: #94a3b8;
line-height: 1.8;
}
/* ── Print styles ── */
@media print {
#print_bar { display: none !important; }
body { background: #fff; }
.page-wrap { padding: 0; }
.sheet { width: 100%; min-height: auto; padding: 16mm 18mm; box-shadow: none; }
@page { size: A4; margin: 0; }
}
</style>
</head>
<body>
<!-- Print bar (hidden on print) -->
<div id="print_bar">
<a href="javascript:history.back()" class="btn-back">← Back</a>
<span><?= h($doc_label) ?> &nbsp;/&nbsp; <?= h($inv['invoice_number']) ?></span>
<button class="btn-print" onclick="window.print()">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>
Print / Save PDF
</button>
</div>
<div class="page-wrap">
<div class="sheet">
<!-- Header: company left, doc info right -->
<div class="inv-header">
<div class="co-block">
<?php if ($logo_url): ?>
<img src="<?= h($logo_url) ?>" alt="Logo">
<?php endif; ?>
<div class="co-name"><?= h($co['company_name'] ?? '') ?></div>
<?php if (!empty($co['company_name2'])): ?>
<div class="co-name2"><?= h($co['company_name2']) ?></div>
<?php endif; ?>
<div class="co-detail">
<?php
$co_lines = array_filter([
$co['address'] ?? '',
$co['address2'] ?? '',
!empty($co['tax_id']) ? 'Tax ID: ' . $co['tax_id'] : '',
!empty($co['phone']) ? 'Tel: ' . $co['phone'] : '',
!empty($co['email']) ? $co['email'] : '',
]);
echo implode('<br>', array_map('h', $co_lines));
?>
</div>
</div>
<div class="doc-block">
<div class="doc-type"><?= h($doc_label) ?></div>
<div class="doc-number"><?= h($inv['invoice_number']) ?></div>
<div class="doc-meta">
<div><strong>Issued</strong> <?= h(fmt_date($inv['issued_date'] ?? '')) ?></div>
<?php if (!empty($inv['due_date'])): ?>
<div><strong>Due</strong> <?= h(fmt_date($inv['due_date'])) ?></div>
<?php endif; ?>
<?php if (!empty($inv['order_number'])): ?>
<div><strong>Order</strong> <?= h($inv['order_number']) ?></div>
<?php endif; ?>
</div>
<?php
$status_classes = [
'0' => 'status-draft',
'1' => 'status-issued',
'2' => 'status-paid',
'3' => 'status-overdue',
'4' => 'status-void',
];
$sc = $status_classes[strval($inv['status'] ?? 0)] ?? 'status-draft';
?>
<div class="status-pill <?= $sc ?>"><?= h($status_label) ?></div>
</div>
</div>
<!-- Bill To -->
<div class="bill-section">
<div class="bill-box">
<div class="bill-label">Bill To</div>
<div class="bill-name"><?= h($inv['contact_name'] ?? '—') ?></div>
<?php if (!empty($inv['contact_billing_address'])): ?>
<div class="bill-detail"><?= h($inv['contact_billing_address']) ?></div>
<?php endif; ?>
<?php if (!empty($inv['contact_tax_id'])): ?>
<div class="bill-detail" style="margin-top:4px;">Tax ID: <?= h($inv['contact_tax_id']) ?></div>
<?php endif; ?>
</div>
</div>
<!-- Items table -->
<table class="items-table">
<thead>
<tr>
<th style="width:40px;">#</th>
<th>Product</th>
<th class="text-end" style="width:80px;">Qty</th>
<th class="text-end" style="width:110px;">Unit Price</th>
<th class="text-end" style="width:110px;">Amount</th>
</tr>
</thead>
<tbody>
<?php foreach ($items as $i => $item): ?>
<tr>
<td style="color:#94a3b8;"><?= $i + 1 ?></td>
<td>
<?= h($item['product_name'] ?? $item['product_sku'] ?? '') ?>
<?php if (!empty($item['product_sku']) && !empty($item['product_name'])): ?>
<div class="sku"><?= h($item['product_sku']) ?></div>
<?php endif; ?>
</td>
<td class="text-end"><?= h(fmt($item['quantity'] ?? 0)) ?></td>
<td class="text-end"><?= h(fmt($item['price'] ?? 0)) ?></td>
<td class="text-end"><?= h(fmt($item['total_price'] ?? 0)) ?></td>
</tr>
<?php endforeach; ?>
<?php if (empty($items)): ?>
<tr><td colspan="5" style="text-align:center;color:#94a3b8;padding:20px;">No items</td></tr>
<?php endif; ?>
</tbody>
</table>
<!-- Totals -->
<div class="totals-wrap">
<table class="totals-table">
<?php
$rows = [
['Subtotal', $inv['subtotal'] ?? 0],
['Discount', $inv['discount'] ?? 0],
['Tax', $inv['tax'] ?? 0],
['Shipping', $inv['shipping_fee'] ?? 0],
];
foreach ($rows as [$label, $val]):
?>
<tr>
<td><?= h($label) ?></td>
<td><?= h(fmt($val)) ?></td>
</tr>
<?php endforeach; ?>
<tr class="total-row">
<td>Grand Total</td>
<td><?= h(fmt($inv['grand_total'] ?? 0)) ?></td>
</tr>
</table>
</div>
<!-- Notes -->
<?php if (!empty($inv['notes'])): ?>
<div class="notes-section">
<div class="notes-label">Notes</div>
<div class="notes-text"><?= h($inv['notes']) ?></div>
</div>
<?php endif; ?>
<!-- Footer -->
<div class="inv-footer">
<?php
$footer_parts = array_filter([
$co['company_name'] ?? '',
!empty($co['phone']) ? 'Tel: ' . $co['phone'] : '',
$co['email'] ?? '',
$co['website'] ?? '',
]);
echo implode(' &nbsp;·&nbsp; ', array_map('h', $footer_parts));
?>
</div>
</div>
</div>
</body>
</html>