Orders: order, return, invoice

This commit is contained in:
Thanakorn S
2026-05-02 16:49:21 +07:00
parent ac4fd9a5ad
commit a90975d9f1
47 changed files with 4838 additions and 99 deletions
+33
View File
@@ -0,0 +1,33 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/OrderManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid order ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id, $logging) {
$order = new OrderManager($pdo, $company_id);
$order->cancelOrder($id, $logging);
});
$answer['success'] = 1;
$answer['message'] = 'Order cancelled.';
} 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));
?>
+44
View File
@@ -0,0 +1,44 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid 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]);
});
$answer['success'] = 1;
$answer['message'] = 'Return cancelled.';
} 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));
?>
+48
View File
@@ -0,0 +1,48 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/OrderManager.php';
require '../../../assets/utils/classes/StockManager.php';
require '../../../assets/utils/classes/WarehouseManager.php';
require '../../../assets/utils/classes/InvoiceManager.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid order ID.';
http_response_code(400);
exit(json_encode($answer));
}
$csm = new CompanySettingManager($pdo1, $company_id);
$auto_approve = (int)$csm->get('default_stock_status') === 1;
$auto_invoice = (int)$csm->get('auto_invoice_and_credit_note') === 1;
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id, $logging, $uuid, $auto_approve, $auto_invoice) {
$order = new OrderManager($pdo, $company_id);
$order->confirmOrder($id, $uuid, $logging, $auto_approve);
if ($auto_invoice) {
$invMgmt = new InvoiceManager($pdo, $company_id);
$invMgmt->createFromOrder($id, $logging);
}
});
$answer['success'] = 1;
$answer['message'] = 'Order confirmed.';
$answer['auto_approved'] = $auto_approve;
$answer['auto_invoice'] = $auto_invoice;
} 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));
?>
+42
View File
@@ -0,0 +1,42 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/ReturnManager.php';
require '../../../assets/utils/classes/InvoiceManager.php';
require '../../../assets/utils/classes/WarehouseManager.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid ID.';
http_response_code(400);
exit(json_encode($answer));
}
$csm = new CompanySettingManager($pdo1, $company_id);
$auto_cn = (int)$csm->get('auto_invoice_and_credit_note') === 1;
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id, $logging, $uuid, $auto_cn) {
$ret = new ReturnManager($pdo, $company_id);
$whMgmt = new WarehouseManager($pdo, $company_id);
$invMgmt = new InvoiceManager($pdo, $company_id);
$ret->confirmReturn($id, $uuid, $logging, $whMgmt, $invMgmt, $auto_cn);
});
$answer['success'] = 1;
$answer['message'] = 'Return confirmed.';
$answer['auto_cn'] = $auto_cn;
} 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));
?>
+33
View File
@@ -0,0 +1,33 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/InvoiceManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id, $logging) {
$inv = new InvoiceManager($pdo, $company_id);
$inv->issueInvoice($id, $logging);
});
$answer['success'] = 1;
$answer['message'] = 'Invoice issued.';
} 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));
?>
+24
View File
@@ -0,0 +1,24 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/InvoiceManager.php';
try {
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging) {
$inv = new InvoiceManager($pdo, $company_id);
$inv->saveInvoice($data, $logging);
});
$answer['success'] = 1;
} 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));
?>
+32
View File
@@ -0,0 +1,32 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/OrderManager.php';
// Decode items JSON string back to array
$data['items'] = json_decode($data['items'] ?? '[]', true) ?: [];
try {
$new_id = null;
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, &$new_id) {
$order = new OrderManager($pdo, $company_id);
$new_id = $order->saveOrder($data, $logging);
});
$answer['success'] = 1;
if ($new_id) {
$answer['new_id'] = $new_id;
}
} 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));
?>
+31
View File
@@ -0,0 +1,31 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/ReturnManager.php';
$data['items'] = json_decode($data['items'] ?? '[]', true) ?: [];
try {
$new_id = null;
dbTransaction($pdo2, function($pdo) use ($data, $company_id, $logging, &$new_id) {
$ret = new ReturnManager($pdo, $company_id);
$new_id = $ret->saveReturn($data, $logging);
});
$answer['success'] = 1;
if ($new_id) {
$answer['new_id'] = $new_id;
}
} 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));
?>
@@ -0,0 +1,35 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/InvoiceManager.php';
$order_id = (int)($data['order_id'] ?? 0);
if (!$order_id) {
$answer['message'] = 'Invalid order ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
$new_id = null;
dbTransaction($pdo2, function($pdo) use ($order_id, $company_id, $logging, &$new_id) {
$inv = new InvoiceManager($pdo, $company_id);
$new_id = $inv->createFromOrder($order_id, $logging);
});
$answer['success'] = 1;
$answer['new_id'] = $new_id;
} 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));
?>
+57
View File
@@ -0,0 +1,57 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/InvoiceManager.php';
$inv = new InvoiceManager($pdo2, $company_id);
$id = (int)($data['invoice_id'] ?? 0);
if ($id > 0) {
$result = $inv->getInvoiceById($id);
if (!$result) {
http_response_code(404);
$answer['message'] = 'Invoice not found.';
exit(json_encode($answer));
}
$answer['output'] = $result;
// Fetch linked credit notes for invoice detail page
if ($result['doc_type'] === 'invoice') {
$sth = $pdo2->prepare(
"SELECT * FROM td_invoice
WHERE company_id = :company_id
AND ref_invoice_id = :ref_id
AND doc_type = 'credit_note'
ORDER BY id DESC"
);
$sth->execute([':company_id' => $company_id, ':ref_id' => $id]);
$answer['credit_notes'] = $sth->fetchAll(PDO::FETCH_ASSOC);
}
} else {
$order_id = (int)($data['order_id'] ?? 0);
$doc_type = $data['doc_type'] ?? '';
$answer['output'] = $inv->getInvoiceList($order_id, $doc_type);
// For credit note tab — join ref invoice number
if ($doc_type === 'credit_note') {
$rows = $answer['output'];
foreach ($rows as &$row) {
if ((int)$row['ref_invoice_id'] > 0) {
$sth = $pdo2->prepare(
"SELECT invoice_number FROM td_invoice
WHERE id = :id AND company_id = :company_id"
);
$sth->execute([':id' => $row['ref_invoice_id'], ':company_id' => $company_id]);
$row['ref_invoice_number'] = $sth->fetchColumn() ?: '—';
} else {
$row['ref_invoice_number'] = '—';
}
}
unset($row);
$answer['output'] = $rows;
}
}
$answer['success'] = 1;
exit(json_encode($answer));
?>
+73
View File
@@ -0,0 +1,73 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/OrderManager.php';
$order = new OrderManager($pdo2, $company_id);
$id = (int)($data['id'] ?? 0);
if ($id > 0) {
$result = $order->getOrderById($id);
if (!$result) {
http_response_code(404);
$answer['message'] = 'Order not found.';
exit(json_encode($answer));
}
$answer['output'] = $result;
// Check if an active invoice already exists for this order
$sth = $pdo2->prepare(
"SELECT COUNT(*) FROM td_invoice
WHERE company_id = :company_id
AND order_id = :order_id
AND doc_type = 'invoice'
AND status != 4"
);
$sth->execute([':company_id' => $company_id, ':order_id' => $id]);
$answer['has_active_invoice'] = (int)$sth->fetchColumn() > 0;
// Calculate returnable items — order items minus already confirmed returns
$order_items = $result['items'] ?? '[]';
// Aggregate returned qty per item_id from confirmed returns
$sth = $pdo2->prepare(
"SELECT items FROM td_return
WHERE company_id = :company_id
AND order_id = :order_id
AND status = 1"
);
$sth->execute([':company_id' => $company_id, ':order_id' => $id]);
$returned_qty = [];
foreach ($sth->fetchAll(PDO::FETCH_COLUMN) as $items_json) {
$items = json_decode($items_json, true) ?: [];
foreach ($items as $item) {
$item_id = (int)($item['item_id'] ?? -1);
if ($item_id < 0) continue;
$returned_qty[$item_id] = ($returned_qty[$item_id] ?? 0) + (float)$item['quantity'];
}
}
$returnable = [];
foreach ($order_items as $i => $item) {
$already_returned = $returned_qty[$i] ?? 0;
$remaining = (float)$item['quantity'] - $already_returned;
if ($remaining > 0) {
$returnable[] = array_merge($item, [
'item_id' => $i,
'original_qty' => (float)$item['quantity'],
'returned_qty' => $already_returned,
'returnable_qty' => $remaining,
]);
}
}
$answer['returnable_items'] = $returnable;
} else {
$answer['output'] = $order->getOrderList();
}
$answer['success'] = 1;
exit(json_encode($answer));
?>
+24
View File
@@ -0,0 +1,24 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/ReturnManager.php';
$ret = new ReturnManager($pdo2, $company_id);
$id = (int)($data['id'] ?? 0);
if ($id > 0) {
$result = $ret->getReturnById($id);
if (!$result) {
http_response_code(404);
$answer['message'] = 'Return not found.';
exit(json_encode($answer));
}
$answer['output'] = $result;
} else {
$order_id = (int)($data['order_id'] ?? 0);
$answer['output'] = $ret->getReturnList($order_id);
}
$answer['success'] = 1;
exit(json_encode($answer));
?>
@@ -0,0 +1,71 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/InvoiceManager.php';
$id = (int)($data['invoice_id'] ?? 0);
$status = (int)($data['status'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid invoice ID.';
http_response_code(400);
exit(json_encode($answer));
}
// 2=paid 3=overdue 4=void — issue (0→1) handled by issue_invoice.php
if (!in_array($status, [2, 3, 4], true)) {
$answer['message'] = 'Invalid status value.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $status, $company_id, $logging) {
$sth = $pdo->prepare(
"SELECT status, doc_type, `log` FROM td_invoice
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("Invoice not found.");
if ($row['doc_type'] !== 'invoice') throw new Exception("Only invoices can be updated this way.");
$current = (int)$row['status'];
if ($current === 4) throw new Exception("Invoice is already void.");
if ($current === 0) throw new Exception("Issue the invoice before changing payment status.");
$log = json_decode($row['log'] ?? '[]', true) ?: [];
$log[] = array_merge($logging, [
'action' => 'update_status',
'status' => $status,
'previous_status' => $current,
]);
$pdo->prepare(
"UPDATE td_invoice SET status = :status, `log` = :log
WHERE id = :id AND company_id = :company_id"
)->execute([
':status' => $status,
':log' => json_encode($log),
':id' => $id,
':company_id' => $company_id,
]);
});
$answer['success'] = 1;
$answer['message'] = 'Invoice status 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));
?>
@@ -0,0 +1,69 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
$id = (int)($data['id'] ?? 0);
$payment_status = (int)($data['payment_status'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid order ID.';
http_response_code(400);
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)) {
$answer['message'] = 'Invalid payment status.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $payment_status, $company_id, $logging) {
// Load order — verify ownership and that order is not cancelled
$sth = $pdo->prepare(
"SELECT status, payment_status, `log` FROM td_order
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("Order not found.");
if ((int)$row['status'] === -1) throw new Exception("Cannot update payment status of a cancelled order.");
$log = json_decode($row['log'] ?? '[]', true) ?: [];
$log[] = array_merge($logging, [
'action' => 'update_payment_status',
'payment_status' => $payment_status,
'previous_status' => (int)$row['payment_status'],
]);
$pdo->prepare(
"UPDATE td_order SET
payment_status = :payment_status,
`log` = :log
WHERE id = :id AND company_id = :company_id"
)->execute([
':payment_status' => $payment_status,
':log' => json_encode($log),
':id' => $id,
':company_id' => $company_id,
]);
});
$answer['success'] = 1;
$answer['message'] = 'Payment status 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));
?>
+33
View File
@@ -0,0 +1,33 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/InvoiceManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid ID.';
http_response_code(400);
exit(json_encode($answer));
}
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id, $logging) {
$inv = new InvoiceManager($pdo, $company_id);
$inv->voidInvoice($id, $logging);
});
$answer['success'] = 1;
$answer['message'] = 'Invoice voided.';
} 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));
?>
+47
View File
@@ -0,0 +1,47 @@
<?php
session_start();
require '../../../assets/utils/db_auth.php';
require '../../../assets/utils/classes/OrderManager.php';
require '../../../assets/utils/classes/StockManager.php';
require '../../../assets/utils/classes/WarehouseManager.php';
require '../../../assets/utils/classes/InvoiceManager.php';
require '../../../assets/utils/classes/CompanySettingManager.php';
$id = (int)($data['id'] ?? 0);
if (!$id) {
$answer['message'] = 'Invalid order ID.';
http_response_code(400);
exit(json_encode($answer));
}
$csm = new CompanySettingManager($pdo1, $company_id);
$auto_approve = (int)$csm->get('default_stock_status') === 1;
$auto_invoice = (int)$csm->get('auto_invoice_and_credit_note') === 1;
try {
dbTransaction($pdo2, function($pdo) use ($id, $company_id, $logging, $uuid, $auto_approve, $auto_invoice) {
$order = new OrderManager($pdo, $company_id);
$order->confirmOrder($id, $uuid, $logging, $auto_approve);
if ($auto_invoice) {
$invMgmt = new InvoiceManager($pdo, $company_id);
$invMgmt->createFromOrder($id, $logging);
}
});
$answer['success'] = 1;
$answer['message'] = 'Order confirmed.';
$answer['auto_approved'] = $auto_approve;
} 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));
?>
+342
View File
@@ -0,0 +1,342 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="mb-6 d-flex justify-content-between align-items-center">
<div>
<h1 class="fs-3 mb-1">Invoices</h1>
<p class="mb-0">Invoices and credit notes</p>
</div>
</div>
</div>
</div>
<!-- Tabs -->
<div class="row mb-5">
<div class="col-12">
<ul class="nav nav-tabs" id="invoice_tabs">
<li class="nav-item">
<a class="nav-link active" href="#" onclick="switch_tab('invoice'); return false;">
<i class="ti ti-file-invoice me-1"></i>Invoices
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#" onclick="switch_tab('credit_note'); return false;">
<i class="ti ti-file-minus me-1"></i>Credit Notes
</a>
</li>
</ul>
</div>
</div>
<!-- Filter bar -->
<div class="row g-5 mb-5">
<div class="col-12">
<div class="card">
<div class="card-body p-4">
<div class="row g-3 align-items-end">
<div class="col-lg-5">
<div class="position-relative">
<input type="text" id="search_input" class="form-control ps-9"
placeholder="Search number or contact...">
<span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i>
</span>
</div>
</div>
<div class="col-lg-3" id="status_filter_wrap">
<select id="filter_status" class="form-select">
<option value="">All Status</option>
<option value="0">Draft</option>
<option value="1">Issued</option>
<option value="2">Paid</option>
<option value="3">Overdue</option>
<option value="4">Void</option>
</select>
</div>
<div class="col-lg-2">
<button class="btn btn-secondary w-100" onclick="retrieve_invoices()">
<i class="ti ti-search me-1"></i>Search
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<div class="table-responsive">
<!-- Invoice table -->
<table class="table table-hover mb-0 table-centered" id="invoice_list">
<thead class="table-primary border-light">
<tr>
<th class="py-3">Invoice #</th>
<th class="py-3">Order #</th>
<th class="py-3">Contact</th>
<th class="py-3">Issued Date</th>
<th class="py-3">Due Date</th>
<th class="py-3">Grand Total</th>
<th class="py-3">Status</th>
<th class="py-3">Action</th>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
<!-- Credit Note table -->
<table class="table table-hover mb-0 table-centered d-none" id="cn_list">
<thead class="table-warning border-light">
<tr>
<th class="py-3">CN #</th>
<th class="py-3">Order #</th>
<th class="py-3">Ref Invoice #</th>
<th class="py-3">Contact</th>
<th class="py-3">Date</th>
<th class="py-3">Amount</th>
<th class="py-3">Action</th>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
var current_tab = 'invoice';
// ── Badges ────────────────────────────────────────────────────────────────
function invoice_status_badge(status) {
const map = {
'0': '<span class="badge bg-secondary">Draft</span>',
'1': '<span class="badge bg-info text-white">Issued</span>',
'2': '<span class="badge bg-success">Paid</span>',
'3': '<span class="badge bg-danger">Overdue</span>',
'4': '<span class="badge bg-light text-dark">Void</span>',
};
return map[String(status)] ?? '—';
}
// ── Tab switch ────────────────────────────────────────────────────────────
function switch_tab(tab) {
current_tab = tab;
// Update nav tabs
$('#invoice_tabs .nav-link').each(function() {
var is_active = $(this).attr('onclick').includes("'" + tab + "'");
$(this).toggleClass('active', is_active);
});
// Show/hide tables
$('#invoice_list').toggleClass('d-none', tab !== 'invoice');
$('#cn_list').toggleClass('d-none', tab !== 'credit_note');
// Status filter — CN has no draft/paid/overdue, simpler
if (tab === 'credit_note') {
$('#filter_status').html(`
<option value="">All Status</option>
<option value="1">Issued</option>
<option value="4">Void</option>
`);
} else {
$('#filter_status').html(`
<option value="">All Status</option>
<option value="0">Draft</option>
<option value="1">Issued</option>
<option value="2">Paid</option>
<option value="3">Overdue</option>
<option value="4">Void</option>
`);
}
retrieve_invoices();
}
// ── Retrieve ──────────────────────────────────────────────────────────────
function retrieve_invoices() {
return ajax_request({
url: '<?php echo $server_url?>order/api/engine/retrieve_invoice.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { doc_type: current_tab },
onSuccess: function(res) {
var all = res.output || [];
var keyword = $('#search_input').val().toLowerCase();
var fil_st = $('#filter_status').val();
var data = all.filter(function(i) {
var match_kw = !keyword ||
(i.invoice_number || '').toLowerCase().includes(keyword) ||
(i.contact_name || '').toLowerCase().includes(keyword);
var match_st = fil_st === '' || String(i.status) === fil_st;
return match_kw && match_st;
});
if (current_tab === 'invoice') {
render_invoice_table(data);
} else {
render_cn_table(data);
}
}
});
}
// ── Render invoice table ──────────────────────────────────────────────────
function render_invoice_table(data) {
alasql('DROP TABLE IF EXISTS invoice_list');
alasql('CREATE TABLE invoice_list');
alasql.tables.invoice_list.data = data;
var page = generate_pagination('invoice_list', data.length);
$('table#invoice_list tfoot').html(page);
if (typeof change_page_invoice_list === 'function') {
change_page_invoice_list(1);
}
}
function change_page_invoice_list(page_num) {
var offset = (page_num - 1) * prop_limit;
var page_data = alasql(`SELECT * FROM invoice_list LIMIT ${prop_limit} OFFSET ${offset}`);
var body = '';
$.each(page_data, function(i, inv) {
var status = parseInt(inv.status);
var can_void = status >= 1 && status <= 3;
body += `<tr>
<td class="py-3 fw-semibold">${inv.invoice_number}</td>
<td class="py-3">${inv.order_number || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${inv.contact_name || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${inv.issued_date ? format_date(inv.issued_date) : '<span class="text-muted">—</span>'}</td>
<td class="py-3">${inv.due_date ? format_date(inv.due_date) : '<span class="text-muted">—</span>'}</td>
<td class="py-3 fw-semibold">${format_number(inv.grand_total, 2)}</td>
<td class="py-3">${invoice_status_badge(inv.status)}</td>
<td class="py-3">
<a href="<?php echo $server_url?>order/manage_invoice.php?id=${inv.id}" title="View">
<i class="ti ti-eye fs-5"></i>
</a>
${can_void ? `
<a href="javascript:void(0);" class="link-danger ms-2" title="Void"
onclick="void_invoice(${inv.id})">
<i class="ti ti-ban fs-5"></i>
</a>` : ''}
</td>
</tr>`;
});
$('table#invoice_list > tbody').html(body);
}
// ── Render credit note table ──────────────────────────────────────────────
function render_cn_table(data) {
alasql('DROP TABLE IF EXISTS cn_list');
alasql('CREATE TABLE cn_list');
alasql.tables.cn_list.data = data;
var page = generate_pagination('cn_list', data.length);
$('table#cn_list tfoot').html(page);
if (typeof change_page_cn_list === 'function') {
change_page_cn_list(1);
}
}
function change_page_cn_list(page_num) {
var offset = (page_num - 1) * prop_limit;
var page_data = alasql(`SELECT * FROM cn_list LIMIT ${prop_limit} OFFSET ${offset}`);
var body = '';
$.each(page_data, function(i, cn) {
var amount = '<span class="text-danger">' + format_number(cn.grand_total, 2) + '</span>';
body += `<tr>
<td class="py-3 fw-semibold">${cn.invoice_number}</td>
<td class="py-3">${cn.order_number || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${cn.ref_invoice_number || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${cn.contact_name || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${cn.issued_date ? format_date(cn.issued_date) : '<span class="text-muted">—</span>'}</td>
<td class="py-3 fw-semibold">${amount}</td>
<td class="py-3">
<a href="<?php echo $server_url?>order/manage_invoice.php?id=${cn.id}" title="View">
<i class="ti ti-eye fs-5"></i>
</a>
</td>
</tr>`;
});
$('table#cn_list > tbody').html(body);
}
// ── Void invoice ──────────────────────────────────────────────────────────
function void_invoice(id) {
bootbox.confirm({
message: '<strong>Void this invoice?</strong><br>This action cannot be undone.',
buttons: {
confirm: { label: 'Void Invoice', className: 'btn-danger' },
cancel: { label: 'Back', className: 'btn-secondary' }
},
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/update_invoice_status.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: { invoice_id: id, status: 4 },
onSuccess: function() { retrieve_invoices(); }
});
}
});
}
// ── Boot ─────────────────────────────────────────────────────────────────
$(function() {
retrieve_invoices();
$('#search_input').on('keyup', function(e) {
if (e.key === 'Enter') retrieve_invoices();
});
});
</script>
</body>
</html>
+358
View File
@@ -0,0 +1,358 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
$invoice_id = (int)($_GET['id'] ?? 0);
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="mb-6 d-flex justify-content-between align-items-center">
<div>
<h1 class="fs-3 mb-1" id="page_title">Invoice Detail</h1>
<p class="mb-0 text-muted" id="page_subtitle"></p>
</div>
<a href="<?php echo $server_url?>order/invoice.php" class="btn btn-light">
<i class="ti ti-arrow-left me-1"></i>Back
</a>
</div>
</div>
</div>
<div class="row g-5">
<!-- Left: Items table -->
<div class="col-lg-8">
<div class="card mb-5">
<div class="card-body p-5">
<h2 class="fs-5 mb-4">Items</h2>
<div class="table-responsive">
<table class="table mb-0">
<thead class="table-light">
<tr>
<th>Product</th>
<th class="text-end">Qty</th>
<th class="text-end">Unit Price</th>
<th class="text-end">Total</th>
</tr>
</thead>
<tbody id="item_tbody"></tbody>
<tfoot id="item_tfoot"></tfoot>
</table>
</div>
</div>
</div>
<!-- Linked credit notes -->
<div class="card d-none" id="cn_card">
<div class="card-body p-5">
<h2 class="fs-5 mb-4">Credit Notes</h2>
<div class="table-responsive">
<table class="table mb-0 table-hover">
<thead class="table-light">
<tr>
<th>CN #</th>
<th>Date</th>
<th class="text-end">Amount</th>
<th>Status</th>
<th></th>
</tr>
</thead>
<tbody id="cn_tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Right: Info + actions -->
<div class="col-lg-4">
<!-- Status + info -->
<div class="card mb-5">
<div class="card-body p-4">
<h2 class="fs-5 mb-3">Document Info</h2>
<div class="mb-2 d-flex gap-2">
<span id="badge_doc_type"></span>
<span id="badge_status"></span>
</div>
<div class="mb-2 small">
<span class="text-muted">Order:</span>
<a href="#" id="link_order" class="ms-1 fw-semibold"></a>
</div>
<div class="mb-2 small">
<span class="text-muted">Contact:</span>
<span class="ms-1 fw-semibold" id="display_contact"></span>
</div>
<div class="mb-2 small">
<span class="text-muted">Issued:</span>
<span class="ms-1" id="display_issued"></span>
</div>
<div class="mb-2 small">
<span class="text-muted">Due:</span>
<input type="text" id="due_date" class="form-control form-control-sm ms-1 d-inline-block"
style="width:140px;" placeholder="DD/MM/YYYY">
</div>
<div class="mt-3 small">
<span class="text-muted">Notes:</span>
<textarea id="notes" class="form-control form-control-sm mt-1" rows="2"></textarea>
</div>
</div>
</div>
<!-- Actions -->
<div class="card">
<div class="card-body p-4 d-flex flex-column gap-2">
<button class="btn btn-primary w-100" id="btn_save"
onclick="save_invoice()">
<i class="ti ti-device-floppy me-1"></i>Save
</button>
<button class="btn btn-success w-100" id="btn_issue"
onclick="issue_invoice()">
<i class="ti ti-send me-1"></i>Issue Invoice
</button>
<!-- Status selector (issued and above) -->
<div class="" id="invoice_status_group">
<label class="form-label small text-muted mb-1">Invoice Status</label>
<div class="d-flex gap-2">
<select id="invoice_status_select" class="form-select form-select-sm">
<option value="1">Issued</option>
<option value="2">Paid</option>
<option value="3">Overdue</option>
<option value="4">Void</option>
</select>
<button class="btn btn-sm btn-outline-primary text-nowrap"
onclick="update_invoice_status($('#invoice_status_select').val())">
<i class="ti ti-check"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
var invoice_id = <?php echo $invoice_id; ?>;
var invoice_data = null;
function doc_type_badge(doc_type) {
const map = {
'invoice': '<span class="badge bg-primary">Invoice</span>',
'credit_note': '<span class="badge bg-warning text-dark">Credit Note</span>',
'debit_note': '<span class="badge bg-info text-white">Debit Note</span>',
};
return map[doc_type] ?? '—';
}
function invoice_status_badge(status) {
const map = {
'0': '<span class="badge bg-secondary">Draft</span>',
'1': '<span class="badge bg-info text-white">Issued</span>',
'2': '<span class="badge bg-success">Paid</span>',
'3': '<span class="badge bg-danger">Overdue</span>',
'4': '<span class="badge bg-light text-dark">Void</span>',
};
return map[String(status)] ?? '—';
}
function set_mode(inv) {
var status = parseInt(inv.status);
var doc_type = inv.doc_type;
var is_invoice = doc_type === 'invoice';
var is_draft = status === 0;
var is_active = status >= 1 && status <= 3;
// Fields editable only on draft invoice
$('#due_date, #notes').prop('disabled', !(is_draft && is_invoice));
// Save + Issue
var is_draft_invoice = is_draft && is_invoice;
var not_invoice_hint = doc_type !== 'invoice' ? 'Only original invoices can be edited' : '';
set_btn_state('#btn_save', is_draft_invoice,
not_invoice_hint || 'Only draft invoices can be edited');
set_btn_state('#btn_issue', is_draft_invoice,
not_invoice_hint || (status > 0 ? 'Invoice has already been issued' : 'Only draft invoices can be issued'));
// Status selector
var can_change_status = is_active && is_invoice;
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);
}
function retrieve_invoice() {
return ajax_request({
url: '<?php echo $server_url?>order/api/engine/retrieve_invoice.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { invoice_id: invoice_id },
onSuccess: function(res) {
invoice_data = res.output;
var inv = invoice_data;
$('#page_title').text(inv.invoice_number);
$('#badge_doc_type').html(doc_type_badge(inv.doc_type));
$('#badge_status').html(invoice_status_badge(inv.status));
$('#display_contact').text(inv.contact_name || '—');
$('#display_issued').text(format_date(inv.issued_date) || '—');
$('#due_date').val(inv.due_date ? format_date(inv.due_date) : '');
$('#notes').val(inv.notes || '');
if (inv.order_id) {
$('#link_order')
.attr('href', '<?php echo $server_url?>order/manage_order.php?id=' + inv.order_id)
.text(inv.order_number || 'Order #' + inv.order_id);
}
// Items
var items = inv.items || [];
var tbody = '';
$.each(items, function(i, item) {
tbody += `<tr>
<td>${item.product_name || item.product_sku}</td>
<td class="text-end">${format_number(item.quantity, 2)}</td>
<td class="text-end">${format_number(item.price, 2)}</td>
<td class="text-end">${format_number(item.total_price, 2)}</td>
</tr>`;
});
$('#item_tbody').html(tbody);
// Totals
var tfoot = `
<tr><td colspan="3" class="text-muted border-bottom-0">Subtotal</td><td class="text-end border-bottom-0">${format_number(inv.subtotal, 2)}</td></tr>
<tr><td colspan="3" class="text-muted border-bottom-0">Discount</td><td class="text-end border-bottom-0">${format_number(inv.discount, 2)}</td></tr>
<tr><td colspan="3" class="text-muted border-bottom-0">Tax</td><td class="text-end border-bottom-0">${format_number(inv.tax, 2)}</td></tr>
<tr><td colspan="3" class="text-muted">Shipping</td><td class="text-end">${format_number(inv.shipping_fee, 2)}</td></tr>
<tr><td colspan="3" class="fw-bold">Grand Total</td><td class="text-end fw-bold">${format_number(inv.grand_total, 2)}</td></tr>`;
$('#item_tfoot').html(tfoot);
// Linked credit notes
if (res.credit_notes && res.credit_notes.length) {
var cn_rows = '';
$.each(res.credit_notes, function(i, cn) {
cn_rows += `<tr>
<td>${cn.invoice_number}</td>
<td>${format_date(cn.issued_date)}</td>
<td class="text-end text-danger">${format_number(cn.grand_total, 2)}</td>
<td>${invoice_status_badge(cn.status)}</td>
<td><a href="<?php echo $server_url?>order/manage_invoice.php?id=${cn.id}"><i class="ti ti-eye fs-5"></i></a></td>
</tr>`;
});
$('#cn_tbody').html(cn_rows);
$('#cn_card').removeClass('d-none');
}
set_mode(inv);
$('#invoice_status_select').val(inv.status);
}
});
}
function save_invoice() {
ajax_request({
url: '<?php echo $server_url?>order/api/engine/manage_invoice.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: {
id: invoice_id,
due_date: $('#due_date').val().split('/').reverse().join('-'),
notes: $('#notes').val(),
},
onSuccess: function() { retrieve_invoice(); }
});
}
function issue_invoice() {
bootbox.confirm({
message: 'Issue this invoice? It will be sent to the customer.',
buttons: {
confirm: { label: 'Issue', className: 'btn-success' },
cancel: { label: 'Back', className: 'btn-secondary' }
},
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/issue_invoice.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: { invoice_id: invoice_id },
onSuccess: function() { retrieve_invoice(); }
});
}
});
}
function update_invoice_status(status) {
const labels = { 2: 'Mark as Paid', 3: 'Mark as Overdue', 4: 'Void Invoice' };
const classes = { 2: 'btn-success', 3: 'btn-warning', 4: 'btn-danger' };
const messages = {
2: 'Mark this invoice as paid?',
3: 'Mark this invoice as overdue?',
4: '<strong>Void this invoice?</strong><br>This action cannot be undone.',
};
bootbox.confirm({
message: messages[status],
buttons: {
confirm: { label: labels[status], className: classes[status] },
cancel: { label: 'Back', className: 'btn-secondary' }
},
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/update_invoice_status.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: { invoice_id: invoice_id, status: status },
onSuccess: function() { retrieve_invoice(); }
});
}
});
}
$(function() {
if (invoice_id) {
retrieve_invoice();
}
flatpickr('#due_date', { dateFormat: 'd/m/Y', allowInput: true });
});
</script>
</body>
</html>
+696
View File
@@ -0,0 +1,696 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
$order_id = (int)($_GET['id'] ?? 0);
$is_edit = isset($_GET['edit']) || $order_id === 0;
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar.php'; ?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="mb-6 d-flex justify-content-between align-items-center">
<div>
<h1 class="fs-3 mb-1" id="page_title">
<?php echo $order_id ? 'Order Detail' : 'New Order'; ?>
</h1>
<p class="mb-0 text-muted" id="page_subtitle">
<?php echo $order_id ? 'View or manage this order' : 'Create a new sales order'; ?>
</p>
</div>
<div class="d-flex gap-2" id="header_actions">
<a href="<?php echo $server_url?>order/order.php" class="btn btn-light">
<i class="ti ti-arrow-left me-1"></i>Back
</a>
</div>
</div>
</div>
</div>
<div class="row g-5">
<!-- Left: Order form -->
<div class="col-lg-8">
<!-- Order header card -->
<div class="card mb-5">
<div class="card-body p-5">
<h2 class="fs-5 mb-4">Order Information</h2>
<div class="row gx-5">
<div class="mb-3 col-lg-6">
<label class="form-label">Contact</label>
<input type="text" id="contact" class="form-control contact_search"
placeholder="Search contact..." autocomplete="off">
<input type="hidden" id="contact_id">
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">Order Date</label>
<input type="text" id="order_date" class="form-control"
placeholder="DD/MM/YYYY" autocomplete="off">
</div>
<div class="mb-3 col-lg-12">
<label class="form-label">Notes</label>
<textarea id="notes" class="form-control" rows="2"
placeholder="Internal notes..."></textarea>
</div>
</div>
</div>
</div>
<!-- Order items card -->
<div class="card mb-5">
<div class="card-body p-5">
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="fs-5 mb-0">Order Items</h2>
<button class="btn btn-sm btn-outline-primary" id="btn_add_item"
onclick="add_item_row()">
<i class="ti ti-plus me-1"></i>Add Item
</button>
</div>
<div class="table-responsive">
<table class="table mb-0" id="item_table">
<thead class="table-light">
<tr>
<th>Product</th>
<th>Warehouse</th>
<th style="width:120px;">Qty</th>
<th style="width:130px;">Unit Price</th>
<th style="width:130px;">Total</th>
<th style="width:40px;"></th>
</tr>
</thead>
<tbody id="item_tbody"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Right: Summary + actions -->
<div class="col-lg-4">
<!-- Status card (view mode only) -->
<div class="card mb-5 d-none" id="status_card">
<div class="card-body p-4">
<h2 class="fs-5 mb-3">Status</h2>
<div class="d-flex gap-2 mb-2">
<span id="badge_status"></span>
<span id="badge_payment"></span>
</div>
<p class="mb-0 small text-muted" id="order_number_display"></p>
</div>
</div>
<!-- Totals card -->
<div class="card mb-5">
<div class="card-body p-4">
<h2 class="fs-5 mb-3">Summary</h2>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Subtotal</span>
<span id="summary_subtotal">0.00</span>
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Discount</span>
<input type="number" id="discount" class="form-control form-control-sm text-end"
style="width:110px;" value="0" min="0" oninput="recalc_totals()">
</div>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">Tax</span>
<input type="number" id="tax" class="form-control form-control-sm text-end"
style="width:110px;" value="0" min="0" oninput="recalc_totals()">
</div>
<div class="d-flex justify-content-between mb-3">
<span class="text-muted">Shipping</span>
<input type="number" id="shipping_fee" class="form-control form-control-sm text-end"
style="width:110px;" value="0" min="0" oninput="recalc_totals()">
</div>
<hr>
<div class="d-flex justify-content-between fw-bold fs-5">
<span>Grand Total</span>
<span id="summary_grand_total">0.00</span>
</div>
</div>
</div>
<!-- Action buttons -->
<div class="card">
<div class="card-body p-4 d-flex flex-column gap-2">
<!-- Save draft (edit mode) -->
<button class="btn btn-primary w-100" id="btn_save"
onclick="save_order()">
<i class="ti ti-device-floppy me-1"></i>Save Draft
</button>
<!-- Confirm order (draft only) -->
<?php if(isset($_GET["id"])){ ?>
<button class="btn btn-success w-100" id="btn_confirm"
onclick="confirm_order()">
<i class="ti ti-check me-1"></i>Confirm Order
</button>
<?php } ?>
<!-- Proceed to invoice (confirmed, manual mode) -->
<button class="btn btn-outline-primary w-100" id="btn_invoice"
onclick="proceed_to_invoice()">
<i class="ti ti-file-invoice me-1"></i>Proceed to Invoice
</button>
<!-- Update payment status (confirmed and above) -->
<div id="payment_status_group">
<label class="form-label small text-muted mb-1">Payment Status</label>
<div class="d-flex gap-2">
<select id="payment_status_select" class="form-select form-select-sm">
<option value="0">Unpaid</option>
<option value="1">Paid</option>
<option value="2">Partial</option>
<option value="3">Refunded</option>
</select>
<button class="btn btn-sm btn-outline-primary text-nowrap"
onclick="update_payment_status()">
<i class="ti ti-check"></i>
</button>
</div>
</div>
<!-- Create Return (confirmed and above) -->
<button class="btn btn-outline-warning w-100" id="btn_return"
onclick="create_return()">
<i class="ti ti-arrow-back-up me-1"></i>Create Return
</button>
<!-- Cancel order -->
<button class="btn btn-outline-danger w-100" id="btn_cancel"
onclick="cancel_order()">
<i class="ti ti-x me-1"></i>Cancel Order
</button>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
var order_id = <?php echo $order_id; ?>;
var order_data = null;
var warehouses = [];
var item_index = 0;
// ── Status helpers ────────────────────────────────────────────────────────
function order_status_badge(status) {
const map = {
'-1': '<span class="badge bg-secondary">Cancelled</span>',
'0': '<span class="badge bg-warning text-dark">Draft</span>',
'1': '<span class="badge bg-info text-white">Confirmed</span>',
'2': '<span class="badge bg-primary">Processing</span>',
'3': '<span class="badge bg-success">Completed</span>',
};
return map[String(status)] ?? '—';
}
function payment_status_badge(status) {
const map = {
'0': '<span class="badge bg-warning text-dark">Unpaid</span>',
'1': '<span class="badge bg-success">Paid</span>',
'2': '<span class="badge bg-info text-white">Partial</span>',
'3': '<span class="badge bg-secondary">Refunded</span>',
};
return map[String(status)] ?? '—';
}
// ── Recalculate totals ────────────────────────────────────────────────────
function recalc_totals() {
var subtotal = 0;
$('#item_tbody tr').each(function() {
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
var price = parseFloat($(this).find('.item_price').val()) || 0;
var total = qty * price;
$(this).find('.item_total').text(format_number(total, 2));
subtotal += total;
});
var discount = parseFloat($('#discount').val()) || 0;
var tax = parseFloat($('#tax').val()) || 0;
var shipping_fee = parseFloat($('#shipping_fee').val()) || 0;
var grand_total = subtotal - discount + tax + shipping_fee;
$('#summary_subtotal').text(format_number(subtotal, 2));
$('#summary_grand_total').text(format_number(grand_total, 2));
}
// ── Add item row ──────────────────────────────────────────────────────────
function add_item_row(item) {
item = item || {};
var idx = item_index++;
var wh_options = warehouses.map(w =>
`<option value="${w.id}" ${item.warehouse_id == w.id ? 'selected' : ''}>${w.warehouse_name}</option>`
).join('');
var row = `<tr id="item_row_${idx}">
<td>
<input type="text" class="form-control form-control-sm item_sku product_search"
placeholder="SKU..." value="${item.product_sku || ''}" autocomplete="off"
data-idx="${idx}">
<input type="hidden" class="item_name" value="${item.product_name || ''}">
<input type="hidden" class="item_id" value="${item.item_id ?? idx}">
<small class="text-muted item_name_display">${item.product_name || ''}</small>
</td>
<td>
<select class="form-select form-select-sm item_warehouse">
${wh_options}
</select>
</td>
<td>
<input type="number" class="form-control form-control-sm item_qty"
value="${item.quantity || 1}" min="0.0001" step="any" oninput="recalc_totals()">
</td>
<td>
<input type="number" class="form-control form-control-sm item_price"
value="${item.price || 0}" min="0" step="any" oninput="recalc_totals()">
</td>
<td class="item_total text-end">${format_number((item.quantity || 1) * (item.price || 0), 2)}</td>
<td>
<a href="javascript:void(0);" class="link-danger" onclick="remove_item(${idx})">
<i class="ti ti-trash fs-5"></i>
</a>
</td>
</tr>`;
$('#item_tbody').append(row);
recalc_totals();
}
function remove_item(idx) {
$(`#item_row_${idx}`).remove();
recalc_totals();
}
// ── Collect items from DOM ────────────────────────────────────────────────
function collect_items() {
var items = [];
var idx = 0;
$('#item_tbody tr').each(function() {
var sku = $(this).find('.item_sku').val().trim();
var name = $(this).find('.item_name').val().trim();
var wh = parseInt($(this).find('.item_warehouse').val()) || 0;
var qty = parseFloat($(this).find('.item_qty').val()) || 0;
var price = parseFloat($(this).find('.item_price').val()) || 0;
if (!sku || !wh || qty <= 0) { idx++; return; }
items.push({
item_id: idx,
product_sku: sku,
product_name: name,
warehouse_id: wh,
zone: '',
aisle: '',
rack: '',
lot_number: '',
serial_number: '',
quantity: qty,
price: price,
total_price: qty * price,
stock_out_id: 0,
});
idx++;
});
return items;
}
// ── Set view/edit mode ────────────────────────────────────────────────────
function set_mode(status, has_active_invoice, returnable_count) {
status = parseInt(status);
has_active_invoice = has_active_invoice || false;
returnable_count = returnable_count ?? 1;
var is_draft = status === 0;
var can_confirm = status === 0;
var can_cancel = status === 0 || status === 1;
var is_cancelled = status === -1;
// Edit controls
$('#btn_add_item, #contact, #order_date, #notes, #discount, #tax, #shipping_fee')
.prop('disabled', !is_draft);
$('.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
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);
// 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
var can_return = status >= 1 && !is_cancelled && returnable_count > 0;
var return_title = is_cancelled ? 'Order is cancelled'
: status < 1 ? 'Confirm the order before creating a return'
: returnable_count === 0 ? 'All items have already been returned' : '';
set_btn_state('#btn_return', can_return, return_title);
$('#btn_return').html(returnable_count === 0 && status >= 1 && !is_cancelled
? '<i class="ti ti-arrow-back-up me-1"></i>All Items Returned'
: '<i class="ti ti-arrow-back-up me-1"></i>Create Return');
// Proceed to Invoice
var can_invoice = status === 1 && !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
? '<i class="ti ti-file-invoice me-1"></i>Invoice Already Created'
: '<i class="ti ti-file-invoice me-1"></i>Proceed to Invoice');
}
// ── Load existing order ───────────────────────────────────────────────────
function retrieve_order() {
if (!order_id) return Promise.resolve();
return ajax_request({
url: '<?php echo $server_url?>order/api/engine/retrieve_order.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { id: order_id },
onSuccess: function(res) {
order_data = res.output;
var o = order_data;
// Header
$('#page_title').text(o.order_number);
$('#page_subtitle').text('');
$('#status_card').removeClass('d-none');
$('#badge_status').html(order_status_badge(o.status));
$('#badge_payment').html(payment_status_badge(o.payment_status));
$('#order_number_display').text(o.order_number);
// Fields
$('#contact').val(o.contact_name || '');
$('#contact_id').val(o.contact_id);
$('#order_date').val(o.order_date ? format_date(o.order_date) : '');
$('#notes').val(o.notes || '');
$('#discount').val(o.discount || 0);
$('#tax').val(o.tax || 0);
$('#shipping_fee').val(o.shipping_fee || 0);
// Items
$('#item_tbody').empty();
(o.items || []).forEach(function(item) { add_item_row(item); });
recalc_totals();
set_mode(o.status, res.has_active_invoice, (res.returnable_items || []).length);
$('#payment_status_select').val(o.payment_status ?? 0);
$('#order_status_select').val(o.status);
}
});
}
// ── Save draft ────────────────────────────────────────────────────────────
function save_order() {
var items = collect_items();
if (!items.length) {
bootbox.alert('Please add at least one item.');
return;
}
const dateStr = $('#order_date').val();
const [day, month, year] = dateStr.split('/');
const formatted = `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/manage_order.php',
autoPrepare: true,
checkRequired: 0,
action: order_id ? 'update' : 'create',
data: {
id: order_id,
contact_id: $('#contact_id').val() || 0,
order_date: to_iso_date($('#order_date').val()),
items: JSON.stringify(items),
discount: $('#discount').val() || 0,
tax: $('#tax').val() || 0,
shipping_fee: $('#shipping_fee').val() || 0,
notes: $('#notes').val(),
},
onSuccess: function(res) {
if (!order_id && res.new_id) {
window.location.href = '<?php echo $server_url?>order/manage_order.php?id=' + res.new_id;
} else {
retrieve_order();
}
}
});
}
// ── Confirm order ─────────────────────────────────────────────────────────
function confirm_order() {
bootbox.confirm({
message: 'Confirm this order? Stock-out rows will be created for all items using FIFO rack selection.',
buttons: {
confirm: { label: 'Confirm Order', className: 'btn-success' },
cancel: { label: 'Back', className: 'btn-secondary' }
},
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/confirm_order.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: { id: order_id },
onSuccess: function() {
retrieve_order();
}
});
}
});
}
// ── Update order status ───────────────────────────────────────────────────
function update_order_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(),
},
onSuccess: function() {
retrieve_order();
}
});
}
// ── Update payment status ─────────────────────────────────────────────────
function update_payment_status() {
ajax_request({
url: '<?php echo $server_url?>order/api/engine/update_payment_status.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: {
id: order_id,
payment_status: $('#payment_status_select').val(),
},
onSuccess: function() {
retrieve_order();
}
});
}
// ── Create return ─────────────────────────────────────────────────────────
function create_return() {
window.location.href = '<?php echo $server_url?>order/manage_return.php?order_id=' + order_id;
}
// ── Proceed to invoice ────────────────────────────────────────────────────
function proceed_to_invoice() {
ajax_request({
url: '<?php echo $server_url?>order/api/engine/proceed_to_invoice.php',
autoPrepare: true,
checkRequired: 0,
action: 'create',
data: { order_id: order_id },
onSuccess: function(res) {
if (res.new_id) {
window.location.href = '<?php echo $server_url?>order/manage_invoice.php?id=' + res.new_id;
}
}
});
}
// ── Cancel order ──────────────────────────────────────────────────────────
function cancel_order() {
bootbox.confirm({
message: 'Cancel this order? All pending stock-out rows will be cancelled.',
buttons: {
confirm: { label: 'Yes, Cancel Order', className: 'btn-danger' },
cancel: { label: 'Back', className: 'btn-secondary' }
},
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/cancel_order.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: { id: order_id },
onSuccess: function() {
retrieve_order();
}
});
}
});
}
// ── Retrieve warehouses ───────────────────────────────────────────────────
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 || [];
}
});
}
// ── Contact autocomplete ──────────────────────────────────────────────────
function init_contact_search() {
$('#contact').autocomplete({
source: function(req, res) {
ajax_request({
url: '<?php echo $server_url?>contact/api/engine/contact.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { keyword: req.term },
onSuccess: function(data) {
res(data.output.map(c => ({
label: c.contact_name,
value: c.contact_name,
id: c.id,
})));
}
});
},
select: function(e, ui) {
$('#contact_id').val(ui.item.id);
}
});
}
// ── Product autocomplete (per row) ────────────────────────────────────────
function init_product_search() {
$(document).on('input', '.item_sku', function() {
var $input = $(this);
var $row = $input.closest('tr');
$input.autocomplete({
source: function(req, res) {
ajax_request({
url: '<?php echo $server_url?>inventory/api/engine/product.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { keyword: req.term },
onSuccess: function(data) {
res((data.output || []).map(p => ({
label: p.sku + ' — ' + p.product_name,
value: p.sku,
name: p.product_name,
})));
}
});
},
select: function(e, ui) {
$row.find('.item_name').val(ui.item.name);
$row.find('.item_name_display').text(ui.item.name);
recalc_totals();
}
});
});
}
// ── Boot ─────────────────────────────────────────────────────────────────
$(async function() {
try {
await retrieve_warehouses();
if (order_id) {
await retrieve_order();
} else {
// New order — set today's date, add one empty row
var today = flatpickr.formatDate(new Date(), 'd/m/Y');
$('#order_date').val(today);
add_item_row();
set_mode(0);
}
// Init flatpickr on order date
flatpickr('#order_date', { dateFormat: 'd/m/Y', allowInput: true });
init_contact_search();
init_product_search();
} catch(e) {
console.log(e);
}
});
</script>
</body>
</html>
+520
View File
@@ -0,0 +1,520 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
$return_id = (int)($_GET['id'] ?? 0);
$invoice_id = (int)($_GET['invoice_id'] ?? 0);
$order_id = (int)($_GET['order_id'] ?? 0);
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="mb-6 d-flex justify-content-between align-items-center">
<div>
<h1 class="fs-3 mb-1" id="page_title">
<?php echo $return_id ? 'Return Detail' : 'New Return'; ?>
</h1>
<p class="mb-0 text-muted" id="page_subtitle"></p>
</div>
<a href="<?php echo $server_url?>order/returns.php" class="btn btn-light">
<i class="ti ti-arrow-left me-1"></i>Back
</a>
</div>
</div>
</div>
<div class="row g-5">
<!-- Left: Items -->
<div class="col-lg-8">
<!-- Return header -->
<div class="card mb-5">
<div class="card-body p-5">
<h2 class="fs-5 mb-4">Return Information</h2>
<div class="row gx-5">
<div class="mb-3 col-lg-6">
<label class="form-label">Order</label>
<input type="text" id="order_display" class="form-control" readonly
placeholder="Loading...">
<input type="hidden" id="order_id" value="<?php echo $order_id; ?>">
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">Invoice <span class="text-muted small">(optional)</span></label>
<select id="invoice_id" class="form-select">
<option value="0">— no invoice —</option>
</select>
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">Contact</label>
<input type="text" id="contact" class="form-control" readonly
placeholder="Auto-filled from order">
<input type="hidden" id="contact_id">
</div>
<div class="mb-3 col-lg-6">
<label class="form-label">Return Date</label>
<input type="text" id="return_date" class="form-control"
placeholder="DD/MM/YYYY" autocomplete="off">
</div>
<div class="mb-3 col-lg-12">
<label class="form-label">Reason</label>
<textarea id="reason" class="form-control" rows="2"
placeholder="Reason for return..."></textarea>
</div>
</div>
</div>
</div>
<!-- Return items — loaded from order -->
<div class="card">
<div class="card-body p-5">
<h2 class="fs-5 mb-3">Select Items to Return</h2>
<p class="text-muted small mb-4">Only items with remaining returnable quantity are shown.</p>
<div class="table-responsive">
<table class="table mb-0" id="item_table">
<thead class="table-light">
<tr>
<th style="width:40px;"></th>
<th>Product</th>
<th class="text-end" style="width:100px;">Order Qty</th>
<th class="text-end" style="width:100px;">Returned</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>
</tr>
</thead>
<tbody id="item_tbody">
<tr id="loading_row">
<td colspan="7" class="text-center text-muted py-4">
<i class="ti ti-loader me-1"></i>Loading items...
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Right: Summary + actions -->
<div class="col-lg-4">
<!-- Status card (view mode) -->
<div class="card mb-5 d-none" id="status_card">
<div class="card-body p-4">
<h2 class="fs-5 mb-3">Status</h2>
<span id="badge_status"></span>
<p class="mb-0 small text-muted mt-2" id="return_number_display"></p>
</div>
</div>
<!-- Refund summary -->
<div class="card mb-5">
<div class="card-body p-4">
<h2 class="fs-5 mb-3">Refund Summary</h2>
<div class="d-flex justify-content-between fw-bold fs-5">
<span>Total Refund</span>
<span id="summary_refund">0.00</span>
</div>
</div>
</div>
<!-- Actions -->
<div class="card">
<div class="card-body p-4 d-flex flex-column gap-2">
<button class="btn btn-primary w-100" id="btn_save"
onclick="save_return()">
<i class="ti ti-device-floppy me-1"></i>Save Draft
</button>
<button class="btn btn-success w-100" id="btn_confirm"
onclick="confirm_return()">
<i class="ti ti-check me-1"></i>Confirm Return
</button>
<button class="btn btn-outline-danger w-100" id="btn_cancel"
onclick="cancel_return()">
<i class="ti ti-x me-1"></i>Cancel Return
</button>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
var return_id = <?php echo $return_id; ?>;
var return_data = null;
var order_data = null;
var returnable = []; // returnable_items from order
// ── Status badge ──────────────────────────────────────────────────────────
function return_status_badge(status) {
const map = {
'-1': '<span class="badge bg-secondary">Cancelled</span>',
'0': '<span class="badge bg-warning text-dark">Draft</span>',
'1': '<span class="badge bg-success">Confirmed</span>',
};
return map[String(status)] ?? '—';
}
// ── Recalculate totals ────────────────────────────────────────────────────
function recalc_totals() {
var total = 0;
$('#item_tbody tr.return-item-row').each(function() {
var $row = $(this);
var checked = $row.find('.item_check').is(':checked');
if (!checked) return;
var qty = parseFloat($row.find('.item_qty').val()) || 0;
var price = parseFloat($row.find('.item_price').val()) || 0;
var t = qty * price;
$row.find('.item_total').text(format_number(t, 2));
total += t;
});
$('#summary_refund').text(format_number(total, 2));
}
// ── Render returnable items ───────────────────────────────────────────────
function render_returnable_items(items, selected_items) {
// selected_items: array from existing return (edit mode)
// keyed by item_id for quick lookup
var selected_map = {};
if (selected_items) {
selected_items.forEach(function(si) {
selected_map[si.item_id] = si;
});
}
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>'
);
return;
}
var rows = '';
items.forEach(function(item) {
var si = selected_map[item.item_id] || null;
var checked = si ? 'checked' : '';
var qty = si ? si.quantity : item.returnable_qty;
var price = item.price || 0;
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}>
<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 || ''}">
<input type="hidden" class="item_warehouse_id" value="${item.warehouse_id || 0}">
<input type="hidden" class="item_zone" value="${item.zone || ''}">
<input type="hidden" class="item_aisle" value="${item.aisle || ''}">
<input type="hidden" class="item_rack" value="${item.rack || ''}">
<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}">
</td>
<td>
<div class="fw-semibold">${item.product_sku}</div>
<small class="text-muted">${item.product_name || ''}</small>
</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>
<td>
<input type="number" class="form-control form-control-sm item_qty text-end"
value="${format_number(qty, 2)}"
min="0.0001" max="${item.returnable_qty}" step="any"
${!checked ? 'disabled' : ''}
oninput="recalc_totals()">
</td>
<td>
<input type="number" class="form-control form-control-sm item_price text-end"
value="${price}"
min="0" step="any"
${!checked ? 'disabled' : ''}
oninput="recalc_totals()">
</td>
<td class="text-end item_total">${checked ? format_number(qty * price, 2) : '—'}</td>
</tr>`;
});
$('#item_tbody').html(rows);
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('—');
}
// ── Load order + items ────────────────────────────────────────────────────
function load_order(oid, selected_items) {
if (!oid) return Promise.resolve();
return ajax_request({
url: '<?php echo $server_url?>order/api/engine/retrieve_order.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { id: oid },
onSuccess: function(res) {
order_data = res.output;
returnable = res.returnable_items || [];
$('#order_display').val(order_data.order_number || '');
$('#contact').val(order_data.contact_name || '');
$('#contact_id').val(order_data.contact_id || 0);
// Load invoices dropdown
load_invoices_for_order(oid);
// Render items
render_returnable_items(returnable, selected_items || null);
}
});
}
// ── Load invoices for order ───────────────────────────────────────────────
function load_invoices_for_order(oid) {
ajax_request({
url: '<?php echo $server_url?>order/api/engine/retrieve_invoice.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { order_id: oid, doc_type: 'invoice' },
onSuccess: function(res) {
var opts = '<option value="0">— no invoice —</option>';
$.each(res.output || [], function(i, inv) {
opts += `<option value="${inv.id}">${inv.invoice_number}</option>`;
});
$('#invoice_id').html(opts);
// Restore selected invoice if editing existing return
if (return_data && return_data.invoice_id) {
$('#invoice_id').val(return_data.invoice_id);
}
}
});
}
// ── Collect selected items ────────────────────────────────────────────────
function collect_items() {
var items = [];
$('#item_tbody tr.return-item-row').each(function() {
var $row = $(this);
if (!$row.find('.item_check').is(':checked')) return;
var qty = parseFloat($row.find('.item_qty').val()) || 0;
var price = parseFloat($row.find('.item_price').val()) || 0;
if (qty <= 0) return;
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(),
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,
quantity: qty,
price: price,
total_price: qty * price,
});
});
return items;
}
// ── Mode ──────────────────────────────────────────────────────────────────
function set_mode(status) {
status = parseInt(status);
var is_draft = status === 0;
$('#return_date, #reason, #invoice_id').prop('disabled', !is_draft);
$('.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);
});
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');
set_btn_state('#btn_cancel', is_draft,
status === -1 ? 'Return is already cancelled' : 'Only draft returns can be cancelled');
}
// ── Load existing return (edit mode) ──────────────────────────────────────
function retrieve_return() {
if (!return_id) return Promise.resolve();
return ajax_request({
url: '<?php echo $server_url?>order/api/engine/retrieve_return.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
data: { id: return_id },
onSuccess: async function(res) {
return_data = res.output;
var r = return_data;
$('#page_title').text(r.return_number);
$('#status_card').removeClass('d-none');
$('#badge_status').html(return_status_badge(r.status));
$('#return_number_display').text(r.return_number);
$('#return_date').val(r.return_date ? format_date(r.return_date) : '');
$('#reason').val(r.reason || '');
// Load order context (which also renders items with existing selection)
await load_order(r.order_id, r.items || []);
set_mode(r.status);
}
});
}
// ── 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();
}
}
});
}
// ── Confirm return ────────────────────────────────────────────────────────
function confirm_return() {
bootbox.confirm({
message: 'Confirm this return? Items will be restocked and a credit note may be generated.',
buttons: {
confirm: { label: 'Confirm Return', className: 'btn-success' },
cancel: { label: 'Back', className: 'btn-secondary' }
},
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(); }
});
}
});
}
// ── Cancel return ─────────────────────────────────────────────────────────
function cancel_return() {
bootbox.confirm({
message: 'Cancel this return request?',
buttons: {
confirm: { label: 'Yes, Cancel', className: 'btn-danger' },
cancel: { label: 'Back', className: 'btn-secondary' }
},
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/cancel_return.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: { id: return_id },
onSuccess: function() { retrieve_return(); }
});
}
});
}
// ── Boot ─────────────────────────────────────────────────────────────────
$(async function() {
try {
flatpickr('#return_date', { dateFormat: 'd/m/Y', allowInput: true });
if (return_id) {
// Edit/view mode — load existing return
await retrieve_return();
} else {
// New return from order
var pre_order = <?php echo $order_id; ?>;
if (pre_order) {
await load_order(pre_order, null);
}
var today = flatpickr.formatDate(new Date(), 'd/m/Y');
$('#return_date').val(today);
set_mode(0);
}
} catch(e) {
console.log(e);
}
});
</script>
</body>
</html>
+312
View File
@@ -0,0 +1,312 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar.php'; ?>
<!-- MAIN CONTENT -->
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="mb-6 d-flex justify-content-between align-items-center">
<div>
<h1 class="fs-3 mb-1">Orders</h1>
<p class="mb-0">Manage and track all sales orders</p>
</div>
<a href="<?php echo $server_url?>order/manage_order.php" class="btn btn-primary">
<i class="ti ti-plus"></i>
<span>New Order</span>
</a>
</div>
</div>
</div>
<!-- Stat cards -->
<div class="row g-5 mb-5">
<div class="col-lg-3 col-sm-6 col-12">
<div class="card p-4">
<div class="d-flex align-items-center gap-3">
<div class="icon-shape icon-md bg-primary bg-opacity-10 text-primary rounded-2">
<i class="ti ti-shopping-cart fs-4"></i>
</div>
<div>
<p class="mb-0 text-muted small">Total Orders</p>
<h3 class="fw-bold mb-0" id="stat_total">—</h3>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 col-12">
<div class="card p-4">
<div class="d-flex align-items-center gap-3">
<div class="icon-shape icon-md bg-warning bg-opacity-10 text-warning rounded-2">
<i class="ti ti-clock fs-4"></i>
</div>
<div>
<p class="mb-0 text-muted small">Draft</p>
<h3 class="fw-bold mb-0" id="stat_draft">—</h3>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 col-12">
<div class="card p-4">
<div class="d-flex align-items-center gap-3">
<div class="icon-shape icon-md bg-info bg-opacity-10 text-info rounded-2">
<i class="ti ti-package fs-4"></i>
</div>
<div>
<p class="mb-0 text-muted small">Confirmed</p>
<h3 class="fw-bold mb-0" id="stat_confirmed">—</h3>
</div>
</div>
</div>
</div>
<div class="col-lg-3 col-sm-6 col-12">
<div class="card p-4">
<div class="d-flex align-items-center gap-3">
<div class="icon-shape icon-md bg-success bg-opacity-10 text-success rounded-2">
<i class="ti ti-circle-check fs-4"></i>
</div>
<div>
<p class="mb-0 text-muted small">Completed</p>
<h3 class="fw-bold mb-0" id="stat_completed">—</h3>
</div>
</div>
</div>
</div>
</div>
<!-- Filter + Table -->
<div class="row g-5">
<!-- Search & filter bar -->
<div class="col-12">
<div class="card">
<div class="card-body p-4">
<div class="row g-3 align-items-end">
<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...">
<span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i>
</span>
</div>
</div>
<div class="col-lg-3">
<select id="filter_status" class="form-select">
<option value="">All Status</option>
<option value="-1">Cancelled</option>
<option value="0">Draft</option>
<option value="1">Confirmed</option>
<option value="2">Processing</option>
<option value="3">Completed</option>
</select>
</div>
<div class="col-lg-2">
<select id="filter_payment" class="form-select">
<option value="">All Payment</option>
<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">
<button class="btn btn-secondary w-100" onclick="retrieve_orders()">
<i class="ti ti-search me-1"></i>Search
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Table -->
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<div class="table-responsive">
<table class="table table-hover mb-0 table-centered" id="order_list">
<thead class="table-primary border-light">
<tr>
<th class="py-3">Order #</th>
<th class="py-3">Date</th>
<th class="py-3">Contact</th>
<th class="py-3">Items</th>
<th class="py-3">Grand Total</th>
<th class="py-3">Status</th>
<th class="py-3">Payment</th>
<th class="py-3">Action</th>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
// ── Status helpers ────────────────────────────────────────────────────────
function order_status_badge(status) {
status = parseInt(status);
const map = {
'-1': '<span class="badge bg-secondary">Cancelled</span>',
'0': '<span class="badge bg-warning text-dark">Draft</span>',
'1': '<span class="badge bg-info text-white">Confirmed</span>',
'2': '<span class="badge bg-primary">Processing</span>',
'3': '<span class="badge bg-success">Completed</span>',
};
return map[status] ?? '<span class="badge bg-light text-dark">—</span>';
}
function payment_status_badge(status) {
status = parseInt(status);
const map = {
'0': '<span class="badge bg-warning text-dark">Unpaid</span>',
'1': '<span class="badge bg-success">Paid</span>',
'2': '<span class="badge bg-info text-white">Partial</span>',
'3': '<span class="badge bg-secondary">Refunded</span>',
};
return map[status] ?? '<span class="badge bg-light text-dark">—</span>';
}
// ── Retrieve orders ───────────────────────────────────────────────────────
function retrieve_orders() {
return ajax_request({
url: '<?php echo $server_url?>order/api/engine/retrieve_order.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var all = res.output || [];
// Client-side filter
var keyword = $('#search_input').val().toLowerCase();
var fil_status = $('#filter_status').val();
var fil_payment = $('#filter_payment').val();
var data = all.filter(function(o) {
var match_kw = !keyword ||
(o.order_number || '').toLowerCase().includes(keyword) ||
(o.contact_name || '').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;
});
// Stat cards
$('#stat_total').text(format_number(all.length));
$('#stat_draft').text(format_number(all.filter(o => parseInt(o.status) === 0).length));
$('#stat_confirmed').text(format_number(all.filter(o => parseInt(o.status) === 1).length));
$('#stat_completed').text(format_number(all.filter(o => parseInt(o.status) === 3).length));
// Pagination via alasql
alasql('CREATE TABLE IF NOT EXISTS order_list');
alasql.tables.order_list.data = data;
var page = generate_pagination('order_list', data.length);
$('table#order_list tfoot').html(page);
if (typeof change_page_order_list === 'function') {
change_page_order_list(1);
}
}
});
}
// ── Render page ───────────────────────────────────────────────────────────
function change_page_order_list(page_num) {
var offset = (page_num - 1) * prop_limit;
var page_data = alasql(`SELECT * FROM order_list LIMIT ${prop_limit} OFFSET ${offset}`);
var body = '';
$.each(page_data, function(i, o) {
var items = JSON.parse(o.items || '[]');
var item_count = items.length;
var can_edit = parseInt(o.status) === 0;
var can_cancel = parseInt(o.status) >= 0 && parseInt(o.status) <= 1;
body += `<tr>
<td class="py-3 fw-semibold">${o.order_number}</td>
<td class="py-3">${format_date(o.order_date)}</td>
<td class="py-3">${o.contact_name || '<span class="text-muted">—</span>'}</td>
<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">${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">
<i class="ti ti-eye fs-5"></i>
</a>
${can_cancel ? `
<a href="javascript:void(0);" class="link-danger ms-2" title="Cancel"
onclick="cancel_order(${o.id})">
<i class="ti ti-x fs-5"></i>
</a>` : ''}
</td>
</tr>`;
});
$('table#order_list > tbody').html(body);
}
// ── Cancel order ─────────────────────────────────────────────────────────
function cancel_order(id) {
bootbox.confirm({
message: 'Cancel this order? All pending stock-out rows will be cancelled.',
buttons: {
confirm: { label: 'Yes, Cancel Order', className: 'btn-danger' },
cancel: { label: 'Back', className: 'btn-secondary' }
},
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/cancel_order.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: { id: id },
onSuccess: function() {
retrieve_orders();
}
});
}
});
}
// ── Boot ─────────────────────────────────────────────────────────────────
$(function() {
retrieve_orders();
$('#search_input').on('keyup', function(e) {
if (e.key === 'Enter') retrieve_orders();
});
});
</script>
</body>
</html>
+208
View File
@@ -0,0 +1,208 @@
<?php
session_start();
require '../config.php';
require '../include_header.php';
?>
<body>
<?php require '../include_topbar.php'; ?>
<?php require '../include_sidebar.php'; ?>
<main id="content" class="content py-15">
<div class="container-fluid">
<!-- Page header -->
<div class="row">
<div class="col-12">
<div class="mb-6 d-flex justify-content-between align-items-center">
<div>
<h1 class="fs-3 mb-1">Returns</h1>
<p class="mb-0">Manage customer return requests</p>
</div>
<a href="<?php echo $server_url?>order/manage_return.php" class="btn btn-primary">
<i class="ti ti-plus me-1"></i>New Return
</a>
</div>
</div>
</div>
<!-- Filter bar -->
<div class="row g-5 mb-5">
<div class="col-12">
<div class="card">
<div class="card-body p-4">
<div class="row g-3 align-items-end">
<div class="col-lg-5">
<div class="position-relative">
<input type="text" id="search_input" class="form-control ps-9"
placeholder="Search return number or contact...">
<span class="position-absolute top-25 ms-4">
<i class="ti ti-search text-muted"></i>
</span>
</div>
</div>
<div class="col-lg-3">
<select id="filter_status" class="form-select">
<option value="">All Status</option>
<option value="-1">Cancelled</option>
<option value="0">Draft</option>
<option value="1">Confirmed</option>
</select>
</div>
<div class="col-lg-2">
<button class="btn btn-secondary w-100" onclick="retrieve_returns()">
<i class="ti ti-search me-1"></i>Search
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Table -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-body p-5">
<div class="table-responsive">
<table class="table table-hover mb-0 table-centered" id="return_list">
<thead class="table-primary border-light">
<tr>
<th class="py-3">Return #</th>
<th class="py-3">Order #</th>
<th class="py-3">Contact</th>
<th class="py-3">Date</th>
<th class="py-3">Items</th>
<th class="py-3">Refund Amount</th>
<th class="py-3">Status</th>
<th class="py-3">Action</th>
</tr>
</thead>
<tbody></tbody>
<tfoot></tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<?php require '../include_ending.php'; ?>
<script>
function return_status_badge(status) {
const map = {
'-1': '<span class="badge bg-secondary">Cancelled</span>',
'0': '<span class="badge bg-warning text-dark">Draft</span>',
'1': '<span class="badge bg-success">Confirmed</span>',
};
return map[String(status)] ?? '—';
}
function retrieve_returns() {
return ajax_request({
url: '<?php echo $server_url?>order/api/engine/retrieve_return.php',
autoPrepare: true,
checkRequired: 0,
action: 'read',
onSuccess: function(res) {
var all = res.output || [];
var keyword = $('#search_input').val().toLowerCase();
var fil_st = $('#filter_status').val();
var data = all.filter(function(r) {
var match_kw = !keyword ||
(r.return_number || '').toLowerCase().includes(keyword) ||
(r.contact_name || '').toLowerCase().includes(keyword);
var match_st = fil_st === '' || String(r.status) === fil_st;
return match_kw && match_st;
});
alasql('CREATE TABLE IF NOT EXISTS return_list');
alasql.tables.return_list.data = data;
var page = generate_pagination('return_list', data.length);
$('table#return_list tfoot').html(page);
if (typeof change_page_return_list === 'function') {
change_page_return_list(1);
}
}
});
}
function change_page_return_list(page_num) {
var offset = (page_num - 1) * prop_limit;
var page_data = alasql(`SELECT * FROM return_list LIMIT ${prop_limit} OFFSET ${offset}`);
var body = '';
$.each(page_data, function(i, r) {
var items = JSON.parse(r.items || '[]');
var can_cancel = parseInt(r.status) === 0;
body += `<tr>
<td class="py-3 fw-semibold">${r.return_number}</td>
<td class="py-3">${r.order_number || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${r.contact_name || '<span class="text-muted">—</span>'}</td>
<td class="py-3">${format_date(r.return_date)}</td>
<td class="py-3">${items.length} item${items.length !== 1 ? 's' : ''}</td>
<td class="py-3 fw-semibold">${format_number(r.refund_amount, 2)}</td>
<td class="py-3">${return_status_badge(r.status)}</td>
<td class="py-3">
<a href="<?php echo $server_url?>order/manage_return.php?id=${r.id}" title="View">
<i class="ti ti-eye fs-5"></i>
</a>
${can_cancel ? `
<a href="javascript:void(0);" class="link-danger ms-2" title="Cancel"
onclick="cancel_return(${r.id})">
<i class="ti ti-x fs-5"></i>
</a>` : ''}
</td>
</tr>`;
});
$('table#return_list > tbody').html(body);
}
function cancel_return(id) {
bootbox.confirm({
message: 'Cancel this return request?',
buttons: {
confirm: { label: 'Yes, Cancel', className: 'btn-danger' },
cancel: { label: 'Back', className: 'btn-secondary' }
},
callback: function(result) {
if (!result) return;
ajax_request({
url: '<?php echo $server_url?>order/api/engine/cancel_return.php',
autoPrepare: true,
checkRequired: 0,
action: 'update',
data: { id: id },
onSuccess: function() { retrieve_returns(); }
});
}
});
}
$(function() {
retrieve_returns();
$('#search_input').on('keyup', function(e) {
if (e.key === 'Enter') retrieve_returns();
});
});
</script>
</body>
</html>