rename /user/ to /agent/ and update links to use agent/ instead

This commit is contained in:
johnnyq
2025-09-23 18:04:23 -04:00
parent 6b6c70f1df
commit edabc5c33f
373 changed files with 93 additions and 21 deletions

View File

@@ -0,0 +1,14 @@
$('#summaryModal').on('shown.bs.modal', function (e) {
// Perform AJAX request to get the summary
$.ajax({
url: 'post.php?ai_ticket_summary',
method: 'POST',
data: { ticket_id: <?php echo $ticket_id; ?> },
success: function(response) {
$('#summaryContent').html(response);
},
error: function() {
$('#summaryContent').html('Error generating summary.');
}
});
});

View File

@@ -0,0 +1,23 @@
// Initialize Stripe.js
const stripe = Stripe(document.getElementById("stripe_publishable_key").value);
initialize();
// Fetch Checkout Session and retrieve the client secret
async function initialize() {
const fetchClientSecret = async () => {
const response = await fetch("/client/post.php?create_stripe_checkout", {
method: "POST",
});
const { clientSecret } = await response.json();
return clientSecret;
};
// Initialize Checkout
const checkout = await stripe.initEmbeddedCheckout({
fetchClientSecret,
});
// Mount Checkout
checkout.mount('#checkout');
}

View File

@@ -0,0 +1,37 @@
function fetchSSL(type)
{
// Get the domain name input & issued/expire/key fields, based on whether this is a new cert or updating an existing
if (type == 'new') {
var domain = document.getElementById("domain").value;
var issuedBy = document.getElementById("issuedBy");
var expire = document.getElementById("expire");
var publicKey = document.getElementById("publicKey");
}
if (type == 'edit') {
var domain = document.getElementById("editCertificateDomain").value;
var issuedBy = document.getElementById("editCertificateIssuedBy");
var expire = document.getElementById("editCertificateExpire");
var publicKey = document.getElementById("editCertificatePublicKey");
}
//Send a GET request to post.php as post.php?certificate_fetch_parse_json_details=TRUE&domain=DOMAIN
jQuery.get(
"ajax.php",
{certificate_fetch_parse_json_details: 'TRUE', domain: domain},
function(data) {
//If we get a response from post.php, parse it as JSON
const ssl_data = JSON.parse(data);
if (ssl_data.success == "TRUE") {
// Fill the form fields with the cert data
issuedBy.value = ssl_data.issued_by;
expire.value = ssl_data.expire;
publicKey.value = ssl_data.public_key;
}
else{
alert("Error whilst parsing/retrieving details for domain")
}
}
);
}

View File

@@ -0,0 +1,9 @@
// Force the user to correctly type the client name to be deleted before activating the button to delete a client
function validateClientNameDelete(client_id) {
if (document.getElementById("clientNameProvided" + client_id).value === document.getElementById("clientName" + client_id).value) {
document.getElementById("clientDeleteButton" + client_id).className = "btn btn-danger btn-lg px-5";
}
else{
document.getElementById("clientDeleteButton" + client_id).className = "btn btn-danger btn-lg px-5 disabled";
}
}

View File

@@ -0,0 +1,16 @@
function showOTPViaCredentialID(credential_id) {
// Send a GET request to ajax.php as ajax.php?get_totp_token_via_id=true&credential_id=ID
jQuery.get(
"ajax.php", {
get_totp_token_via_id: 'true',
credential_id: credential_id
},
function(data) {
//If we get a response from post.php, parse it as JSON
const token = JSON.parse(data);
document.getElementById("otp_" + credential_id).innerText = token
}
);
}

View File

@@ -0,0 +1,5 @@
function populateFileDeleteModal(file_id, file_name) {
// Dynamically populate the file delete modal with the file id (hidden) and name
document.getElementById("file_delete_id").value = file_id;
document.getElementById("file_delete_name").innerText = file_name;
}

View File

@@ -0,0 +1,14 @@
function generatePassword(login_id) {
// Send a GET request to ajax.php as ajax.php?get_readable_pass=true
jQuery.get(
"ajax.php", {
get_readable_pass: 'true'
},
function(data) {
//If we get a response from post.php, parse it as JSON
const password = JSON.parse(data);
document.getElementById("password").value = password;
}
);
}

View File

