Files
tinker_tickets/assets/js/dashboard.js
Jared Vititoe e0b7ce374d Phase 5: Update modals and hamburger menus with ASCII frames
## Hamburger Menu Updates
- Ticket page menu: Added ascii-subsection-header and ascii-frame-inner wrapper
- Dashboard menu: Added ascii-subsection-header and dashboard-filters wrapper
- Maintains all inline editing functionality for ticket fields
- Preserves all filter checkbox functionality for dashboard

## Settings Modal Enhancement
- Wrapped in ascii-frame-outer with ╚╝ bottom corners
- Added ascii-section-header for title
- Nested content in ascii-content → ascii-frame-inner
- Added ascii-divider before footer
- Moved close button to footer for better layout

## Bulk Operations Modals
- Bulk Assign Modal: Full ASCII frame structure with nested sections
- Bulk Priority Modal: Full ASCII frame structure with nested sections
- Both modals now have:
  * ascii-frame-outer with corner decorations
  * ascii-section-header for title
  * ascii-content and ascii-frame-inner for body
  * ascii-divider before footer
  * Consistent visual hierarchy with rest of app

## Code Quality
- All event handlers and functionality preserved
- No breaking changes to JavaScript logic
- Consistent frame structure across all dynamically generated UI
- All modals and menus now match the nested frame aesthetic

## Files Modified
- assets/js/dashboard.js: Updated 5 HTML generation functions
  * createHamburgerMenu() - ticket page version
  * createHamburgerMenu() - dashboard version
  * createSettingsModal()
  * showBulkAssignModal()
  * showBulkPriorityModal()

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 10:54:47 -05:00

1172 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
initSettingsModal();
// Force dark mode only (terminal aesthetic - no theme switching)
document.documentElement.setAttribute('data-theme', 'dark');
document.body.classList.add('dark-mode');
});
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 ascii-frame-outer">
<span class="bottom-left-corner">╚</span>
<span class="bottom-right-corner">╝</span>
<div class="ascii-section-header">Dashboard Settings</div>
<div class="ascii-content">
<div class="ascii-frame-inner">
<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>
<div class="ascii-divider"></div>
<div class="ascii-content">
<div class="settings-modal-footer">
<button class="save-settings">Save Settings</button>
<button class="cancel-settings">Cancel</button>
<button class="close-modal">×</button>
</div>
</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 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>
<div class="ascii-subsection-header">Ticket Actions</div>
<div class="ascii-frame-inner">
<div class="ticket-info-editable">
<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>
</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>
<div class="ascii-subsection-header">Filters</div>
<div class="ascii-frame-inner">
<div class="dashboard-filters">
<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>
</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 ascii-frame-outer">
<span class="bottom-left-corner">╚</span>
<span class="bottom-right-corner">╝</span>
<div class="ascii-section-header">Assign ${ticketIds.length} Ticket(s)</div>
<div class="ascii-content">
<div class="ascii-frame-inner">
<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>
</div>
<div class="ascii-divider"></div>
<div class="ascii-content">
<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>
</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 ascii-frame-outer">
<span class="bottom-left-corner">╚</span>
<span class="bottom-right-corner">╝</span>
<div class="ascii-section-header">Change Priority for ${ticketIds.length} Ticket(s)</div>
<div class="ascii-content">
<div class="ascii-frame-inner">
<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>
</div>
<div class="ascii-divider"></div>
<div class="ascii-content">
<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>
</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);
});
}