feat: gestione gruppi di ticket. feat: miglioramento filtri ticket a mio carico e ticket. feat: possiblità di copiare il numero del tocket con pulsante copia
This commit is contained in:
+446
-157
@@ -26,6 +26,16 @@ const Filters = {
|
||||
}
|
||||
},
|
||||
|
||||
customerCache: {},
|
||||
presets: [],
|
||||
selectedPresetId: null,
|
||||
|
||||
saveCustomerCache() {
|
||||
try {
|
||||
localStorage.setItem('otrs_turbo_customer_cache', JSON.stringify(this.customerCache));
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
|
||||
// Dynamic state getter based on active mode
|
||||
get state() {
|
||||
return this.allStates[this.currentMode];
|
||||
@@ -50,6 +60,11 @@ const Filters = {
|
||||
const activeAgentId = localStorage.getItem('activeAgentId') || '1';
|
||||
this.allStates.my.user_id = activeAgentId;
|
||||
}
|
||||
|
||||
const savedCache = localStorage.getItem('otrs_turbo_customer_cache');
|
||||
if (savedCache) {
|
||||
this.customerCache = JSON.parse(savedCache);
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
},
|
||||
|
||||
@@ -79,37 +94,31 @@ 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(',') : []);
|
||||
|
||||
/** Helper to compute trigger button label text for multiselect dropdowns */
|
||||
getMultiselectLabel(selectedVal, itemsList, labelField = 'name', idField = 'id') {
|
||||
const selectedList = Array.isArray(selectedVal)
|
||||
? selectedVal
|
||||
: (typeof selectedVal === 'string' && selectedVal ? selectedVal.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) {
|
||||
|
||||
const selectedNames = (itemsList || [])
|
||||
.filter(item => selectedList.includes(String(item[idField])))
|
||||
.map(item => typeof labelField === 'function' ? labelField(item) : item[labelField]);
|
||||
|
||||
if (selectedNames.length === 0) {
|
||||
return 'Tutti';
|
||||
} else if (selectedNames.length <= 2) {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
|
||||
/** Helper to compute trigger button label text for customer users */
|
||||
getCustomerUserMultiselectLabel(lookups) {
|
||||
const selectedList = Array.isArray(this.state.customer_user_id)
|
||||
? this.state.customer_user_id
|
||||
@@ -118,13 +127,22 @@ const Filters = {
|
||||
if (selectedList.length === 0) {
|
||||
return 'Tutti';
|
||||
}
|
||||
const selectedNames = (lookups.customerUsers || [])
|
||||
.filter(u => selectedList.includes(String(u.login)))
|
||||
.map(u => `${u.last_name} ${u.first_name}`);
|
||||
|
||||
const selectedNames = selectedList.map(login => {
|
||||
if (this.customerCache && this.customerCache[login]) {
|
||||
return this.customerCache[login];
|
||||
}
|
||||
const found = (lookups.customerUsers || []).find(u => String(u.login) === String(login));
|
||||
if (found) {
|
||||
const fullName = `${found.last_name} ${found.first_name}`.trim();
|
||||
this.customerCache[login] = fullName;
|
||||
this.saveCustomerCache();
|
||||
return fullName;
|
||||
}
|
||||
return login;
|
||||
});
|
||||
|
||||
if (selectedNames.length === (lookups.customerUsers || []).length) {
|
||||
return 'Tutti';
|
||||
} else if (selectedNames.length <= 2) {
|
||||
if (selectedNames.length <= 2) {
|
||||
return selectedNames.join(', ');
|
||||
} else {
|
||||
return `${selectedNames.length} selezionati`;
|
||||
@@ -145,72 +163,147 @@ const Filters = {
|
||||
* @returns {string} HTML string
|
||||
*/
|
||||
renderBar(lookups) {
|
||||
const makeOptions = (items, valueKey, labelKey, selectedVal) => {
|
||||
return items.map(item => {
|
||||
const val = item[valueKey];
|
||||
const label = typeof labelKey === 'function' ? labelKey(item) : item[labelKey];
|
||||
const sel = String(val) === String(selectedVal) ? 'selected' : '';
|
||||
return `<option value="${val}" ${sel}>${label}</option>`;
|
||||
}).join('');
|
||||
};
|
||||
|
||||
const currentLabel = this.getStateMultiselectLabel(lookups);
|
||||
const stateLabel = this.getMultiselectLabel(this.state.state_id, lookups.states);
|
||||
const queueLabel = this.getMultiselectLabel(this.state.queue_id, lookups.queues);
|
||||
const priorityLabel = this.getMultiselectLabel(this.state.priority_id, lookups.priorities);
|
||||
const ownerLabel = this.getMultiselectLabel(this.state.user_id, lookups.users, u => `${u.first_name} ${u.last_name}`);
|
||||
const customerUserLabel = this.getCustomerUserMultiselectLabel(lookups);
|
||||
|
||||
const presetOptions = (this.presets || []).map(p => {
|
||||
const sel = String(p.id) === String(this.selectedPresetId) ? 'selected' : '';
|
||||
return `<option value="${p.id}" ${sel}>${App.escapeHtml(p.name)}</option>`;
|
||||
}).join('');
|
||||
|
||||
return `
|
||||
<div class="filters-bar" id="filters-bar">
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">Preset</span>
|
||||
<div style="display:flex; gap:4px; align-items:center;">
|
||||
<select class="form-select" id="filter-presets-select" style="padding:4px 20px 4px 8px; font-size:0.78rem; height:28px; margin:0; min-width:130px; border-color:var(--border-subtle);">
|
||||
<option value="">-- Nessuno --</option>
|
||||
${presetOptions}
|
||||
</select>
|
||||
<button class="btn btn-ghost btn-xs" id="btn-save-preset" style="height:28px; padding:0 8px; font-size:0.75rem;" title="Salva filtri attuali come preset">Salva</button>
|
||||
<button class="btn btn-ghost btn-xs" id="btn-delete-preset" style="height:28px; padding:0 8px; font-size:0.75rem; color:var(--danger);" title="Elimina il preset selezionato">Elimina</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Stato -->
|
||||
<div class="filter-group" style="position:relative;">
|
||||
<span class="filter-label">Stato</span>
|
||||
<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>
|
||||
<div class="multiselect-dropdown" id="state-multiselect-dropdown" style="min-width: 110px; max-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;">${App.escapeHtml(stateLabel)}</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);">
|
||||
<input type="text" class="form-input ms-search" placeholder="Cerca..." style="width:100%; padding:4px 8px; font-size:0.78rem; margin-bottom:6px; box-sizing:border-box;" autocomplete="off" />
|
||||
<div class="ms-items-container" style="display:flex; flex-direction:column; gap:2px; 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 selectedList = String(this.state.state_id || '').split(',').filter(Boolean);
|
||||
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}">
|
||||
<div class="ms-item ${isSelected ? 'active' : ''}" data-value="${s.id}" style="padding: 4px 8px; border-radius: var(--radius-sm); font-size: 0.8rem; 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>
|
||||
<button type="button" class="btn btn-ghost btn-xs ms-clear" style="height:20px; padding:0 8px; font-size:0.75rem;">Reset</button>
|
||||
<button type="button" class="btn btn-primary btn-xs ms-ok" style="height:20px; padding:0 8px; font-size:0.75rem; line-height:20px;">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
|
||||
<!-- Coda -->
|
||||
<div class="filter-group" style="position:relative;">
|
||||
<span class="filter-label">Coda</span>
|
||||
<select class="filter-select" data-filter="queue_id" id="filter-queue">
|
||||
<option value="">Tutte</option>
|
||||
${makeOptions(lookups.queues || [], 'id', 'name', this.state.queue_id)}
|
||||
</select>
|
||||
<div class="multiselect-dropdown" id="queue-multiselect-dropdown" style="min-width: 120px; max-width: 150px; 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="queue-multiselect-label" style="text-overflow:ellipsis; overflow:hidden; white-space:nowrap;">${App.escapeHtml(queueLabel)}</span>
|
||||
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
|
||||
</div>
|
||||
<div class="multiselect-popover" id="queue-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; width: 280px; 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;">
|
||||
<input type="text" class="form-input ms-search" placeholder="Cerca coda..." style="width:100%; padding:4px 8px; font-size:0.78rem; margin-bottom:6px; box-sizing:border-box;" autocomplete="off" />
|
||||
<div class="ms-items-container" style="display:flex; flex-direction:column; gap:2px; max-height:300px; overflow-y:auto; padding-bottom:6px; border-bottom:1px solid var(--border-subtle);">
|
||||
${(lookups.queues || []).map(q => {
|
||||
const selectedList = String(this.state.queue_id || '').split(',').filter(Boolean);
|
||||
const isSelected = selectedList.includes(String(q.id));
|
||||
const activeStyle = isSelected ? 'background: var(--accent-primary); color: #fff;' : '';
|
||||
return `
|
||||
<div class="ms-item ${isSelected ? 'active' : ''}" data-value="${q.id}" style="padding: 4px 8px; border-radius: var(--radius-sm); font-size: 0.8rem; cursor: pointer; user-select: none; transition: background 0.1s ease; ${activeStyle}">
|
||||
${App.escapeHtml(q.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 ms-clear" style="height:20px; padding:0 8px; font-size:0.75rem;">Reset</button>
|
||||
<button type="button" class="btn btn-primary btn-xs ms-ok" style="height:20px; padding:0 8px; font-size:0.75rem; line-height:20px;">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
|
||||
<!-- Priorità -->
|
||||
<div class="filter-group" style="position:relative;">
|
||||
<span class="filter-label">Priorità</span>
|
||||
<select class="filter-select" data-filter="priority_id" id="filter-priority">
|
||||
<option value="">Tutte</option>
|
||||
${makeOptions(lookups.priorities || [], 'id', 'name', this.state.priority_id)}
|
||||
</select>
|
||||
<div class="multiselect-dropdown" id="priority-multiselect-dropdown" style="min-width: 90px; max-width: 120px; 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="priority-multiselect-label" style="text-overflow:ellipsis; overflow:hidden; white-space:nowrap;">${App.escapeHtml(priorityLabel)}</span>
|
||||
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
|
||||
</div>
|
||||
<div class="multiselect-popover" id="priority-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; width: 200px; 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;">
|
||||
<input type="text" class="form-input ms-search" placeholder="Cerca priorità..." style="width:100%; padding:4px 8px; font-size:0.78rem; margin-bottom:6px; box-sizing:border-box;" autocomplete="off" />
|
||||
<div class="ms-items-container" style="display:flex; flex-direction:column; gap:2px; max-height:180px; overflow-y:auto; padding-bottom:6px; border-bottom:1px solid var(--border-subtle);">
|
||||
${(lookups.priorities || []).map(p => {
|
||||
const selectedList = String(this.state.priority_id || '').split(',').filter(Boolean);
|
||||
const isSelected = selectedList.includes(String(p.id));
|
||||
const activeStyle = isSelected ? 'background: var(--accent-primary); color: #fff;' : '';
|
||||
return `
|
||||
<div class="ms-item ${isSelected ? 'active' : ''}" data-value="${p.id}" style="padding: 4px 8px; border-radius: var(--radius-sm); font-size: 0.8rem; cursor: pointer; user-select: none; transition: background 0.1s ease; ${activeStyle}">
|
||||
${App.escapeHtml(p.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 ms-clear" style="height:20px; padding:0 8px; font-size:0.75rem;">Reset</button>
|
||||
<button type="button" class="btn btn-primary btn-xs ms-ok" style="height:20px; padding:0 8px; font-size:0.75rem; line-height:20px;">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
|
||||
<!-- Owner -->
|
||||
<div class="filter-group" style="position:relative;">
|
||||
<span class="filter-label">Owner</span>
|
||||
<select class="filter-select" data-filter="user_id" id="filter-owner">
|
||||
<option value="">Tutti</option>
|
||||
${makeOptions(lookups.users || [], 'id', (u) => `${u.first_name} ${u.last_name}`, this.state.user_id)}
|
||||
</select>
|
||||
<div class="multiselect-dropdown" id="owner-multiselect-dropdown" style="min-width: 120px; max-width: 150px; 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="owner-multiselect-label" style="text-overflow:ellipsis; overflow:hidden; white-space:nowrap;">${App.escapeHtml(ownerLabel)}</span>
|
||||
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
|
||||
</div>
|
||||
<div class="multiselect-popover" id="owner-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; width: 240px; 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;">
|
||||
<input type="text" class="form-input ms-search" placeholder="Cerca owner..." style="width:100%; padding:4px 8px; font-size:0.78rem; margin-bottom:6px; box-sizing:border-box;" autocomplete="off" />
|
||||
<div class="ms-items-container" style="display:flex; flex-direction:column; gap:2px; max-height:180px; overflow-y:auto; padding-bottom:6px; border-bottom:1px solid var(--border-subtle);">
|
||||
${(lookups.users || []).map(u => {
|
||||
const selectedList = String(this.state.user_id || '').split(',').filter(Boolean);
|
||||
const isSelected = selectedList.includes(String(u.id));
|
||||
const activeStyle = isSelected ? 'background: var(--accent-primary); color: #fff;' : '';
|
||||
return `
|
||||
<div class="ms-item ${isSelected ? 'active' : ''}" data-value="${u.id}" style="padding: 4px 8px; border-radius: var(--radius-sm); font-size: 0.8rem; cursor: pointer; user-select: none; transition: background 0.1s ease; ${activeStyle}">
|
||||
${App.escapeHtml(u.first_name)} ${App.escapeHtml(u.last_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 ms-clear" style="height:20px; padding:0 8px; font-size:0.75rem;">Reset</button>
|
||||
<button type="button" class="btn btn-primary btn-xs ms-ok" style="height:20px; padding:0 8px; font-size:0.75rem; line-height:20px;">OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-group" style="position:relative;">
|
||||
<span class="filter-label">Utente Cliente</span>
|
||||
<div class="multiselect-dropdown" id="customer-user-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="customer-user-multiselect-label" style="text-overflow:ellipsis; overflow:hidden; white-space:nowrap; max-width:180px;">${App.escapeHtml(customerUserLabel)}</span>
|
||||
<div class="multiselect-dropdown" id="customer-user-multiselect-dropdown" style="min-width: 130px; max-width: 160px; 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="customer-user-multiselect-label" style="text-overflow:ellipsis; overflow:hidden; white-space:nowrap;">${App.escapeHtml(customerUserLabel)}</span>
|
||||
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
|
||||
</div>
|
||||
<div class="multiselect-popover" id="customer-user-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; width: 280px; 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;">
|
||||
@@ -226,11 +319,11 @@ const Filters = {
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">Da Data/Ora</span>
|
||||
<input type="datetime-local" class="filter-select" data-filter="date_from" id="filter-date-from" value="${this.state.date_from || ''}" style="width: 190px; padding: 4px 8px; font-family: inherit; font-size: 0.85rem;" />
|
||||
<input type="datetime-local" class="filter-select" data-filter="date_from" id="filter-date-from" value="${this.state.date_from || ''}" style="width: 170px; padding: 4px 8px; font-family: inherit; font-size: 0.85rem;" />
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<span class="filter-label">A Data/Ora</span>
|
||||
<input type="datetime-local" class="filter-select" data-filter="date_to" id="filter-date-to" value="${this.state.date_to || ''}" style="width: 190px; padding: 4px 8px; font-family: inherit; font-size: 0.85rem;" />
|
||||
<input type="datetime-local" class="filter-select" data-filter="date_to" id="filter-date-to" value="${this.state.date_to || ''}" style="width: 170px; padding: 4px 8px; font-family: inherit; font-size: 0.85rem;" />
|
||||
</div>
|
||||
<div class="filters-actions">
|
||||
<button class="btn btn-ghost btn-xs" id="filter-reset">Reset</button>
|
||||
@@ -239,7 +332,6 @@ const Filters = {
|
||||
`;
|
||||
},
|
||||
|
||||
/** Bind change events to filter selects */
|
||||
bindEvents(onFilterChange) {
|
||||
const isMyTickets = window.location.hash.startsWith('#/tickets/my');
|
||||
const selects = document.querySelectorAll('.filter-select[data-filter]');
|
||||
@@ -247,73 +339,117 @@ const Filters = {
|
||||
sel.disabled = false;
|
||||
|
||||
sel.addEventListener('change', (e) => {
|
||||
this.selectedPresetId = null;
|
||||
this.state[e.target.dataset.filter] = e.target.value;
|
||||
this.save();
|
||||
if (onFilterChange) onFilterChange();
|
||||
});
|
||||
});
|
||||
|
||||
// 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');
|
||||
// Helper to bind standard multiselect popover events
|
||||
const bindStandardMultiselect = (filterKey, dropdownId, popoverId, labelElId, itemsList, labelField, idField = 'id') => {
|
||||
const dropdown = document.getElementById(dropdownId);
|
||||
const popover = document.getElementById(popoverId);
|
||||
if (!dropdown || !popover) return;
|
||||
|
||||
if (dropdown && popover) {
|
||||
const searchInput = popover.querySelector('.ms-search');
|
||||
const itemsContainer = popover.querySelector('.ms-items-container');
|
||||
const okBtn = popover.querySelector('.ms-ok');
|
||||
const clearBtn = popover.querySelector('.ms-clear');
|
||||
|
||||
// Toggle popover visibility
|
||||
dropdown.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
// Close all other popovers
|
||||
document.querySelectorAll('.multiselect-popover').forEach(p => {
|
||||
if (p !== popover) p.style.display = 'none';
|
||||
});
|
||||
|
||||
const isOpen = popover.style.display === 'block';
|
||||
popover.style.display = isOpen ? 'none' : 'block';
|
||||
if (!isOpen && searchInput) {
|
||||
searchInput.value = '';
|
||||
searchInput.dispatchEvent(new Event('input'));
|
||||
setTimeout(() => searchInput.focus(), 50);
|
||||
}
|
||||
});
|
||||
|
||||
popover.addEventListener('click', (e) => e.stopPropagation());
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
popover.style.display = 'none';
|
||||
});
|
||||
// Search matching items
|
||||
if (searchInput && itemsContainer) {
|
||||
searchInput.addEventListener('input', () => {
|
||||
const q = searchInput.value.toLowerCase().trim();
|
||||
itemsContainer.querySelectorAll('.ms-item').forEach(item => {
|
||||
const text = item.textContent.toLowerCase();
|
||||
item.style.display = text.includes(q) ? 'block' : '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 {
|
||||
// Bind selection clicks
|
||||
if (itemsContainer) {
|
||||
itemsContainer.querySelectorAll('.ms-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 = '';
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Apply selection (OK click)
|
||||
if (okBtn) {
|
||||
okBtn.addEventListener('click', () => {
|
||||
const activeItems = itemsContainer.querySelectorAll('.ms-item.active');
|
||||
const ids = Array.from(activeItems).map(item => item.dataset.value);
|
||||
this.selectedPresetId = null;
|
||||
this.state[filterKey] = ids.join(',');
|
||||
this.save();
|
||||
|
||||
const labelEl = document.getElementById(labelElId);
|
||||
if (labelEl) {
|
||||
labelEl.textContent = this.getMultiselectLabel(this.state[filterKey], itemsList, labelField, idField);
|
||||
}
|
||||
popover.style.display = 'none';
|
||||
if (onFilterChange) onFilterChange();
|
||||
});
|
||||
}
|
||||
|
||||
// Reset selection
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
itemsContainer.querySelectorAll('.ms-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
item.style.background = '';
|
||||
item.style.color = '';
|
||||
});
|
||||
this.selectedPresetId = null;
|
||||
this.state[filterKey] = '';
|
||||
this.save();
|
||||
|
||||
const labelEl = document.getElementById(labelElId);
|
||||
if (labelEl) {
|
||||
labelEl.textContent = 'Tutti';
|
||||
}
|
||||
popover.style.display = 'none';
|
||||
if (onFilterChange) onFilterChange();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
// Bind standard multiselects
|
||||
bindStandardMultiselect('state_id', 'state-multiselect-dropdown', 'state-multiselect-popover', 'state-multiselect-label', App.lookups.states, 'name');
|
||||
bindStandardMultiselect('queue_id', 'queue-multiselect-dropdown', 'queue-multiselect-popover', 'queue-multiselect-label', App.lookups.queues, 'name');
|
||||
bindStandardMultiselect('priority_id', 'priority-multiselect-dropdown', 'priority-multiselect-popover', 'priority-multiselect-label', App.lookups.priorities, 'name');
|
||||
bindStandardMultiselect('user_id', 'owner-multiselect-dropdown', 'owner-multiselect-popover', 'owner-multiselect-label', App.lookups.users, u => `${u.first_name} ${u.last_name}`);
|
||||
|
||||
// Customer User Multiselect Popover binding
|
||||
const cuDropdown = document.getElementById('customer-user-multiselect-dropdown');
|
||||
@@ -322,34 +458,62 @@ const Filters = {
|
||||
const cuItemsContainer = document.getElementById('customer-user-items-container');
|
||||
const cuOkBtn = document.getElementById('customer-user-multiselect-ok');
|
||||
const cuClearBtn = document.getElementById('customer-user-multiselect-clear');
|
||||
|
||||
let activeSearchController = null;
|
||||
|
||||
const renderCustomerUserItems = () => {
|
||||
const renderCustomerUserItems = (searchResults = null) => {
|
||||
if (!cuItemsContainer) return;
|
||||
const q = (cuSearchInput ? cuSearchInput.value : '').toLowerCase().trim();
|
||||
|
||||
const selectedList = Array.isArray(this.state.customer_user_id)
|
||||
? this.state.customer_user_id
|
||||
: (typeof this.state.customer_user_id === 'string' && this.state.customer_user_id ? this.state.customer_user_id.split(',') : []);
|
||||
|
||||
const filtered = (App.lookups.customerUsers || []).filter(u => {
|
||||
const fullName = `${u.last_name} ${u.first_name} (${u.login})`.toLowerCase();
|
||||
return fullName.includes(q);
|
||||
});
|
||||
const uniqueSelectedLogins = Array.from(new Set(selectedList)).filter(Boolean);
|
||||
|
||||
cuItemsContainer.innerHTML = filtered.map(u => {
|
||||
const isSelected = selectedList.includes(String(u.login));
|
||||
const activeStyle = isSelected ? 'background: var(--accent-primary); color: #fff;' : '';
|
||||
return `
|
||||
<div class="customer-user-multiselect-item ${isSelected ? 'active' : ''}" data-value="${u.login}" 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(u.last_name)} ${App.escapeHtml(u.first_name)} <span style="font-size:0.75rem;opacity:0.8;">(${App.escapeHtml(u.login)})</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
let html = '';
|
||||
|
||||
// 1. Show selected items at the top
|
||||
if (uniqueSelectedLogins.length > 0) {
|
||||
html += `<div style="font-size:0.72rem; font-weight:700; color:var(--accent-primary); text-transform:uppercase; padding: 2px var(--space-sm); border-bottom:1px solid var(--border-subtle); margin-bottom:4px;">Selezionati</div>`;
|
||||
uniqueSelectedLogins.forEach(login => {
|
||||
const displayName = this.customerCache[login] || login;
|
||||
html += `
|
||||
<div class="customer-user-multiselect-item active" data-value="${login}" 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; background: var(--accent-primary); color: #fff; margin-bottom: 2px;">
|
||||
${App.escapeHtml(displayName)} <span style="font-size:0.75rem;opacity:0.8;">(${App.escapeHtml(login)})</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Show search results below
|
||||
let listToRender = searchResults || App.lookups.customerUsers || [];
|
||||
listToRender = listToRender.filter(u => !uniqueSelectedLogins.includes(String(u.login)));
|
||||
|
||||
if (listToRender.length > 0) {
|
||||
if (uniqueSelectedLogins.length > 0) {
|
||||
html += `<div style="font-size:0.72rem; font-weight:700; color:var(--text-secondary); text-transform:uppercase; padding: 4px var(--space-sm) 2px; border-bottom:1px solid var(--border-subtle); margin-top:6px; margin-bottom:4px;">Risultati</div>`;
|
||||
}
|
||||
listToRender.forEach(u => {
|
||||
const displayName = `${u.last_name} ${u.first_name}`.trim() || u.login;
|
||||
html += `
|
||||
<div class="customer-user-multiselect-item" data-value="${u.login}" data-display-name="${displayName}" 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; margin-bottom: 2px;">
|
||||
${App.escapeHtml(u.last_name)} ${App.escapeHtml(u.first_name)} <span style="font-size:0.75rem;opacity:0.8;">(${App.escapeHtml(u.login)})</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
} else if (uniqueSelectedLogins.length === 0) {
|
||||
html = `<div style="text-align:center; padding:var(--space-md); color:var(--text-muted); font-size:0.8rem;">Cerca digitando sopra...</div>`;
|
||||
}
|
||||
|
||||
cuItemsContainer.innerHTML = html;
|
||||
|
||||
// Bind clicks to items
|
||||
cuItemsContainer.querySelectorAll('.customer-user-multiselect-item').forEach(item => {
|
||||
item.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const login = item.dataset.value;
|
||||
const displayName = item.dataset.displayName || this.customerCache[login] || login;
|
||||
|
||||
const currentSelected = Array.isArray(this.state.customer_user_id)
|
||||
? [...this.state.customer_user_id]
|
||||
: (typeof this.state.customer_user_id === 'string' && this.state.customer_user_id ? this.state.customer_user_id.split(',') : []);
|
||||
@@ -357,16 +521,13 @@ const Filters = {
|
||||
const idx = currentSelected.indexOf(login);
|
||||
if (idx > -1) {
|
||||
currentSelected.splice(idx, 1);
|
||||
item.classList.remove('active');
|
||||
item.style.background = '';
|
||||
item.style.color = '';
|
||||
} else {
|
||||
currentSelected.push(login);
|
||||
item.classList.add('active');
|
||||
item.style.background = 'var(--accent-primary)';
|
||||
item.style.color = '#fff';
|
||||
this.customerCache[login] = displayName;
|
||||
this.saveCustomerCache();
|
||||
}
|
||||
this.state.customer_user_id = currentSelected.join(',');
|
||||
renderCustomerUserItems(searchResults);
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -376,8 +537,9 @@ const Filters = {
|
||||
e.stopPropagation();
|
||||
|
||||
// Close other popovers
|
||||
const statePopover = document.getElementById('state-multiselect-popover');
|
||||
if (statePopover) statePopover.style.display = 'none';
|
||||
document.querySelectorAll('.multiselect-popover').forEach(p => {
|
||||
if (p !== cuPopover) p.style.display = 'none';
|
||||
});
|
||||
|
||||
const isOpen = cuPopover.style.display === 'block';
|
||||
cuPopover.style.display = isOpen ? 'none' : 'block';
|
||||
@@ -394,19 +556,38 @@ const Filters = {
|
||||
|
||||
cuPopover.addEventListener('click', (e) => e.stopPropagation());
|
||||
|
||||
document.addEventListener('click', () => {
|
||||
cuPopover.style.display = 'none';
|
||||
});
|
||||
|
||||
let cuSearchDebounce;
|
||||
if (cuSearchInput) {
|
||||
cuSearchInput.addEventListener('input', () => {
|
||||
renderCustomerUserItems();
|
||||
clearTimeout(cuSearchDebounce);
|
||||
const q = cuSearchInput.value.trim();
|
||||
|
||||
cuSearchDebounce = setTimeout(async () => {
|
||||
if (activeSearchController) activeSearchController.abort();
|
||||
activeSearchController = new AbortController();
|
||||
|
||||
try {
|
||||
cuItemsContainer.innerHTML = '<div style="display:flex; justify-content:center; padding:12px;"><div class="spinner" style="width:18px;height:18px;border-width:2px;"></div></div>';
|
||||
|
||||
const users = await fetch(`/api/customer-users/search?q=${encodeURIComponent(q)}`, {
|
||||
signal: activeSearchController.signal
|
||||
}).then(r => r.json());
|
||||
|
||||
renderCustomerUserItems(users);
|
||||
} catch (err) {
|
||||
if (err.name !== 'AbortError') {
|
||||
console.error('Search failed:', err);
|
||||
renderCustomerUserItems([]);
|
||||
}
|
||||
}
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (cuOkBtn) {
|
||||
cuOkBtn.addEventListener('click', () => {
|
||||
this.selectedPresetId = null;
|
||||
this.save();
|
||||
this.updateCustomerUserMultiselectLabel(App.lookups);
|
||||
if (cuPopover) cuPopover.style.display = 'none';
|
||||
@@ -416,6 +597,7 @@ const Filters = {
|
||||
|
||||
if (cuClearBtn) {
|
||||
cuClearBtn.addEventListener('click', () => {
|
||||
this.selectedPresetId = null;
|
||||
this.state.customer_user_id = '';
|
||||
this.save();
|
||||
this.updateCustomerUserMultiselectLabel(App.lookups);
|
||||
@@ -426,33 +608,30 @@ const Filters = {
|
||||
});
|
||||
}
|
||||
|
||||
// Close all popovers on backdrop click
|
||||
document.addEventListener('click', () => {
|
||||
document.querySelectorAll('.multiselect-popover').forEach(p => {
|
||||
p.style.display = 'none';
|
||||
});
|
||||
});
|
||||
|
||||
const resetBtn = document.getElementById('filter-reset');
|
||||
if (resetBtn) {
|
||||
resetBtn.addEventListener('click', () => {
|
||||
this.selectedPresetId = null;
|
||||
this.reset();
|
||||
if (isMyTickets) {
|
||||
const activeAgentId = localStorage.getItem('activeAgentId') || '1';
|
||||
this.state.user_id = activeAgentId;
|
||||
this.save();
|
||||
}
|
||||
selects.forEach(s => {
|
||||
if (isMyTickets && s.dataset.filter === 'user_id') {
|
||||
s.value = localStorage.getItem('activeAgentId') || '1';
|
||||
} else {
|
||||
s.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// Also clear state 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);
|
||||
// Reset highlight states in popovers
|
||||
document.querySelectorAll('.ms-items-container .ms-item').forEach(item => {
|
||||
item.classList.remove('active');
|
||||
item.style.background = '';
|
||||
item.style.color = '';
|
||||
});
|
||||
|
||||
// Also clear customer user multiselect items and label
|
||||
const cuPopover = document.getElementById('customer-user-multiselect-popover');
|
||||
@@ -465,11 +644,121 @@ const Filters = {
|
||||
item.style.color = '';
|
||||
});
|
||||
}
|
||||
|
||||
// Recompute labels
|
||||
document.getElementById('state-multiselect-label').textContent = 'Tutti';
|
||||
document.getElementById('queue-multiselect-label').textContent = 'Tutti';
|
||||
document.getElementById('priority-multiselect-label').textContent = 'Tutti';
|
||||
document.getElementById('owner-multiselect-label').textContent = isMyTickets ? this.getMultiselectLabel(this.state.user_id, App.lookups.users, u => `${u.first_name} ${u.last_name}`) : 'Tutti';
|
||||
this.updateCustomerUserMultiselectLabel(App.lookups);
|
||||
|
||||
if (onFilterChange) onFilterChange();
|
||||
});
|
||||
}
|
||||
|
||||
// Load and bind presets
|
||||
const loadPresets = async () => {
|
||||
try {
|
||||
const agentId = localStorage.getItem('activeAgentId') || '1';
|
||||
this.presets = await App.api(`/api/presets?page_mode=${this.currentMode}`);
|
||||
const select = document.getElementById('filter-presets-select');
|
||||
if (select) {
|
||||
select.innerHTML = '<option value="">-- Nessuno --</option>' + this.presets.map(p => {
|
||||
const sel = String(p.id) === String(this.selectedPresetId) ? 'selected' : '';
|
||||
return `<option value="${p.id}" ${sel}>${App.escapeHtml(p.name)}</option>`;
|
||||
}).join('');
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to load presets:', err);
|
||||
}
|
||||
};
|
||||
|
||||
loadPresets();
|
||||
|
||||
const selectPresets = document.getElementById('filter-presets-select');
|
||||
if (selectPresets) {
|
||||
selectPresets.addEventListener('change', (e) => {
|
||||
const presetId = e.target.value;
|
||||
if (!presetId) {
|
||||
this.selectedPresetId = null;
|
||||
if (onFilterChange) onFilterChange();
|
||||
return;
|
||||
}
|
||||
|
||||
const preset = this.presets.find(p => String(p.id) === String(presetId));
|
||||
if (preset) {
|
||||
try {
|
||||
const filters = JSON.parse(preset.filters_json);
|
||||
this.state = Object.assign({
|
||||
queue_id: '',
|
||||
state_id: '',
|
||||
priority_id: '',
|
||||
user_id: '',
|
||||
customer_user_id: '',
|
||||
date_from: '',
|
||||
date_to: ''
|
||||
}, filters);
|
||||
this.selectedPresetId = preset.id;
|
||||
this.save();
|
||||
if (onFilterChange) onFilterChange();
|
||||
} catch (err) {
|
||||
Toast.error('Errore nel caricamento del preset: ' + err.message);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const btnSavePreset = document.getElementById('btn-save-preset');
|
||||
if (btnSavePreset) {
|
||||
btnSavePreset.addEventListener('click', async () => {
|
||||
const name = await App.prompt('Nuovo Preset', 'Inserisci il nome per questo preset di filtri:');
|
||||
if (!name || !name.trim()) return;
|
||||
|
||||
try {
|
||||
btnSavePreset.disabled = true;
|
||||
const newPreset = await App.api('/api/presets', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: name.trim(),
|
||||
page_mode: this.currentMode,
|
||||
filters: this.state
|
||||
})
|
||||
});
|
||||
Toast.success('Preset salvato con successo!');
|
||||
this.selectedPresetId = newPreset.id;
|
||||
if (onFilterChange) onFilterChange();
|
||||
} catch (err) {
|
||||
Toast.error('Errore nel salvataggio del preset: ' + err.message);
|
||||
btnSavePreset.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const btnDeletePreset = document.getElementById('btn-delete-preset');
|
||||
if (btnDeletePreset) {
|
||||
btnDeletePreset.addEventListener('click', async () => {
|
||||
const select = document.getElementById('filter-presets-select');
|
||||
const presetId = select ? select.value : '';
|
||||
if (!presetId) {
|
||||
Toast.warning('Seleziona prima un preset da eliminare');
|
||||
return;
|
||||
}
|
||||
|
||||
const ok = await App.confirm('Elimina Preset', 'Sei sicuro di voler eliminare questo preset?');
|
||||
if (!ok) return;
|
||||
|
||||
try {
|
||||
btnDeletePreset.disabled = true;
|
||||
await App.api(`/api/presets/${presetId}`, { method: 'DELETE' });
|
||||
Toast.success('Preset eliminato con successo');
|
||||
this.selectedPresetId = null;
|
||||
if (onFilterChange) onFilterChange();
|
||||
} catch (err) {
|
||||
Toast.error('Errore nell\'eliminazione del preset: ' + err.message);
|
||||
btnDeletePreset.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user