Sidebar with no hamburger menu

This commit is contained in:
2026-01-07 17:47:11 -05:00
parent 0f25c49d5c
commit cf2d596219
3 changed files with 534 additions and 808 deletions

View File

@@ -90,7 +90,6 @@ h1 {
.user-header { .user-header {
background: var(--bg-secondary); background: var(--bg-secondary);
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
margin-left: 50px; /* Space for hamburger menu */
color: var(--terminal-green); color: var(--terminal-green);
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -1163,223 +1162,307 @@ input[type="checkbox"]:checked {
cursor: pointer; cursor: pointer;
} }
/* ===== HAMBURGER MENU STYLES - TERMINAL EDITION ===== */ /* ===== COLLAPSIBLE ASCII BANNER ===== */
.hamburger-menu { .ascii-banner-wrapper {
position: absolute; max-width: 1600px;
top: 20px; margin: 0 auto 1rem auto;
left: 20px; border: 2px solid var(--terminal-green);
z-index: 100;
}
.hamburger-icon {
cursor: pointer;
font-size: 24px;
background: var(--bg-secondary); background: var(--bg-secondary);
padding: 12px;
border: 2px solid var(--terminal-green);
border-radius: 0;
box-shadow: none;
font-family: var(--font-mono);
color: var(--terminal-green);
} }
.hamburger-icon::before { .ascii-banner-wrapper.collapsed .banner-content {
content: '['; display: none;
margin-right: 4px;
} }
.hamburger-icon::after { .banner-toggle {
content: ']';
margin-left: 4px;
}
.hamburger-content {
position: fixed;
top: 0;
left: -300px;
width: 250px;
height: 100%;
background: var(--bg-primary);
border-right: 3px double var(--terminal-green);
transition: left 0.3s ease;
padding: 20px;
overflow-y: auto;
z-index: 99;
font-family: var(--font-mono);
box-shadow: 0 0 30px rgba(0, 255, 65, 0.3);
}
/* ASCII decoration at top */
.hamburger-content::before {
content: '╔═══════════════════╗\A║ MENU SYSTEM ║\A╚═══════════════════╝';
white-space: pre;
display: block;
color: var(--terminal-green);
font-family: var(--font-mono);
font-size: 0.8rem;
margin-bottom: 20px;
line-height: 1.2;
}
.hamburger-content.open {
left: 0;
}
.hamburger-content h3 {
color: var(--terminal-amber);
text-shadow: var(--glow-amber);
font-family: var(--font-mono);
text-transform: uppercase;
margin-top: 20px;
margin-bottom: 10px;
}
.hamburger-content h3::before {
content: '> ';
color: var(--terminal-green);
}
.close-hamburger {
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
font-size: 24px;
background: transparent;
padding: 10px;
border: 2px solid var(--terminal-green);
border-radius: 0;
box-shadow: none;
color: var(--terminal-green);
font-family: var(--font-mono);
}
.close-hamburger::before {
content: '[';
}
.close-hamburger::after {
content: ']';
}
.close-hamburger:hover {
color: var(--priority-1);
border-color: var(--priority-1);
text-shadow: var(--glow-red);
}
/* Hamburger menu inline editing styles - TERMINAL */
.ticket-info-editable {
padding: 10px 0;
}
.editable-field, .info-field {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
position: relative;
padding: 10px;
border: 1px solid var(--terminal-green);
background: rgba(0, 255, 65, 0.03);
}
.editable-field label, .info-field label {
flex: 0 0 auto;
margin-right: 10px;
color: var(--terminal-green);
font-family: var(--font-mono);
}
.editable-value {
flex: 1;
text-align: right;
min-height: 20px;
display: inline-block;
cursor: pointer;
padding: 4px 8px;
border-radius: 0;
transition: all 0.2s;
color: var(--terminal-amber);
}
.editable-value::before {
content: '[ ';
color: var(--terminal-green);
}
.editable-value::after {
content: ' ]';
color: var(--terminal-green);
}
.editable-value:hover {
background-color: rgba(0, 255, 65, 0.1) !important;
text-shadow: var(--glow-amber);
}
.edit-dropdown {
position: absolute;
top: 100%;
right: 0;
background: var(--bg-primary);
border: 2px solid var(--terminal-green);
border-radius: 0;
padding: 8px;
box-shadow: 0 0 20px rgba(0, 255, 65, 0.3);
z-index: 1000;
min-width: 150px;
}
.field-select {
width: 100%; width: 100%;
padding: 4px 8px; background: var(--bg-secondary);
border: none;
color: var(--terminal-amber);
padding: 0.5rem 1rem;
cursor: pointer;
font-family: var(--font-mono);
text-align: left;
font-size: 0.9rem;
text-shadow: var(--glow-amber);
transition: all 0.2s ease;
}
.banner-toggle:hover {
background: var(--hover-bg);
box-shadow: inset 0 0 20px rgba(255, 176, 0, 0.2);
}
.toggle-icon {
display: inline-block;
width: 1rem;
text-align: center;
color: var(--terminal-green);
transition: transform 0.3s ease;
}
.banner-content {
padding: 1rem;
text-align: center;
}
/* ===== CONDENSED TOOLBAR ===== */
.dashboard-toolbar {
display: flex;
flex-wrap: wrap;
gap: 1rem;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: var(--bg-secondary);
border: 2px solid var(--terminal-green); border: 2px solid var(--terminal-green);
margin-bottom: 1rem;
box-shadow: var(--glow-green);
}
.toolbar-left {
display: flex;
align-items: center;
gap: 1rem;
flex: 1;
min-width: 300px;
}
.dashboard-title {
font-family: var(--font-mono);
color: var(--terminal-green);
text-shadow: var(--glow-green);
font-size: 1.5rem;
margin: 0;
white-space: nowrap;
}
.toolbar-search {
display: flex;
gap: 0.5rem;
flex: 1;
}
.toolbar-center {
display: flex;
align-items: center;
gap: 1rem;
}
.toolbar-right {
display: flex;
align-items: center;
}
.ticket-count {
font-family: var(--font-mono);
color: var(--terminal-amber);
text-shadow: var(--glow-amber);
font-size: 0.9rem;
white-space: nowrap;
}
.clear-search-btn {
background: var(--bg-primary);
color: var(--priority-1);
border: 2px solid var(--priority-1);
padding: 0.5rem 0.75rem;
font-family: var(--font-mono);
text-decoration: none;
cursor: pointer;
transition: all 0.2s ease;
}
.clear-search-btn:hover {
background: var(--priority-1);
color: var(--bg-primary);
box-shadow: var(--glow-red);
}
.search-results-info {
background: var(--bg-secondary);
border: 2px solid var(--terminal-amber);
padding: 0.75rem 1rem;
margin-bottom: 1rem;
font-family: var(--font-mono);
color: var(--terminal-amber);
text-shadow: var(--glow-amber);
}
/* Inline Bulk Actions */
.bulk-actions-inline {
padding: 0.75rem 1rem;
background: var(--bg-primary);
border: 2px solid var(--terminal-amber);
border-radius: 0; border-radius: 0;
margin-bottom: 8px; margin-bottom: 1rem;
display: flex;
gap: 1rem;
align-items: center;
font-family: var(--font-mono);
color: var(--terminal-amber);
text-shadow: var(--glow-amber);
box-shadow: inset 0 0 20px rgba(255, 176, 0, 0.2);
}
.bulk-actions-inline .btn {
padding: 0.4rem 0.8rem;
font-size: 0.85rem;
}
/* Mobile: Stack toolbar items */
@media (max-width: 1024px) {
.dashboard-toolbar {
flex-direction: column;
align-items: stretch;
}
.toolbar-left,
.toolbar-center,
.toolbar-right {
width: 100%;
justify-content: space-between;
}
.toolbar-search {
min-width: 100%;
}
.dashboard-title {
font-size: 1.2rem;
}
}
@media (max-width: 768px) {
.toolbar-left {
flex-direction: column;
align-items: stretch;
}
.dashboard-title {
text-align: center;
}
}
/* ===== DASHBOARD SIDEBAR LAYOUT ===== */
.dashboard-layout {
display: flex;
gap: 1.5rem;
max-width: 1600px;
margin: 0 auto;
padding: 0 1rem;
}
.dashboard-sidebar {
width: 250px;
flex-shrink: 0;
position: sticky;
top: 1rem;
max-height: calc(100vh - 2rem);
overflow-y: auto;
}
.dashboard-sidebar .ascii-frame-inner {
background: var(--bg-secondary);
padding: 1rem;
border: 2px solid var(--terminal-green);
box-shadow: var(--glow-green);
}
.dashboard-sidebar .ascii-subsection-header {
color: var(--terminal-amber);
text-shadow: var(--glow-amber);
font-family: var(--font-mono);
font-size: 1.1rem;
text-transform: uppercase;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--terminal-green);
}
.filter-group {
margin-bottom: 1.5rem;
}
.filter-group h4 {
color: var(--terminal-amber);
text-shadow: var(--glow-amber);
margin: 0 0 0.5rem 0;
font-size: 0.9rem;
text-transform: uppercase;
font-family: var(--font-mono);
}
.filter-group label {
display: block;
margin: 0.4rem 0;
color: var(--terminal-green);
cursor: pointer;
font-size: 0.85rem;
font-family: var(--font-mono);
transition: all 0.2s ease;
}
.filter-group label:hover {
color: var(--terminal-amber);
text-shadow: var(--glow-amber);
padding-left: 4px;
}
.filter-group input[type="checkbox"] {
margin-right: 0.5rem;
cursor: pointer;
accent-color: var(--terminal-green);
}
.dashboard-sidebar .btn {
width: 100%;
margin-top: 0.5rem;
padding: 0.5rem;
background: var(--bg-primary); background: var(--bg-primary);
color: var(--terminal-green); color: var(--terminal-green);
border: 2px solid var(--terminal-green);
font-family: var(--font-mono); font-family: var(--font-mono);
color: var(--text-primary);
}
.edit-actions {
display: flex;
gap: 4px;
justify-content: flex-end;
}
.save-btn, .cancel-btn {
padding: 4px 8px;
border: none;
border-radius: 0;
cursor: pointer; cursor: pointer;
font-size: 12px; transition: all 0.2s ease;
min-width: 24px;
} }
.save-btn { .dashboard-sidebar .btn:hover {
background: #28a745; background: var(--terminal-green);
color: white; color: var(--bg-primary);
box-shadow: var(--glow-green);
} }
.save-btn:hover { .dashboard-sidebar .btn-secondary {
background: #218838; background: transparent;
color: var(--terminal-amber);
border-color: var(--terminal-amber);
} }
.cancel-btn { .dashboard-sidebar .btn-secondary:hover {
background: #dc3545; background: var(--terminal-amber);
color: white; color: var(--bg-primary);
box-shadow: var(--glow-amber);
} }
.cancel-btn:hover { .dashboard-main {
background: #c82333;
}
.info-field span {
flex: 1; flex: 1;
text-align: right; min-width: 0;
color: var(--text-secondary); }
/* Mobile: Stack sidebar above content */
@media (max-width: 768px) {
.dashboard-layout {
flex-direction: column;
padding: 0 0.5rem;
}
.dashboard-sidebar {
width: 100%;
position: static;
max-height: none;
margin-bottom: 1rem;
}
} }
/* ===== UTILITY STYLES ===== */ /* ===== UTILITY STYLES ===== */

View File

@@ -17,23 +17,7 @@ document.addEventListener('DOMContentLoaded', function() {
// Dashboard-specific initialization // Dashboard-specific initialization
initStatusFilter(); initStatusFilter();
initTableSorting(); initTableSorting();
initSidebarFilters();
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 // Initialize for all pages
@@ -55,6 +39,72 @@ function initTableSorting() {
}); });
} }
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() { function initSettingsModal() {
const settingsIcon = document.querySelector('.settings-icon'); const settingsIcon = document.querySelector('.settings-icon');
if (settingsIcon) { if (settingsIcon) {
@@ -354,452 +404,6 @@ function quickSave() {
}); });
} }
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) // Ticket page functions (if needed)
function saveTicket() { function saveTicket() {
@@ -921,9 +525,10 @@ function toggleSelectAll() {
function updateSelectionCount() { function updateSelectionCount() {
const checkboxes = document.querySelectorAll('.ticket-checkbox:checked'); const checkboxes = document.querySelectorAll('.ticket-checkbox:checked');
const count = checkboxes.length; const count = checkboxes.length;
const toolbar = document.querySelector('.bulk-actions-toolbar'); const toolbar = document.querySelector('.bulk-actions-inline');
const countDisplay = document.getElementById('selected-count'); const countDisplay = document.getElementById('selected-count');
if (toolbar && countDisplay) {
if (count > 0) { if (count > 0) {
toolbar.style.display = 'flex'; toolbar.style.display = 'flex';
countDisplay.textContent = count; countDisplay.textContent = count;
@@ -931,6 +536,7 @@ function updateSelectionCount() {
toolbar.style.display = 'none'; toolbar.style.display = 'none';
} }
} }
}
function getSelectedTicketIds() { function getSelectedTicketIds() {
const checkboxes = document.querySelectorAll('.ticket-checkbox:checked'); const checkboxes = document.querySelectorAll('.ticket-checkbox:checked');

View File

@@ -28,46 +28,105 @@
</div> </div>
</div> </div>
<!-- ASCII Banner Container --> <!-- Collapsible ASCII Banner -->
<div id="ascii-banner-container" style="margin: 2rem auto; text-align: center; max-width: 1200px;"></div> <div class="ascii-banner-wrapper collapsed">
<button class="banner-toggle" onclick="toggleBanner()">
<span class="toggle-icon">▼</span> ASCII Banner
</button>
<div id="ascii-banner-container" class="banner-content"></div>
</div>
<script> <script>
// Render ASCII banner on page load with typewriter effect function toggleBanner() {
document.addEventListener('DOMContentLoaded', function() { const wrapper = document.querySelector('.ascii-banner-wrapper');
renderResponsiveBanner('#ascii-banner-container', 3); const icon = document.querySelector('.toggle-icon');
}); wrapper.classList.toggle('collapsed');
icon.textContent = wrapper.classList.contains('collapsed') ? '▼' : '▲';
// Render banner on first expand (no animation for instant display)
if (!wrapper.classList.contains('collapsed') && !wrapper.dataset.rendered) {
renderResponsiveBanner('#ascii-banner-container', 0); // Speed 0 = no animation
wrapper.dataset.rendered = 'true';
}
}
</script> </script>
<!-- OUTER FRAME: Dashboard Container --> <!-- Dashboard Layout with Sidebar -->
<div class="ascii-frame-outer"> <div class="dashboard-layout">
<span class="bottom-left-corner">╚</span> <!-- Left Sidebar with Filters -->
<span class="bottom-right-corner">╝</span> <aside class="dashboard-sidebar">
<!-- SECTION 1: Dashboard Header & Actions -->
<div class="ascii-section-header">Dashboard Control Center</div>
<div class="ascii-content">
<div class="ascii-frame-inner"> <div class="ascii-frame-inner">
<div class="dashboard-header"> <div class="ascii-subsection-header">Filters</div>
<h1>Ticket Dashboard</h1>
<button onclick="window.location.href='<?php echo $GLOBALS['config']['BASE_URL']; ?>/ticket/create'" class="btn create-ticket">New Ticket</button> <!-- Status Filter -->
</div> <div class="filter-group">
</div> <h4>Status</h4>
<?php
$currentStatus = isset($_GET['status']) ? explode(',', $_GET['status']) : ['Open', 'In Progress'];
$allStatuses = ['Open', 'In Progress', 'Closed'];
foreach ($allStatuses as $status):
?>
<label>
<input type="checkbox"
name="status"
value="<?php echo $status; ?>"
<?php echo in_array($status, $currentStatus) ? 'checked' : ''; ?>>
<?php echo $status; ?>
</label>
<?php endforeach; ?>
</div> </div>
<!-- DIVIDER --> <!-- Category Filter -->
<div class="ascii-divider"></div> <div class="filter-group">
<h4>Category</h4>
<?php
$currentCategories = isset($_GET['category']) ? explode(',', $_GET['category']) : [];
foreach ($categories as $cat):
?>
<label>
<input type="checkbox"
name="category"
value="<?php echo $cat; ?>"
<?php echo in_array($cat, $currentCategories) ? 'checked' : ''; ?>>
<?php echo htmlspecialchars($cat); ?>
</label>
<?php endforeach; ?>
</div>
<!-- SECTION 2: Search & Filter --> <!-- Type Filter -->
<div class="ascii-section-header">Search & Filter</div> <div class="filter-group">
<div class="ascii-content"> <h4>Type</h4>
<div class="ascii-frame-inner"> <?php
<form method="GET" action="" class="search-form"> $currentTypes = isset($_GET['type']) ? explode(',', $_GET['type']) : [];
foreach ($types as $type):
?>
<label>
<input type="checkbox"
name="type"
value="<?php echo $type; ?>"
<?php echo in_array($type, $currentTypes) ? 'checked' : ''; ?>>
<?php echo htmlspecialchars($type); ?>
</label>
<?php endforeach; ?>
</div>
<button id="apply-filters-btn" class="btn">Apply Filters</button>
<button id="clear-filters-btn" class="btn btn-secondary">Clear All</button>
</div>
</aside>
<!-- Main Content Area -->
<main class="dashboard-main">
<!-- CONDENSED TOOLBAR: Combined Header, Search, Actions, Pagination -->
<div class="dashboard-toolbar">
<!-- Left: Title + Search -->
<div class="toolbar-left">
<h1 class="dashboard-title">🎫 Tickets</h1>
<form method="GET" action="" class="toolbar-search">
<!-- Preserve existing parameters --> <!-- Preserve existing parameters -->
<?php if (isset($_GET['status'])): ?> <?php if (isset($_GET['status'])): ?>
<input type="hidden" name="status" value="<?php echo htmlspecialchars($_GET['status']); ?>"> <input type="hidden" name="status" value="<?php echo htmlspecialchars($_GET['status']); ?>">
<?php endif; ?> <?php endif; ?>
<?php if (isset($_GET['show_all'])): ?>
<input type="hidden" name="show_all" value="<?php echo htmlspecialchars($_GET['show_all']); ?>">
<?php endif; ?>
<?php if (isset($_GET['category'])): ?> <?php if (isset($_GET['category'])): ?>
<input type="hidden" name="category" value="<?php echo htmlspecialchars($_GET['category']); ?>"> <input type="hidden" name="category" value="<?php echo htmlspecialchars($_GET['category']); ?>">
<?php endif; ?> <?php endif; ?>
@@ -83,35 +142,24 @@
<input type="text" <input type="text"
name="search" name="search"
placeholder="Search tickets..." placeholder="🔍 Search tickets..."
class="search-box" class="search-box"
value="<?php echo isset($_GET['search']) ? htmlspecialchars($_GET['search']) : ''; ?>"> value="<?php echo isset($_GET['search']) ? htmlspecialchars($_GET['search']) : ''; ?>">
<button type="submit" class="search-btn">Search</button> <button type="submit" class="btn search-btn">Search</button>
<?php if (isset($_GET['search']) && !empty($_GET['search'])): ?> <?php if (isset($_GET['search']) && !empty($_GET['search'])): ?>
<a href="?" class="clear-search-btn">Clear</a> <a href="?" class="clear-search-btn"></a>
<?php endif; ?> <?php endif; ?>
</form> </form>
<?php if (isset($_GET['search']) && !empty($_GET['search'])): ?>
<div class="search-results-info">
Showing results for: "<strong><?php echo htmlspecialchars($_GET['search']); ?></strong>"
(<?php echo $totalTickets; ?> ticket<?php echo $totalTickets != 1 ? 's' : ''; ?> found)
</div>
<?php endif; ?>
</div>
</div> </div>
<!-- DIVIDER --> <!-- Center: Actions + Count -->
<div class="ascii-divider"></div> <div class="toolbar-center">
<button onclick="window.location.href='<?php echo $GLOBALS['config']['BASE_URL']; ?>/ticket/create'" class="btn create-ticket">+ New Ticket</button>
<!-- SECTION 3: Table Controls --> <span class="ticket-count">Total: <?php echo $totalTickets; ?></span>
<div class="ascii-section-header">Table Controls</div>
<div class="ascii-content">
<div class="ascii-frame-inner">
<div class="table-controls">
<div class="ticket-count">
Total Tickets: <?php echo $totalTickets; ?>
</div> </div>
<div class="table-actions">
<!-- Right: Pagination -->
<div class="toolbar-right">
<div class="pagination"> <div class="pagination">
<?php <?php
$currentParams = $_GET; $currentParams = $_GET;
@@ -139,47 +187,36 @@
} }
?> ?>
</div> </div>
<div class="settings-icon">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
</div>
</div>
</div>
</div>
</div>
<!-- DIVIDER -->
<div class="ascii-divider"></div>
<!-- SECTION 4: Bulk Actions (if admin) -->
<?php if ($GLOBALS['currentUser']['is_admin'] ?? false): ?>
<div class="ascii-section-header">Bulk Operations</div>
<div class="ascii-content">
<div class="ascii-frame-inner">
<div class="bulk-actions-toolbar" style="display: none;">
<div class="bulk-actions-info">
<span id="selected-count">0</span> tickets selected
</div>
<div class="bulk-actions-buttons">
<button onclick="bulkClose()" class="btn btn-bulk">Close Selected</button>
<button onclick="showBulkAssignModal()" class="btn btn-bulk">Assign Selected</button>
<button onclick="showBulkPriorityModal()" class="btn btn-bulk">Change Priority</button>
<button onclick="clearSelection()" class="btn btn-secondary">Clear Selection</button>
</div>
</div>
</div> </div>
</div> </div>
<!-- DIVIDER --> <?php if (isset($_GET['search']) && !empty($_GET['search'])): ?>
<div class="ascii-divider"></div> <div class="search-results-info">
Showing results for: "<strong><?php echo htmlspecialchars($_GET['search']); ?></strong>"
(<?php echo $totalTickets; ?> ticket<?php echo $totalTickets != 1 ? 's' : ''; ?> found)
</div>
<?php endif; ?> <?php endif; ?>
<!-- SECTION 5: Ticket Table --> <!-- TICKET TABLE WITH INLINE BULK ACTIONS -->
<div class="ascii-frame-outer">
<span class="bottom-left-corner">╚</span>
<span class="bottom-right-corner">╝</span>
<div class="ascii-section-header">Ticket List</div> <div class="ascii-section-header">Ticket List</div>
<div class="ascii-content"> <div class="ascii-content">
<div class="ascii-frame-inner"> <div class="ascii-frame-inner">
<!-- Inline Bulk Actions (appears above table when items selected) -->
<?php if ($GLOBALS['currentUser']['is_admin'] ?? false): ?>
<div class="bulk-actions-inline" style="display: none;">
<span id="selected-count">0</span> tickets selected
<button onclick="bulkClose()" class="btn btn-bulk">Close</button>
<button onclick="showBulkAssignModal()" class="btn btn-bulk">Assign</button>
<button onclick="showBulkPriorityModal()" class="btn btn-bulk">Priority</button>
<button onclick="clearSelection()" class="btn btn-secondary">Clear</button>
</div>
<?php endif; ?>
<!-- Table -->
<table> <table>
<thead> <thead>
<tr> <tr>