@@ -0,0 +1,117 @@
const stripe = Stripe(document.getElementById("stripe_publishable_key").value);
let elements;
initialize();
checkStatus();
document
.querySelector("#payment-form")
.addEventListener("submit", handleSubmit);
let emailAddress = '';
// Fetches a payment intent and captures the client secret
async function initialize() {
const { clientSecret } = await fetch("guest_ajax.php?stripe_create_pi=true", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
invoice_id: document.getElementById("invoice_id").value,
url_key: document.getElementById("url_key").value
}),
}).then((r) => r.json());
elements = stripe.elements({ clientSecret });
const paymentElementOptions = {
layout: "tabs",
};
const paymentElement = elements.create("payment", paymentElementOptions);
paymentElement.mount("#payment-element");
// Unhide the submit button once everything has loaded
document.getElementById("submit").hidden = false;
}
async function handleSubmit(e) {
e.preventDefault();
setLoading(true);
const { error } = await stripe.confirmPayment({
elements,
confirmParams: {
return_url: window.location.href,
receipt_email: emailAddress,
},
});
// This point will only be reached if there is an immediate error when
// confirming the payment. Otherwise, your customer will be redirected to
// your `return_url`. For some payment methods like iDEAL, your customer will
// be redirected to an intermediate site first to authorize the payment, then
// redirected to the `return_url`.
if (error.type === "card_error" || error.type === "validation_error") {
showMessage(error.message);
} else {
showMessage("An unexpected error occurred.");
}
setLoading(false);
}
// Fetches the payment intent status after payment submission
async function checkStatus() {
const clientSecret = new URLSearchParams(window.location.search).get(
"payment_intent_client_secret"
);
if (!clientSecret) {
return;
}
const { paymentIntent } = await stripe.retrievePaymentIntent(clientSecret);
switch (paymentIntent.status) {
case "succeeded":
showMessage("Payment succeeded!");
break;
case "processing":
showMessage("Your payment is processing.");
break;
case "requires_payment_method":
showMessage("Your payment was not successful, please try again.");
break;
default:
showMessage("Something went wrong.");
break;
}
}
// ------- UI helpers -------
function showMessage(messageText) {
const messageContainer = document.querySelector("#payment-message");
messageContainer.classList.remove("hidden");
messageContainer.textContent = messageText;
setTimeout(function () {
messageContainer.classList.add("hidden");
messageText.textContent = "";
}, 4000);
}
// Show a spinner on payment submission
function setLoading(isLoading) {
if (isLoading) {
// Disable the button and show a spinner
document.querySelector("#submit").disabled = true;
document.querySelector("#spinner").classList.remove("hidden");
document.querySelector("#button-text").classList.add("hidden");
} else {
document.querySelector("#submit").disabled = false;
document.querySelector("#spinner").classList.add("hidden");
document.querySelector("#button-text").classList.remove("hidden");
}
}

18
agent/js/keepalive.js Normal file
View File

@@ -0,0 +1,18 @@
// Keep PHP sessions alive
// Sends requests to keepalive.php in the background every 10 mins to prevent PHP garbage collection ending sessions
function keep_alive() {
//Send a GET request to keepalive.php as keepalive.php?keepalive
jQuery.get(
"../keepalive.php",
{keepalive: 'true'},
function(data) {
// Don't care about a response
}
);
}
// Run every 10 mins
setInterval(keep_alive, 600000);

58
agent/js/share_modal.js Normal file
View File

