[test] contact to setting modules
This commit is contained in:
@@ -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));
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -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));
|
||||
?>
|
||||
?>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
}[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>
|
||||
|
||||
@@ -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>';
|
||||
}
|
||||
|
||||
@@ -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) ?> / <?= 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(' · ', array_map('h', $footer_parts));
|
||||
?>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user