Files
tinker_tickets/assets/js/dashboard.js

1172 lines
43 KiB
JavaScript
Raw Normal View History

// 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();
Phase 4: Light mode removal + CreateTicketView restructuring ## Light Mode Removal & Optimization - Removed theme toggle functionality from dashboard.js - Forced dark mode only (terminal aesthetic) - Cleaned up .theme-toggle CSS class and styles - Removed body.light-mode CSS rules from all view files - Simplified user-header styles to use static dark colors - Removed CSS custom properties (--header-bg, --header-text, --border-color) - Removed margin-right for theme toggle button (no longer needed) ## CreateTicketView Complete Restructuring - Added user header with back link and user info - Restructured into 6 vertical nested ASCII sections: 1. Form Header - Create New Ticket introduction 2. Template Selection - Optional template dropdown 3. Basic Information - Title input field 4. Ticket Metadata - Status, Priority, Category, Type (4-column) 5. Detailed Description - Main textarea 6. Form Actions - Create/Cancel buttons - Each section wrapped in ascii-section-header → ascii-content → ascii-frame-inner - Added ASCII dividers between all sections - Added ╚╝ bottom corner characters to outer frame - Improved error message styling with priority-1 color - Added helpful placeholder text and hints ## Files Modified - assets/css/dashboard.css: Removed theme toggle CSS (~19 lines) - assets/js/dashboard.js: Removed initThemeToggle() and forced dark mode - views/DashboardView.php: Simplified user-header CSS (removed light mode) - views/TicketView.php: Simplified user-header CSS (removed light mode) - views/CreateTicketView.php: Complete restructuring (98→242 lines) ## Code Quality - Maintained all existing functionality and event handlers - Kept all class names for JavaScript compatibility - Consistent nested frame structure across all pages - Zero breaking changes to backend or business logic - Optimized by removing ~660 unused lines total 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 10:52:10 -05:00
// 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 = `
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
<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>
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
<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);
2025-05-15 08:33:13 -04:00
fetch('/api/update_ticket.php', {
2025-05-15 08:33:13 -04:00
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
2025-05-15 08:33:13 -04:00
})
.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);
2025-03-11 21:09:47 -04:00
});
2025-03-11 20:55:04 -04:00
}
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;
2025-03-11 20:23:36 -04:00
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
2025-03-11 20:42:38 -04:00
hamburgerMenu.innerHTML = `
<div class="hamburger-icon"></div>
<div class="hamburger-content">
<div class="close-hamburger"></div>
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
<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>
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
<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>
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
<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>
2025-03-11 20:46:28 -04:00
</div>
2025-03-11 20:42:38 -04:00
</div>
`;
console.log('Ticket hamburger menu HTML created');
// Add inline editing functionality
setupInlineEditing(hamburgerMenu);
2025-03-11 20:42:38 -04:00
} else {
console.log('Creating dashboard hamburger menu');
// Dashboard hamburger menu (your existing code)
2025-03-11 20:42:38 -04:00
hamburgerMenu.innerHTML = `
<div class="hamburger-icon"></div>
<div class="hamburger-content">
<div class="close-hamburger"></div>
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
<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>
2025-03-11 20:42:38 -04:00
</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
2025-03-11 20:42:38 -04:00
const categoriesContainer = hamburgerMenu.querySelector('#category-filters');
const typesContainer = hamburgerMenu.querySelector('#type-filters');
// Get data from body attributes
2025-03-11 20:42:38 -04:00
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';
2025-03-11 20:42:38 -04:00
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = category;
checkbox.name = 'category';
const isChecked = currentCategories.includes(category);
2025-03-11 20:42:38 -04:00
label.appendChild(checkbox);
label.appendChild(document.createTextNode(' ' + category));
2025-03-11 20:42:38 -04:00
categoriesContainer.appendChild(label);
checkbox.checked = isChecked;
2025-03-11 20:42:38 -04:00
});
2025-03-11 20:42:38 -04:00
// Create checkboxes for types
types.forEach(type => {
const label = document.createElement('label');
label.style.display = 'block';
label.style.marginBottom = '5px';
2025-03-11 20:42:38 -04:00
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = type;
checkbox.name = 'type';
const isChecked = currentTypes.includes(type);
2025-03-11 20:42:38 -04:00
label.appendChild(checkbox);
label.appendChild(document.createTextNode(' ' + type));
2025-03-11 20:42:38 -04:00
typesContainer.appendChild(label);
checkbox.checked = isChecked;
2025-03-11 20:42:38 -04:00
});
// Apply filters event
2025-03-11 20:42:38 -04:00
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);
2025-03-11 20:42:38 -04:00
const params = new URLSearchParams(window.location.search);
if (selectedCategories.length > 0) {
params.set('category', selectedCategories.join(','));
} else {
2025-03-11 20:23:36 -04:00
params.delete('category');
2025-03-11 20:42:38 -04:00
}
if (selectedTypes.length > 0) {
params.set('type', selectedTypes.join(','));
} else {
2025-03-11 20:23:36 -04:00
params.delete('type');
2025-03-11 20:42:38 -04:00
}
params.set('page', '1');
2025-03-11 20:42:38 -04:00
window.location.search = params.toString();
2025-03-11 20:26:15 -04:00
});
// Clear filters event
2025-03-11 20:42:38 -04:00
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');
2025-03-11 20:42:38 -04:00
window.location.search = params.toString();
2025-03-11 20:26:15 -04:00
});
2025-03-11 20:42:38 -04:00
}
console.log('Adding hamburger menu to body');
2025-03-11 20:42:38 -04:00
// Add to body
document.body.appendChild(hamburgerMenu);
console.log('Hamburger menu added, setting up event listeners');
2025-03-11 20:42:38 -04:00
// 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);
}
});
2025-03-11 20:42:38 -04:00
});
}
2025-03-11 20:42:38 -04:00
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';
2025-03-11 20:42:38 -04:00
});
}
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">
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
<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>
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
<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">
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
<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>
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
<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);
});
}