@@ -0,0 +1,58 @@
function populateShareModal(client_id, item_type, item_ref_id) {
// Populate HTML fields
document.getElementById("share_client_id").value = client_id;
document.getElementById("share_item_type").value = item_type;
document.getElementById("share_item_ref_id").value = item_ref_id;
// (re)Hide the URL/div (incase we're re-generating it)
document.getElementById("div_share_link_output").hidden = true;
document.getElementById("share_link").value = '';
// Show form and generate button
document.getElementById("div_share_link_form").hidden = false;
document.getElementById("div_share_link_generate").hidden = false;
$(document).ready(function() {
$('#share_email').select2({
tags: true,
placeholder: 'Select or type a value',
allowClear: true
});
});
}
function generateShareLink() {
let client_id = document.getElementById("share_client_id").value;
let item_type = document.getElementById("share_item_type").value;
let item_ref_id = document.getElementById("share_item_ref_id").value;
let item_note = document.getElementById("share_note").value;
let item_views = document.getElementById("share_views").checked ? 1 : 0;
let item_expires = document.querySelector('input[name="expires"]:checked').value;
let contact_email = document.getElementById("share_email").value;
// Check values are provided
if (item_expires) {
// Send a GET request to ajax.php as ajax.php?share_generate_link=true....
jQuery.get(
"ajax.php",
{share_generate_link: 'true', client_id: client_id, type: item_type, id: item_ref_id, note: item_note ,views: item_views, expires: item_expires, contact_email},
function(data) {
// If we get a response from ajax.php, parse it as JSON
const response = JSON.parse(data);
// Hide the div/form & button used to generate the link
document.getElementById("div_share_link_form").hidden = true;
document.getElementById("div_share_link_generate").hidden = true;
// Show the readonly input containing the shared link
document.getElementById("div_share_link_output").hidden = false;
document.getElementById("share_link").value = response;
// Copy link to clipboard
navigator.clipboard.writeText(response);
}
);
}
}

View File

@@ -0,0 +1,94 @@
/*
* LISTENERS
*/
// Modal loaded listener - populate client select
const changeClientModalLoad = document.getElementById('clientChangeTicketModalLoad');
changeClientModalLoad.addEventListener('click', function() {
populateChangeClientModal_Clients();
})
// Client selected listener - populate contact select
// We seem to have to use jQuery to listen for events, as the client input is a select2 component?
const clientSelectDropdown = document.getElementById("changeClientSelect");
$(clientSelectDropdown).on('select2:select', function (e) {
let client_id = $(this).find(':selected').val();
populateChangeClientModal_Contacts(client_id);
});
/*
* FUNCTIONS
*/
// Populate client list function
function populateChangeClientModal_Clients() {
// Get current client ID
let current_client_id = document.getElementById("client_id").value;
// Send a GET request to ajax.php as ajax.php?get_active_clients=true
jQuery.get(
"ajax.php",
{get_active_clients: 'true'},
function(data) {
// If we get a response from ajax.php, parse it as JSON
const response = JSON.parse(data);
// Access the data for clients (multiple)
const clients = response.clients;
// Client dropdown already defined in listeners as clientSelectDropdown
// Clear dropdown
let i, L = clientSelectDropdown.options.length - 1;
for (i = L; i >= 0; i--) {
clientSelectDropdown.remove(i);
}
clientSelectDropdown[clientSelectDropdown.length] = new Option('- Client -', '0');
// Populate dropdown
clients.forEach(client => {
if (parseInt(current_client_id) !== parseInt(client.client_id)) {
// Show clients returned (excluding the current client ID - we can't change a ticket client to itself)
clientSelectDropdown[clientSelectDropdown.length] = new Option(client.client_name, client.client_id);
}
});
}
);
}
// Populate client contact function (after a client is selected)
function populateChangeClientModal_Contacts(client_id) {
// Send a GET request to ajax.php as ajax.php?get_client_contacts=true&client_id=NUM
jQuery.get(
"ajax.php",
{get_client_contacts: 'true', client_id: client_id},
function(data) {
// If we get a response from ajax.php, parse it as JSON
const response = JSON.parse(data);
// Access the data for contacts (multiple)
const contacts = response.contacts;
// Contacts dropdown
const contactSelectDropdown = document.getElementById("changeContactSelect");
// Clear Category dropdown
let i, L = contactSelectDropdown.options.length - 1;
for (i = L; i >= 0; i--) {
contactSelectDropdown.remove(i);
}
contactSelectDropdown[contactSelectDropdown.length] = new Option('- Contact -', '0');
// Populate dropdown
contacts.forEach(contact => {
contactSelectDropdown[contactSelectDropdown.length] = new Option(contact.contact_name, contact.contact_id);
});
}
);
}

View File

