Fix QA review findings: server-side validation, notes encoding, dashboard totals
Validate document lines on the server and recompute their totals, store notes with quotes/markup/emoji (utf8mb4, idempotent escaping, decode in form fields), exclude transfers from company-wide stock in/out, count revenue from confirmed orders only, one low-stock rule everywhere, list unapproved lots, natural bin sort, stable order/PO sort, status tiles that add up.
This commit is contained in:
@@ -13,14 +13,15 @@ $qm = new QuotationManager($pdo2, $company_id);
|
||||
try {
|
||||
if ($action === 'create') {
|
||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||
$new_id = $qm->save(array_merge($data, ['id' => 0, 'items' => $items]), $logging);
|
||||
// One transaction: a failure while writing the lines must not leave the header behind.
|
||||
$new_id = dbTransaction($pdo2, fn() => $qm->save(array_merge($data, ['id' => 0, 'items' => $items]), $logging));
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = 'Quotation created.';
|
||||
$answer['new_id'] = $new_id;
|
||||
(new UsageGuard($pdo1, $company_id, $packages))->increment();
|
||||
} elseif ($action === 'update') {
|
||||
require_role($user_role, ['owner', 'admin', 'staff']);
|
||||
$qm->save(array_merge($data, ['id' => $id, 'items' => $items]), $logging);
|
||||
dbTransaction($pdo2, fn() => $qm->save(array_merge($data, ['id' => $id, 'items' => $items]), $logging));
|
||||
$answer['success'] = 1;
|
||||
$answer['message'] = 'Quotation updated.';
|
||||
} else {
|
||||
|
||||
+22
-6
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row g-5 mb-5">
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="col-lg 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">
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="col-lg 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">
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="col-lg 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">
|
||||
@@ -65,7 +65,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="col-lg 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-secondary bg-opacity-10 text-secondary rounded-2">
|
||||
<i class="ti ti-ban fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Void</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_void">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg 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-danger bg-opacity-10 text-danger rounded-2">
|
||||
@@ -255,8 +268,11 @@
|
||||
|
||||
function update_stats() {
|
||||
$('#stat_invoice').text(format_number(all_invoices.filter(i => i.doc_type === 'invoice').length));
|
||||
$('#stat_paid').text(format_number(all_invoices.filter(i => i.doc_type === 'invoice' && String(i.status) === '2').length));
|
||||
$('#stat_open').text(format_number(all_invoices.filter(i => i.doc_type === 'invoice' && ['0', '1'].includes(String(i.status))).length));
|
||||
// Same test as the row badge, so a tile never disagrees with the list below it
|
||||
var is_paid = i => String(i.status) === '2' || i.payment_state === 'paid';
|
||||
$('#stat_paid').text(format_number(all_invoices.filter(i => i.doc_type === 'invoice' && String(i.status) !== '4' && is_paid(i)).length));
|
||||
$('#stat_open').text(format_number(all_invoices.filter(i => i.doc_type === 'invoice' && ['0', '1'].includes(String(i.status)) && !is_paid(i)).length));
|
||||
$('#stat_void').text(format_number(all_invoices.filter(i => i.doc_type === 'invoice' && String(i.status) === '4').length));
|
||||
$('#stat_cn').text(format_number(all_invoices.filter(i => i.doc_type === 'credit_note').length));
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,13 @@
|
||||
placeholder="DD/MM/YYYY" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-6">
|
||||
<label class="form-label">Department</label>
|
||||
<select id="department_id" class="form-select">
|
||||
<option value="0">— No Department —</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-3 col-lg-12">
|
||||
<label class="form-label">Notes</label>
|
||||
<textarea id="notes" class="form-control" rows="2"
|
||||
@@ -293,7 +300,7 @@
|
||||
has_active_invoice = has_active_invoice || false;
|
||||
active_invoice_id = parseInt(active_invoice_id) || 0;
|
||||
var editable = status === -2;
|
||||
$('#contact, #order_date, #notes, #discount, #tax_adjustment, #shipping_fee, #btn_add_item')
|
||||
$('#contact, #order_date, #department_id, #notes, #discount, #tax_adjustment, #shipping_fee, #btn_add_item')
|
||||
.prop('disabled', !editable);
|
||||
$('.item_sku, .item_desc, .item_qty, .item_price, .item_tax_rate').prop('disabled', !editable);
|
||||
$('.remove_item_btn').toggleClass('d-none', !editable);
|
||||
@@ -357,6 +364,7 @@
|
||||
$('#contact').val(o.contact_name || '');
|
||||
$('#contact_id').val(o.contact_id || 0);
|
||||
$('#order_date').val(o.order_date ? format_date_input(o.order_date) : '');
|
||||
$('#department_id').val(o.department_id || 0);
|
||||
$('#notes').val(o.notes || '');
|
||||
$('#discount').val(o.discount || 0);
|
||||
$('#tax_adjustment').val(o.tax_adjustment || 0);
|
||||
@@ -388,6 +396,7 @@
|
||||
id: order_id,
|
||||
contact_id: $('#contact_id').val() || 0,
|
||||
order_date: to_iso_date($('#order_date').val()),
|
||||
department_id: $('#department_id').val() || 0,
|
||||
items: JSON.stringify(items),
|
||||
discount: $('#discount').val() || 0,
|
||||
tax_adjustment: $('#tax_adjustment').val() || 0,
|
||||
@@ -490,6 +499,8 @@
|
||||
|
||||
$(async function() {
|
||||
try {
|
||||
// Options must exist before retrieve_order() selects the saved department
|
||||
await Promise.resolve(load_departments('department_id')).catch(function() {});
|
||||
if (order_id) {
|
||||
await retrieve_order();
|
||||
} else {
|
||||
|
||||
+19
-5
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
|
||||
<div class="row g-5 mb-5">
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="col-lg 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">
|
||||
@@ -39,7 +39,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="col-lg 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-danger bg-opacity-10 text-danger rounded-2">
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="col-lg 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">
|
||||
@@ -65,7 +65,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg-3 col-sm-6 col-12">
|
||||
<div class="col-lg 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">
|
||||
@@ -78,7 +78,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-lg 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-secondary bg-opacity-10 text-secondary rounded-2">
|
||||
<i class="ti ti-x fs-4"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="mb-0 text-muted small">Cancelled</p>
|
||||
<h3 class="fw-bold mb-0" id="stat_cancelled">—</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row g-5">
|
||||
<div class="col-12">
|
||||
@@ -197,6 +210,7 @@
|
||||
$('#stat_total').text(format_number(all.length));
|
||||
$('#stat_pending').text(format_number(all.filter(o => String(o.status) === '-2').length));
|
||||
$('#stat_draft').text(format_number(all.filter(o => String(o.status) === '0').length));
|
||||
$('#stat_cancelled').text(format_number(all.filter(o => String(o.status) === '-1').length));
|
||||
$('#stat_confirmed').text(format_number(all.filter(o => parseInt(o.status) >= 1).length));
|
||||
|
||||
alasql('CREATE TABLE IF NOT EXISTS revenue_order_list');
|
||||
|
||||
Reference in New Issue
Block a user