mirror of
https://github.com/itflow-org/itflow
synced 2026-09-22 14:41:17 +00:00
Fix undefined item name when selecting from product list in quote or recurring invoice
This commit is contained in:
@@ -180,32 +180,7 @@ if (isset($_GET['invoice_id'])) {
|
|||||||
$invoice_badge_color = getInvoiceBadgeColor($invoice_status);
|
$invoice_badge_color = getInvoiceBadgeColor($invoice_status);
|
||||||
|
|
||||||
//Product autocomplete
|
//Product autocomplete
|
||||||
$products_sql = mysqli_query($mysqli, "
|
$json_products = getProductsForAutocomplete($mysqli);
|
||||||
SELECT
|
|
||||||
IF(product_code IS NULL OR product_code = '', product_name, CONCAT(product_code, ' - ', product_name)) AS label,
|
|
||||||
product_name,
|
|
||||||
product_code,
|
|
||||||
product_type AS type,
|
|
||||||
product_description AS description,
|
|
||||||
product_price AS price,
|
|
||||||
product_tax_id AS tax,
|
|
||||||
tax_percent,
|
|
||||||
product_id AS prod_id,
|
|
||||||
COALESCE(SUM(product_stock.stock_qty), 0) AS available_stock
|
|
||||||
FROM products
|
|
||||||
LEFT JOIN product_stock ON product_id = stock_product_id
|
|
||||||
LEFT JOIN taxes ON product_tax_id = tax_id
|
|
||||||
WHERE product_archived_at IS NULL
|
|
||||||
GROUP BY product_id
|
|
||||||
ORDER BY product_name ASC
|
|
||||||
");
|
|
||||||
|
|
||||||
if (mysqli_num_rows($products_sql) > 0) {
|
|
||||||
while ($row = mysqli_fetch_assoc($products_sql)) {
|
|
||||||
$products[] = $row;
|
|
||||||
}
|
|
||||||
$json_products = json_encode($products);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Saved Payment Methods
|
// Saved Payment Methods
|
||||||
$sql_saved_payment_methods = mysqli_query($mysqli, "
|
$sql_saved_payment_methods = mysqli_query($mysqli, "
|
||||||
@@ -815,67 +790,12 @@ require_once "../includes/footer.php";
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- JSON Autocomplete / type ahead -->
|
<!-- Product autocomplete for the add-item row -->
|
||||||
|
<script src="/js/product_autocomplete.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
initProductAutocomplete(<?= $json_products ?? '[]' ?>);
|
||||||
var availableProducts = <?= $json_products ?? '[]' ?>;
|
|
||||||
|
|
||||||
var nameInput = document.getElementById('name');
|
|
||||||
if (!nameInput) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
itflowAutocomplete(nameInput, {
|
|
||||||
minLength: 1,
|
|
||||||
source: availableProducts,
|
|
||||||
match: function (item, term) {
|
|
||||||
return String(item.label || '').toLowerCase().indexOf(term) !== -1
|
|
||||||
|| String(item.product_name || '').toLowerCase().indexOf(term) !== -1
|
|
||||||
|| String(item.product_code || '').toLowerCase().indexOf(term) !== -1;
|
|
||||||
},
|
|
||||||
render: function (item) {
|
|
||||||
var esc = itflowEscapeHtml;
|
|
||||||
var typeText = item.type ? item.type.charAt(0).toUpperCase() + item.type.slice(1).toLowerCase() : "";
|
|
||||||
var showStock = (typeText.toLowerCase() !== "service");
|
|
||||||
var taxText = (item.tax_percent != null) ? (parseFloat(item.tax_percent) + "%") : "No tax";
|
|
||||||
var priceText = (item.price != null && item.price !== "") ? String(item.price) : "";
|
|
||||||
var stockText = (item.available_stock ?? 0);
|
|
||||||
|
|
||||||
return "<div class='d-flex justify-content-between align-items-start'>" +
|
|
||||||
"<div class='flex-fill pe-2'>" +
|
|
||||||
"<div class='fw-bold'>" + esc(item.label) +
|
|
||||||
(typeText ? " <small class='text-muted'>(" + esc(typeText) + ")</small>" : "") +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='small text-muted'>" + esc(item.description) + "</div>" +
|
|
||||||
"<div class='mt-1'>" +
|
|
||||||
"<span class='badge bg-secondary me-1'>Tax: " + esc(taxText) + "</span>" +
|
|
||||||
(showStock ? "<span class='badge " + (stockText > 0 ? "bg-success" : "bg-danger") + "'>Stock: " + esc(stockText) + "</span>" : "") +
|
|
||||||
"</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='text-end'>" +
|
|
||||||
"<div class='fw-bold'>" + esc(priceText) + "</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>";
|
|
||||||
},
|
|
||||||
onSelect: function (item) {
|
|
||||||
document.getElementById('name').value = item.product_name;
|
|
||||||
document.getElementById('desc').value = item.description;
|
|
||||||
document.getElementById('qty').value = 1;
|
|
||||||
document.getElementById('price').value = item.price;
|
|
||||||
setTomSelectValue(document.getElementById('tax'), item.tax);
|
|
||||||
document.getElementById('product_id').value = item.prod_id;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Typing over the name by hand breaks the link to the product
|
|
||||||
nameInput.addEventListener('input', function () {
|
|
||||||
document.getElementById('product_id').value = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="../libs/SortableJS/Sortable.min.js"></script>
|
<script src="../libs/SortableJS/Sortable.min.js"></script>
|
||||||
|
|||||||
@@ -128,14 +128,7 @@ if (isset($_GET['quote_id'])) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Product autocomplete
|
//Product autocomplete
|
||||||
$products_sql = mysqli_query($mysqli, "SELECT product_name AS label, product_description AS description, product_price AS price, product_tax_id AS tax FROM products WHERE product_archived_at IS NULL");
|
$json_products = getProductsForAutocomplete($mysqli);
|
||||||
|
|
||||||
if (mysqli_num_rows($products_sql) > 0) {
|
|
||||||
while ($row = mysqli_fetch_assoc($products_sql)) {
|
|
||||||
$products[] = $row;
|
|
||||||
}
|
|
||||||
$json_products = json_encode($products);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Quote File Attachments
|
// Quote File Attachments
|
||||||
$sql_quote_files = mysqli_query(
|
$sql_quote_files = mysqli_query(
|
||||||
@@ -403,6 +396,7 @@ if (isset($_GET['quote_id'])) {
|
|||||||
<form action="post.php" method="post" autocomplete="off">
|
<form action="post.php" method="post" autocomplete="off">
|
||||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||||
<input type="hidden" name="quote_id" value="<?= $quote_id ?>">
|
<input type="hidden" name="quote_id" value="<?= $quote_id ?>">
|
||||||
|
<input type="hidden" id="product_id" name="product_id" value="0">
|
||||||
<input type="hidden" name="item_order" value="<?php
|
<input type="hidden" name="item_order" value="<?php
|
||||||
//find largest order number and add 1
|
//find largest order number and add 1
|
||||||
$sql = mysqli_query($mysqli, "SELECT MAX(item_order) AS item_order FROM quote_items WHERE item_quote_id = $quote_id");
|
$sql = mysqli_query($mysqli, "SELECT MAX(item_order) AS item_order FROM quote_items WHERE item_quote_id = $quote_id");
|
||||||
@@ -643,68 +637,12 @@ require_once "../includes/footer.php";
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- JSON Autocomplete / type ahead -->
|
<!-- Product autocomplete for the add-item row -->
|
||||||
<!-- //TODO: Move to js/ -->
|
<script src="/js/product_autocomplete.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
initProductAutocomplete(<?= $json_products ?? '[]' ?>);
|
||||||
var availableProducts = <?= $json_products ?? '[]' ?>;
|
|
||||||
|
|
||||||
var nameInput = document.getElementById('name');
|
|
||||||
if (!nameInput) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
itflowAutocomplete(nameInput, {
|
|
||||||
minLength: 1,
|
|
||||||
source: availableProducts,
|
|
||||||
match: function (item, term) {
|
|
||||||
return String(item.label || '').toLowerCase().indexOf(term) !== -1
|
|
||||||
|| String(item.product_name || '').toLowerCase().indexOf(term) !== -1
|
|
||||||
|| String(item.product_code || '').toLowerCase().indexOf(term) !== -1;
|
|
||||||
},
|
|
||||||
render: function (item) {
|
|
||||||
var esc = itflowEscapeHtml;
|
|
||||||
var typeText = item.type ? item.type.charAt(0).toUpperCase() + item.type.slice(1).toLowerCase() : "";
|
|
||||||
var showStock = (typeText.toLowerCase() !== "service");
|
|
||||||
var taxText = (item.tax_percent != null) ? (parseFloat(item.tax_percent) + "%") : "No tax";
|
|
||||||
var priceText = (item.price != null && item.price !== "") ? String(item.price) : "";
|
|
||||||
var stockText = (item.available_stock ?? 0);
|
|
||||||
|
|
||||||
return "<div class='d-flex justify-content-between align-items-start'>" +
|
|
||||||
"<div class='flex-fill pe-2'>" +
|
|
||||||
"<div class='fw-bold'>" + esc(item.label) +
|
|
||||||
(typeText ? " <small class='text-muted'>(" + esc(typeText) + ")</small>" : "") +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='small text-muted'>" + esc(item.description) + "</div>" +
|
|
||||||
"<div class='mt-1'>" +
|
|
||||||
"<span class='badge bg-secondary me-1'>Tax: " + esc(taxText) + "</span>" +
|
|
||||||
(showStock ? "<span class='badge " + (stockText > 0 ? "bg-success" : "bg-danger") + "'>Stock: " + esc(stockText) + "</span>" : "") +
|
|
||||||
"</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='text-end'>" +
|
|
||||||
"<div class='fw-bold'>" + esc(priceText) + "</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>";
|
|
||||||
},
|
|
||||||
onSelect: function (item) {
|
|
||||||
document.getElementById('name').value = item.product_name;
|
|
||||||
document.getElementById('desc').value = item.description;
|
|
||||||
document.getElementById('qty').value = 1;
|
|
||||||
document.getElementById('price').value = item.price;
|
|
||||||
setTomSelectValue(document.getElementById('tax'), item.tax);
|
|
||||||
document.getElementById('product_id').value = item.prod_id;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Typing over the name by hand breaks the link to the product
|
|
||||||
nameInput.addEventListener('input', function () {
|
|
||||||
document.getElementById('product_id').value = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="../libs/SortableJS/Sortable.min.js"></script>
|
<script src="../libs/SortableJS/Sortable.min.js"></script>
|
||||||
|
|||||||
@@ -117,14 +117,7 @@ if (isset($_GET['recurring_invoice_id'])) {
|
|||||||
$sql_history = mysqli_query($mysqli, "SELECT history_created_at, history_description, history_status FROM history WHERE history_recurring_invoice_id = $recurring_invoice_id ORDER BY history_id DESC");
|
$sql_history = mysqli_query($mysqli, "SELECT history_created_at, history_description, history_status FROM history WHERE history_recurring_invoice_id = $recurring_invoice_id ORDER BY history_id DESC");
|
||||||
|
|
||||||
//Product autocomplete
|
//Product autocomplete
|
||||||
$products_sql = mysqli_query($mysqli, "SELECT product_name AS label, product_description AS description, product_price AS price, product_tax_id AS tax FROM products WHERE product_archived_at IS NULL");
|
$json_products = getProductsForAutocomplete($mysqli);
|
||||||
|
|
||||||
if (mysqli_num_rows($products_sql) > 0) {
|
|
||||||
while ($row = mysqli_fetch_assoc($products_sql)) {
|
|
||||||
$products[] = $row;
|
|
||||||
}
|
|
||||||
$json_products = json_encode($products);
|
|
||||||
}
|
|
||||||
|
|
||||||
enforceClientAccess();
|
enforceClientAccess();
|
||||||
|
|
||||||
@@ -340,6 +333,7 @@ if (isset($_GET['recurring_invoice_id'])) {
|
|||||||
<form action="post.php" method="post">
|
<form action="post.php" method="post">
|
||||||
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
<input type="hidden" name="csrf_token" value="<?= $_SESSION['csrf_token'] ?>">
|
||||||
<input type="hidden" name="recurring_invoice_id" value="<?= $recurring_invoice_id ?>">
|
<input type="hidden" name="recurring_invoice_id" value="<?= $recurring_invoice_id ?>">
|
||||||
|
<input type="hidden" id="product_id" name="product_id" value="0">
|
||||||
<input type="hidden" name="item_order" value="<?php
|
<input type="hidden" name="item_order" value="<?php
|
||||||
//find largest order number and add 1
|
//find largest order number and add 1
|
||||||
$sql = mysqli_query($mysqli, "SELECT MAX(item_order) AS item_order FROM recurring_invoice_items WHERE item_recurring_invoice_id = $recurring_invoice_id");
|
$sql = mysqli_query($mysqli, "SELECT MAX(item_order) AS item_order FROM recurring_invoice_items WHERE item_recurring_invoice_id = $recurring_invoice_id");
|
||||||
@@ -361,7 +355,7 @@ if (isset($_GET['recurring_invoice_id'])) {
|
|||||||
<input type="text" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" class="form-control" style="text-align: right;" id="price" name="price" placeholder="Price (<?= $recurring_invoice_currency_code ?>)">
|
<input type="text" inputmode="decimal" pattern="[0-9]*\.?[0-9]{0,2}" class="form-control" style="text-align: right;" id="price" name="price" placeholder="Price (<?= $recurring_invoice_currency_code ?>)">
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<select class="form-select" name="tax_id" id="tax" required>
|
<select class="form-select select2" name="tax_id" id="tax" required>
|
||||||
<option value="0">No Tax</option>
|
<option value="0">No Tax</option>
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
@@ -506,67 +500,12 @@ require_once "../includes/footer.php";
|
|||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- JSON Autocomplete / type ahead -->
|
<!-- Product autocomplete for the add-item row -->
|
||||||
|
<script src="/js/product_autocomplete.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
initProductAutocomplete(<?= $json_products ?? '[]' ?>);
|
||||||
var availableProducts = <?= $json_products ?? '[]' ?>;
|
|
||||||
|
|
||||||
var nameInput = document.getElementById('name');
|
|
||||||
if (!nameInput) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
itflowAutocomplete(nameInput, {
|
|
||||||
minLength: 1,
|
|
||||||
source: availableProducts,
|
|
||||||
match: function (item, term) {
|
|
||||||
return String(item.label || '').toLowerCase().indexOf(term) !== -1
|
|
||||||
|| String(item.product_name || '').toLowerCase().indexOf(term) !== -1
|
|
||||||
|| String(item.product_code || '').toLowerCase().indexOf(term) !== -1;
|
|
||||||
},
|
|
||||||
render: function (item) {
|
|
||||||
var esc = itflowEscapeHtml;
|
|
||||||
var typeText = item.type ? item.type.charAt(0).toUpperCase() + item.type.slice(1).toLowerCase() : "";
|
|
||||||
var showStock = (typeText.toLowerCase() !== "service");
|
|
||||||
var taxText = (item.tax_percent != null) ? (parseFloat(item.tax_percent) + "%") : "No tax";
|
|
||||||
var priceText = (item.price != null && item.price !== "") ? String(item.price) : "";
|
|
||||||
var stockText = (item.available_stock ?? 0);
|
|
||||||
|
|
||||||
return "<div class='d-flex justify-content-between align-items-start'>" +
|
|
||||||
"<div class='flex-fill pe-2'>" +
|
|
||||||
"<div class='fw-bold'>" + esc(item.label) +
|
|
||||||
(typeText ? " <small class='text-muted'>(" + esc(typeText) + ")</small>" : "") +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='small text-muted'>" + esc(item.description) + "</div>" +
|
|
||||||
"<div class='mt-1'>" +
|
|
||||||
"<span class='badge bg-secondary me-1'>Tax: " + esc(taxText) + "</span>" +
|
|
||||||
(showStock ? "<span class='badge " + (stockText > 0 ? "bg-success" : "bg-danger") + "'>Stock: " + esc(stockText) + "</span>" : "") +
|
|
||||||
"</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"<div class='text-end'>" +
|
|
||||||
"<div class='fw-bold'>" + esc(priceText) + "</div>" +
|
|
||||||
"</div>" +
|
|
||||||
"</div>";
|
|
||||||
},
|
|
||||||
onSelect: function (item) {
|
|
||||||
document.getElementById('name').value = item.product_name;
|
|
||||||
document.getElementById('desc').value = item.description;
|
|
||||||
document.getElementById('qty').value = 1;
|
|
||||||
document.getElementById('price').value = item.price;
|
|
||||||
setTomSelectValue(document.getElementById('tax'), item.tax);
|
|
||||||
document.getElementById('product_id').value = item.prod_id;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Typing over the name by hand breaks the link to the product
|
|
||||||
nameInput.addEventListener('input', function () {
|
|
||||||
document.getElementById('product_id').value = 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script src="../libs/SortableJS/Sortable.min.js"></script>
|
<script src="../libs/SortableJS/Sortable.min.js"></script>
|
||||||
|
|||||||
@@ -871,3 +871,46 @@ function getSentMethods() {
|
|||||||
'Other'
|
'Other'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Products for the line-item autocomplete on invoices, quotes and recurring
|
||||||
|
* invoices.
|
||||||
|
*
|
||||||
|
* All three pages share js/product_autocomplete.js, so they must all be handed
|
||||||
|
* the same shape. They used to carry a SELECT each and they drifted: quote and
|
||||||
|
* recurring invoice only selected label/description/price/tax, so the shared
|
||||||
|
* onSelect wrote item.product_name - undefined - into the item name field.
|
||||||
|
*
|
||||||
|
* Returns a JSON string ready to emit into the page.
|
||||||
|
*/
|
||||||
|
function getProductsForAutocomplete($mysqli): string
|
||||||
|
{
|
||||||
|
$products = [];
|
||||||
|
|
||||||
|
$sql = mysqli_query($mysqli, "
|
||||||
|
SELECT
|
||||||
|
IF(product_code IS NULL OR product_code = '', product_name, CONCAT(product_code, ' - ', product_name)) AS label,
|
||||||
|
product_name,
|
||||||
|
product_code,
|
||||||
|
product_type AS type,
|
||||||
|
product_description AS description,
|
||||||
|
product_price AS price,
|
||||||
|
product_tax_id AS tax,
|
||||||
|
tax_percent,
|
||||||
|
product_id AS prod_id,
|
||||||
|
COALESCE(SUM(product_stock.stock_qty), 0) AS available_stock
|
||||||
|
FROM products
|
||||||
|
LEFT JOIN product_stock ON product_id = stock_product_id
|
||||||
|
LEFT JOIN taxes ON product_tax_id = tax_id
|
||||||
|
WHERE product_archived_at IS NULL
|
||||||
|
GROUP BY product_id
|
||||||
|
ORDER BY product_name ASC
|
||||||
|
");
|
||||||
|
|
||||||
|
while ($row = mysqli_fetch_assoc($sql)) {
|
||||||
|
$products[] = $row;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json_encode($products) ?: '[]';
|
||||||
|
}
|
||||||
|
|||||||
76
js/product_autocomplete.js
Normal file
76
js/product_autocomplete.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* Product autocomplete for the add-item row on invoices, quotes and recurring
|
||||||
|
* invoices.
|
||||||
|
*
|
||||||
|
* The three pages used to carry a copy of this each, and they drifted - the
|
||||||
|
* quote and recurring invoice copies were fed a four column product query, so
|
||||||
|
* item.product_name was undefined and selecting a product wrote the literal
|
||||||
|
* string "undefined" into the item name.
|
||||||
|
*
|
||||||
|
* Call with the array emitted by getProductsForAutocomplete():
|
||||||
|
*
|
||||||
|
* initProductAutocomplete(<?= $json_products ?? '[]' ?>);
|
||||||
|
*/
|
||||||
|
function initProductAutocomplete(availableProducts) {
|
||||||
|
|
||||||
|
var nameInput = document.getElementById('name');
|
||||||
|
if (!nameInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quote and recurring invoice do not store the product link yet, so treat
|
||||||
|
// the hidden input as optional rather than throwing on every keystroke.
|
||||||
|
var productIdInput = document.getElementById('product_id');
|
||||||
|
|
||||||
|
itflowAutocomplete(nameInput, {
|
||||||
|
minLength: 1,
|
||||||
|
source: availableProducts || [],
|
||||||
|
match: function (item, term) {
|
||||||
|
return String(item.label || '').toLowerCase().indexOf(term) !== -1
|
||||||
|
|| String(item.product_name || '').toLowerCase().indexOf(term) !== -1
|
||||||
|
|| String(item.product_code || '').toLowerCase().indexOf(term) !== -1;
|
||||||
|
},
|
||||||
|
render: function (item) {
|
||||||
|
var esc = itflowEscapeHtml;
|
||||||
|
var typeText = item.type ? item.type.charAt(0).toUpperCase() + item.type.slice(1).toLowerCase() : "";
|
||||||
|
var showStock = (typeText.toLowerCase() !== "service");
|
||||||
|
var taxText = (item.tax_percent != null) ? (parseFloat(item.tax_percent) + "%") : "No tax";
|
||||||
|
var priceText = (item.price != null && item.price !== "") ? String(item.price) : "";
|
||||||
|
var stockText = (item.available_stock ?? 0);
|
||||||
|
|
||||||
|
return "<div class='d-flex justify-content-between align-items-start'>" +
|
||||||
|
"<div class='flex-fill pe-2'>" +
|
||||||
|
"<div class='fw-bold'>" + esc(item.label) +
|
||||||
|
(typeText ? " <small class='text-muted'>(" + esc(typeText) + ")</small>" : "") +
|
||||||
|
"</div>" +
|
||||||
|
"<div class='small text-muted'>" + esc(item.description) + "</div>" +
|
||||||
|
"<div class='mt-1'>" +
|
||||||
|
"<span class='badge bg-secondary me-1'>Tax: " + esc(taxText) + "</span>" +
|
||||||
|
(showStock ? "<span class='badge " + (stockText > 0 ? "bg-success" : "bg-danger") + "'>Stock: " + esc(stockText) + "</span>" : "") +
|
||||||
|
"</div>" +
|
||||||
|
"</div>" +
|
||||||
|
"<div class='text-end'>" +
|
||||||
|
"<div class='fw-bold'>" + esc(priceText) + "</div>" +
|
||||||
|
"</div>" +
|
||||||
|
"</div>";
|
||||||
|
},
|
||||||
|
onSelect: function (item) {
|
||||||
|
nameInput.value = item.product_name;
|
||||||
|
document.getElementById('desc').value = item.description;
|
||||||
|
document.getElementById('qty').value = 1;
|
||||||
|
document.getElementById('price').value = item.price;
|
||||||
|
setTomSelectValue(document.getElementById('tax'), item.tax);
|
||||||
|
if (productIdInput) {
|
||||||
|
productIdInput.value = item.prod_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Typing over the name by hand breaks the link to the product
|
||||||
|
if (productIdInput) {
|
||||||
|
nameInput.addEventListener('input', function () {
|
||||||
|
productIdInput.value = 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user