@@ -0,0 +1,33 @@
// Collision detection
// Adds a "view" entry of the current ticket every 2 mins into the database
// Updates the currently viewing (ticket_collision_viewing) element with anyone that's looked at this ticket in the last two mins
function ticket_collision_detection() {
// Get the page ticket id
var ticket_id = document.getElementById("ticket_id").value;
//Send a GET request to ajax.php as ajax.php?ticket_add_view=true&ticket_id=NUMBER
jQuery.get(
"ajax.php",
{ticket_add_view: 'true', ticket_id: ticket_id},
function(data) {
// We don't care about a response
}
);
//Send a GET request to ajax.php as ajax.php?ticket_query_views=true&ticket_id=NUMBER
jQuery.get(
"ajax.php",
{ticket_query_views: 'true', ticket_id: ticket_id},
function(data) {
//If we get a response from ajax.php, parse it as JSON
const ticket_view_data = JSON.parse(data);
document.getElementById("ticket_collision_viewing").innerHTML = ticket_view_data.message;
}
);
}
// Call on page load
ticket_collision_detection();
// Run every 2 mins
setInterval(ticket_collision_detection, 120*1000);

40
agent/js/ticket_merge.js Normal file
View File

@@ -0,0 +1,40 @@
// Ticket merging
// Gets details of the ticket we're going to merge this ticket into
// Shows the details under the comments box & enables the merge button if the status of the merge into ticket is not closed
function merge_into_number_get_details() {
// Get the ticket number to merge into
var merge_into_ticket_number = document.getElementById("merge_into_ticket_number").value;
// Reset the form
document.getElementById("merge_ticket_btn").disabled = true;
document.getElementById("merge_into_details_div").hidden = true;
// Send a GET request to post.php as post.php?merge_ticket_get_json_details=true&merge_into_ticket_number=NUMBER
jQuery.get(
"ajax.php",
{merge_ticket_get_json_details: 'true', merge_into_ticket_number: merge_into_ticket_number},
function(data){
// If we get a response from post.php, parse it as JSON
const merge_into_ticket_info = JSON.parse(data);
// Check that the current ticket ID isn't also the new/merge ticket ID
if(parseInt(merge_into_ticket_info.ticket_id) !== parseInt(document.getElementById("current_ticket_id").value)){
// Show the div with the parent ("master") ticket details, populate
document.getElementById("merge_into_details_div").hidden = false;
document.getElementById("merge_into_details_number").innerText = "Parent ticket details: " + merge_into_ticket_info.ticket_prefix + merge_into_ticket_info.ticket_number;
document.getElementById("merge_into_details_client").innerText = "Client Contact: " + merge_into_ticket_info.client_name + " / " + merge_into_ticket_info.contact_name;
document.getElementById("merge_into_details_subject").innerText = "Subject: " + merge_into_ticket_info.ticket_subject;
document.getElementById("merge_into_details_priority").innerText = "Priority: " + merge_into_ticket_info.ticket_priority;
document.getElementById("merge_into_details_status").innerText = "Status: " + merge_into_ticket_info.ticket_status_name;
// Enable the merge button if the merge into ticket isn't in a closed state
if(merge_into_ticket_info.ticket_status_name.toLowerCase() != "closed"){
document.getElementById("merge_ticket_btn").disabled = false;
}
}
}
);
}

View File

