788 lines
22 KiB
JavaScript
788 lines
22 KiB
JavaScript
/** =========================
|
|
* SIDEBAR ACTIVE STATE
|
|
* Override: activate the parent listing page for manage_* sub-pages.
|
|
* main.js (theme) handles exact-match pages; this covers second-depth pages.
|
|
* ========================= */
|
|
$(function () {
|
|
var $links = $('#sidebar').find('a.nav-link');
|
|
var path = window.location.pathname;
|
|
|
|
// Theme already handled it — an active link whose href matches the current path
|
|
var alreadyActive = $links.filter('.active').toArray().some(function (a) {
|
|
return a.pathname === path;
|
|
});
|
|
if (alreadyActive) return;
|
|
|
|
// For manage_* pages: strip "manage_" to derive the parent listing filename
|
|
var filename = path.split('/').pop();
|
|
if (!/^manage_/.test(filename)) return;
|
|
|
|
var dir = path.substring(0, path.lastIndexOf('/'));
|
|
var parentPath = dir + '/' + filename.replace(/^manage_/, '');
|
|
|
|
var $parent = $links.filter(function () { return this.pathname === parentPath; });
|
|
if ($parent.length) {
|
|
$links.removeClass('active');
|
|
$parent.addClass('active');
|
|
}
|
|
});
|
|
|
|
|
|
/** =========================
|
|
* UTILITIES
|
|
* ========================= */
|
|
|
|
/**
|
|
* Returns a debounced version of fn that delays invoking until
|
|
* after `wait` ms have elapsed since the last call.
|
|
*/
|
|
function debounce(fn, wait) {
|
|
wait = wait || 300;
|
|
var timer;
|
|
return function (event) {
|
|
var ctx = this;
|
|
var args = arguments;
|
|
// Persist the jQuery event so it's accessible after the timeout
|
|
if (event && event.persist) event.persist();
|
|
clearTimeout(timer);
|
|
timer = setTimeout(function () {
|
|
fn.apply(ctx, args);
|
|
}, wait);
|
|
};
|
|
}
|
|
|
|
|
|
/** =========================
|
|
* DEFAULT ON LOAD FUNCTION
|
|
* ========================= */
|
|
$(function () {
|
|
|
|
// Prevent all forms from refreshing the page
|
|
$("form").on("submit", function (e) {
|
|
e.preventDefault();
|
|
});
|
|
|
|
// Initialize autocomplete for product search inputs
|
|
$('input.product_search').each(function (i, el) {
|
|
|
|
var $el = $(el);
|
|
$el.autocomplete({
|
|
|
|
// Fetch data from server
|
|
source: function (request, response) {
|
|
|
|
// Abort previous request (prevent multiple firing)
|
|
if ($el.data('xhr')) {
|
|
$el.data('xhr').abort();
|
|
}
|
|
|
|
// Prepare payload
|
|
let q = prepare_form_data(0, true);
|
|
q['keyword'] = request.term;
|
|
let json = JSON.stringify(q);
|
|
|
|
// Send AJAX request
|
|
var xhr = $.ajax({
|
|
url: server_url + 'ics/api/engine/product_search.php',
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
data: { json: json },
|
|
// for CSRF validation
|
|
headers: {
|
|
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
|
},
|
|
|
|
success: function (res) {
|
|
|
|
var aca = [];
|
|
|
|
// Map result to autocomplete format
|
|
$.each(res.result, function (i, item) {
|
|
|
|
var temp = "";
|
|
temp += "<div class='product-result-item' style='font-family:\"Poppins\",sans-serif;padding:4px 0;'>";
|
|
temp += "<div style='font-size:10px;text-transform:uppercase;color:#E66239;font-weight:600;line-height:1.2;'>";
|
|
temp += "SKU: " + (item.sku || 'N/A');
|
|
temp += "</div>";
|
|
temp += "<div style='font-size:13px;font-weight:500;color:#171717;margin-top:2px;'>";
|
|
temp += item.product_name;
|
|
temp += "</div>";
|
|
temp += "</div>";
|
|
|
|
aca.push({
|
|
label: temp, // HTML dropdown
|
|
value: item.product_name, // Default value
|
|
data: item // Full object
|
|
});
|
|
|
|
});
|
|
|
|
response(aca);
|
|
}
|
|
});
|
|
|
|
// Save request reference
|
|
$el.data('xhr', xhr);
|
|
},
|
|
|
|
minLength: 0,
|
|
delay: 300,
|
|
|
|
// When selecting item
|
|
select: function (e, ui) {
|
|
|
|
if (ui.item && ui.item.data) {
|
|
|
|
var product = ui.item.data;
|
|
|
|
// Replace input value with SKU
|
|
$(this)
|
|
.val(product.sku)
|
|
.attr("secondary", product.sku);
|
|
$('input#product_name')
|
|
.val(product.product_name)
|
|
.attr("secondary", product.product_name)
|
|
|
|
console.log("Input updated with SKU:", product.sku);
|
|
|
|
return false; // Prevent overwrite by default value
|
|
}
|
|
}
|
|
|
|
})
|
|
|
|
// Allow HTML rendering in dropdown
|
|
.data("ui-autocomplete")._renderItem = function (ul, item) {
|
|
return $("<li>")
|
|
.append($("<div>").html(item.label))
|
|
.appendTo(ul);
|
|
};
|
|
|
|
// Add this ↓
|
|
$el.on('blur', function () {
|
|
var secondary = $(this).attr('secondary');
|
|
if (secondary) {
|
|
$(this).val(secondary);
|
|
$('input#product_name').val($('input#product_name').attr('secondary'));
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
|
|
$('input.contact_search').each(function (i, el) {
|
|
|
|
var $el = $(el);
|
|
$el.autocomplete({
|
|
|
|
// Fetch data from server
|
|
source: function (request, response) {
|
|
|
|
// Abort previous request (prevent multiple firing)
|
|
if ($el.data('xhr')) {
|
|
$el.data('xhr').abort();
|
|
}
|
|
|
|
// Prepare payload
|
|
let q = prepare_form_data(0, true);
|
|
q['keyword'] = request.term;
|
|
let json = JSON.stringify(q);
|
|
|
|
// Send AJAX request
|
|
var xhr = $.ajax({
|
|
url: server_url + 'ics/api/engine/contact_search.php',
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
data: { json: json },
|
|
// for CSRF validation
|
|
headers: {
|
|
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
|
},
|
|
|
|
success: function (res) {
|
|
|
|
var aca = [];
|
|
|
|
// Map result to autocomplete format
|
|
$.each(res.result, function (i, item) {
|
|
|
|
var temp = "";
|
|
temp += "<div class='contact-result-item' style='font-family:\"Poppins\",sans-serif;padding:4px 0;'>";
|
|
temp += "<div style='font-size:10px;text-transform:uppercase;color:#E66239;font-weight:600;line-height:1.2;'>";
|
|
temp += "Tax ID: " + (item.tax_id || 'N/A');
|
|
temp += "</div>";
|
|
temp += "<div style='font-size:13px;font-weight:500;color:#171717;margin-top:2px;'>";
|
|
temp += item.contact_name;
|
|
temp += "</div>";
|
|
temp += "</div>";
|
|
|
|
aca.push({
|
|
label: temp, // HTML dropdown
|
|
value: item.contact_name, // Default value
|
|
data: item // Full object
|
|
});
|
|
|
|
});
|
|
|
|
response(aca);
|
|
}
|
|
});
|
|
|
|
// Save request reference
|
|
$el.data('xhr', xhr);
|
|
},
|
|
|
|
minLength: 0,
|
|
delay: 300,
|
|
|
|
// When selecting item
|
|
select: function (e, ui) {
|
|
|
|
if (ui.item && ui.item.data) {
|
|
|
|
var contact = ui.item.data;
|
|
|
|
// Replace input value with SKU
|
|
$(this)
|
|
.val(contact.contact_name)
|
|
.attr("data-id", contact.id)
|
|
.attr("secondary", contact.contact_name);
|
|
|
|
console.log("Input updated with contact name:", contact.contact_name);
|
|
|
|
return false; // Prevent overwrite by default value
|
|
}
|
|
}
|
|
|
|
})
|
|
|
|
// Allow HTML rendering in dropdown
|
|
.data("ui-autocomplete")._renderItem = function (ul, item) {
|
|
return $("<li>")
|
|
.append($("<div>").html(item.label))
|
|
.appendTo(ul);
|
|
};
|
|
|
|
// Add this ↓
|
|
$el.on('blur', function () {
|
|
var secondary = $(this).attr('secondary');
|
|
console.log($(this).attr('data-id'))
|
|
if (secondary) {
|
|
$('input#contact').val($('input#contact').attr('secondary'));
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
|
|
/** =========================
|
|
* LOT SEARCH AUTOCOMPLETE
|
|
* ========================= */
|
|
$('input.lot_search').each(function (i, el) {
|
|
|
|
var $el = $(el);
|
|
$el.autocomplete({
|
|
|
|
source: function (request, response) {
|
|
|
|
if ($el.data('xhr')) {
|
|
$el.data('xhr').abort();
|
|
}
|
|
|
|
let q = prepare_form_data(0, true);
|
|
q['keyword'] = request.term;
|
|
q['product_sku'] = $('input#product_sku').attr('secondary') || $('input#product_sku').val();
|
|
let json = JSON.stringify(q);
|
|
|
|
var xhr = $.ajax({
|
|
url: server_url + 'ics/api/engine/retrieve_lot.php',
|
|
type: 'POST',
|
|
dataType: 'json',
|
|
data: { json: json },
|
|
headers: {
|
|
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
|
},
|
|
success: function (res) {
|
|
|
|
var aca = [];
|
|
|
|
$.each(res.result, function (i, item) {
|
|
var temp = "";
|
|
temp += "<div class='lot-result-item' style='font-family:\"Poppins\",sans-serif;padding:4px 0;'>";
|
|
temp += "<div style='font-size:10px;text-transform:uppercase;color:#E66239;font-weight:600;line-height:1.2;'>";
|
|
temp += "Lot: " + item.lot_number;
|
|
temp += "</div>";
|
|
temp += "<div style='font-size:13px;font-weight:500;color:#171717;margin-top:2px;'>";
|
|
temp += "Expiry: " + (item.expiry_date || 'N/A');
|
|
temp += "</div>";
|
|
temp += "</div>";
|
|
|
|
aca.push({
|
|
label: temp,
|
|
value: item.lot_number,
|
|
data: item
|
|
});
|
|
});
|
|
|
|
response(aca);
|
|
}
|
|
});
|
|
|
|
$el.data('xhr', xhr);
|
|
},
|
|
|
|
minLength: 0,
|
|
delay: 300,
|
|
|
|
select: function (e, ui) {
|
|
if (ui.item && ui.item.data) {
|
|
|
|
var lot = ui.item.data;
|
|
|
|
// Set lot_number value
|
|
$(this)
|
|
.val(lot.lot_number)
|
|
.attr('secondary', lot.lot_number)
|
|
.attr('data-existing', '1'); // flag: this is an existing lot
|
|
|
|
// Populate + lock expiry_date — existing lot, not editable
|
|
if (typeof expiryPicker !== 'undefined' && expiryPicker) {
|
|
expiryPicker.setDate(lot.expiry_date || '', true, 'Y-m-d');
|
|
if (expiryPicker.altInput) { expiryPicker.altInput.disabled = true; }
|
|
}
|
|
$('#expiry_date').prop('disabled', true)
|
|
.attr('title', 'Expiry date is locked for existing lots');
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
})
|
|
|
|
.data("ui-autocomplete")._renderItem = function (ul, item) {
|
|
return $("<li>")
|
|
.append($("<div>").html(item.label))
|
|
.appendTo(ul);
|
|
};
|
|
|
|
// On blur — restore value if user didn't pick from dropdown
|
|
$el.on('blur', function () {
|
|
var secondary = $(this).attr('secondary');
|
|
if (secondary) {
|
|
$(this).val(secondary);
|
|
}
|
|
});
|
|
|
|
// On manual edit — clear existing flag and unlock expiry_date
|
|
$el.on('input', function () {
|
|
$(this).removeAttr('data-existing');
|
|
$('#expiry_date').prop('disabled', false).removeAttr('title');
|
|
if (typeof expiryPicker !== 'undefined' && expiryPicker) {
|
|
expiryPicker.clear();
|
|
if (expiryPicker.altInput) { expiryPicker.altInput.disabled = false; }
|
|
}
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
/** =========================
|
|
* PREPARE FORM DATA
|
|
* ========================= */
|
|
function prepare_form_data(check_required, raw_data) {
|
|
|
|
var q = {};
|
|
|
|
// Get session context
|
|
const session_element = document.getElementById('session-context');
|
|
|
|
if (session_element) {
|
|
q['company_id'] = session_element.dataset.companyId;
|
|
q['otp'] = session_element.dataset.otp;
|
|
}
|
|
|
|
// Include GET parameters
|
|
const url_params = new URLSearchParams(window.location.search);
|
|
url_params.forEach((value, key) => {
|
|
q[key] = value;
|
|
});
|
|
|
|
// Collect form inputs (exclude search inputs — UI-only filters, not API data)
|
|
$(".form-control:not([type=search]), .form-select").each(function () {
|
|
if (!$(this).attr("id")) return true;
|
|
var el = $(this).get(0);
|
|
if (!el || !el.nodeName) return true;
|
|
q[$(this).attr("id")] = $(this).val();
|
|
});
|
|
|
|
// Validate required fields
|
|
if (check_required === 1) {
|
|
|
|
const required_inputs = document.querySelectorAll('[required]');
|
|
let is_valid = true;
|
|
|
|
required_inputs.forEach(input => {
|
|
if (!input.value.trim()) {
|
|
input.classList.add('is-invalid');
|
|
is_valid = false;
|
|
} else {
|
|
input.classList.remove('is-invalid');
|
|
input.classList.add('is-valid');
|
|
}
|
|
});
|
|
|
|
if (!is_valid) {
|
|
alert("Please fill in all mandatory fields.");
|
|
isAjaxProcessing = false;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return (raw_data) ? q : JSON.stringify(q);
|
|
}
|
|
|
|
|
|
/** =========================
|
|
* AJAX WRAPPER
|
|
* ========================= */
|
|
|
|
// Prevent double firing
|
|
let isAjaxProcessing = false;
|
|
|
|
function ajax_request(options) {
|
|
|
|
if (isAjaxProcessing && options.queueLock !== false) {
|
|
// Instead of rejecting, we just return a "never-ending" promise
|
|
// or a resolved promise that does nothing.
|
|
console.warn("Request is busy... ignoring click.");
|
|
return new Promise(() => { }); // This stays pending and won't trigger .then or .catch
|
|
}
|
|
|
|
if (options.queueLock !== false) {
|
|
isAjaxProcessing = true;
|
|
}
|
|
|
|
// Auto prepare form data
|
|
if (options.autoPrepare === true) {
|
|
|
|
let payloadJson = prepare_form_data(options.checkRequired ?? 0, true);
|
|
if (payloadJson === false) {
|
|
isAjaxProcessing = false;
|
|
return Promise.reject("validation_failed");
|
|
}
|
|
|
|
if (options.data) {
|
|
Object.entries(options.data).forEach(([key, value]) => {
|
|
payloadJson[key] = value;
|
|
});
|
|
}
|
|
|
|
if (options.action) {
|
|
// modify action
|
|
if (options.action === 'manage') {
|
|
options.action = (payloadJson['id']) ? 'update' : 'create';
|
|
}
|
|
// add action to JSON
|
|
payloadJson['action'] = options.action;
|
|
} else {
|
|
isAjaxProcessing = false;
|
|
return Promise.reject("please_define_action");
|
|
}
|
|
|
|
options.data = { json: JSON.stringify(payloadJson) };
|
|
|
|
if (options.debugMode) {
|
|
isAjaxProcessing = false;
|
|
// Show FormData contents if applicable
|
|
if (options.formData instanceof FormData) {
|
|
// Log original formData before merging
|
|
for (let [key, value] of options.formData.entries()) {
|
|
console.log("FORMDATA: " + key, value);
|
|
}
|
|
}
|
|
// Show stringified JSON payload
|
|
console.log("REQUEST DATA:", options.data);
|
|
}
|
|
|
|
// IF formData exist, we pass as $_POST [not json]
|
|
if (options.formData instanceof FormData) {
|
|
// THE BYPASS: If formData exists, move all text data into it
|
|
Object.entries(payloadJson).forEach(([key, value]) => {
|
|
options.formData.append(key, value);
|
|
});
|
|
// Override options.data with the full FormData object
|
|
options.data = options.formData;
|
|
}
|
|
}
|
|
|
|
// --- START MODIFIED $.AJAX BLOCK ---
|
|
let isSendingFiles = (options.data instanceof FormData);
|
|
|
|
// Show loading overlay
|
|
if (options.noLoading !== true) {
|
|
$.LoadingOverlay("show", {
|
|
imageColor: "#525252",
|
|
imageAnimation: "2s rotate_right",
|
|
background: "rgba(255,255,255,0.8)"
|
|
});
|
|
}
|
|
|
|
return $.ajax({
|
|
async: true,
|
|
type: options.type || "POST",
|
|
url: options.url,
|
|
data: options.data,
|
|
dataType: "json",
|
|
// These two settings are only triggered when sending files
|
|
processData: isSendingFiles ? false : true,
|
|
contentType: isSendingFiles ? false : "application/x-www-form-urlencoded; charset=UTF-8",
|
|
// for CSRF validation
|
|
headers: {
|
|
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
|
|
}
|
|
|
|
})
|
|
.then(function (res) {
|
|
|
|
isAjaxProcessing = false;
|
|
if (options.noLoading !== true) $.LoadingOverlay("hide");
|
|
|
|
if (options.debugMode) {
|
|
console.log("RESPONSE:", res);
|
|
return res;
|
|
}
|
|
|
|
if (!res || res.success != 1) {
|
|
bootbox.alert(res?.message || "Unexpected error");
|
|
throw new Error(res?.message || "api_failed");
|
|
}
|
|
|
|
options.onSuccess?.(res);
|
|
return res;
|
|
|
|
})
|
|
.catch(function (xhr) {
|
|
|
|
isAjaxProcessing = false;
|
|
$.LoadingOverlay("hide");
|
|
|
|
// Errors re-thrown from .then() — pass through
|
|
if (xhr instanceof Error) {
|
|
throw xhr;
|
|
}
|
|
|
|
// Extract server's error message from JSON response
|
|
let serverMessage = xhr?.responseJSON?.message;
|
|
|
|
// Fallback: parse responseText if responseJSON isn't set
|
|
if (!serverMessage && xhr?.responseText) {
|
|
try {
|
|
serverMessage = JSON.parse(xhr.responseText)?.message;
|
|
} catch (e) {
|
|
// Response wasn't JSON — real server crash or HTML error page
|
|
}
|
|
}
|
|
|
|
if (serverMessage) {
|
|
// Known server-reported error (HTTP 4xx/5xx with JSON body)
|
|
bootbox.alert(serverMessage);
|
|
options.onError?.(xhr, serverMessage);
|
|
} else {
|
|
// Unknown error — network failure, PHP fatal, HTML error page
|
|
console.error("AJAX Error:", xhr?.status, xhr?.responseText);
|
|
bootbox.alert("Server error occurred.");
|
|
options.onError?.(xhr, null);
|
|
}
|
|
|
|
throw xhr;
|
|
});
|
|
}
|
|
|
|
|
|
/** =========================
|
|
* GENERATE PAGINATION
|
|
* ========================= */
|
|
function generate_pagination(table_id, total_records, records_per_page, current_page = 1) {
|
|
records_per_page = records_per_page ?? prop_limit;
|
|
const total_pages = Math.ceil(total_records / records_per_page);
|
|
|
|
// 1. Find the table and thead
|
|
const table = document.getElementById(table_id);
|
|
let theme_class = '';
|
|
let col_count = 1;
|
|
|
|
if (table) {
|
|
const thead = table.querySelector('thead');
|
|
if (thead) {
|
|
// 2. Look through classes for one that starts with "table-"
|
|
const bootstrap_theme = Array.from(thead.classList).find(c => c.startsWith('table-'));
|
|
|
|
if (bootstrap_theme) {
|
|
// 3. Convert "table-secondary" to "page-item-secondary"
|
|
theme_class = bootstrap_theme.replace('table-', 'page-item-');
|
|
}
|
|
|
|
// 4. Count <th> in the first row of thead
|
|
const header_row = thead.querySelector('tr');
|
|
if (header_row) {
|
|
col_count = header_row.querySelectorAll('th').length || 1;
|
|
}
|
|
}
|
|
}
|
|
|
|
const left_span = Math.floor(col_count / 2);
|
|
const right_span = Math.ceil(col_count / 2);
|
|
|
|
let pagination_html = `
|
|
<tr>
|
|
<td colspan="${left_span}" class="border-bottom-0">
|
|
<span class="text-muted">Showing ${records_per_page} per page</span><br>
|
|
<strong>Total: ${total_records}</strong>
|
|
</td>
|
|
<td colspan="${right_span}" class="border-bottom-0">
|
|
<nav aria-label="Page navigation" class="d-flex justify-content-end">
|
|
<ul class="pagination mb-0">`;
|
|
|
|
for (let i = 1; i <= total_pages; i++) {
|
|
// Check if 'i' matches the current_page passed to the function
|
|
let active_class = (i === current_page) ? 'active' : '';
|
|
|
|
pagination_html += `
|
|
<li class="page-item ${theme_class} ${active_class}" data-page="${i}">
|
|
<a class="page-link" href="javascript:;" onclick="handlePageClick('${table_id}', this, ${i})">${i}</a>
|
|
</li>`;
|
|
}
|
|
|
|
pagination_html += `</ul></nav></td></tr>`;
|
|
return pagination_html;
|
|
}
|
|
|
|
|
|
function handlePageClick(table_id, element, pageNumber) {
|
|
// 1. Remove 'active' class from all siblings within this specific pagination bar
|
|
const paginationUl = element.closest('.pagination');
|
|
paginationUl.querySelectorAll('.page-item').forEach(li => {
|
|
li.classList.remove('active');
|
|
});
|
|
|
|
// 2. Add 'active' class to the clicked parent <li>
|
|
element.parentElement.classList.add('active');
|
|
|
|
// 3. Define the dynamic function name
|
|
const dynamicFunctionName = `change_page_${table_id}`;
|
|
|
|
// 4. Check if the specific dynamic function exists and call it
|
|
if (typeof window[dynamicFunctionName] === "function") {
|
|
window[dynamicFunctionName](pageNumber);
|
|
} else {
|
|
console.error(`Function ${dynamicFunctionName} not found!`);
|
|
}
|
|
}
|
|
|
|
|
|
/** =========================
|
|
* FLATPICKR INITIALIZE
|
|
* ========================= */
|
|
flatpickr(".flatpickr", {
|
|
enableTime: true,
|
|
time_24hr: true,
|
|
defaultDate: new Date(),
|
|
altInput: true,
|
|
altFormat: "d/m/Y H:i:s",
|
|
dateFormat: "Y-m-d H:i:s"
|
|
});
|
|
|
|
|
|
/** =========================
|
|
* REAL-TIME REQUIRED VALIDATION
|
|
* ========================= */
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
|
|
const required_inputs = document.querySelectorAll('[required]');
|
|
|
|
required_inputs.forEach(input => {
|
|
input.addEventListener('input', function () {
|
|
if (this.value.trim() !== "") {
|
|
this.classList.remove('is-invalid');
|
|
this.classList.add('is-valid');
|
|
} else {
|
|
this.classList.remove('is-valid');
|
|
this.classList.add('is-invalid');
|
|
}
|
|
});
|
|
});
|
|
|
|
});
|
|
|
|
|
|
/**
|
|
* Helper function to format date strings (assuming input is in ISO format)
|
|
*/
|
|
function format_date(iso_string) {
|
|
var split = iso_string.split(" ");
|
|
if (split.length === 2) {
|
|
var datePart = split[0].split("-");
|
|
}
|
|
return (datePart) ? `${datePart[2]}/${datePart[1]}/${datePart[0]} ${split[1]}` : iso_string;
|
|
}
|
|
|
|
|
|
function to_iso_date(dateStr) {
|
|
if (!dateStr) return null;
|
|
|
|
const parts = dateStr.split('/');
|
|
if (parts.length !== 3) return null;
|
|
|
|
let [day, month, year] = parts;
|
|
day = day.padStart(2, '0');
|
|
month = month.padStart(2, '0');
|
|
|
|
return `${year}-${month}-${day}`;
|
|
}
|
|
|
|
|
|
function format_number(value, decimal) {
|
|
var n = Number(value);
|
|
if (isNaN(n)) return '--';
|
|
if (decimal !== undefined) n = n.toFixed(decimal);
|
|
var parts = n.toString().split('.');
|
|
parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
return parts.join('.');
|
|
}
|
|
|
|
/**
|
|
* set_btn_state(selector, enabled, hint)
|
|
*
|
|
* Enable or disable a button with consistent visual feedback:
|
|
* enabled → restores original btn-* classes, removes disabled, clears title
|
|
* disabled → swaps to btn-secondary, adds disabled attr, sets title hint
|
|
*
|
|
* Stores the original class on first call via data-original-class so
|
|
* repeated calls always restore the correct variant.
|
|
*
|
|
* @param {string} selector jQuery selector e.g. '#btn_confirm'
|
|
* @param {boolean} enabled true = enable, false = disable
|
|
* @param {string} hint Tooltip text shown when disabled
|
|
*/
|
|
function set_btn_state(selector, enabled, hint) {
|
|
var $btn = $(selector);
|
|
if (!$btn.length) return;
|
|
|
|
// Store original classes once
|
|
if (!$btn.data('original-class')) {
|
|
$btn.data('original-class', $btn.attr('class'));
|
|
}
|
|
|
|
if (enabled) {
|
|
$btn.attr('class', $btn.data('original-class'))
|
|
.prop('disabled', false)
|
|
.attr('title', '');
|
|
} else {
|
|
$btn.attr('class', $btn.data('original-class')
|
|
.replace(/btn-(outline-)?[a-z]+/g, '') + ' btn-light')
|
|
.prop('disabled', true)
|
|
.attr('title', hint || '');
|
|
}
|
|
} |