warehouse layers [switchable]

This commit is contained in:
Thanakorn S
2026-05-04 13:43:32 +07:00
parent a90975d9f1
commit 414f1cd81d
26 changed files with 2022 additions and 584 deletions
+7 -16
View File
@@ -1,31 +1,22 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/ReturnManager.php';
require '../../../assets/utils/classes/WarehouseManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid ID.';
$answer['message'] = 'Invalid return ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id) {
$sth = $pdo->prepare(
"SELECT status FROM td_return
WHERE company_id = :company_id AND id = :id"
);
$sth->execute([':company_id' => $company_id, ':id' => $id]);
$row = $sth->fetch(PDO::FETCH_ASSOC);
if (!$row) throw new Exception("Return not found.");
if ((int)$row['status'] !== 0) throw new Exception("Only draft returns can be cancelled.");
$pdo->prepare(
"UPDATE td_return SET status = -1
WHERE id = :id AND company_id = :company_id"
)->execute([':id' => $id, ':company_id' => $company_id]);
dbTransaction($pdo2, function($pdo) use ($id, $company_id, $logging) {
$ret = new ReturnManager($pdo, $company_id);
$whMgmt = new WarehouseManager($pdo, $company_id);
$ret->cancelReturn($id, $logging, $whMgmt);
});
$answer['success'] = 1;
@@ -0,0 +1,39 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/OrderManager.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
$id = (int)($data['id'] ?? 0);
$fulfillment_status = (int)($data['fulfillment_status'] ?? 0);
$tracking_number = trim((string)($data['shipping_tracking_number'] ?? ''));
if (!$id) {
$answer['message'] = 'Invalid order ID.';
http_response_code(400);
exit(json_encode($answer));
}
$csm = new CompanySettingManager($pdo1, $company_id);
$auto_complete = (int)$csm->get('auto_complete_on_ship') === 1;
try {
dbTransaction($pdo2, function($pdo) use ($id, $fulfillment_status, $tracking_number, $company_id, $logging, $auto_complete) {
$order = new OrderManager($pdo, $company_id);
$order->updateFulfillmentStatus($id, $fulfillment_status, $tracking_number, $logging, $auto_complete);
});
$answer['success'] = 1;
$answer['message'] = 'Order updated.';
} catch (PDOException $e) {
$answer['message'] = 'Database error, please try again.';
http_response_code(500);
} catch (Exception $e) {
$answer['message'] = $e->getMessage();
http_response_code(400);
}
exit(json_encode($answer));
?>
+1 -1
View File
@@ -201,7 +201,7 @@
var status_hint = !is_invoice ? 'Status cannot be changed on credit/debit notes'
: status === 0 ? 'Issue the invoice first before changing status'
: status === 4 ? 'Invoice is void' : '';
set_btn_state('#invoice_status_group button', can_change_status, status_hint);
$('#invoice_status_select').prop('disabled', !can_change_status);
}
+63 -18
View File
@@ -60,6 +60,12 @@
placeholder="DD/MM/YYYY" autocomplete="off">
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">Shipping Tracking Number</label>
<input type="text" id="shipping_tracking_number" class="form-control"
placeholder="Carrier tracking number..." maxlength="100" autocomplete="off">
</div>
<div class="mb-3 col-lg-12">
<label class="form-label">Notes</label>
<textarea id="notes" class="form-control" rows="2"
@@ -112,8 +118,10 @@
<div class="d-flex gap-2 mb-2">
<span id="badge_status"></span>
<span id="badge_payment"></span>
<span id="badge_fulfillment"></span>
</div>
<p class="mb-0 small text-muted" id="order_number_display"></p>
<p class="mb-0 small text-muted d-none" id="tracking_number_display"></p>
</div>
</div>
@@ -172,7 +180,21 @@
<i class="ti ti-file-invoice me-1"></i>Proceed to Invoice
</button>
<!-- Update payment status (confirmed and above) -->
<!-- Update fulfillment status (confirmed and above) -->
<div id="fulfillment_status_group">
<label class="form-label small text-muted mb-1">Fulfillment Status</label>
<div class="d-flex gap-2">
<select id="fulfillment_status_select" class="form-select form-select-sm">
<option value="1">Picking</option>
<option value="2">Packed</option>
<option value="3">Shipped</option>
</select>
<button class="btn btn-sm btn-outline-primary text-nowrap"
onclick="update_fulfillment_status()">
<i class="ti ti-check"></i>
</button>
</div>
</div>
<div id="payment_status_group">
<label class="form-label small text-muted mb-1">Payment Status</label>
<div class="d-flex gap-2">
@@ -242,6 +264,15 @@
return map[String(status)] ?? '—';
}
function fulfillment_status_badge(status) {
const map = {
'1': '<span class="badge bg-warning text-dark"><i class="ti ti-list-check me-1"></i>Picking</span>',
'2': '<span class="badge bg-info text-white"><i class="ti ti-package me-1"></i>Packed</span>',
'3': '<span class="badge bg-success"><i class="ti ti-truck me-1"></i>Shipped</span>',
};
return map[String(status)] ?? '';
}
// ── Recalculate totals ────────────────────────────────────────────────────
function recalc_totals() {
@@ -346,37 +377,45 @@
// ── Set view/edit mode ────────────────────────────────────────────────────
function set_mode(status, has_active_invoice, returnable_count) {
function set_mode(status, has_active_invoice, returnable_count, fulfillment_status) {
status = parseInt(status);
has_active_invoice = has_active_invoice || false;
returnable_count = returnable_count ?? 1;
fulfillment_status = parseInt(fulfillment_status) || 0;
var is_draft = status === 0;
var can_confirm = status === 0;
var can_cancel = status === 0 || status === 1;
var is_cancelled = status === -1;
var is_completed = status === 3;
var can_fulfill = status >= 1 && !is_cancelled && !is_completed;
// Edit controls
$('#btn_add_item, #contact, #order_date, #notes, #discount, #tax, #shipping_fee')
.prop('disabled', !is_draft);
$('#shipping_tracking_number').prop('disabled', !(is_draft || can_fulfill));
$('.item_sku, .item_qty, .item_price, .item_warehouse').prop('disabled', !is_draft);
$('a[onclick^="remove_item"]').toggleClass('d-none', !is_draft);
// Action buttons — set_btn_state handles class swap + disabled + title
// Action buttons
set_btn_state('#btn_save', is_draft, 'Only draft orders can be edited');
set_btn_state('#btn_confirm', can_confirm, 'Order must be in draft to confirm');
set_btn_state('#btn_cancel', can_cancel && !is_cancelled,
is_cancelled ? 'Order is already cancelled' : 'Only draft or confirmed orders can be cancelled');
// Order status selector — confirmed and above, not cancelled
var can_change_status = status >= 1 && !is_cancelled;
$('#order_status_group').toggleClass('d-none', !can_change_status);
set_btn_state('#order_status_group button', can_change_status,
is_cancelled ? 'Order is cancelled' : 'Confirm the order first');
$('#order_status_select').prop('disabled', !can_change_status);
// Fulfillment status — confirmed and above, not cancelled, not completed
$('#fulfillment_status_group').toggleClass('d-none', !can_fulfill);
if (can_fulfill) {
// Only show options ahead of current fulfillment
$('#fulfillment_status_select option').each(function() {
$(this).prop('disabled', parseInt($(this).val()) <= fulfillment_status);
});
// Default to next step
var next = fulfillment_status + 1;
if (next <= 3) $('#fulfillment_status_select').val(next);
else $('#fulfillment_status_select').val(fulfillment_status);
}
// Payment status selector
set_btn_state('#payment_status_group button', status >= 1 && !is_cancelled,
'Confirm the order before updating payment status');
$('#payment_status_select').prop('disabled', status < 1 || is_cancelled);
// Create Return
@@ -421,12 +460,17 @@
$('#status_card').removeClass('d-none');
$('#badge_status').html(order_status_badge(o.status));
$('#badge_payment').html(payment_status_badge(o.payment_status));
$('#badge_fulfillment').html(fulfillment_status_badge(o.fulfillment_status));
$('#order_number_display').text(o.order_number);
$('#tracking_number_display')
.toggleClass('d-none', !o.shipping_tracking_number)
.text(o.shipping_tracking_number ? 'Tracking: ' + o.shipping_tracking_number : '');
// Fields
$('#contact').val(o.contact_name || '');
$('#contact_id').val(o.contact_id);
$('#order_date').val(o.order_date ? format_date(o.order_date) : '');
$('#shipping_tracking_number').val(o.shipping_tracking_number || '');
$('#notes').val(o.notes || '');
$('#discount').val(o.discount || 0);
$('#tax').val(o.tax || 0);
@@ -437,9 +481,8 @@
(o.items || []).forEach(function(item) { add_item_row(item); });
recalc_totals();
set_mode(o.status, res.has_active_invoice, (res.returnable_items || []).length);
set_mode(o.status, res.has_active_invoice, (res.returnable_items || []).length, o.fulfillment_status);
$('#payment_status_select').val(o.payment_status ?? 0);
$('#order_status_select').val(o.status);
}
});
}
@@ -470,6 +513,7 @@
discount: $('#discount').val() || 0,
tax: $('#tax').val() || 0,
shipping_fee: $('#shipping_fee').val() || 0,
shipping_tracking_number: $('#shipping_tracking_number').val().trim(),
notes: $('#notes').val(),
},
onSuccess: function(res) {
@@ -508,16 +552,17 @@
}
// ── Update order status ───────────────────────────────────────────────────
function update_order_status() {
// ── Update fulfillment status ─────────────────────────────────────────────
function update_fulfillment_status() {
ajax_request({
url: '<?php echo $server_url?>order/api/engine/update_order_status.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: {
id: order_id,
status: $('#order_status_select').val(),
id: order_id,
fulfillment_status: $('#fulfillment_status_select').val(),
shipping_tracking_number: $('#shipping_tracking_number').val().trim(),
},
onSuccess: function() {
retrieve_order();
@@ -693,4 +738,4 @@
</script>
</body>
</html>
</html>
+2 -2
View File
@@ -376,8 +376,8 @@
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');
set_btn_state('#btn_cancel', is_draft,
status === -1 ? 'Return is already cancelled' : 'Only draft returns can be cancelled');
set_btn_state('#btn_cancel', is_draft || status === 1,
status === -1 ? 'Return is already cancelled' : 'Return is completed and cannot be cancelled');
}
+17 -3
View File
@@ -95,7 +95,7 @@
<div class="col-lg-5">
<div class="position-relative">
<input type="text" id="search_input" class="form-control ps-9"
placeholder="Search order number or contact...">
placeholder="Search order number, contact, or tracking...">
<span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i>
</span>
@@ -144,6 +144,8 @@
<th class="py-3">Items</th>
<th class="py-3">Grand Total</th>
<th class="py-3">Status</th>
<th class="py-3">Fulfillment</th>
<th class="py-3">Tracking #</th>
<th class="py-3">Payment</th>
<th class="py-3">Action</th>
</tr>
@@ -188,6 +190,15 @@
return map[status] ?? '<span class="badge bg-light text-dark">—</span>';
}
function fulfillment_status_badge(status) {
const map = {
'1': '<span class="badge bg-warning text-dark"><i class="ti ti-list-check me-1"></i>Picking</span>',
'2': '<span class="badge bg-info text-white"><i class="ti ti-package me-1"></i>Packed</span>',
'3': '<span class="badge bg-success"><i class="ti ti-truck me-1"></i>Shipped</span>',
};
return map[String(status)] ?? '<span class="text-muted">—</span>';
}
// ── Retrieve orders ───────────────────────────────────────────────────────
function retrieve_orders() {
@@ -208,7 +219,8 @@
var data = all.filter(function(o) {
var match_kw = !keyword ||
(o.order_number || '').toLowerCase().includes(keyword) ||
(o.contact_name || '').toLowerCase().includes(keyword);
(o.contact_name || '').toLowerCase().includes(keyword) ||
(o.shipping_tracking_number || '').toLowerCase().includes(keyword);
var match_st = fil_status === '' || String(o.status) === fil_status;
var match_pm = fil_payment === '' || String(o.payment_status) === fil_payment;
return match_kw && match_st && match_pm;
@@ -254,6 +266,8 @@
<td class="py-3">${item_count} item${item_count !== 1 ? 's' : ''}</td>
<td class="py-3 fw-semibold">${format_number(o.grand_total, 2)}</td>
<td class="py-3">${order_status_badge(o.status)}</td>
<td class="py-3">${fulfillment_status_badge(o.fulfillment_status)}</td>
<td class="py-3">${o.shipping_tracking_number || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${payment_status_badge(o.payment_status)}</td>
<td class="py-3">
<a href="<?php echo $server_url?>order/manage_order.php?id=${o.id}" title="View">
@@ -309,4 +323,4 @@
</script>
</body>
</html>
</html>
+1 -1
View File
@@ -146,7 +146,7 @@
$.each(page_data, function(i, r) {
var items = JSON.parse(r.items || '[]');
var can_cancel = parseInt(r.status) === 0;
var can_cancel = parseInt(r.status) >= 0 && parseInt(r.status) <= 1;
body += `<tr>
<td class="py-3 fw-semibold">${r.return_number}</td>