@@ -0,0 +1,175 @@
(function() {
document.addEventListener("DOMContentLoaded", function() {
// Initialize variables
var timerInterval = null;
var ticketID = getCurrentTicketID();
var elapsedSecs = getElapsedSeconds();
function getCurrentTicketID() {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get('ticket_id');
}
function getLocalStorageKey(suffix) {
return ticketID + "-" + suffix;
}
function getElapsedSeconds() {
let storedStartTime = parseInt(localStorage.getItem(getLocalStorageKey("startTime")) || "0");
let pausedTime = parseInt(localStorage.getItem(getLocalStorageKey("pausedTime")) || "0");
if (!storedStartTime) return pausedTime;
let timeSinceStart = Math.floor((Date.now() - storedStartTime) / 1000);
return pausedTime + timeSinceStart;
}
function displayTime() {
let totalSeconds = elapsedSecs;
let hours = Math.floor(totalSeconds / 3600);
totalSeconds %= 3600;
let minutes = Math.floor(totalSeconds / 60);
let seconds = totalSeconds % 60;
document.getElementById("hours").value = pad(hours);
document.getElementById("minutes").value = pad(minutes);
document.getElementById("seconds").value = pad(seconds);
}
function pad(val) {
return val < 10 ? "0" + val : val;
}
function countTime() {
elapsedSecs++;
displayTime();
}
function startTimer() {
if (!localStorage.getItem(getLocalStorageKey("startTime"))) {
localStorage.setItem(getLocalStorageKey("startTime"), Date.now().toString());
}
timerInterval = setInterval(countTime, 1000);
document.getElementById("startStopTimer").innerHTML = "<i class='fas fa-pause'></i>";
localStorage.setItem("ticket-timer-running-" + ticketID, "true");
}
function pauseTimer() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
let currentElapsed = getElapsedSeconds();
localStorage.setItem(getLocalStorageKey("pausedTime"), currentElapsed.toString());
localStorage.removeItem(getLocalStorageKey("startTime"));
document.getElementById("startStopTimer").innerHTML = "<i class='fas fa-play'></i>";
localStorage.setItem("ticket-timer-running-" + ticketID, "false");
}
function clearTimeStorage() {
localStorage.removeItem(getLocalStorageKey("startTime"));
localStorage.removeItem(getLocalStorageKey("pausedTime"));
localStorage.removeItem("ticket-timer-running-" + ticketID);
}
function resetTimer() {
if (confirm("Are you sure you want to reset the timer?")) {
clearInterval(timerInterval);
timerInterval = null;
elapsedSecs = 0;
clearTimeStorage();
displayTime();
document.getElementById("startStopTimer").innerHTML = "<i class='fas fa-play'></i>";
}
localStorage.setItem("ticket-timer-running-" + ticketID, "false");
}
function forceResetTimer() {
clearInterval(timerInterval);
timerInterval = null;
elapsedSecs = 0;
clearTimeStorage();
displayTime();
document.getElementById("startStopTimer").innerHTML = "<i class='fas fa-play'></i>";
}
function handleInputFocus() {
pauseTimer();
}
function updateTimeFromInput() {
const hours = parseInt(document.getElementById("hours").value, 10) || 0;
const minutes = parseInt(document.getElementById("minutes").value, 10) || 0;
const seconds = parseInt(document.getElementById("seconds").value, 10) || 0;
elapsedSecs = (hours * 3600) + (minutes * 60) + seconds;
if (!timerInterval) {
localStorage.setItem(getLocalStorageKey("pausedTime"), elapsedSecs.toString());
} else {
const newStartTime = Date.now() - (elapsedSecs * 1000);
localStorage.setItem(getLocalStorageKey("startTime"), newStartTime.toString());
localStorage.removeItem(getLocalStorageKey("pausedTime"));
}
}
function checkStatusAndPauseTimer() {
var status = document.querySelector('select[name="status"]').value;
if (status.includes("Pending") || status.includes("Close")) {
pauseTimer();
}
}
// Attach input listeners
document.getElementById("hours").addEventListener('change', updateTimeFromInput);
document.getElementById("minutes").addEventListener('change', updateTimeFromInput);
document.getElementById("seconds").addEventListener('change', updateTimeFromInput);
document.getElementById("hours").addEventListener('focus', handleInputFocus);
document.getElementById("minutes").addEventListener('focus', handleInputFocus);
document.getElementById("seconds").addEventListener('focus', handleInputFocus);
document.querySelector('select[name="status"]').addEventListener('change', checkStatusAndPauseTimer);
document.getElementById("startStopTimer").addEventListener('click', function() {
if (timerInterval === null) {
startTimer();
} else {
pauseTimer();
}
});
document.getElementById("resetTimer").addEventListener('click', function() {
resetTimer();
});
document.getElementById("ticket_add_reply").addEventListener('click', function() {
setTimeout(forceResetTimer, 100);
});
document.getElementById("ticket_close").addEventListener('click', function() {
setTimeout(clearTimeStorage, 100);
});
// Final initialization logic
try {
displayTime();
// If no timer state, respect ticketAutoStart
if (!localStorage.getItem(getLocalStorageKey("startTime")) && !localStorage.getItem(getLocalStorageKey("pausedTime"))) {
if (ticketAutoStart === 1) {
startTimer();
} else {
pauseTimer();
}
}
// If timer already running, resume it
else if (localStorage.getItem(getLocalStorageKey("startTime"))) {
startTimer();
}
// Check and pause timer if status is pending
checkStatusAndPauseTimer();
} catch (error) {
console.error("There was an issue initializing the timer:", error);
}
});
})();

View File

