Files
tinker_tickets/assets/js/dashboard.js
Jared Vititoe ac094c8706 Feature 5: Implement Bulk Actions (Admin Only)
Add comprehensive bulk operations system for admins:

- Created BulkOperationsModel.php with operation tracking and processing
- Added bulk_operation.php API endpoint for bulk operations
- Created get_users.php API endpoint for user dropdown in bulk assign
- Updated DashboardView.php with checkboxes and bulk actions toolbar
- Added JavaScript functions for:
  - Select all/clear selection
  - Bulk close tickets
  - Bulk assign tickets
  - Bulk change priority
- Added comprehensive CSS for bulk actions toolbar and modals
- All bulk operations are admin-only (enforced server-side)
- Operations tracked in bulk_operations table with audit logging
- Supports bulk_close, bulk_assign, and bulk_priority operations

Admins can now select multiple tickets and perform batch operations, significantly improving workflow efficiency.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-01 19:06:33 -05:00

1157 lines
43 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Main initialization
document.addEventListener('DOMContentLoaded', function() {
console.log('DOM loaded, initializing dashboard...');
// Check if we're on the dashboard page
const hasTable = document.querySelector('table');
const isTicketPage = window.location.pathname.includes('/ticket/') ||
window.location.href.includes('ticket.php') ||
document.querySelector('.ticket-details') !== null;
const isDashboard = hasTable && !isTicketPage;
console.log('Has table:', hasTable);
console.log('Is ticket page:', isTicketPage);
console.log('Is dashboard:', isDashboard);
if (isDashboard) {
// Dashboard-specific initialization
initStatusFilter();
initTableSorting();
console.log('Creating hamburger menu for dashboard...');
try {
createHamburgerMenu();
console.log('Hamburger menu created successfully');
} catch (error) {
console.error('Error creating hamburger menu:', error);
}
} else if (isTicketPage) {
// Ticket page initialization
console.log('Creating hamburger menu for ticket page...');
try {
createHamburgerMenu();
console.log('Hamburger menu created successfully');
} catch (error) {
console.error('Error creating hamburger menu:', error);
}
}
// Initialize for all pages
initThemeToggle();
initSettingsModal();
// Load saved theme preference
const savedTheme = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', savedTheme);
});
function initTableSorting() {
const tableHeaders = document.querySelectorAll('th');
tableHeaders.forEach((header, index) => {
header.style.cursor = 'pointer';
header.addEventListener('click', () => {
const table = header.closest('table');
sortTable(table, index);
});
});
}
function initSettingsModal() {
const settingsIcon = document.querySelector('.settings-icon');
if (settingsIcon) {
settingsIcon.addEventListener('click', function(e) {
e.preventDefault();
createSettingsModal();
});
}
}
function sortTable(table, column) {
const headers = table.querySelectorAll('th');
headers.forEach(header => {
header.classList.remove('sort-asc', 'sort-desc');
});
const rows = Array.from(table.querySelectorAll('tbody tr'));
const currentDirection = table.dataset.sortColumn == column
? (table.dataset.sortDirection === 'asc' ? 'desc' : 'asc')
: 'asc';
table.dataset.sortColumn = column;
table.dataset.sortDirection = currentDirection;
rows.sort((a, b) => {
const aValue = a.children[column].textContent.trim();
const bValue = b.children[column].textContent.trim();
// Check if this is a date column
const headerText = headers[column].textContent.toLowerCase();
if (headerText === 'created' || headerText === 'updated') {
const dateA = new Date(aValue);
const dateB = new Date(bValue);
return currentDirection === 'asc' ? dateA - dateB : dateB - dateA;
}
// Numeric comparison
const numA = parseFloat(aValue);
const numB = parseFloat(bValue);
if (!isNaN(numA) && !isNaN(numB)) {
return currentDirection === 'asc' ? numA - numB : numB - numA;
}
// String comparison
return currentDirection === 'asc'
? aValue.localeCompare(bValue)
: bValue.localeCompare(aValue);
});
const currentHeader = headers[column];
currentHeader.classList.add(currentDirection === 'asc' ? 'sort-asc' : 'sort-desc');
const tbody = table.querySelector('tbody');
rows.forEach(row => tbody.appendChild(row));
}
function createSettingsModal() {
const backdrop = document.createElement('div');
backdrop.className = 'settings-modal-backdrop';
backdrop.innerHTML = `
<div class="settings-modal">
<div class="settings-modal-header">
<h2>Dashboard Settings</h2>
<button class="close-modal">×</button>
</div>
<div class="settings-modal-content">
<div class="setting-group">
<h3>Rows per Page</h3>
<select id="rows-per-page">
<option value="15">15</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</div>
</div>
<div class="settings-modal-footer">
<button class="save-settings">Save Settings</button>
<button class="cancel-settings">Cancel</button>
</div>
</div>
`;
document.body.appendChild(backdrop);
// Load saved rows per page setting
const savedRowsPerPage = localStorage.getItem('ticketsPerPage') || '15';
const rowsPerPageSelect = backdrop.querySelector('#rows-per-page');
rowsPerPageSelect.value = savedRowsPerPage;
// Event listeners
backdrop.querySelector('.close-modal').addEventListener('click', closeSettingsModal);
backdrop.querySelector('.cancel-settings').addEventListener('click', closeSettingsModal);
backdrop.querySelector('.save-settings').addEventListener('click', saveSettings);
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) {
closeSettingsModal();
}
});
}
function closeSettingsModal() {
const backdrop = document.querySelector('.settings-modal-backdrop');
if (backdrop) {
backdrop.remove();
}
}
function saveSettings() {
// Save rows per page
const rowsPerPage = document.querySelector('#rows-per-page').value;
localStorage.setItem('ticketsPerPage', rowsPerPage);
// Set cookie for PHP to read
document.cookie = `ticketsPerPage=${rowsPerPage}; path=/`;
// Reload page to apply pagination changes
window.location.reload();
}
function initThemeToggle() {
const toggle = document.createElement('button');
toggle.className = 'theme-toggle';
toggle.innerHTML = '🌓';
toggle.onclick = () => {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
};
document.body.appendChild(toggle);
}
function initStatusFilter() {
const filterContainer = document.createElement('div');
filterContainer.className = 'status-filter-container';
const dropdown = document.createElement('div');
dropdown.className = 'status-dropdown';
const dropdownHeader = document.createElement('div');
dropdownHeader.className = 'dropdown-header';
dropdownHeader.textContent = 'Status Filter';
const dropdownContent = document.createElement('div');
dropdownContent.className = 'dropdown-content';
const statuses = ['Open', 'In Progress', 'Closed'];
statuses.forEach(status => {
const label = document.createElement('label');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = status;
checkbox.id = `status-${status.toLowerCase().replace(/\s+/g, '-')}`;
const urlParams = new URLSearchParams(window.location.search);
const currentStatuses = urlParams.get('status') ? urlParams.get('status').split(',') : [];
const showAll = urlParams.get('show_all');
// FIXED LOGIC: Determine checkbox state
if (showAll === '1') {
// If show_all=1 parameter exists, all should be checked
checkbox.checked = true;
} else if (currentStatuses.length === 0) {
// No status parameter - default: Open and In Progress checked, Closed unchecked
checkbox.checked = status !== 'Closed';
} else {
// Status parameter exists - check if this status is in the list
checkbox.checked = currentStatuses.includes(status);
}
label.appendChild(checkbox);
label.appendChild(document.createTextNode(' ' + status));
dropdownContent.appendChild(label);
});
const saveButton = document.createElement('button');
saveButton.className = 'btn save-filter';
saveButton.textContent = 'Apply Filter';
saveButton.onclick = () => {
const checkedBoxes = dropdownContent.querySelectorAll('input:checked');
const selectedStatuses = Array.from(checkedBoxes).map(cb => cb.value);
const params = new URLSearchParams(window.location.search);
if (selectedStatuses.length === 0) {
// No statuses selected - show default (Open + In Progress)
params.delete('status');
params.delete('show_all');
} else if (selectedStatuses.length === 3) {
// All statuses selected - show all tickets
params.delete('status');
params.set('show_all', '1');
} else {
// Some statuses selected - set the parameter
params.set('status', selectedStatuses.join(','));
params.delete('show_all');
}
params.set('page', '1');
window.location.search = params.toString();
dropdown.classList.remove('active');
};
dropdownHeader.onclick = () => {
dropdown.classList.toggle('active');
};
dropdown.appendChild(dropdownHeader);
dropdown.appendChild(dropdownContent);
dropdownContent.appendChild(saveButton);
filterContainer.appendChild(dropdown);
const tableActions = document.querySelector('.table-controls .table-actions');
if (tableActions) {
tableActions.prepend(filterContainer);
}
}
function quickSave() {
if (!window.ticketData) {
console.error('No ticket data available');
return;
}
const statusSelect = document.getElementById('status-select');
const prioritySelect = document.getElementById('priority-select');
if (!statusSelect || !prioritySelect) {
console.error('Status or priority select not found');
return;
}
const data = {
ticket_id: parseInt(window.ticketData.id),
status: statusSelect.value,
priority: parseInt(prioritySelect.value)
};
console.log('Saving ticket data:', data);
fetch('/api/update_ticket.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => {
console.log('Response status:', response.status);
return response.text().then(text => {
console.log('Raw response:', text);
try {
return JSON.parse(text);
} catch (e) {
throw new Error('Invalid JSON response: ' + text);
}
});
})
.then(result => {
console.log('Update result:', result);
if (result.success) {
// Update the hamburger menu display
const hamburgerStatus = document.getElementById('hamburger-status');
const hamburgerPriority = document.getElementById('hamburger-priority');
if (hamburgerStatus) hamburgerStatus.textContent = statusSelect.value;
if (hamburgerPriority) hamburgerPriority.textContent = 'P' + prioritySelect.value;
// Update window.ticketData
window.ticketData.status = statusSelect.value;
window.ticketData.priority = parseInt(prioritySelect.value);
// Update main page elements if they exist
const statusDisplay = document.getElementById('statusDisplay');
if (statusDisplay) {
statusDisplay.className = `status-${statusSelect.value}`;
statusDisplay.textContent = statusSelect.value;
}
console.log('Ticket updated successfully');
// Close hamburger menu after successful save
const hamburgerContent = document.querySelector('.hamburger-content');
if (hamburgerContent) {
hamburgerContent.classList.remove('open');
document.body.classList.remove('menu-open');
}
} else {
console.error('Error updating ticket:', result.error || 'Unknown error');
alert('Error updating ticket: ' + (result.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error updating ticket:', error);
alert('Error updating ticket: ' + error.message);
});
}
function createHamburgerMenu() {
console.log('createHamburgerMenu called');
// Remove any existing hamburger menu first
const existingMenu = document.querySelector('.hamburger-menu');
if (existingMenu) {
console.log('Removing existing menu');
existingMenu.remove();
}
const hamburgerMenu = document.createElement('div');
hamburgerMenu.className = 'hamburger-menu';
// Better detection for ticket pages
const isTicketPage = window.location.pathname.includes('/ticket/') ||
window.location.href.includes('ticket.php') ||
document.querySelector('.ticket-details') !== null;
console.log('Is ticket page:', isTicketPage);
console.log('Has ticketData:', !!window.ticketData);
console.log('TicketData contents:', window.ticketData);
if (isTicketPage) {
// Wait for ticketData if it's not loaded yet
if (!window.ticketData) {
console.log('Waiting for ticket data...');
setTimeout(() => {
if (window.ticketData) {
console.log('Ticket data now available, recreating menu');
createHamburgerMenu();
}
}, 100);
return;
}
console.log('Creating ticket hamburger menu with data:', window.ticketData);
// Ticket page hamburger menu with inline editing
hamburgerMenu.innerHTML = `
<div class="hamburger-icon">☰</div>
<div class="hamburger-content">
<div class="close-hamburger">☰</div>
<h3>Ticket Actions</h3>
<div class="ticket-info-editable">
<div class="editable-field" data-field="status">
<label><strong>Status:</strong></label>
<span class="editable-value" data-current="${window.ticketData.status}">${window.ticketData.status}</span>
<div class="edit-dropdown" style="display: none;">
<select class="field-select">
<option value="Open" ${window.ticketData.status === 'Open' ? 'selected' : ''}>Open</option>
<option value="In Progress" ${window.ticketData.status === 'In Progress' ? 'selected' : ''}>In Progress</option>
<option value="Closed" ${window.ticketData.status === 'Closed' ? 'selected' : ''}>Closed</option>
</select>
<div class="edit-actions">
<button class="save-btn">✓</button>
<button class="cancel-btn">✗</button>
</div>
</div>
</div>
<div class="editable-field" data-field="priority">
<label><strong>Priority:</strong></label>
<span class="editable-value" data-current="${window.ticketData.priority}">P${window.ticketData.priority}</span>
<div class="edit-dropdown" style="display: none;">
<select class="field-select">
<option value="1" ${String(window.ticketData.priority) === '1' ? 'selected' : ''}>P1 - Critical</option>
<option value="2" ${String(window.ticketData.priority) === '2' ? 'selected' : ''}>P2 - High</option>
<option value="3" ${String(window.ticketData.priority) === '3' ? 'selected' : ''}>P3 - Medium</option>
<option value="4" ${String(window.ticketData.priority) === '4' ? 'selected' : ''}>P4 - Low</option>
<option value="5" ${String(window.ticketData.priority) === '5' ? 'selected' : ''}>P5 - Lowest</option>
</select>
<div class="edit-actions">
<button class="save-btn">✓</button>
<button class="cancel-btn">✗</button>
</div>
</div>
</div>
<div class="editable-field" data-field="category">
<label><strong>Category:</strong></label>
<span class="editable-value" data-current="${window.ticketData.category}">${window.ticketData.category}</span>
<div class="edit-dropdown" style="display: none;">
<select class="field-select">
<option value="Hardware" ${window.ticketData.category === 'Hardware' ? 'selected' : ''}>Hardware</option>
<option value="Software" ${window.ticketData.category === 'Software' ? 'selected' : ''}>Software</option>
</select>
<div class="edit-actions">
<button class="save-btn">✓</button>
<button class="cancel-btn">✗</button>
</div>
</div>
</div>
<div class="editable-field" data-field="type">
<label><strong>Type:</strong></label>
<span class="editable-value" data-current="${window.ticketData.type}">${window.ticketData.type}</span>
<div class="edit-dropdown" style="display: none;">
<select class="field-select">
<option value="Install" ${window.ticketData.type === 'Install' ? 'selected' : ''}>Install</option>
<option value="Maintenance" ${window.ticketData.type === 'Maintenance' ? 'selected' : ''}>Maintenance</option>
<option value="Problem" ${window.ticketData.type === 'Problem' ? 'selected' : ''}>Problem</option>
<option value="Request" ${window.ticketData.type === 'Request' ? 'selected' : ''}>Request</option>
<option value="Task" ${window.ticketData.type === 'Task' ? 'selected' : ''}>Task</option>
<option value="Upgrade" ${window.ticketData.type === 'Upgrade' ? 'selected' : ''}>Upgrade</option>
</select>
<div class="edit-actions">
<button class="save-btn">✓</button>
<button class="cancel-btn">✗</button>
</div>
</div>
</div>
</div>
</div>
`;
console.log('Ticket hamburger menu HTML created');
// Add inline editing functionality
setupInlineEditing(hamburgerMenu);
} else {
console.log('Creating dashboard hamburger menu');
// Dashboard hamburger menu (your existing code)
hamburgerMenu.innerHTML = `
<div class="hamburger-icon">☰</div>
<div class="hamburger-content">
<div class="close-hamburger">☰</div>
<h3>Filters</h3>
<div class="filter-section">
<h4>Categories</h4>
<div id="category-filters"></div>
</div>
<div class="filter-section">
<h4>Types</h4>
<div id="type-filters"></div>
</div>
<div class="filter-actions">
<button id="apply-filters">Apply Filters</button>
<button id="clear-filters">Clear Filters</button>
</div>
</div>
`;
// Get current URL parameters
const urlParams = new URLSearchParams(window.location.search);
const currentCategories = urlParams.get('category') ? urlParams.get('category').split(',') : [];
const currentTypes = urlParams.get('type') ? urlParams.get('type').split(',') : [];
// Get containers
const categoriesContainer = hamburgerMenu.querySelector('#category-filters');
const typesContainer = hamburgerMenu.querySelector('#type-filters');
// Get data from body attributes
const categories = JSON.parse(document.body.dataset.categories || '[]');
const types = JSON.parse(document.body.dataset.types || '[]');
// Create checkboxes for categories
categories.forEach(category => {
const label = document.createElement('label');
label.style.display = 'block';
label.style.marginBottom = '5px';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = category;
checkbox.name = 'category';
const isChecked = currentCategories.includes(category);
label.appendChild(checkbox);
label.appendChild(document.createTextNode(' ' + category));
categoriesContainer.appendChild(label);
checkbox.checked = isChecked;
});
// Create checkboxes for types
types.forEach(type => {
const label = document.createElement('label');
label.style.display = 'block';
label.style.marginBottom = '5px';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = type;
checkbox.name = 'type';
const isChecked = currentTypes.includes(type);
label.appendChild(checkbox);
label.appendChild(document.createTextNode(' ' + type));
typesContainer.appendChild(label);
checkbox.checked = isChecked;
});
// Apply filters event
const applyFiltersBtn = hamburgerMenu.querySelector('#apply-filters');
applyFiltersBtn.addEventListener('click', () => {
const selectedCategories = Array.from(
categoriesContainer.querySelectorAll('input:checked')
).map(cb => cb.value);
const selectedTypes = Array.from(
typesContainer.querySelectorAll('input:checked')
).map(cb => cb.value);
const params = new URLSearchParams(window.location.search);
if (selectedCategories.length > 0) {
params.set('category', selectedCategories.join(','));
} else {
params.delete('category');
}
if (selectedTypes.length > 0) {
params.set('type', selectedTypes.join(','));
} else {
params.delete('type');
}
params.set('page', '1');
window.location.search = params.toString();
});
// Clear filters event
const clearFiltersBtn = hamburgerMenu.querySelector('#clear-filters');
clearFiltersBtn.addEventListener('click', () => {
const params = new URLSearchParams(window.location.search);
params.delete('category');
params.delete('type');
params.set('page', '1');
window.location.search = params.toString();
});
}
console.log('Adding hamburger menu to body');
// Add to body
document.body.appendChild(hamburgerMenu);
console.log('Hamburger menu added, setting up event listeners');
// Toggle hamburger menu
const hamburgerIcon = hamburgerMenu.querySelector('.hamburger-icon');
const hamburgerContent = hamburgerMenu.querySelector('.hamburger-content');
if (hamburgerIcon && hamburgerContent) {
hamburgerIcon.addEventListener('click', () => {
console.log('Hamburger icon clicked');
hamburgerContent.classList.toggle('open');
document.body.classList.toggle('menu-open');
});
// Close hamburger menu
const closeButton = hamburgerMenu.querySelector('.close-hamburger');
if (closeButton) {
closeButton.addEventListener('click', () => {
console.log('Close button clicked');
hamburgerContent.classList.remove('open');
document.body.classList.remove('menu-open');
});
}
console.log('Hamburger menu created successfully');
} else {
console.error('Failed to find hamburger icon or content');
}
}
function setupInlineEditing(hamburgerMenu) {
const editableFields = hamburgerMenu.querySelectorAll('.editable-field');
editableFields.forEach(field => {
const valueSpan = field.querySelector('.editable-value');
const dropdown = field.querySelector('.edit-dropdown');
const select = field.querySelector('.field-select');
const saveBtn = field.querySelector('.save-btn');
const cancelBtn = field.querySelector('.cancel-btn');
const fieldName = field.dataset.field;
// Make value span clickable
valueSpan.style.cursor = 'pointer';
valueSpan.style.padding = '4px 8px';
valueSpan.style.borderRadius = '4px';
valueSpan.style.transition = 'background-color 0.2s';
// Hover effect
valueSpan.addEventListener('mouseenter', () => {
valueSpan.style.backgroundColor = 'var(--hover-bg, #f0f0f0)';
});
valueSpan.addEventListener('mouseleave', () => {
if (dropdown.style.display === 'none') {
valueSpan.style.backgroundColor = 'transparent';
}
});
// Click to edit
valueSpan.addEventListener('click', () => {
dropdown.style.display = 'block';
valueSpan.style.backgroundColor = 'var(--hover-bg, #f0f0f0)';
select.focus();
});
// Save changes
saveBtn.addEventListener('click', () => {
const newValue = select.value;
const oldValue = valueSpan.dataset.current;
if (newValue !== oldValue) {
saveFieldChange(fieldName, newValue, valueSpan, dropdown);
} else {
cancelEdit(valueSpan, dropdown);
}
});
// Cancel changes
cancelBtn.addEventListener('click', () => {
select.value = valueSpan.dataset.current;
cancelEdit(valueSpan, dropdown);
});
// Cancel on escape key
select.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
select.value = valueSpan.dataset.current;
cancelEdit(valueSpan, dropdown);
} else if (e.key === 'Enter') {
saveBtn.click();
}
});
// Cancel when clicking outside
document.addEventListener('click', (e) => {
if (!field.contains(e.target) && dropdown.style.display === 'block') {
select.value = valueSpan.dataset.current;
cancelEdit(valueSpan, dropdown);
}
});
});
}
function saveFieldChange(fieldName, newValue, valueSpan, dropdown) {
if (!window.ticketData) {
console.error('No ticket data available');
return;
}
const data = {
ticket_id: parseInt(window.ticketData.id),
[fieldName]: fieldName === 'priority' ? parseInt(newValue) : newValue
};
console.log('Saving field change:', data);
// Show loading state
valueSpan.style.opacity = '0.6';
fetch('/api/update_ticket.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
})
.then(response => {
return response.text().then(text => {
try {
return JSON.parse(text);
} catch (e) {
throw new Error('Invalid JSON response: ' + text);
}
});
})
.then(result => {
console.log('Update result:', result);
if (result.success) {
// Update the hamburger menu display
if (fieldName === 'priority') {
valueSpan.textContent = 'P' + newValue;
} else {
valueSpan.textContent = newValue;
}
valueSpan.dataset.current = newValue;
// Update window.ticketData
window.ticketData[fieldName] = fieldName === 'priority' ? parseInt(newValue) : newValue;
// Update main page elements
if (fieldName === 'status') {
const statusDisplay = document.getElementById('statusDisplay');
if (statusDisplay) {
// Remove all existing status classes
statusDisplay.className = statusDisplay.className.replace(/status-\S+/g, '').trim();
// Create the correct CSS class name to match your CSS
// "Open" -> "status-Open"
// "In Progress" -> "status-In-Progress"
// "Closed" -> "status-Closed"
const cssClass = newValue.replace(/\s+/g, '-'); // "In Progress" -> "In-Progress"
const fullClassName = `status-${cssClass}`;
statusDisplay.className = fullClassName;
statusDisplay.textContent = newValue;
console.log('Updated status display class to:', fullClassName);
console.log('Status display element:', statusDisplay);
}
}
if (fieldName === 'priority') {
const priorityDisplay = document.querySelector('.priority-indicator');
if (priorityDisplay) {
// Remove all priority classes first
priorityDisplay.className = priorityDisplay.className.replace(/priority-\d+/g, '');
priorityDisplay.className = `priority-indicator priority-${newValue}`;
priorityDisplay.textContent = 'P' + newValue;
}
// Update the ticket container's data-priority attribute for styling
const ticketContainer = document.querySelector('.ticket-container');
if (ticketContainer) {
ticketContainer.setAttribute('data-priority', newValue);
}
}
console.log('Field updated successfully');
cancelEdit(valueSpan, dropdown);
} else {
console.error('Error updating field:', result.error || 'Unknown error');
alert('Error updating ' + fieldName + ': ' + (result.error || 'Unknown error'));
cancelEdit(valueSpan, dropdown);
}
})
.catch(error => {
console.error('Error updating field:', error);
alert('Error updating ' + fieldName + ': ' + error.message);
cancelEdit(valueSpan, dropdown);
})
.finally(() => {
valueSpan.style.opacity = '1';
});
}
function cancelEdit(valueSpan, dropdown) {
dropdown.style.display = 'none';
valueSpan.style.backgroundColor = 'transparent';
}
// Ticket page functions (if needed)
function saveTicket() {
const editables = document.querySelectorAll('.editable');
const data = {};
let ticketId;
if (window.location.href.includes('?id=')) {
ticketId = window.location.href.split('id=')[1];
} else {
const matches = window.location.pathname.match(/\/ticket\/(\d+)/);
ticketId = matches ? matches[1] : null;
}
editables.forEach(field => {
if (field.dataset.field) {
data[field.dataset.field] = field.value;
}
});
fetch('/api/update_ticket.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
ticket_id: ticketId,
...data
})
})
.then(response => response.json())
.then(data => {
if(data.success) {
const statusDisplay = document.getElementById('statusDisplay');
if (statusDisplay) {
statusDisplay.className = `status-${data.status}`;
statusDisplay.textContent = data.status;
}
}
});
}
/**
* Load template data into the create ticket form
*/
function loadTemplate() {
const templateSelect = document.getElementById('templateSelect');
const templateId = templateSelect.value;
if (!templateId) {
// Clear form when "No Template" is selected
document.getElementById('title').value = '';
document.getElementById('description').value = '';
document.getElementById('priority').value = '4';
document.getElementById('category').value = 'General';
document.getElementById('type').value = 'Issue';
return;
}
// Fetch template data
fetch(`/api/get_template.php?template_id=${templateId}`)
.then(response => {
if (!response.ok) {
throw new Error('Failed to fetch template');
}
return response.json();
})
.then(data => {
if (data.success && data.template) {
const template = data.template;
// Populate form fields with template data
if (template.title_template) {
document.getElementById('title').value = template.title_template;
}
if (template.description_template) {
document.getElementById('description').value = template.description_template;
}
if (template.category) {
document.getElementById('category').value = template.category;
}
if (template.type) {
document.getElementById('type').value = template.type;
}
if (template.default_priority) {
document.getElementById('priority').value = template.default_priority;
}
console.log('Template loaded:', template.template_name);
} else {
console.error('Failed to load template:', data.error);
alert('Failed to load template: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error loading template:', error);
alert('Error loading template: ' + error.message);
});
}
/**
* Bulk Actions Functions (Admin only)
*/
function toggleSelectAll() {
const selectAll = document.getElementById('selectAllCheckbox');
const checkboxes = document.querySelectorAll('.ticket-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = selectAll.checked;
});
updateSelectionCount();
}
function updateSelectionCount() {
const checkboxes = document.querySelectorAll('.ticket-checkbox:checked');
const count = checkboxes.length;
const toolbar = document.querySelector('.bulk-actions-toolbar');
const countDisplay = document.getElementById('selected-count');
if (count > 0) {
toolbar.style.display = 'flex';
countDisplay.textContent = count;
} else {
toolbar.style.display = 'none';
}
}
function getSelectedTicketIds() {
const checkboxes = document.querySelectorAll('.ticket-checkbox:checked');
return Array.from(checkboxes).map(cb => parseInt(cb.value));
}
function clearSelection() {
document.querySelectorAll('.ticket-checkbox').forEach(cb => cb.checked = false);
const selectAll = document.getElementById('selectAllCheckbox');
if (selectAll) selectAll.checked = false;
updateSelectionCount();
}
function bulkClose() {
const ticketIds = getSelectedTicketIds();
if (ticketIds.length === 0) {
alert('No tickets selected');
return;
}
if (!confirm(`Close ${ticketIds.length} ticket(s)?`)) {
return;
}
fetch('/api/bulk_operation.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
operation_type: 'bulk_close',
ticket_ids: ticketIds
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`Bulk Close Complete:\n${data.processed} succeeded\n${data.failed} failed`);
window.location.reload();
} else {
alert('Error: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error performing bulk close:', error);
alert('Error performing bulk close: ' + error.message);
});
}
function showBulkAssignModal() {
const ticketIds = getSelectedTicketIds();
if (ticketIds.length === 0) {
alert('No tickets selected');
return;
}
// Create modal HTML
const modalHtml = `
<div class="modal-overlay" id="bulkAssignModal">
<div class="modal-content">
<h3>Assign ${ticketIds.length} Ticket(s)</h3>
<div class="modal-body">
<label for="bulkAssignUser">Assign to:</label>
<select id="bulkAssignUser" class="editable">
<option value="">Select User...</option>
<!-- Users will be loaded dynamically -->
</select>
</div>
<div class="modal-footer">
<button onclick="performBulkAssign()" class="btn btn-bulk">Assign</button>
<button onclick="closeBulkAssignModal()" class="btn btn-secondary">Cancel</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
// Fetch users for the dropdown
fetch('/api/get_users.php')
.then(response => response.json())
.then(data => {
if (data.success && data.users) {
const select = document.getElementById('bulkAssignUser');
data.users.forEach(user => {
const option = document.createElement('option');
option.value = user.user_id;
option.textContent = user.display_name || user.username;
select.appendChild(option);
});
}
})
.catch(error => {
console.error('Error loading users:', error);
});
}
function closeBulkAssignModal() {
const modal = document.getElementById('bulkAssignModal');
if (modal) {
modal.remove();
}
}
function performBulkAssign() {
const userId = document.getElementById('bulkAssignUser').value;
const ticketIds = getSelectedTicketIds();
if (!userId) {
alert('Please select a user');
return;
}
fetch('/api/bulk_operation.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
operation_type: 'bulk_assign',
ticket_ids: ticketIds,
parameters: { assigned_to: parseInt(userId) }
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`Bulk Assign Complete:\n${data.processed} succeeded\n${data.failed} failed`);
closeBulkAssignModal();
window.location.reload();
} else {
alert('Error: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error performing bulk assign:', error);
alert('Error performing bulk assign: ' + error.message);
});
}
function showBulkPriorityModal() {
const ticketIds = getSelectedTicketIds();
if (ticketIds.length === 0) {
alert('No tickets selected');
return;
}
const modalHtml = `
<div class="modal-overlay" id="bulkPriorityModal">
<div class="modal-content">
<h3>Change Priority for ${ticketIds.length} Ticket(s)</h3>
<div class="modal-body">
<label for="bulkPriority">Priority:</label>
<select id="bulkPriority" class="editable">
<option value="">Select Priority...</option>
<option value="1">P1 - Critical Impact</option>
<option value="2">P2 - High Impact</option>
<option value="3">P3 - Medium Impact</option>
<option value="4">P4 - Low Impact</option>
<option value="5">P5 - Minimal Impact</option>
</select>
</div>
<div class="modal-footer">
<button onclick="performBulkPriority()" class="btn btn-bulk">Update</button>
<button onclick="closeBulkPriorityModal()" class="btn btn-secondary">Cancel</button>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHtml);
}
function closeBulkPriorityModal() {
const modal = document.getElementById('bulkPriorityModal');
if (modal) {
modal.remove();
}
}
function performBulkPriority() {
const priority = document.getElementById('bulkPriority').value;
const ticketIds = getSelectedTicketIds();
if (!priority) {
alert('Please select a priority');
return;
}
fetch('/api/bulk_operation.php', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
operation_type: 'bulk_priority',
ticket_ids: ticketIds,
parameters: { priority: parseInt(priority) }
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert(`Bulk Priority Update Complete:\n${data.processed} succeeded\n${data.failed} failed`);
closeBulkPriorityModal();
window.location.reload();
} else {
alert('Error: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
console.error('Error performing bulk priority update:', error);
alert('Error performing bulk priority update: ' + error.message);
});
}