feat: aggiunto shortcut per aggiungere il ticket in una nuova scheda. style: spostata consuntivazione accanto ad utente cliente.

This commit is contained in:
Gabriele Cimaschi committed 2026-09-23 14:15:24 +02:00
1 parent 107ba10689
commit ce904756dd
6 files changed
+151 -9

No files matched your search

+53 -1
View File
@@ -69,6 +69,7 @@ const App = {
ricerca_spudorata_open: { ctrl: true, alt: false, shift: true, key: 'f', enabled: true }, ricerca_spudorata_open: { ctrl: true, alt: false, shift: true, key: 'f', enabled: true },
ticket_list_open: { ctrl: true, alt: false, shift: true, key: 'Backspace', enabled: true }, ticket_list_open: { ctrl: true, alt: false, shift: true, key: 'Backspace', enabled: true },
open_terminal_console: { ctrl: true, alt: true, shift: false, key: 't', enabled: true }, open_terminal_console: { ctrl: true, alt: true, shift: false, key: 't', enabled: true },
add_ticket_tab: { ctrl: true, alt: true, shift: false, key: 's', enabled: true },
}; };
return defaults[id] || { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true }; return defaults[id] || { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true };
}, },
@@ -123,11 +124,18 @@ const App = {
return text.replace(urlRegex, url => `<a href="${url}" target="_blank" rel="noopener noreferrer" style="color:var(--accent-primary); text-decoration:underline;">${url}</a>`); return text.replace(urlRegex, url => `<a href="${url}" target="_blank" rel="noopener noreferrer" style="color:var(--accent-primary); text-decoration:underline;">${url}</a>`);
}, },
openTab(id, tn, title) { openTab(id, tn, title, notify = false) {
const exists = this.tabs.find(t => String(t.id) === String(id)); const exists = this.tabs.find(t => String(t.id) === String(id));
if (!exists) { if (!exists) {
this.tabs.push({ id, tn, title }); this.tabs.push({ id, tn, title });
this.saveTabs(); this.saveTabs();
if (notify && typeof Toast !== 'undefined') {
Toast.success(`Ticket #${tn || id} aggiunto alle schede!`);
}
} else {
if (notify && typeof Toast !== 'undefined') {
Toast.info(`Ticket #${tn || id} è già nelle schede`);
}
} }
}, },
@@ -311,6 +319,50 @@ const App = {
if (consoleObj) { if (consoleObj) {
consoleObj.toggle(); consoleObj.toggle();
} }
} else if (this.isShortcutTriggered(e, 'add_ticket_tab')) {
e.preventDefault();
// 1. Check if in ticket detail
const detailTabBtn = document.querySelector('.ticket-header .open-tab-btn, .ticket-detail .open-tab-btn');
if (detailTabBtn) {
detailTabBtn.click();
return;
}
// 2. Check if multiple tickets selected in list
const selectedRows = document.querySelectorAll('.ticket-table tbody tr.selected, .results-table tbody tr.selected');
if (selectedRows.length > 0) {
let count = 0;
selectedRows.forEach(row => {
const tabBtn = row.querySelector('.open-tab-btn');
if (tabBtn) {
const id = tabBtn.dataset.id;
const tn = tabBtn.dataset.tn;
const title = tabBtn.dataset.title || '';
if (id) {
this.openTab(parseInt(id, 10), tn, title);
count++;
}
}
});
if (count > 0 && typeof Toast !== 'undefined') {
Toast.success(`${count} ticket aggiunti alle schede!`);
}
return;
}
// 3. Check hovered or focused row in any ticket table/list
const hoveredRow = document.querySelector('.ticket-table tbody tr:hover, .results-table tbody tr:hover, .ticket-table tbody tr:focus-within, .results-table tbody tr:focus-within');
if (hoveredRow) {
const tabBtn = hoveredRow.querySelector('.open-tab-btn');
if (tabBtn) {
tabBtn.click();
return;
}
}
if (typeof Toast !== 'undefined') {
Toast.info('Seleziona o apri un ticket per aggiungerlo in scheda');
}
} }
}, true); }, true);
+1 -1
View File
@@ -280,7 +280,7 @@ const RicercaSpudorataView = {
<span style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:#1070ca; color:#fff; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; vertical-align:middle; line-height:16px;">O</span> <span style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:#1070ca; color:#fff; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; vertical-align:middle; line-height:16px;">O</span>
</a> </a>
` : ''} ` : ''}
<button class="open-tab-btn" data-id="${t.id}" data-tn="${t.tn}" data-title="${App.escapeHtml(t.title || '')}" onclick="App.openTab(${t.id}, '${t.tn}', this.dataset.title); event.stopPropagation();" title="Apri in scheda" style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:var(--accent-primary); color:#fff; border:none; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; margin-right:6px; cursor:pointer; line-height:16px;">+</button> <button class="open-tab-btn" data-id="${t.id}" data-tn="${t.tn}" data-title="${App.escapeHtml(t.title || '')}" onclick="App.openTab(${t.id}, '${t.tn}', this.dataset.title, true); event.stopPropagation();" title="Apri in scheda" style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:var(--accent-primary); color:#fff; border:none; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; margin-right:6px; cursor:pointer; line-height:16px;">+</button>
<a href="#/tickets/${t.id}" class="ticket-tn-link" onclick="event.stopPropagation()">${t.tn}</a> <a href="#/tickets/${t.id}" class="ticket-tn-link" onclick="event.stopPropagation()">${t.tn}</a>
</span> </span>
</td> </td>
+7
View File
@@ -86,6 +86,13 @@ const ShortcutsView = {
context: 'Globale', context: 'Globale',
defaultCombo: { ctrl: true, alt: true, shift: false, key: 't', enabled: true } defaultCombo: { ctrl: true, alt: true, shift: false, key: 't', enabled: true }
}, },
{
id: 'add_ticket_tab',
name: 'Aggiungi Ticket in Scheda',
description: 'Aggiunge il ticket visualizzato (o selezionato nell\'elenco) alle schede aperte in alto',
context: 'Globale / Dettaglio / Elenco',
defaultCombo: { ctrl: true, alt: true, shift: false, key: 's', enabled: true }
},
{ {
id: 'create_ticket_submit', id: 'create_ticket_submit',
name: 'Invia Nuovo Ticket', name: 'Invia Nuovo Ticket',
+5 -4
View File
@@ -3453,7 +3453,7 @@ const TicketDetailView = {
<span class="copy-full-ticket-title" data-full="${ticket.tn} - ${App.escapeHtml(ticket.title || '(senza titolo)')}" style="cursor: pointer;" title="Clicca per copiare: ${ticket.tn} - ${App.escapeHtml(ticket.title || '')}"> <span class="copy-full-ticket-title" data-full="${ticket.tn} - ${App.escapeHtml(ticket.title || '(senza titolo)')}" style="cursor: pointer;" title="Clicca per copiare: ${ticket.tn} - ${App.escapeHtml(ticket.title || '')}">
${App.escapeHtml(ticket.title || '(senza titolo)')} ${App.escapeHtml(ticket.title || '(senza titolo)')}
</span> </span>
<button class="open-tab-btn" data-id="${ticket.id}" data-tn="${ticket.tn}" data-title="${App.escapeHtml(ticket.title || '(senza titolo)')}" onclick="App.openTab(${ticket.id}, '${ticket.tn}', this.dataset.title); event.stopPropagation();" title="Apri in scheda" style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:var(--accent-primary); color:#fff; border:none; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; margin-left:6px; cursor:pointer; line-height:16px;">+</button> <button class="open-tab-btn" data-id="${ticket.id}" data-tn="${ticket.tn}" data-title="${App.escapeHtml(ticket.title || '(senza titolo)')}" onclick="App.openTab(${ticket.id}, '${ticket.tn}', this.dataset.title, true); event.stopPropagation();" title="Apri in scheda" style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:var(--accent-primary); color:#fff; border:none; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; margin-left:6px; cursor:pointer; line-height:16px;">+</button>
<button class="assign-open-tab-btn" data-id="${ticket.id}" data-tn="${ticket.tn}" data-title="${App.escapeHtml(ticket.title || '(senza titolo)')}" onclick="App.assignAndOpenTab(${ticket.id}, '${ticket.tn}', this.dataset.title); event.stopPropagation();" title="Assegna a me ed apri in scheda" style="display:inline-flex; align-items:center; justify-content:center; height:20px; padding:0 8px; background:var(--accent-primary); color:#fff; border:none; border-radius:var(--radius-full); font-size:11px; font-weight:600; font-family:inherit; margin-left:6px; cursor:pointer; gap:3px;"> <button class="assign-open-tab-btn" data-id="${ticket.id}" data-tn="${ticket.tn}" data-title="${App.escapeHtml(ticket.title || '(senza titolo)')}" onclick="App.assignAndOpenTab(${ticket.id}, '${ticket.tn}', this.dataset.title); event.stopPropagation();" title="Assegna a me ed apri in scheda" style="display:inline-flex; align-items:center; justify-content:center; height:20px; padding:0 8px; background:var(--accent-primary); color:#fff; border:none; border-radius:var(--radius-full); font-size:11px; font-weight:600; font-family:inherit; margin-left:6px; cursor:pointer; gap:3px;">
👤+ Assegna & Scheda 👤+ Assegna & Scheda
</button> </button>
@@ -3562,6 +3562,10 @@ const TicketDetailView = {
<input type="hidden" id="qe-customer-id" data-field="customer_id" value="${ticket.customer_id || ''}" /> <input type="hidden" id="qe-customer-id" data-field="customer_id" value="${ticket.customer_id || ''}" />
<div id="qe-customer-suggestions" class="autocomplete-suggestions" style="display:none; top: 100%;"></div> <div id="qe-customer-suggestions" class="autocomplete-suggestions" style="display:none; top: 100%;"></div>
</div> </div>
<div class="quick-edit-field">
<label class="quick-edit-label">Consuntivazione Tempo (min)</label>
<input type="number" step="any" min="0" class="form-input" id="note-time-units" placeholder="minuti" style="padding: 6px 12px; height: 32px; font-size: 0.85rem;" />
</div>
</div> </div>
<!-- Integrated Note & Article Editor inside Modifica Rapida --> <!-- Integrated Note & Article Editor inside Modifica Rapida -->
@@ -3591,9 +3595,6 @@ const TicketDetailView = {
<label for="note-create-time" style="font-size:0.8rem; font-weight:600; color:var(--text-secondary);">Data/Ora:</label> <label for="note-create-time" style="font-size:0.8rem; font-weight:600; color:var(--text-secondary);">Data/Ora:</label>
<input type="datetime-local" class="note-subject-input" id="note-create-time" title="Data/ora nota (opzionale per retrodatazione)" style="width:185px; margin-bottom:0; height:32px; padding:4px 8px; font-size:0.82rem;" /> <input type="datetime-local" class="note-subject-input" id="note-create-time" title="Data/ora nota (opzionale per retrodatazione)" style="width:185px; margin-bottom:0; height:32px; padding:4px 8px; font-size:0.82rem;" />
<label for="note-time-units" style="font-size:0.8rem; font-weight:600; color:var(--text-secondary); margin-left:4px;">Tempo (min):</label>
<input type="number" step="any" min="0" class="note-subject-input" id="note-time-units" placeholder="minuti" style="width:90px; margin-bottom:0; height:32px; padding:4px 8px; font-size:0.85rem;" />
<button class="btn btn-ghost btn-sm" id="btn-open-email-compose" style="height:32px; display:flex; align-items:center; gap:var(--space-xs); border-color:var(--accent-secondary); color:var(--accent-secondary);" <button class="btn btn-ghost btn-sm" id="btn-open-email-compose" style="height:32px; display:flex; align-items:center; gap:var(--space-xs); border-color:var(--accent-secondary); color:var(--accent-secondary);"
data-ticket-id="${ticket.id}" data-ticket-tn="${ticket.tn}" data-ticket-title="${App.escapeHtml(ticket.title)}" data-customer-email="${App.escapeHtml(ticket.customer_email || '')}"> data-ticket-id="${ticket.id}" data-ticket-tn="${ticket.tn}" data-ticket-title="${App.escapeHtml(ticket.title)}" data-customer-email="${App.escapeHtml(ticket.customer_email || '')}">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
+1 -1
View File
@@ -333,7 +333,7 @@ const TicketListView = {
<span style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:#1070ca; color:#fff; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; vertical-align:middle; line-height:16px;">O</span> <span style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:#1070ca; color:#fff; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; vertical-align:middle; line-height:16px;">O</span>
</a> </a>
` : ''} ` : ''}
<button class="open-tab-btn" data-id="${t.id}" data-tn="${t.tn}" data-title="${App.escapeHtml(t.title || '')}" onclick="App.openTab(${t.id}, '${t.tn}', this.dataset.title); event.stopPropagation();" title="Apri in scheda" style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:var(--accent-primary); color:#fff; border:none; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; margin-right:6px; cursor:pointer; line-height:16px;">+</button> <button class="open-tab-btn" data-id="${t.id}" data-tn="${t.tn}" data-title="${App.escapeHtml(t.title || '')}" onclick="App.openTab(${t.id}, '${t.tn}', this.dataset.title, true); event.stopPropagation();" title="Apri in scheda" style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:var(--accent-primary); color:#fff; border:none; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; margin-right:6px; cursor:pointer; line-height:16px;">+</button>
<a href="#/tickets/${t.id}" class="ticket-tn-link" onclick="event.stopPropagation()">${t.tn}</a> <a href="#/tickets/${t.id}" class="ticket-tn-link" onclick="event.stopPropagation()">${t.tn}</a>
</span> </span>
</td> </td>
+84 -2
View File
@@ -12,6 +12,39 @@ const TimeAccountingView = {
viewMode: 'list', // 'list' | 'calendar' viewMode: 'list', // 'list' | 'calendar'
showAllTime: false, showAllTime: false,
showNonWorkingDays: localStorage.getItem('otrs_ta_show_non_working_days') !== 'false', showNonWorkingDays: localStorage.getItem('otrs_ta_show_non_working_days') !== 'false',
groupByTicket: localStorage.getItem('otrs_ta_group_by_ticket') !== 'false',
groupArticles(articles) {
if (!articles || articles.length === 0) return [];
if (!this.groupByTicket) {
return articles.map(a => ({ type: 'single', article: a }));
}
const groups = [];
let currentGroup = null;
for (const a of articles) {
const ticketId = a.ticket_id;
if (currentGroup && currentGroup.ticket_id === ticketId) {
currentGroup.articles.push(a);
} else {
if (currentGroup) {
groups.push(currentGroup);
}
currentGroup = {
type: 'group',
ticket_id: ticketId,
ticket_tn: a.ticket_tn,
ticket_title: a.ticket_title,
articles: [a]
};
}
}
if (currentGroup) {
groups.push(currentGroup);
}
return groups;
},
formatLocalISO(d) { formatLocalISO(d) {
if (!d) return ''; if (!d) return '';
@@ -174,6 +207,14 @@ const TimeAccountingView = {
<div style="display:flex; gap:var(--space-xs); align-self:flex-end;"> <div style="display:flex; gap:var(--space-xs); align-self:flex-end;">
<button id="ta-btn-today" class="btn btn-ghost btn-sm" style="height:36px;">Oggi</button> <button id="ta-btn-today" class="btn btn-ghost btn-sm" style="height:36px;">Oggi</button>
<button id="ta-btn-toggle-groupby" class="btn btn-ghost btn-sm" style="height:36px; display:flex; align-items:center; gap:6px; background:${this.groupByTicket ? 'var(--accent-primary)' : 'var(--bg-tertiary)'}; color:${this.groupByTicket ? '#ffffff' : 'var(--text-secondary)'}; border:1px solid var(--border-subtle);" title="Raggruppa le note adiacenti appartenenti allo stesso ticket">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:15px; height:15px;">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
<line x1="12" y1="22.08" x2="12" y2="12"/>
</svg>
<span>${this.groupByTicket ? 'Raggruppa per Ticket (Attivo)' : 'Raggruppa per Ticket'}</span>
</button>
<button id="ta-btn-toggle-nonwork" class="btn btn-ghost btn-sm" style="height:36px; display:flex; align-items:center; gap:6px;"> <button id="ta-btn-toggle-nonwork" class="btn btn-ghost btn-sm" style="height:36px; display:flex; align-items:center; gap:6px;">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:15px; height:15px;"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:15px; height:15px;">
${this.showNonWorkingDays ${this.showNonWorkingDays
@@ -193,7 +234,7 @@ const TimeAccountingView = {
<div style="display:flex; gap:var(--space-md); padding:var(--space-md) var(--space-xl); align-items:flex-start; min-height:calc(100vh - 280px);"> <div style="display:flex; gap:var(--space-md); padding:var(--space-md) var(--space-xl); align-items:flex-start; min-height:calc(100vh - 280px);">
<!-- Left Side: List of Articles --> <!-- Left Side: List of Articles -->
<div style="flex:1; min-width:0; display:flex; flex-direction:column; gap:var(--space-sm);"> <div style="flex:1; min-width:0; display:flex; flex-direction:column; gap:var(--space-md);">
${articles.length === 0 ? ` ${articles.length === 0 ? `
<div class="empty-state" style="padding:var(--space-2xl); text-align:center; color:var(--text-muted); background:var(--bg-card); border-radius:var(--radius-md); border:1px solid var(--border-subtle);"> <div class="empty-state" style="padding:var(--space-2xl); text-align:center; color:var(--text-muted); background:var(--bg-card); border-radius:var(--radius-md); border:1px solid var(--border-subtle);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:48px; height:48px; margin:0 auto var(--space-md); display:block; opacity:0.4;"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:48px; height:48px; margin:0 auto var(--space-md); display:block; opacity:0.4;">
@@ -201,7 +242,7 @@ const TimeAccountingView = {
</svg> </svg>
<p style="font-size:0.9rem; font-weight:500;">Nessuna nota o email trovata in questa data per l'agente selezionato.</p> <p style="font-size:0.9rem; font-weight:500;">Nessuna nota o email trovata in questa data per l'agente selezionato.</p>
</div> </div>
` : articles.map(a => this.buildArticleCard(a)).join('')} ` : this.groupArticles(articles).map(g => g.type === 'single' ? this.buildArticleCard(g.article) : this.buildGroupCard(g)).join('')}
</div> </div>
<!-- Right Side: Details & Actions Panel --> <!-- Right Side: Details & Actions Panel -->
@@ -574,6 +615,40 @@ const TimeAccountingView = {
`; `;
}, },
buildGroupCard(group) {
const totalGroupMins = Math.round(group.articles.reduce((acc, a) => acc + (parseFloat(a.time_unit) || 0), 0));
const formattedGroupTime = this.formatPillTime(totalGroupMins);
const hasSelectedArticle = group.articles.some(a => String(a.article_id) === String(this.selectedArticleId));
return `
<div class="card ta-group-card" style="border:1px solid ${hasSelectedArticle ? 'var(--accent-primary)' : 'var(--border-subtle)'}; background:var(--bg-card); border-radius:var(--radius-md); overflow:hidden;">
<!-- Header del Gruppo Ticket -->
<div style="padding:10px 14px; background:var(--bg-secondary); border-bottom:1px solid var(--border-subtle); display:flex; align-items:center; justify-content:space-between; gap:10px;">
<div style="display:flex; align-items:center; gap:8px; min-width:0;">
<span style="font-size:0.9rem;">🎫</span>
<a href="#/tickets/${group.ticket_id}" class="ticket-tn-link" onclick="event.stopPropagation();" style="font-weight:700; font-size:0.9rem;">#${App.escapeHtml(group.ticket_tn || group.ticket_id)}</a>
<span style="font-size:0.85rem; font-weight:600; color:var(--text-primary); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;" title="${App.escapeHtml(group.ticket_title || '')}">
${App.escapeHtml(group.ticket_title || '(senza titolo)')}
</span>
</div>
<div style="display:flex; align-items:center; gap:8px; flex-shrink:0;">
<span class="badge" style="background:var(--bg-tertiary); color:var(--text-secondary); font-size:0.75rem; font-weight:600;">
${group.articles.length} ${group.articles.length === 1 ? 'nota' : 'note'}
</span>
<span class="badge" style="background:${totalGroupMins > 0 ? 'rgba(34,197,94,0.18)' : 'var(--bg-tertiary)'}; color:${totalGroupMins > 0 ? '#15803d' : 'var(--text-muted)'}; font-weight:700; font-size:0.78rem;">
⏱️ Totale: ${formattedGroupTime} (${totalGroupMins} m)
</span>
</div>
</div>
<!-- Note del Gruppo -->
<div style="display:flex; flex-direction:column; gap:6px; padding:10px;">
${group.articles.map(a => this.buildArticleCard(a)).join('')}
</div>
</div>
`;
},
buildDetailPanel(a) { buildDetailPanel(a) {
const timeMins = parseFloat(a.time_unit || 0); const timeMins = parseFloat(a.time_unit || 0);
@@ -687,6 +762,13 @@ const TimeAccountingView = {
this.loadData(); this.loadData();
}); });
// Toggle group by ticket button
document.getElementById('ta-btn-toggle-groupby')?.addEventListener('click', () => {
this.groupByTicket = !this.groupByTicket;
localStorage.setItem('otrs_ta_group_by_ticket', this.groupByTicket);
this.loadData();
});
// Day pill clicks (also handles calendar cell clicks) // Day pill clicks (also handles calendar cell clicks)
document.querySelectorAll('.ta-day-pill[data-date]').forEach(pill => { document.querySelectorAll('.ta-day-pill[data-date]').forEach(pill => {
pill.addEventListener('click', () => { pill.addEventListener('click', () => {