Files
tinker_tickets/assets/js/dashboard.js

817 lines
28 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();
initSidebarFilters();
}
// 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 initSidebarFilters() {
const applyFiltersBtn = document.getElementById('apply-filters-btn');
const clearFiltersBtn = document.getElementById('clear-filters-btn');
if (applyFiltersBtn) {
applyFiltersBtn.addEventListener('click', () => {
const params = new URLSearchParams(window.location.search);
// Collect selected statuses
const selectedStatuses = Array.from(
document.querySelectorAll('.filter-group input[name="status"]:checked')
).map(cb => cb.value);
// Collect selected categories
const selectedCategories = Array.from(
document.querySelectorAll('.filter-group input[name="category"]:checked')
).map(cb => cb.value);
// Collect selected types
const selectedTypes = Array.from(
document.querySelectorAll('.filter-group input[name="type"]:checked')
).map(cb => cb.value);
// Update URL parameters
if (selectedStatuses.length > 0) {
params.set('status', selectedStatuses.join(','));
} else {
params.delete('status');
}
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');
}
// Reset to page 1 when filters change
params.set('page', '1');
// Reload with new parameters
window.location.search = params.toString();
});
}
if (clearFiltersBtn) {
clearFiltersBtn.addEventListener('click', () => {
const params = new URLSearchParams(window.location.search);
// Remove filter parameters
params.delete('status');
params.delete('category');
params.delete('type');
params.set('page', '1');
// Reload with cleared filters
window.location.search = params.toString();
});
}
}
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);
});
}
// 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-inline');
const countDisplay = document.getElementById('selected-count');
if (toolbar && countDisplay) {
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);
});
}
// Make table rows clickable
document.addEventListener('DOMContentLoaded', function() {
const tableRows = document.querySelectorAll('tbody tr');
tableRows.forEach(row => {
// Skip if row already has click handler
if (row.dataset.clickable) return;
row.dataset.clickable = 'true';
row.style.cursor = 'pointer';
row.addEventListener('click', function(e) {
// Don't navigate if clicking on a link, button, checkbox, or select
if (e.target.tagName === 'A' ||
e.target.tagName === 'BUTTON' ||
e.target.tagName === 'INPUT' ||
e.target.tagName === 'SELECT' ||
e.target.closest('a') ||
e.target.closest('button')) {
return;
}
// Find the ticket link in the row
const ticketLink = row.querySelector('.ticket-link');
if (ticketLink) {
window.location.href = ticketLink.href;
}
});
// Add hover effect
row.addEventListener('mouseenter', function() {
this.style.backgroundColor = 'rgba(0, 255, 65, 0.08)';
});
row.addEventListener('mouseleave', function() {
this.style.backgroundColor = '';
});
});
});