@@ -0,0 +1,176 @@
// Used to populate dynamic content in recurring_ticket_add_modal and ticket_add_modal_v2 based on selected client
// Client selected listener
// We seem to have to use jQuery to listen for events, as the client input is a select2 component?
const clientSelectDropdown = document.getElementById("changeClientSelect"); // Define client selector
// // If the client selector is disabled, we must be on a client-specific page instead. Trigger the lists to update.
if (clientSelectDropdown.disabled) {
let client_id = $(clientSelectDropdown).find(':selected').val();
populateLists(client_id);
}
// Listener for client selection. Populate select lists when a client is selected
$(clientSelectDropdown).on('select2:select', function (e) {
let client_id = $(this).find(':selected').val();
// Update the contacts dropdown list
populateLists(client_id);
});
// Populates dropdowns with dynamic content based on the client ID
// Called the client select dropdown is used or if the client select is disabled
function populateLists(client_id) {
populateContactsDropdown(client_id);
populateAssetsDropdown(client_id);
populateLocationsDropdown(client_id);
populateVendorsDropdown(client_id);
}
// Populate client contacts
function populateContactsDropdown(client_id) {
// Send a GET request to ajax.php as ajax.php?get_client_contacts=true&client_id=NUM
jQuery.get(
"ajax.php",
{get_client_contacts: 'true', client_id: client_id},
function(data) {
// If we get a response from ajax.php, parse it as JSON
const response = JSON.parse(data);
// Access the data for contacts (multiple)
const contacts = response.contacts;
// Contacts dropdown
const contactSelectDropdown = document.getElementById("contactSelect");
// Clear dropdown
let i, L = contactSelectDropdown.options.length - 1;
for (i = L; i >= 0; i--) {
contactSelectDropdown.remove(i);
}
contactSelectDropdown[contactSelectDropdown.length] = new Option('- Contact -', '0');
// Populate dropdown
contacts.forEach(contact => {
var appendText = "";
if (contact.contact_primary == "1") {
appendText = " (Primary)";
} else if (contact.contact_technical == "1") {
appendText = " (Technical)";
}
contactSelectDropdown[contactSelectDropdown.length] = new Option(contact.contact_name + appendText, contact.contact_id);
});
}
);
}
// Populate client assets
function populateAssetsDropdown(client_id) {
jQuery.get(
"ajax.php",
{get_client_assets: 'true', client_id: client_id},
function(data) {
// If we get a response from ajax.php, parse it as JSON
const response = JSON.parse(data);
// Access the data for assets (multiple)
const assets = response.assets;
// Assets dropdown
const assetSelectDropdown = document.getElementById("assetSelect");
// Clear dropdown
let i, L = assetSelectDropdown.options.length - 1;
for (i = L; i >= 0; i--) {
assetSelectDropdown.remove(i);
}
assetSelectDropdown[assetSelectDropdown.length] = new Option('- Asset -', '0');
// Populate dropdown with asset name (and contact, if set)
assets.forEach(asset => {
let displayText = asset.asset_name;
if (asset.contact_name !== null) {
displayText = asset.asset_name + " - " + asset.contact_name;
}
assetSelectDropdown[assetSelectDropdown.length] = new Option(displayText, asset.asset_id);
});
}
);
}
// Populate client locations
function populateLocationsDropdown(client_id) {
jQuery.get(
"ajax.php",
{get_client_locations: 'true', client_id: client_id},
function(data) {
// If we get a response from ajax.php, parse it as JSON
const response = JSON.parse(data);
// Access the data for locations (multiple)
const locations = response.locations;
// Locations dropdown
const locationSelectDropdown = document.getElementById("locationSelect");
// Clear dropdown
let i, L = locationSelectDropdown.options.length - 1;
for (i = L; i >= 0; i--) {
locationSelectDropdown.remove(i);
}
locationSelectDropdown[locationSelectDropdown.length] = new Option('- Location -', '0');
// Populate dropdown
locations.forEach(location => {
locationSelectDropdown[locationSelectDropdown.length] = new Option(location.location_name, location.location_id);
});
}
);
}
// Populate client vendors
function populateVendorsDropdown(client_id) {
jQuery.get(
"ajax.php",
{get_client_vendors: 'true', client_id: client_id},
function(data) {
// If we get a response from ajax.php, parse it as JSON
const response = JSON.parse(data);
// Access the data for locations (multiple)
const vendors = response.vendors;
// Locations dropdown
const vendorSelectDropdown = document.getElementById("vendorSelect");
// Clear dropdown
let i, L = vendorSelectDropdown.options.length - 1;
for (i = L; i >= 0; i--) {
vendorSelectDropdown.remove(i);
}
vendorSelectDropdown[vendorSelectDropdown.length] = new Option('- Vendor -', '0');
// Populate dropdown
vendors.forEach(vendor => {
vendorSelectDropdown[vendorSelectDropdown.length] = new Option(vendor.vendor_name, vendor.vendor_id);
});
}
);
}

