mirror of
https://github.com/itflow-org/itflow
synced 2026-08-16 04:25:13 +00:00
All Jquery removed replaced with stand JS and non jqery dependent libs
This commit is contained in:
@@ -49,34 +49,48 @@ ob_start();
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function(){
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
$('#generateAIContent').on('click', function(){
|
var button = document.getElementById('generateAIContent');
|
||||||
var prompt = $('#aiPrompt').val().trim();
|
var promptField = document.getElementById('aiPrompt');
|
||||||
if(prompt === '') {
|
if (!button || !promptField) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
button.addEventListener('click', function () {
|
||||||
|
var prompt = promptField.value.trim();
|
||||||
|
if (prompt === '') {
|
||||||
alert('Please enter a prompt.');
|
alert('Please enter a prompt.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#generateAIContent').prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Generating...');
|
button.disabled = true;
|
||||||
|
button.innerHTML = '<i class="fa fa-spinner fa-spin"></i> Generating...';
|
||||||
|
|
||||||
$.ajax({
|
fetch('/agent/ajax.php?ai_create_document_template', {
|
||||||
url: '/agent/ajax.php?ai_create_document_template', // The PHP script that calls the OpenAI API
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { prompt: prompt },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
dataType: 'html',
|
credentials: 'same-origin',
|
||||||
success: function(response) {
|
body: new URLSearchParams({ prompt: prompt }).toString()
|
||||||
|
})
|
||||||
|
.then(function (res) {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('HTTP ' + res.status);
|
||||||
|
}
|
||||||
|
return res.text();
|
||||||
|
})
|
||||||
|
.then(function (response) {
|
||||||
// Assuming you have exactly one TinyMCE instance on the page
|
// Assuming you have exactly one TinyMCE instance on the page
|
||||||
// and it's targeting the .tinymce textarea:
|
// and it's targeting the .tinymce textarea:
|
||||||
tinymce.activeEditor.setContent(response);
|
tinymce.activeEditor.setContent(response);
|
||||||
},
|
})
|
||||||
error: function() {
|
.catch(function () {
|
||||||
alert('Error generating content. Please try again.');
|
alert('Error generating content. Please try again.');
|
||||||
},
|
})
|
||||||
complete: function() {
|
.finally(function () {
|
||||||
$('#generateAIContent').prop('disabled', false).html('<i class="fa fa-fw fa-magic me-1"></i>Generate with AI');
|
button.disabled = false;
|
||||||
}
|
button.innerHTML = '<i class="fa fa-fw fa-magic me-1"></i>Generate with AI';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ new Sortable(document.querySelector('table#ticket_templates tbody'), {
|
|||||||
order: index
|
order: index
|
||||||
}));
|
}));
|
||||||
|
|
||||||
$.post('/agent/ajax.php', {
|
itflowPostForm('/agent/ajax.php', {
|
||||||
update_project_template_ticket_order: true,
|
update_project_template_ticket_order: true,
|
||||||
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
|
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
|
||||||
project_template_id: <?= $project_template_id ?>,
|
project_template_id: <?= $project_template_id ?>,
|
||||||
|
|||||||
@@ -145,7 +145,7 @@ new Sortable(document.querySelector('table#tasks tbody'), {
|
|||||||
order: index
|
order: index
|
||||||
}));
|
}));
|
||||||
|
|
||||||
$.post('/agent/ajax.php', {
|
itflowPostForm('/agent/ajax.php', {
|
||||||
update_task_templates_order: true,
|
update_task_templates_order: true,
|
||||||
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
|
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
|
||||||
ticket_template_id: <?= $ticket_template_id ?>,
|
ticket_template_id: <?= $ticket_template_id ?>,
|
||||||
|
|||||||
@@ -1352,16 +1352,24 @@ if (isset($_GET['asset_id'])) {
|
|||||||
|
|
||||||
<!-- JavaScript to Show/Hide Password Form Group -->
|
<!-- JavaScript to Show/Hide Password Form Group -->
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
$('.authMethod').on('change', function() {
|
function syncAuthForm(select) {
|
||||||
var $form = $(this).closest('.authForm');
|
var form = select.closest('.authForm');
|
||||||
if ($(this).val() === 'local') {
|
if (!form) {
|
||||||
$form.find('.passwordGroup').show();
|
return;
|
||||||
} else {
|
|
||||||
$form.find('.passwordGroup').hide();
|
|
||||||
}
|
}
|
||||||
|
form.querySelectorAll('.passwordGroup').forEach(function (group) {
|
||||||
|
group.style.display = select.value === 'local' ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.authMethod').forEach(function (select) {
|
||||||
|
select.addEventListener('change', function () {
|
||||||
|
syncAuthForm(this);
|
||||||
|
});
|
||||||
|
syncAuthForm(select);
|
||||||
});
|
});
|
||||||
$('.authMethod').trigger('change');
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ while ($row = mysqli_fetch_assoc($sql)) {
|
|||||||
const allDayToggle = document.getElementById("event_add_all_day");
|
const allDayToggle = document.getElementById("event_add_all_day");
|
||||||
if (allDayToggle) {
|
if (allDayToggle) {
|
||||||
allDayToggle.checked = true;
|
allDayToggle.checked = true;
|
||||||
$(allDayToggle).trigger("change");
|
allDayToggle.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
}
|
}
|
||||||
bootstrap.Modal.getOrCreateInstance(document.getElementById('addCalendarEventModal')).show();
|
bootstrap.Modal.getOrCreateInstance(document.getElementById('addCalendarEventModal')).show();
|
||||||
}
|
}
|
||||||
@@ -270,7 +270,7 @@ while ($row = mysqli_fetch_assoc($sql)) {
|
|||||||
// Last, so the handler in app.js shows or hides the time row to match
|
// Last, so the handler in app.js shows or hides the time row to match
|
||||||
if (allDayToggle) {
|
if (allDayToggle) {
|
||||||
allDayToggle.checked = selectionInfo.allDay;
|
allDayToggle.checked = selectionInfo.allDay;
|
||||||
$(allDayToggle).trigger("change");
|
allDayToggle.dispatchEvent(new Event('change', { bubbles: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
calendar.unselect();
|
calendar.unselect();
|
||||||
@@ -278,15 +278,14 @@ while ($row = mysqli_fetch_assoc($sql)) {
|
|||||||
},
|
},
|
||||||
eventClick: function(editEvent) {
|
eventClick: function(editEvent) {
|
||||||
var eventId = editEvent.event.id;
|
var eventId = editEvent.event.id;
|
||||||
var $link = $('<a>', {
|
var link = document.createElement('a');
|
||||||
href: '#',
|
link.href = '#';
|
||||||
'class': 'ajax-modal',
|
link.className = 'ajax-modal';
|
||||||
'data-modal-url': 'modals/calendar/calendar_event_edit.php?<?= $client_url ?>&id=' + eventId
|
link.dataset.modalUrl = 'modals/calendar/calendar_event_edit.php?<?= $client_url ?>&id=' + eventId;
|
||||||
});
|
|
||||||
|
|
||||||
$('body').append($link); // Append to the body
|
document.body.appendChild(link); // Append to the body
|
||||||
$link.trigger('click'); // Trigger the modal
|
link.click(); // Trigger the modal
|
||||||
$link.remove(); // Cleanup
|
link.remove(); // Cleanup
|
||||||
},
|
},
|
||||||
dayMaxEvents: true, // allow "more" link when too many events
|
dayMaxEvents: true, // allow "more" link when too many events
|
||||||
views: {
|
views: {
|
||||||
|
|||||||
@@ -1228,16 +1228,24 @@ if (isset($_GET['contact_id'])) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
$('.authMethod').on('change', function() {
|
function syncAuthForm(select) {
|
||||||
var $form = $(this).closest('.authForm');
|
var form = select.closest('.authForm');
|
||||||
if ($(this).val() === 'local') {
|
if (!form) {
|
||||||
$form.find('.passwordGroup').show();
|
return;
|
||||||
} else {
|
|
||||||
$form.find('.passwordGroup').hide();
|
|
||||||
}
|
}
|
||||||
|
form.querySelectorAll('.passwordGroup').forEach(function (group) {
|
||||||
|
group.style.display = select.value === 'local' ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.authMethod').forEach(function (select) {
|
||||||
|
select.addEventListener('change', function () {
|
||||||
|
syncAuthForm(this);
|
||||||
|
});
|
||||||
|
syncAuthForm(select);
|
||||||
});
|
});
|
||||||
$('.authMethod').trigger('change');
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -747,78 +747,64 @@ require_once "../includes/footer.php";
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- JSON Autocomplete / type ahead -->
|
<!-- JSON Autocomplete / type ahead -->
|
||||||
<link rel="stylesheet" href="../libs/jquery-ui/jquery-ui.min.css">
|
|
||||||
<script src="../libs/jquery-ui/jquery-ui.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
$(function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
var availableProducts = <?= $json_products ?? '[]' ?>;
|
var availableProducts = <?= $json_products ?? '[]' ?>;
|
||||||
|
|
||||||
$("#name").autocomplete({
|
var nameInput = document.getElementById('name');
|
||||||
|
if (!nameInput) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
itflowAutocomplete(nameInput, {
|
||||||
minLength: 1,
|
minLength: 1,
|
||||||
delay: 0,
|
source: availableProducts,
|
||||||
source: function(request, response) {
|
match: function (item, term) {
|
||||||
var term = $.ui.autocomplete.escapeRegex(request.term.toLowerCase());
|
return String(item.label || '').toLowerCase().indexOf(term) !== -1
|
||||||
var matcher = new RegExp(term, "i");
|
|| String(item.product_name || '').toLowerCase().indexOf(term) !== -1
|
||||||
var matches = $.grep(availableProducts, function(item) {
|
|| String(item.product_code || '').toLowerCase().indexOf(term) !== -1;
|
||||||
return matcher.test(item.label || "") || matcher.test(item.product_name || "") || matcher.test(item.product_code || "");
|
|
||||||
});
|
|
||||||
response(matches);
|
|
||||||
},
|
},
|
||||||
select: function (event, ui) {
|
render: function (item) {
|
||||||
$("#name").val(ui.item.product_name);
|
var esc = itflowEscapeHtml;
|
||||||
$("#desc").val(ui.item.description);
|
var typeText = item.type ? item.type.charAt(0).toUpperCase() + item.type.slice(1).toLowerCase() : "";
|
||||||
$("#qty").val(1);
|
var showStock = (typeText.toLowerCase() !== "service");
|
||||||
$("#price").val(ui.item.price);
|
var taxText = (item.tax_percent != null) ? (parseFloat(item.tax_percent) + "%") : "No tax";
|
||||||
$("#tax").val(ui.item.tax).trigger('change');
|
var priceText = (item.price != null && item.price !== "") ? String(item.price) : "";
|
||||||
$("#product_id").val(ui.item.prod_id);
|
var stockText = (item.available_stock ?? 0);
|
||||||
return false;
|
|
||||||
|
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
|
// Typing over the name by hand breaks the link to the product
|
||||||
$("#name").on("input", function() {
|
nameInput.addEventListener('input', function () {
|
||||||
$("#product_id").val(0);
|
document.getElementById('product_id').value = 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Product names and descriptions are user supplied - escape before
|
|
||||||
// building markup, the default renderer uses .text() for this reason
|
|
||||||
function esc(value) {
|
|
||||||
return $("<div>").text(value == null ? "" : value).html();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep it simple: default jQuery UI look, just richer content
|
|
||||||
$("#name").autocomplete("instance")._renderItem = function(ul, item) {
|
|
||||||
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);
|
|
||||||
|
|
||||||
var infoLeft =
|
|
||||||
"<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>";
|
|
||||||
|
|
||||||
// Use the jQuery UI wrapper so default hover/focus styles apply
|
|
||||||
return $("<li>")
|
|
||||||
.append($("<div class='ui-menu-item-wrapper'>").append(infoLeft))
|
|
||||||
.appendTo(ul);
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -835,7 +821,7 @@ new Sortable(document.querySelector('table#items tbody'), {
|
|||||||
order: index
|
order: index
|
||||||
}));
|
}));
|
||||||
|
|
||||||
$.post('ajax.php', {
|
itflowPostForm('ajax.php', {
|
||||||
update_invoice_items_order: true,
|
update_invoice_items_order: true,
|
||||||
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
|
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
|
||||||
invoice_id: <?= $invoice_id ?>,
|
invoice_id: <?= $invoice_id ?>,
|
||||||
|
|||||||
@@ -1,38 +1,3 @@
|
|||||||
/**
|
|
||||||
* $.post replacement. jQuery serialised nested arrays/objects into PHP-style
|
|
||||||
* bracket params (positions[0][status_id]=...), which is what ajax.php parses,
|
|
||||||
* so that encoding is reproduced here rather than sending JSON.
|
|
||||||
*/
|
|
||||||
function itflowPostForm(url, data) {
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
(function add(prefix, value) {
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
value.forEach(function (v, i) {
|
|
||||||
add(prefix + '[' + i + ']', v);
|
|
||||||
});
|
|
||||||
} else if (value !== null && typeof value === 'object') {
|
|
||||||
Object.keys(value).forEach(function (k) {
|
|
||||||
add(prefix ? prefix + '[' + k + ']' : k, value[k]);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
params.append(prefix, value === true ? 'true' : String(value));
|
|
||||||
}
|
|
||||||
})('', data);
|
|
||||||
|
|
||||||
return fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
||||||
credentials: 'same-origin',
|
|
||||||
body: params.toString()
|
|
||||||
}).then(function (res) {
|
|
||||||
if (!res.ok) {
|
|
||||||
throw new Error('HTTP ' + res.status);
|
|
||||||
}
|
|
||||||
return res.text();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// -------------------------------
|
// -------------------------------
|
||||||
// Drag: Kanban Columns (Statuses)
|
// Drag: Kanban Columns (Statuses)
|
||||||
|
|||||||
@@ -460,18 +460,24 @@ ob_start();
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- JSON Autocomplete / type ahead -->
|
<!-- JSON Autocomplete / type ahead -->
|
||||||
<link rel="stylesheet" href="/libs/jquery-ui/jquery-ui.min.css">
|
|
||||||
<script src="/libs/jquery-ui/jquery-ui.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
$(function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
||||||
var operatingSystems = <?= $json_os ?>;
|
var operatingSystems = <?= $json_os ?>;
|
||||||
$("#os").autocomplete({
|
|
||||||
source: operatingSystems, // Should be an array of objects with 'label' and 'value'
|
var osInput = document.getElementById('os');
|
||||||
select: function(event, ui) {
|
if (!osInput) {
|
||||||
$("#os").val(ui.item.label); // Set the input field value to the selected label
|
return;
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
itflowAutocomplete(osInput, {
|
||||||
|
minLength: 1,
|
||||||
|
source: operatingSystems,
|
||||||
|
onSelect: function (item) {
|
||||||
|
osInput.value = item.label;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -296,17 +296,23 @@ function generatePassword() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
$('.authMethod').on('change', function() {
|
function syncAuthForm(select) {
|
||||||
var $form = $(this).closest('.authForm');
|
var form = select.closest('.authForm');
|
||||||
if ($(this).val() === 'local') {
|
if (!form) {
|
||||||
$form.find('.passwordGroup').show();
|
return;
|
||||||
} else {
|
|
||||||
$form.find('.passwordGroup').hide();
|
|
||||||
}
|
}
|
||||||
});
|
form.querySelectorAll('.passwordGroup').forEach(function (group) {
|
||||||
$('.authMethod').trigger('change');
|
group.style.display = select.value === 'local' ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.authMethod').forEach(function (select) {
|
||||||
|
select.addEventListener('change', function () {
|
||||||
|
syncAuthForm(this);
|
||||||
|
});
|
||||||
|
syncAuthForm(select);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -336,17 +336,23 @@ function generatePassword() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
$('.authMethod').on('change', function() {
|
function syncAuthForm(select) {
|
||||||
var $form = $(this).closest('.authForm');
|
var form = select.closest('.authForm');
|
||||||
if ($(this).val() === 'local') {
|
if (!form) {
|
||||||
$form.find('.passwordGroup').show();
|
return;
|
||||||
} else {
|
|
||||||
$form.find('.passwordGroup').hide();
|
|
||||||
}
|
}
|
||||||
});
|
form.querySelectorAll('.passwordGroup').forEach(function (group) {
|
||||||
$('.authMethod').trigger('change');
|
group.style.display = select.value === 'local' ? '' : 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.authMethod').forEach(function (select) {
|
||||||
|
select.addEventListener('change', function () {
|
||||||
|
syncAuthForm(this);
|
||||||
|
});
|
||||||
|
syncAuthForm(select);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -248,8 +248,6 @@ ob_start();
|
|||||||
|
|
||||||
|
|
||||||
<!-- Recurring Ticket Client/Contact JS -->
|
<!-- Recurring Ticket Client/Contact JS -->
|
||||||
<link rel="stylesheet" href="/libs/jquery-ui/jquery-ui.min.css">
|
|
||||||
<script src="/libs/jquery-ui/jquery-ui.min.js"></script>
|
|
||||||
<script src="/agent/js/tickets_add_modal.js"></script>
|
<script src="/agent/js/tickets_add_modal.js"></script>
|
||||||
|
|
||||||
<script src="/agent/js/ticket_tasks_modal.js"></script>
|
<script src="/agent/js/ticket_tasks_modal.js"></script>
|
||||||
|
|||||||
@@ -285,8 +285,6 @@ ob_start();
|
|||||||
|
|
||||||
|
|
||||||
<!-- Ticket Client/Contact JS -->
|
<!-- Ticket Client/Contact JS -->
|
||||||
<link rel="stylesheet" href="/libs/jquery-ui/jquery-ui.min.css">
|
|
||||||
<script src="/libs/jquery-ui/jquery-ui.min.js"></script>
|
|
||||||
<script src="/agent/js/tickets_add_modal.js"></script>
|
<script src="/agent/js/tickets_add_modal.js"></script>
|
||||||
|
|
||||||
<script src="/agent/js/ticket_tasks_modal.js"></script>
|
<script src="/agent/js/ticket_tasks_modal.js"></script>
|
||||||
|
|||||||
@@ -19,18 +19,30 @@ ob_start();
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(function() {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
$.ajax({
|
var target = document.getElementById('summaryContent');
|
||||||
url: 'ajax.php?ai_ticket_summary',
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch('ajax.php?ai_ticket_summary', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: { ticket_id: <?= $ticket_id ?> },
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
success: function(response) {
|
credentials: 'same-origin',
|
||||||
$('#summaryContent').html(response);
|
body: new URLSearchParams({ ticket_id: '<?= $ticket_id ?>' }).toString()
|
||||||
},
|
})
|
||||||
error: function() {
|
.then(function (res) {
|
||||||
$('#summaryContent').html('Error generating summary.');
|
if (!res.ok) {
|
||||||
}
|
throw new Error('HTTP ' + res.status);
|
||||||
});
|
}
|
||||||
|
return res.text();
|
||||||
|
})
|
||||||
|
.then(function (html) {
|
||||||
|
target.innerHTML = html;
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
target.textContent = 'Error generating summary.';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -81,57 +81,93 @@ ob_start();
|
|||||||
|
|
||||||
<!-- JS to make the correct boxes appear depending on if internal/client approval) -->
|
<!-- JS to make the correct boxes appear depending on if internal/client approval) -->
|
||||||
<script>
|
<script>
|
||||||
$('#approval_scope').on('change', function() {
|
(function () {
|
||||||
const scope = $(this).val();
|
var scopeSelect = document.getElementById('approval_scope');
|
||||||
const typeSelect = $('#approval_type');
|
var typeSelect = document.getElementById('approval_type');
|
||||||
const wrapper = $('#approval_type_wrapper');
|
var typeWrapper = document.getElementById('approval_type_wrapper');
|
||||||
|
var userWrapper = document.getElementById('specific_user_wrapper');
|
||||||
|
var userSelect = document.getElementById('specific_user_select');
|
||||||
|
|
||||||
typeSelect.empty();
|
if (!scopeSelect || !typeSelect) {
|
||||||
$('#specific_user_wrapper').addClass('d-none');
|
|
||||||
|
|
||||||
if (!scope) {
|
|
||||||
wrapper.addClass('d-none');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapper.removeClass('d-none');
|
function setOptions(select, pairs) {
|
||||||
|
select.innerHTML = '';
|
||||||
if (scope === 'internal') {
|
pairs.forEach(function (pair) {
|
||||||
typeSelect.append('<option value="">Select...</option>');
|
// new Option() assigns text, so nothing here is parsed as markup
|
||||||
typeSelect.append('<option value="any">Any internal reviewer</option>');
|
select.appendChild(new Option(pair[1], pair[0]));
|
||||||
typeSelect.append('<option value="specific">Specific agent</option>');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scope === 'client') {
|
|
||||||
typeSelect.append('<option value="">Select...</option>');
|
|
||||||
typeSelect.append('<option value="any">Ticket contact</option>');
|
|
||||||
typeSelect.append('<option value="technical">Technical contacts</option>');
|
|
||||||
typeSelect.append('<option value="billing">Billing contacts</option>');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Specific user (internal only for now)
|
|
||||||
$('#approval_type').on('change', function() {
|
|
||||||
const type = $(this).val();
|
|
||||||
const scope = $('#approval_scope').val();
|
|
||||||
const userSelect = $('#specific_user_select');
|
|
||||||
|
|
||||||
if (type !== 'specific' || scope !== 'internal') {
|
|
||||||
$('#specific_user_wrapper').addClass('d-none');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#specific_user_wrapper').removeClass('d-none');
|
|
||||||
userSelect.empty().append('<option value="">Loading...</option>');
|
|
||||||
|
|
||||||
$.getJSON('ajax.php?get_internal_users=true', function(data) {
|
|
||||||
userSelect.empty().append('<option value="">Select user...</option>');
|
|
||||||
data.users.forEach(function(u) {
|
|
||||||
userSelect.append(`<option value="${u.user_id}">${u.user_name}</option>`);
|
|
||||||
});
|
});
|
||||||
});
|
// the selects are Tom Select enhanced, so it has to re-read them
|
||||||
});
|
refreshTomSelect(select);
|
||||||
|
}
|
||||||
|
|
||||||
|
scopeSelect.addEventListener('change', function () {
|
||||||
|
var scope = this.value;
|
||||||
|
|
||||||
|
setOptions(typeSelect, []);
|
||||||
|
userWrapper.classList.add('d-none');
|
||||||
|
|
||||||
|
if (!scope) {
|
||||||
|
typeWrapper.classList.add('d-none');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
typeWrapper.classList.remove('d-none');
|
||||||
|
|
||||||
|
if (scope === 'internal') {
|
||||||
|
setOptions(typeSelect, [
|
||||||
|
['', 'Select...'],
|
||||||
|
['any', 'Any internal reviewer'],
|
||||||
|
['specific', 'Specific agent']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scope === 'client') {
|
||||||
|
setOptions(typeSelect, [
|
||||||
|
['', 'Select...'],
|
||||||
|
['any', 'Ticket contact'],
|
||||||
|
['technical', 'Technical contacts'],
|
||||||
|
['billing', 'Billing contacts']
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Specific user (internal only for now)
|
||||||
|
typeSelect.addEventListener('change', function () {
|
||||||
|
var type = this.value;
|
||||||
|
var scope = scopeSelect.value;
|
||||||
|
|
||||||
|
if (type !== 'specific' || scope !== 'internal') {
|
||||||
|
userWrapper.classList.add('d-none');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
userWrapper.classList.remove('d-none');
|
||||||
|
setOptions(userSelect, [['', 'Loading...']]);
|
||||||
|
|
||||||
|
fetch('ajax.php?get_internal_users=true', {
|
||||||
|
headers: { 'Accept': 'application/json' },
|
||||||
|
credentials: 'same-origin'
|
||||||
|
})
|
||||||
|
.then(function (res) {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('HTTP ' + res.status);
|
||||||
|
}
|
||||||
|
return res.json();
|
||||||
|
})
|
||||||
|
.then(function (data) {
|
||||||
|
var pairs = [['', 'Select user...']];
|
||||||
|
data.users.forEach(function (u) {
|
||||||
|
pairs.push([u.user_id, u.user_name]);
|
||||||
|
});
|
||||||
|
setOptions(userSelect, pairs);
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
setOptions(userSelect, [['', 'Failed to load users']]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
@@ -588,24 +588,66 @@ require_once "../includes/footer.php";
|
|||||||
|
|
||||||
<!-- JSON Autocomplete / type ahead -->
|
<!-- JSON Autocomplete / type ahead -->
|
||||||
<!-- //TODO: Move to js/ -->
|
<!-- //TODO: Move to js/ -->
|
||||||
<link rel="stylesheet" href="../libs/jquery-ui/jquery-ui.min.css">
|
|
||||||
<script src="../libs/jquery-ui/jquery-ui.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
$(function() {
|
|
||||||
var availableProducts = <?= $json_products ?? '[]' ?>;
|
|
||||||
|
|
||||||
$("#name").autocomplete({
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
source: availableProducts,
|
|
||||||
select: function(event, ui) {
|
var availableProducts = <?= $json_products ?? '[]' ?>;
|
||||||
$("#name").val(ui.item.label); // Product name field - this seemingly has to referenced as label
|
|
||||||
$("#desc").val(ui.item.description); // Product description field
|
var nameInput = document.getElementById('name');
|
||||||
$("#qty").val(1); // Product quantity field automatically make it a 1
|
if (!nameInput) {
|
||||||
$("#price").val(ui.item.price); // Product price field
|
return;
|
||||||
setTomSelectValue(document.getElementById("tax"), ui.item.tax); // Tax field - setValue repaints the Tom Select widget
|
}
|
||||||
return false;
|
|
||||||
}
|
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>
|
||||||
@@ -620,7 +662,7 @@ new Sortable(document.querySelector('table#items tbody'), {
|
|||||||
order: index
|
order: index
|
||||||
}));
|
}));
|
||||||
|
|
||||||
$.post('ajax.php', {
|
itflowPostForm('ajax.php', {
|
||||||
update_quote_items_order: true,
|
update_quote_items_order: true,
|
||||||
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
|
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
|
||||||
quote_id: <?= $quote_id ?>,
|
quote_id: <?= $quote_id ?>,
|
||||||
|
|||||||
@@ -495,24 +495,66 @@ require_once "../includes/footer.php";
|
|||||||
?>
|
?>
|
||||||
|
|
||||||
<!-- JSON Autocomplete / type ahead -->
|
<!-- JSON Autocomplete / type ahead -->
|
||||||
<link rel="stylesheet" href="../libs/jquery-ui/jquery-ui.min.css">
|
|
||||||
<script src="../libs/jquery-ui/jquery-ui.min.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
$(function() {
|
|
||||||
var availableProducts = <?= $json_products ?? '[]' ?>;
|
|
||||||
|
|
||||||
$("#name").autocomplete({
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
source: availableProducts,
|
|
||||||
select: function (event, ui) {
|
var availableProducts = <?= $json_products ?? '[]' ?>;
|
||||||
$("#name").val(ui.item.label); // Product name field - this seemingly has to referenced as label
|
|
||||||
$("#desc").val(ui.item.description); // Product description field
|
var nameInput = document.getElementById('name');
|
||||||
$("#qty").val(1); // Product quantity field automatically make it a 1
|
if (!nameInput) {
|
||||||
$("#price").val(ui.item.price); // Product price field
|
return;
|
||||||
setTomSelectValue(document.getElementById("tax"), ui.item.tax); // Tax field - setValue repaints the Tom Select widget
|
}
|
||||||
return false;
|
|
||||||
}
|
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>
|
||||||
@@ -527,7 +569,7 @@ new Sortable(document.querySelector('table#items tbody'), {
|
|||||||
order: index
|
order: index
|
||||||
}));
|
}));
|
||||||
|
|
||||||
$.post('ajax.php', {
|
itflowPostForm('ajax.php', {
|
||||||
update_recurring_invoice_items_order: true,
|
update_recurring_invoice_items_order: true,
|
||||||
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
|
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
|
||||||
recurring_invoice_id: <?= $recurring_invoice_id ?>,
|
recurring_invoice_id: <?= $recurring_invoice_id ?>,
|
||||||
|
|||||||
@@ -1414,7 +1414,7 @@ require_once "../includes/footer.php";
|
|||||||
order: index
|
order: index
|
||||||
}));
|
}));
|
||||||
|
|
||||||
$.post('ajax.php', {
|
itflowPostForm('ajax.php', {
|
||||||
update_ticket_tasks_order: true,
|
update_ticket_tasks_order: true,
|
||||||
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
|
csrf_token: '<?= $_SESSION['csrf_token'] ?>',
|
||||||
ticket_id: <?= $ticket_id ?>,
|
ticket_id: <?= $ticket_id ?>,
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ $data = "otpauth://totp/ITFlow:$session_email?secret=$token";
|
|||||||
<link rel="stylesheet" href="../../libs/adminlte/css/adminlte.min.css">
|
<link rel="stylesheet" href="../../libs/adminlte/css/adminlte.min.css">
|
||||||
|
|
||||||
<!-- jQuery -->
|
<!-- jQuery -->
|
||||||
<script src="../../libs/jquery/jquery.min.js"></script>
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body class="hold-transition login-page">
|
<body class="hold-transition login-page">
|
||||||
@@ -103,10 +102,20 @@ $data = "otpauth://totp/ITFlow:$session_email?secret=$token";
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
// Slide alert up after 4 secs
|
// Fade the alert out after 5s, then collapse it
|
||||||
$("#alert").fadeTo(5000, 500).slideUp(500, function(){
|
(function () {
|
||||||
$("#alert").slideUp(500);
|
const alertEl = document.getElementById('alert');
|
||||||
});
|
if (!alertEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(function () {
|
||||||
|
alertEl.style.transition = 'opacity .5s linear';
|
||||||
|
alertEl.style.opacity = '0';
|
||||||
|
setTimeout(function () {
|
||||||
|
alertEl.style.display = 'none';
|
||||||
|
}, 500);
|
||||||
|
}, 5000);
|
||||||
|
})();
|
||||||
|
|
||||||
// ClipboardJS
|
// ClipboardJS
|
||||||
|
|
||||||
@@ -114,7 +123,7 @@ $data = "otpauth://totp/ITFlow:$session_email?secret=$token";
|
|||||||
// on screen. This page is standalone and does not load js/app.js, so it
|
// on screen. This page is standalone and does not load js/app.js, so it
|
||||||
// carries its own copy of the helper.
|
// carries its own copy of the helper.
|
||||||
function flashTooltip(button, message) {
|
function flashTooltip(button, message) {
|
||||||
const el = button instanceof Element ? button : $(button)[0];
|
const el = button instanceof Element ? button : document.querySelector(button);
|
||||||
if (!el) {
|
if (!el) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -144,10 +153,8 @@ $data = "otpauth://totp/ITFlow:$session_email?secret=$token";
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Enable Popovers
|
// Enable Popovers
|
||||||
$(function () {
|
document.querySelectorAll('[data-bs-toggle="popover"]').forEach(function (el) {
|
||||||
document.querySelectorAll('[data-bs-toggle="popover"]').forEach(function (el) {
|
bootstrap.Popover.getOrCreateInstance(el);
|
||||||
bootstrap.Popover.getOrCreateInstance(el);
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -24,7 +24,6 @@
|
|||||||
<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/inc_confirm_modal.php'; ?>
|
<?php require_once $_SERVER['DOCUMENT_ROOT'] . '/includes/inc_confirm_modal.php'; ?>
|
||||||
|
|
||||||
<!-- jQuery -->
|
<!-- jQuery -->
|
||||||
<script src="/libs/jquery/jquery.min.js"></script>
|
|
||||||
|
|
||||||
<!-- Bootstrap 4 -->
|
<!-- Bootstrap 4 -->
|
||||||
<script src="/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
<script src="/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|||||||
@@ -276,7 +276,6 @@ if ($_SERVER['REQUEST_METHOD'] == "POST") {
|
|||||||
<!-- /.login-box -->
|
<!-- /.login-box -->
|
||||||
|
|
||||||
<!-- jQuery -->
|
<!-- jQuery -->
|
||||||
<script src="../libs/jquery/jquery.min.js"></script>
|
|
||||||
|
|
||||||
<!-- Bootstrap 4 -->
|
<!-- Bootstrap 4 -->
|
||||||
<script src="../libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
<script src="../libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ if (isset($_GET['invoice_id'], $_GET['url_key']) && !isset($_GET['payment_intent
|
|||||||
|
|
||||||
<!-- Stripe & jQuery -->
|
<!-- Stripe & jQuery -->
|
||||||
<script src="https://js.stripe.com/v3/"></script>
|
<script src="https://js.stripe.com/v3/"></script>
|
||||||
<script src="../libs/jquery/jquery.min.js"></script>
|
|
||||||
|
|
||||||
<div class="row pt-5">
|
<div class="row pt-5">
|
||||||
<div class="col-sm">
|
<div class="col-sm">
|
||||||
|
|||||||
@@ -26,7 +26,6 @@
|
|||||||
<link rel="stylesheet" href="/libs/tom-select/css/tom-select.bootstrap5.min.css">
|
<link rel="stylesheet" href="/libs/tom-select/css/tom-select.bootstrap5.min.css">
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="/libs/jquery/jquery.min.js"></script>
|
|
||||||
|
|
||||||
</head>
|
</head>
|
||||||
<body class="layout-fixed theme-<?= escapeHtml($config_theme) ?>">
|
<body class="layout-fixed theme-<?= escapeHtml($config_theme) ?>">
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ if (basename(dirname($_SERVER['REQUEST_URI'])) === 'guest') { ?>
|
|||||||
|
|
||||||
<!-- AdminLTE App -->
|
<!-- AdminLTE App -->
|
||||||
<script src="/libs/adminlte/js/adminlte.min.js"></script>
|
<script src="/libs/adminlte/js/adminlte.min.js"></script>
|
||||||
|
<script src="/js/autocomplete.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
<script src="/js/ajax_modal.js"></script>
|
<script src="/js/ajax_modal.js"></script>
|
||||||
<script src="/js/confirm_modal.js"></script>
|
<script src="/js/confirm_modal.js"></script>
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ header("X-Frame-Options: DENY");
|
|||||||
<link rel="stylesheet" href="/css/itflow_custom.css">
|
<link rel="stylesheet" href="/css/itflow_custom.css">
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="/libs/jquery/jquery.min.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body class="layout-fixed sidebar-expand-lg app-loaded theme-<?= escapeHtml($config_theme) ?>">
|
<body class="layout-fixed sidebar-expand-lg app-loaded theme-<?= escapeHtml($config_theme) ?>">
|
||||||
<div class="app-wrapper text-sm">
|
<div class="app-wrapper text-sm">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<script src="/js/autocomplete.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
|
|||||||
120
js/app.js
120
js/app.js
@@ -1,13 +1,92 @@
|
|||||||
$(document).ready(function() {
|
/**
|
||||||
|
* Delegated listener that binds exactly once.
|
||||||
|
*
|
||||||
|
* modal_footer.php re-loads this file every time an ajax modal opens, which is
|
||||||
|
* why the jQuery originals used a namespaced .off().on() - without it the
|
||||||
|
* handlers stacked up and fired once per modal ever opened. The named flag on
|
||||||
|
* window is the vanilla equivalent. `this` is the matched element, matching
|
||||||
|
* jQuery's delegation contract so the handler bodies are unchanged.
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* $.post replacement. jQuery serialised nested arrays/objects into PHP-style
|
||||||
|
* bracket params (positions[0][status_id]=...), which is what ajax.php parses,
|
||||||
|
* so that encoding is reproduced here rather than sending JSON.
|
||||||
|
*/
|
||||||
|
function itflowPostForm(url, data) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
(function add(prefix, value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
value.forEach(function (v, i) {
|
||||||
|
add(prefix + '[' + i + ']', v);
|
||||||
|
});
|
||||||
|
} else if (value !== null && typeof value === 'object') {
|
||||||
|
Object.keys(value).forEach(function (k) {
|
||||||
|
add(prefix ? prefix + '[' + k + ']' : k, value[k]);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
params.append(prefix, value === true ? 'true' : String(value));
|
||||||
|
}
|
||||||
|
})('', data);
|
||||||
|
|
||||||
|
return fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
credentials: 'same-origin',
|
||||||
|
body: params.toString()
|
||||||
|
}).then(function (res) {
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error('HTTP ' + res.status);
|
||||||
|
}
|
||||||
|
return res.text();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function itflowBindOnce(name, type, selector, handler) {
|
||||||
|
window.itflowBound = window.itflowBound || {};
|
||||||
|
if (window.itflowBound[name]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
window.itflowBound[name] = true;
|
||||||
|
document.addEventListener(type, function (e) {
|
||||||
|
const match = e.target.closest(selector);
|
||||||
|
if (match) {
|
||||||
|
handler.call(match, e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function itflowInit() {
|
||||||
// Prevents resubmit on forms
|
// Prevents resubmit on forms
|
||||||
if (window.history.replaceState) {
|
if (window.history.replaceState) {
|
||||||
window.history.replaceState(null, null, window.location.href);
|
window.history.replaceState(null, null, window.location.href);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Slide alert up after 4 secs
|
// Fade the legacy #alert box out after 5s, then collapse it
|
||||||
$("#alert").fadeTo(5000, 500).slideUp(500, function() {
|
(function () {
|
||||||
$("#alert").slideUp(500);
|
const alertEl = document.getElementById('alert');
|
||||||
});
|
if (!alertEl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(function () {
|
||||||
|
alertEl.style.transition = 'opacity .5s linear';
|
||||||
|
alertEl.style.opacity = '0';
|
||||||
|
setTimeout(function () {
|
||||||
|
alertEl.style.overflow = 'hidden';
|
||||||
|
alertEl.style.transition = 'height .5s ease, margin .5s ease, padding .5s ease';
|
||||||
|
alertEl.style.height = alertEl.offsetHeight + 'px';
|
||||||
|
void alertEl.offsetHeight;
|
||||||
|
alertEl.style.height = '0px';
|
||||||
|
alertEl.style.marginTop = '0';
|
||||||
|
alertEl.style.marginBottom = '0';
|
||||||
|
alertEl.style.paddingTop = '0';
|
||||||
|
alertEl.style.paddingBottom = '0';
|
||||||
|
setTimeout(function () {
|
||||||
|
alertEl.style.display = 'none';
|
||||||
|
}, 500);
|
||||||
|
}, 500);
|
||||||
|
}, 5000);
|
||||||
|
})();
|
||||||
|
|
||||||
// Initialize Tom Select (replaces Select2). Every instance is reachable
|
// Initialize Tom Select (replaces Select2). Every instance is reachable
|
||||||
// afterwards as element.tomselect, which is how the helpers below reach it.
|
// afterwards as element.tomselect, which is how the helpers below reach it.
|
||||||
@@ -446,8 +525,11 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ClipboardJS fix for Bootstrap modals
|
// Bootstrap 4 needed _enforceFocus patched out so ClipboardJS could reach
|
||||||
$.fn.modal.Constructor.prototype._enforceFocus = function() {};
|
// its textarea inside a modal. Bootstrap 5 registers no jQuery plugin, so
|
||||||
|
// the old $.fn.modal line threw and killed everything below it. If copying
|
||||||
|
// from inside a modal ever misbehaves, ClipboardJS's `container` option is
|
||||||
|
// the lever, not a Bootstrap patch.
|
||||||
|
|
||||||
// Clipboard
|
// Clipboard
|
||||||
var clipboard = new ClipboardJS('.clipboardjs');
|
var clipboard = new ClipboardJS('.clipboardjs');
|
||||||
@@ -461,15 +543,21 @@ $(document).ready(function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Enable Popovers
|
// Enable Popovers
|
||||||
$(function() {
|
document.querySelectorAll('[data-bs-toggle="popover"]').forEach(function (el) {
|
||||||
document.querySelectorAll('[data-bs-toggle="popover"]').forEach(function (el) {
|
bootstrap.Popover.getOrCreateInstance(el);
|
||||||
bootstrap.Popover.getOrCreateInstance(el);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Data Tables
|
// Data Tables
|
||||||
new DataTable('.dataTables');
|
new DataTable('.dataTables');
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// modal_footer.php re-loads this file on every ajax modal open, so run now if
|
||||||
|
// the document is already parsed, otherwise wait for it.
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', itflowInit);
|
||||||
|
} else {
|
||||||
|
itflowInit();
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Calendar event modals - the All day switch shows or hides the time row.
|
* Calendar event modals - the All day switch shows or hides the time row.
|
||||||
@@ -487,7 +575,7 @@ $(document).ready(function() {
|
|||||||
* namespaced .off() keeps this to a single handler - modal_footer.php re-loads this
|
* namespaced .off() keeps this to a single handler - modal_footer.php re-loads this
|
||||||
* file on every ajax modal open.
|
* file on every ajax modal open.
|
||||||
*/
|
*/
|
||||||
$(document).off('change.itflowAllDay').on('change.itflowAllDay', '.event-all-day-toggle', function () {
|
itflowBindOnce('itflowAllDay', 'change', '.event-all-day-toggle', function () {
|
||||||
|
|
||||||
const allDay = this.checked;
|
const allDay = this.checked;
|
||||||
const timeFields = document.getElementById(this.id.replace(/_all_day$/, '_time_fields'));
|
const timeFields = document.getElementById(this.id.replace(/_all_day$/, '_time_fields'));
|
||||||
@@ -511,7 +599,7 @@ $(document).off('change.itflowAllDay').on('change.itflowAllDay', '.event-all-day
|
|||||||
* Keep the end date at or after the start date, without shortening a longer span
|
* Keep the end date at or after the start date, without shortening a longer span
|
||||||
* the user has already chosen.
|
* the user has already chosen.
|
||||||
*/
|
*/
|
||||||
$(document).off('change.itflowEventDate').on('change.itflowEventDate', '.event-start-date', function () {
|
itflowBindOnce('itflowEventDate', 'change', '.event-start-date', function () {
|
||||||
|
|
||||||
const endField = document.getElementById(this.id.replace(/_start_date$/, '_end_date'));
|
const endField = document.getElementById(this.id.replace(/_start_date$/, '_end_date'));
|
||||||
|
|
||||||
@@ -529,7 +617,7 @@ $(document).off('change.itflowEventDate').on('change.itflowEventDate', '.event-s
|
|||||||
* A start late in the evening rolls the end onto the following day rather than
|
* A start late in the evening rolls the end onto the following day rather than
|
||||||
* wrapping round to an end that precedes the start.
|
* wrapping round to an end that precedes the start.
|
||||||
*/
|
*/
|
||||||
$(document).off('change.itflowEventTime').on('change.itflowEventTime', '.event-start-time', function () {
|
itflowBindOnce('itflowEventTime', 'change', '.event-start-time', function () {
|
||||||
|
|
||||||
const prefix = this.id.replace(/_start_time$/, '');
|
const prefix = this.id.replace(/_start_time$/, '');
|
||||||
const endField = document.getElementById(prefix + '_end_time');
|
const endField = document.getElementById(prefix + '_end_time');
|
||||||
@@ -575,7 +663,7 @@ $(document).off('change.itflowEventTime').on('change.itflowEventTime', '.event-s
|
|||||||
* until the next page load.
|
* until the next page load.
|
||||||
*/
|
*/
|
||||||
function flashTooltip(button, message) {
|
function flashTooltip(button, message) {
|
||||||
const el = button instanceof Element ? button : $(button)[0];
|
const el = button instanceof Element ? button : document.querySelector(button);
|
||||||
if (!el) {
|
if (!el) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
204
js/autocomplete.js
Normal file
204
js/autocomplete.js
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
/**
|
||||||
|
* Minimal autocomplete, replacing jQuery UI's.
|
||||||
|
*
|
||||||
|
* itflowAutocomplete(input, {
|
||||||
|
* source: array of items, or fn(term) -> array
|
||||||
|
* minLength: default 1
|
||||||
|
* maxItems: cap on rendered results, default 50
|
||||||
|
* match: fn(item, term) -> bool (default: substring over item.label)
|
||||||
|
* render: fn(item) -> HTML string (default: escaped item.label)
|
||||||
|
* onSelect: fn(item)
|
||||||
|
* })
|
||||||
|
*
|
||||||
|
* Keyboard: up/down to move, Enter to pick, Escape to dismiss.
|
||||||
|
* The menu is capped to the space actually available in the viewport and
|
||||||
|
* scrolls internally, and flips above the input when there is more room there.
|
||||||
|
*/
|
||||||
|
function itflowEscapeHtml(value) {
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.textContent = value == null ? '' : String(value);
|
||||||
|
return div.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function itflowAutocomplete(input, options) {
|
||||||
|
if (!input || input.itflowAutocomplete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var opts = options || {};
|
||||||
|
var minLength = opts.minLength == null ? 1 : opts.minLength;
|
||||||
|
var maxItems = opts.maxItems == null ? 50 : opts.maxItems;
|
||||||
|
var GAP = 4; // breathing room against the input
|
||||||
|
var MARGIN = 8; // never touch the viewport edge
|
||||||
|
var MIN_HEIGHT = 120;
|
||||||
|
var items = [];
|
||||||
|
var active = -1;
|
||||||
|
|
||||||
|
var menu = document.createElement('div');
|
||||||
|
menu.className = 'itflow-ac-menu dropdown-menu p-0';
|
||||||
|
menu.setAttribute('role', 'listbox');
|
||||||
|
document.body.appendChild(menu);
|
||||||
|
|
||||||
|
function defaultMatch(item, term) {
|
||||||
|
return String(item.label || '').toLowerCase().indexOf(term) !== -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function close() {
|
||||||
|
menu.classList.remove('show');
|
||||||
|
menu.innerHTML = '';
|
||||||
|
active = -1;
|
||||||
|
input.setAttribute('aria-expanded', 'false');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Size and place the menu against whatever room the viewport actually has.
|
||||||
|
* Without this a long product list runs off the bottom of the page.
|
||||||
|
*/
|
||||||
|
function position() {
|
||||||
|
var r = input.getBoundingClientRect();
|
||||||
|
var below = window.innerHeight - r.bottom - GAP - MARGIN;
|
||||||
|
var above = r.top - GAP - MARGIN;
|
||||||
|
var flip = below < MIN_HEIGHT && above > below;
|
||||||
|
var room = Math.max(flip ? above : below, MIN_HEIGHT);
|
||||||
|
|
||||||
|
menu.style.position = 'fixed';
|
||||||
|
menu.style.left = r.left + 'px';
|
||||||
|
menu.style.minWidth = r.width + 'px';
|
||||||
|
menu.style.maxWidth = Math.max(r.width, Math.min(520, window.innerWidth - (MARGIN * 2))) + 'px';
|
||||||
|
menu.style.maxHeight = room + 'px';
|
||||||
|
menu.style.overflowY = 'auto';
|
||||||
|
menu.style.overflowX = 'hidden';
|
||||||
|
menu.style.zIndex = '2000';
|
||||||
|
|
||||||
|
if (flip) {
|
||||||
|
menu.style.top = '';
|
||||||
|
menu.style.bottom = (window.innerHeight - r.top + GAP) + 'px';
|
||||||
|
} else {
|
||||||
|
menu.style.bottom = '';
|
||||||
|
menu.style.top = (r.bottom + GAP) + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep it on screen horizontally too
|
||||||
|
var width = menu.offsetWidth || r.width;
|
||||||
|
if (r.left + width > window.innerWidth - MARGIN) {
|
||||||
|
menu.style.left = Math.max(MARGIN, window.innerWidth - width - MARGIN) + 'px';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function highlight(next) {
|
||||||
|
var nodes = menu.querySelectorAll('.itflow-ac-item');
|
||||||
|
if (!nodes.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (active >= 0 && nodes[active]) {
|
||||||
|
nodes[active].classList.remove('active');
|
||||||
|
}
|
||||||
|
active = next < 0 ? nodes.length - 1 : (next >= nodes.length ? 0 : next);
|
||||||
|
nodes[active].classList.add('active');
|
||||||
|
nodes[active].scrollIntoView({ block: 'nearest' });
|
||||||
|
}
|
||||||
|
|
||||||
|
function choose(index) {
|
||||||
|
var item = items[index];
|
||||||
|
if (!item) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
if (typeof opts.onSelect === 'function') {
|
||||||
|
opts.onSelect(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(term) {
|
||||||
|
var source = typeof opts.source === 'function' ? opts.source(term) : (opts.source || []);
|
||||||
|
var matcher = typeof opts.match === 'function' ? opts.match : defaultMatch;
|
||||||
|
var all = source.filter(function (item) {
|
||||||
|
return matcher(item, term);
|
||||||
|
});
|
||||||
|
items = all.slice(0, maxItems);
|
||||||
|
if (!items.length) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
menu.innerHTML = '';
|
||||||
|
items.forEach(function (item, i) {
|
||||||
|
var el = document.createElement('button');
|
||||||
|
el.type = 'button';
|
||||||
|
el.className = 'itflow-ac-item dropdown-item text-wrap';
|
||||||
|
el.setAttribute('role', 'option');
|
||||||
|
el.innerHTML = typeof opts.render === 'function'
|
||||||
|
? opts.render(item)
|
||||||
|
: itflowEscapeHtml(item.label);
|
||||||
|
el.addEventListener('mousedown', function (e) {
|
||||||
|
// mousedown, not click - blur would close the menu first
|
||||||
|
e.preventDefault();
|
||||||
|
choose(i);
|
||||||
|
});
|
||||||
|
menu.appendChild(el);
|
||||||
|
});
|
||||||
|
if (all.length > items.length) {
|
||||||
|
var more = document.createElement('div');
|
||||||
|
more.className = 'itflow-ac-more small text-muted px-3 py-2 border-top';
|
||||||
|
more.textContent = 'Showing ' + items.length + ' of ' + all.length + ' - keep typing to narrow';
|
||||||
|
menu.appendChild(more);
|
||||||
|
}
|
||||||
|
menu.classList.add('show');
|
||||||
|
input.setAttribute('aria-expanded', 'true');
|
||||||
|
active = -1;
|
||||||
|
position();
|
||||||
|
menu.scrollTop = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
input.setAttribute('autocomplete', 'off');
|
||||||
|
input.setAttribute('aria-autocomplete', 'list');
|
||||||
|
|
||||||
|
input.addEventListener('input', function () {
|
||||||
|
var term = input.value.trim().toLowerCase();
|
||||||
|
if (term.length < minLength) {
|
||||||
|
close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
open(term);
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('keydown', function (e) {
|
||||||
|
if (!menu.classList.contains('show')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault();
|
||||||
|
highlight(active + 1);
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault();
|
||||||
|
highlight(active - 1);
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
if (active >= 0) {
|
||||||
|
e.preventDefault();
|
||||||
|
choose(active);
|
||||||
|
}
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', function () {
|
||||||
|
setTimeout(close, 150);
|
||||||
|
});
|
||||||
|
|
||||||
|
function reposition() {
|
||||||
|
if (menu.classList.contains('show')) {
|
||||||
|
position();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.addEventListener('resize', reposition);
|
||||||
|
// position: fixed does not follow the page, and these menus open inside
|
||||||
|
// scrollable modal bodies - so track scroll on the way up the tree too
|
||||||
|
window.addEventListener('scroll', reposition, true);
|
||||||
|
|
||||||
|
document.addEventListener('click', function (e) {
|
||||||
|
if (e.target !== input && !menu.contains(e.target)) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.itflowAutocomplete = { close: close, reposition: reposition };
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
JQuery UI 1.13.0
|
|
||||||
7
libs/jquery-ui/jquery-ui.min.css
vendored
7
libs/jquery-ui/jquery-ui.min.css
vendored
File diff suppressed because one or more lines are too long
6
libs/jquery-ui/jquery-ui.min.js
vendored
6
libs/jquery-ui/jquery-ui.min.js
vendored
File diff suppressed because one or more lines are too long
2
libs/jquery/jquery.min.js
vendored
2
libs/jquery/jquery.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -810,7 +810,6 @@ if (!$config_whitelabel_enabled) {
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<script src="libs/jquery/jquery.min.js"></script>
|
|
||||||
<script src="libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
<script src="libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||||
<script src="libs/adminlte/js/adminlte.min.js"></script>
|
<script src="libs/adminlte/js/adminlte.min.js"></script>
|
||||||
<script src="js/login_prevent_resubmit.js"></script>
|
<script src="js/login_prevent_resubmit.js"></script>
|
||||||
|
|||||||
@@ -94,36 +94,57 @@ ob_start();
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
var perPage = 8;
|
var perPage = 8;
|
||||||
var $items = $(".notification-item");
|
var items = Array.from(document.querySelectorAll('.notification-item'));
|
||||||
var totalItems = $items.length;
|
var totalItems = items.length;
|
||||||
var totalPages = Math.ceil(totalItems / perPage);
|
var totalPages = Math.ceil(totalItems / perPage);
|
||||||
var currentPage = 0;
|
var currentPage = 0;
|
||||||
|
|
||||||
|
var prevBtn = document.getElementById('prev-btn');
|
||||||
|
var nextBtn = document.getElementById('next-btn');
|
||||||
|
var indicator = document.getElementById('page-indicator');
|
||||||
|
|
||||||
function showPage(page) {
|
function showPage(page) {
|
||||||
$items.hide().slice(page * perPage, (page + 1) * perPage).show();
|
items.forEach(function (item, i) {
|
||||||
$("#prev-btn").prop("disabled", page === 0);
|
var visible = i >= page * perPage && i < (page + 1) * perPage;
|
||||||
$("#next-btn").prop("disabled", page >= totalPages - 1);
|
item.style.display = visible ? '' : 'none';
|
||||||
$("#page-indicator").text(`Page ${page + 1} of ${totalPages} (${totalItems} total)`);
|
});
|
||||||
|
if (prevBtn) {
|
||||||
|
prevBtn.disabled = page === 0;
|
||||||
|
}
|
||||||
|
if (nextBtn) {
|
||||||
|
nextBtn.disabled = page >= totalPages - 1;
|
||||||
|
}
|
||||||
|
if (indicator) {
|
||||||
|
indicator.textContent = `Page ${page + 1} of ${totalPages} (${totalItems} total)`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$("#prev-btn").on("click", function () {
|
if (prevBtn) {
|
||||||
if (currentPage > 0) {
|
prevBtn.addEventListener('click', function () {
|
||||||
currentPage--;
|
if (currentPage > 0) {
|
||||||
showPage(currentPage);
|
currentPage--;
|
||||||
}
|
showPage(currentPage);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
$("#next-btn").on("click", function () {
|
if (nextBtn) {
|
||||||
if (currentPage < totalPages - 1) {
|
nextBtn.addEventListener('click', function () {
|
||||||
currentPage++;
|
if (currentPage < totalPages - 1) {
|
||||||
showPage(currentPage);
|
currentPage++;
|
||||||
}
|
showPage(currentPage);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (totalItems <= perPage) {
|
if (totalItems <= perPage) {
|
||||||
$("#prev-btn, #next-btn, #page-indicator").hide();
|
[prevBtn, nextBtn, indicator].forEach(function (el) {
|
||||||
|
if (el) {
|
||||||
|
el.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
showPage(currentPage);
|
showPage(currentPage);
|
||||||
|
|||||||
@@ -1457,7 +1457,6 @@ if (isset($_POST['add_telemetry'])) {
|
|||||||
<!-- REQUIRED SCRIPTS -->
|
<!-- REQUIRED SCRIPTS -->
|
||||||
|
|
||||||
<!-- jQuery -->
|
<!-- jQuery -->
|
||||||
<script src="/libs/jquery/jquery.min.js"></script>
|
|
||||||
<!-- Bootstrap 5 -->
|
<!-- Bootstrap 5 -->
|
||||||
<script src="/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
<script src="/libs/bootstrap/js/bootstrap.bundle.min.js"></script>
|
||||||
<!-- Custom js-->
|
<!-- Custom js-->
|
||||||
|
|||||||
Reference in New Issue
Block a user