style: riviste parti dell'interfaccia dei ticket. feat: aggiunta possiblità di modificare il tempo inserito nelle note ed aggiunta voce per tempo nel menu azioni rapide

This commit is contained in:
2026-07-07 21:14:24 +02:00
parent 7b0b29e6c6
commit 3f6d2cc3a8
8 changed files with 832 additions and 140 deletions
+176 -21
View File
@@ -3,21 +3,50 @@
* Manages ticket list filter state and renders filter dropdowns.
*/
const Filters = {
state: {
queue_id: '',
state_id: '',
priority_id: '',
user_id: '',
date_from: '',
date_to: '',
currentMode: 'general', // 'general' or 'my'
allStates: {
general: {
queue_id: '',
state_id: '',
priority_id: '',
user_id: '',
date_from: '',
date_to: '',
},
my: {
queue_id: '',
state_id: '',
priority_id: '',
user_id: '',
date_from: '',
date_to: '',
}
},
// Dynamic state getter based on active mode
get state() {
return this.allStates[this.currentMode];
},
set state(val) {
this.allStates[this.currentMode] = val;
},
/** Load saved filters from localStorage */
load() {
try {
const saved = localStorage.getItem('otrs_turbo_filters');
if (saved) {
Object.assign(this.state, JSON.parse(saved));
const savedGeneral = localStorage.getItem('otrs_turbo_filters_general');
if (savedGeneral) {
Object.assign(this.allStates.general, JSON.parse(savedGeneral));
}
const savedMy = localStorage.getItem('otrs_turbo_filters_my');
if (savedMy) {
Object.assign(this.allStates.my, JSON.parse(savedMy));
} else {
// Default to active agent if not saved yet
const activeAgentId = localStorage.getItem('activeAgentId') || '1';
this.allStates.my.user_id = activeAgentId;
}
} catch (e) { /* ignore */ }
},
@@ -25,13 +54,17 @@ const Filters = {
/** Save filters to localStorage */
save() {
try {
localStorage.setItem('otrs_turbo_filters', JSON.stringify(this.state));
localStorage.setItem(`otrs_turbo_filters_${this.currentMode}`, JSON.stringify(this.state));
} catch (e) { /* ignore */ }
},
/** Reset all filters */
reset() {
this.state = { queue_id: '', state_id: '', priority_id: '', user_id: '', date_from: '', date_to: '' };
if (this.currentMode === 'my') {
const activeAgentId = localStorage.getItem('activeAgentId') || '1';
this.state.user_id = activeAgentId;
}
this.save();
},
@@ -44,6 +77,36 @@ const Filters = {
return params;
},
/** Helper to compute trigger button label text */
getStateMultiselectLabel(lookups) {
const selectedList = Array.isArray(this.state.state_id)
? this.state.state_id
: (typeof this.state.state_id === 'string' && this.state.state_id ? this.state.state_id.split(',') : []);
if (selectedList.length === 0) {
return 'Tutti';
}
const selectedNames = (lookups.states || [])
.filter(s => selectedList.includes(String(s.id)))
.map(s => s.name);
if (selectedNames.length === (lookups.states || []).length) {
return 'Tutti';
} else if (selectedNames.length <= 2) {
return selectedNames.join(', ');
} else {
return `${selectedNames.length} selezionati`;
}
},
/** Update label DOM element dynamically */
updateStateMultiselectLabel(lookups) {
const labelEl = document.getElementById('state-multiselect-label');
if (labelEl) {
labelEl.textContent = this.getStateMultiselectLabel(lookups);
}
},
/**
* Render filter bar HTML.
* @param {Object} lookups - { queues, states, priorities, users }
@@ -59,14 +122,36 @@ const Filters = {
}).join('');
};
const currentLabel = this.getStateMultiselectLabel(lookups);
return `
<div class="filters-bar" id="filters-bar">
<div class="filter-group">
<div class="filter-group" style="position:relative;">
<span class="filter-label">Stato</span>
<select class="filter-select" data-filter="state_id" id="filter-state">
<option value="">Tutti</option>
${makeOptions(lookups.states || [], 'id', 'name', this.state.state_id)}
</select>
<div class="multiselect-dropdown" id="state-multiselect-dropdown" style="min-width: 140px; background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-sm); padding: 4px var(--space-sm); font-size: 0.85rem; cursor: pointer; display: flex; justify-content: space-between; align-items: center; user-select: none;">
<span class="multiselect-label" id="state-multiselect-label" style="text-overflow:ellipsis; overflow:hidden; white-space:nowrap; max-width:130px;">${App.escapeHtml(currentLabel)}</span>
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
</div>
<div class="multiselect-popover" id="state-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; width: 220px; background:var(--bg-card); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); box-shadow:var(--shadow-md); z-index:1002; padding:var(--space-xs); margin-top:4px;">
<div style="display:flex; flex-direction:column; gap:4px; padding-bottom:6px; border-bottom:1px solid var(--border-subtle);">
${(lookups.states || []).map(s => {
const selectedList = Array.isArray(this.state.state_id)
? this.state.state_id
: (typeof this.state.state_id === 'string' && this.state.state_id ? this.state.state_id.split(',') : []);
const isSelected = selectedList.includes(String(s.id));
const activeStyle = isSelected ? 'background: var(--accent-primary); color: #fff;' : '';
return `
<div class="state-multiselect-item ${isSelected ? 'active' : ''}" data-value="${s.id}" style="padding: 6px var(--space-sm); border-radius: var(--radius-sm); font-size: 0.85rem; cursor: pointer; user-select: none; transition: background 0.1s ease; ${activeStyle}">
${App.escapeHtml(s.name)}
</div>
`;
}).join('')}
</div>
<div style="display:flex; justify-content:flex-end; gap:8px; padding-top:6px; font-size:0.75rem;">
<button type="button" class="btn btn-ghost btn-xs" id="state-multiselect-clear" style="height:20px; padding:0 8px; font-size:0.75rem;">Reset</button>
<button type="button" class="btn btn-primary btn-xs" id="state-multiselect-ok" style="height:20px; padding:0 8px; font-size:0.75rem; line-height:20px;">OK</button>
</div>
</div>
</div>
<div class="filter-group">
<span class="filter-label">Coda</span>
@@ -109,11 +194,7 @@ const Filters = {
const isMyTickets = window.location.hash.startsWith('#/tickets/my');
const selects = document.querySelectorAll('.filter-select[data-filter]');
selects.forEach(sel => {
if (isMyTickets && sel.dataset.filter === 'user_id') {
sel.disabled = true;
} else {
sel.disabled = false;
}
sel.disabled = false;
sel.addEventListener('change', (e) => {
this.state[e.target.dataset.filter] = e.target.value;
@@ -122,6 +203,68 @@ const Filters = {
});
});
// Multiselect dropdown toggle event
const dropdown = document.getElementById('state-multiselect-dropdown');
const popover = document.getElementById('state-multiselect-popover');
const okBtn = document.getElementById('state-multiselect-ok');
const clearBtn = document.getElementById('state-multiselect-clear');
if (dropdown && popover) {
dropdown.addEventListener('click', (e) => {
e.stopPropagation();
const isOpen = popover.style.display === 'block';
popover.style.display = isOpen ? 'none' : 'block';
});
popover.addEventListener('click', (e) => e.stopPropagation());
document.addEventListener('click', () => {
popover.style.display = 'none';
});
// Bind item clicks
popover.querySelectorAll('.state-multiselect-item').forEach(item => {
item.addEventListener('click', (e) => {
e.stopPropagation();
const isActive = item.classList.toggle('active');
if (isActive) {
item.style.background = 'var(--accent-primary)';
item.style.color = '#fff';
} else {
item.style.background = '';
item.style.color = '';
}
});
});
}
if (okBtn) {
okBtn.addEventListener('click', () => {
const activeItems = popover.querySelectorAll('.state-multiselect-item.active');
const ids = Array.from(activeItems).map(item => item.dataset.value);
this.state.state_id = ids.join(',');
this.save();
this.updateStateMultiselectLabel(App.lookups);
popover.style.display = 'none';
if (onFilterChange) onFilterChange();
});
}
if (clearBtn) {
clearBtn.addEventListener('click', () => {
popover.querySelectorAll('.state-multiselect-item').forEach(item => {
item.classList.remove('active');
item.style.background = '';
item.style.color = '';
});
this.state.state_id = '';
this.save();
this.updateStateMultiselectLabel(App.lookups);
popover.style.display = 'none';
if (onFilterChange) onFilterChange();
});
}
const resetBtn = document.getElementById('filter-reset');
if (resetBtn) {
resetBtn.addEventListener('click', () => {
@@ -138,6 +281,18 @@ const Filters = {
s.value = '';
}
});
// Also clear multiselect items and label
const statePopover = document.getElementById('state-multiselect-popover');
if (statePopover) {
statePopover.querySelectorAll('.state-multiselect-item').forEach(item => {
item.classList.remove('active');
item.style.background = '';
item.style.color = '';
});
}
this.updateStateMultiselectLabel(App.lookups);
if (onFilterChange) onFilterChange();
});
}