126
agent/js/tickets_kanban.js Normal file
View File

@@ -0,0 +1,126 @@
$(document).ready(function () {
console.log('CONFIG_TICKET_MOVING_COLUMNS:', CONFIG_TICKET_MOVING_COLUMNS);
console.log('CONFIG_TICKET_ORDERING:', CONFIG_TICKET_ORDERING);
// -------------------------------
// Drag: Kanban Columns (Statuses)
// -------------------------------
new Sortable(document.querySelector('#kanban-board'), {
animation: 150,
handle: '.panel-title',
draggable: '.kanban-column',
onEnd: function () {
const columnPositions = Array.from(document.querySelectorAll('#kanban-board .kanban-column')).map((col, index) => ({
status_id: $(col).data('status-id'),
status_kanban: index
}));
if (CONFIG_TICKET_MOVING_COLUMNS === 1) {
$.post('ajax.php', {
update_kanban_status_position: true,
positions: columnPositions
}).done(() => {
console.log('Ticket status kanban orders updated.');
}).fail((xhr) => {
console.error('Error updating status order:', xhr.responseText);
});
}
}
});
// -------------------------------
// Drag: Tasks within Columns
// -------------------------------
document.querySelectorAll('.kanban-status').forEach(statusCol => {
new Sortable(statusCol, {
group: 'tickets',
animation: 150,
handle: isTouchDevice() ? '.drag-handle-class' : undefined,
onStart: () => hidePlaceholders(),
onEnd: function (evt) {
const target = evt.to;
const movedEl = evt.item;
// Disallow reordering in same column if config says so
if (CONFIG_TICKET_ORDERING === 0 && evt.from === evt.to) {
evt.from.insertBefore(movedEl, evt.from.children[evt.oldIndex]);
showPlaceholders();
return;
}
const columnId = $(target).data('status-id');
const positions = Array.from(target.querySelectorAll('.task')).map((card, index) => {
const ticketId = $(card).data('ticket-id');
const oldStatus = ticketId === $(movedEl).data('ticket-id')
? $(movedEl).data('ticket-status-id')
: false;
$(card).data('ticket-status-id', columnId); // update DOM
return {
ticket_id: ticketId,
ticket_order: index,
ticket_oldStatus: oldStatus,
ticket_status: columnId
};
});
$.post('ajax.php', {
update_kanban_ticket: true,
positions: positions
}).done(() => {
console.log('Updated kanban ticket positions.');
}).fail((xhr) => {
console.error('Error updating ticket positions:', xhr.responseText);
});
// Refresh placeholders after update
showPlaceholders();
}
});
});
// -------------------------------
// 📱 Touch Support: Show drag handle on mobile
// -------------------------------
if (isTouchDevice()) {
$('.drag-handle-class').css('display', 'inline');
}
// -------------------------------
// Placeholder Management
// -------------------------------
function showPlaceholders() {
document.querySelectorAll('.kanban-status').forEach(status => {
const placeholderClass = 'empty-placeholder';
// Remove existing placeholder
const existing = status.querySelector(`.${placeholderClass}`);
if (existing) existing.remove();
// Only show if there are no tasks
if (status.querySelectorAll('.task').length === 0) {
const placeholder = document.createElement('div');
placeholder.className = `${placeholderClass} text-muted text-center p-2`;
placeholder.innerText = 'Drop ticket here';
placeholder.style.pointerEvents = 'none';
status.appendChild(placeholder);
}
});
}
function hidePlaceholders() {
document.querySelectorAll('.empty-placeholder').forEach(el => el.remove());
}
// Run once on load
showPlaceholders();
// -------------------------------
// Utility: Detect touch device
// -------------------------------
function isTouchDevice() {
return 'ontouchstart' in window || navigator.maxTouchPoints > 0;
}
});