Compare commits
7
Commits
9527998b7a
...
57175922c2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57175922c2 | ||
|
|
e2482159bc | ||
|
|
99c4f450f4 | ||
|
|
0cbd3ccdfc | ||
|
|
5ef7e66be5 | ||
|
|
38b2645f6f | ||
|
|
7243b3da49 |
@@ -156,20 +156,8 @@ const App = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let timeout;
|
|
||||||
searchInput.addEventListener('input', () => {
|
searchInput.addEventListener('input', () => {
|
||||||
toggleClearBtn();
|
toggleClearBtn();
|
||||||
clearTimeout(timeout);
|
|
||||||
timeout = setTimeout(() => {
|
|
||||||
const hash = window.location.hash;
|
|
||||||
if (hash.startsWith('#/tickets') && !hash.includes('/new') && !hash.match(/#\/tickets\/\d+/)) {
|
|
||||||
TicketListView.currentPage = 1;
|
|
||||||
TicketListView.render();
|
|
||||||
} else {
|
|
||||||
// Navigate to ticket list with search
|
|
||||||
window.location.hash = '#/tickets';
|
|
||||||
}
|
|
||||||
}, 350);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
searchInput.addEventListener('keydown', (e) => {
|
searchInput.addEventListener('keydown', (e) => {
|
||||||
|
|||||||
@@ -325,7 +325,10 @@ const Filters = {
|
|||||||
<span class="filter-label">A Data/Ora</span>
|
<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: 170px; 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>
|
||||||
<div class="filters-actions">
|
<div class="filters-actions" style="display:flex; gap:6px; align-items:center;">
|
||||||
|
<button class="btn btn-primary btn-xs" id="filter-refresh" title="Aggiorna lista ticket" style="display:inline-flex; align-items:center; gap:4px; height:26px; padding:0 10px; font-size:0.78rem;">
|
||||||
|
🔄 Aggiorna
|
||||||
|
</button>
|
||||||
<button class="btn btn-ghost btn-xs" id="filter-reset">Reset</button>
|
<button class="btn btn-ghost btn-xs" id="filter-reset">Reset</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -622,6 +625,13 @@ const Filters = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const refreshBtn = document.getElementById('filter-refresh');
|
||||||
|
if (refreshBtn) {
|
||||||
|
refreshBtn.addEventListener('click', () => {
|
||||||
|
if (onFilterChange) onFilterChange();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const resetBtn = document.getElementById('filter-reset');
|
const resetBtn = document.getElementById('filter-reset');
|
||||||
if (resetBtn) {
|
if (resetBtn) {
|
||||||
resetBtn.addEventListener('click', () => {
|
resetBtn.addEventListener('click', () => {
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ const TicketCreateView = {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
// 1. Owner & Responsible pre-population with active agent
|
// Active agent pre-population for Owner & Responsible
|
||||||
const currentAgentId = localStorage.getItem('activeAgentId') || '1';
|
const currentAgentId = localStorage.getItem('activeAgentId') || '1';
|
||||||
const activeAgent = (App.lookups.users || []).find(u => String(u.id) === String(currentAgentId));
|
const activeAgent = (App.lookups.users || []).find(u => String(u.id) === String(currentAgentId));
|
||||||
if (activeAgent) {
|
if (activeAgent) {
|
||||||
@@ -344,41 +344,6 @@ const TicketCreateView = {
|
|||||||
responsibleIdInput.value = activeAgent.id;
|
responsibleIdInput.value = activeAgent.id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Customer User pre-population with first match
|
|
||||||
try {
|
|
||||||
const companies = await App.api('/api/customer-companies/search?q=');
|
|
||||||
if (companies.length > 0 && customerIdInput && companySearchInput) {
|
|
||||||
const defaultCompany = companies[0];
|
|
||||||
customerIdInput.value = defaultCompany.customer_id;
|
|
||||||
companySearchInput.value = defaultCompany.customer_id;
|
|
||||||
|
|
||||||
// Search users for this company
|
|
||||||
const users = await App.api(`/api/customer-users/search?q=&customer_company_id=${encodeURIComponent(defaultCompany.customer_id)}`);
|
|
||||||
if (users.length > 0 && userSearchInput && customerUserIdInput) {
|
|
||||||
const defaultUser = users[0];
|
|
||||||
userSearchInput.value = `${defaultUser.first_name} ${defaultUser.last_name}`;
|
|
||||||
customerUserIdInput.value = defaultUser.login;
|
|
||||||
} else {
|
|
||||||
// Fallback: use company name as customer user ID
|
|
||||||
userSearchInput.value = defaultCompany.name;
|
|
||||||
customerUserIdInput.value = defaultCompany.customer_id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error pre-populating defaults:', err);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Queue pre-population
|
|
||||||
try {
|
|
||||||
const queues = await App.api('/api/queues/search?q=');
|
|
||||||
if (queues.length > 0 && queueSearchInput && queueIdInput) {
|
|
||||||
queueSearchInput.value = queues[0].name;
|
|
||||||
queueIdInput.value = queues[0].id;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error pre-populating queues:', err);
|
|
||||||
}
|
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -263,6 +263,7 @@ const TicketDetailView = {
|
|||||||
<button type="button" class="btn btn-ghost btn-sm" id="btn-note-add-attachments" style="height:32px; padding: 4px 10px; font-size: 0.85rem; display: flex; align-items: center; gap: 4px;">📎 Allega file</button>
|
<button type="button" class="btn btn-ghost btn-sm" id="btn-note-add-attachments" style="height:32px; padding: 4px 10px; font-size: 0.85rem; display: flex; align-items: center; gap: 4px;">📎 Allega file</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:var(--space-md); align-items:center;">
|
<div style="display:flex; gap:var(--space-md); align-items:center;">
|
||||||
|
<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="number" step="any" min="0" class="note-subject-input" id="note-time-units" placeholder="Tempo (minuti)" style="width:140px; margin-bottom:0; height:32px; padding:4px 10px; font-size:0.85rem;" />
|
<input type="number" step="any" min="0" class="note-subject-input" id="note-time-units" placeholder="Tempo (minuti)" style="width:140px; margin-bottom:0; height:32px; padding:4px 10px; 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 || '')}">
|
||||||
@@ -830,6 +831,7 @@ const TicketDetailView = {
|
|||||||
}
|
}
|
||||||
let subject = document.getElementById('note-subject').value.trim();
|
let subject = document.getElementById('note-subject').value.trim();
|
||||||
const time_unit = document.getElementById('note-time-units').value.trim();
|
const time_unit = document.getElementById('note-time-units').value.trim();
|
||||||
|
const note_create_time = document.getElementById('note-create-time')?.value;
|
||||||
|
|
||||||
// If body is empty but subject is provided, fill body with subject text
|
// If body is empty but subject is provided, fill body with subject text
|
||||||
if (!body && subject) {
|
if (!body && subject) {
|
||||||
@@ -869,7 +871,22 @@ const TicketDetailView = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.noteAttachments = []; // Clear attachments
|
this.noteAttachments = []; // Clear attachments
|
||||||
|
|
||||||
|
if (note_create_time && res.article_id) {
|
||||||
|
try {
|
||||||
|
const formattedDate = note_create_time.replace('T', ' ');
|
||||||
|
await App.api(`/api/tickets/articles/${res.article_id}/retrodata-article`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ create_time: formattedDate })
|
||||||
|
});
|
||||||
|
Toast.success('Nota aggiunta e retrodatata!');
|
||||||
|
} catch (retErr) {
|
||||||
|
Toast.warning('Nota aggiunta, ma errore durante la retrodatazione: ' + retErr.message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
Toast.success(res.message || 'Nota aggiunta!');
|
Toast.success(res.message || 'Nota aggiunta!');
|
||||||
|
}
|
||||||
|
|
||||||
App.clearDraft(this.ticketId, 'note');
|
App.clearDraft(this.ticketId, 'note');
|
||||||
App.updateDailyTimer();
|
App.updateDailyTimer();
|
||||||
this.render(this.ticketId);
|
this.render(this.ticketId);
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ const TicketListView = {
|
|||||||
|
|
||||||
async render() {
|
async render() {
|
||||||
const container = document.getElementById('view-container');
|
const container = document.getElementById('view-container');
|
||||||
|
if (!container.querySelector('.ticket-table-wrapper') && !container.querySelector('.ticket-table')) {
|
||||||
container.innerHTML = '<div class="loading-screen"><div class="spinner"></div><p>Caricamento ticket...</p></div>';
|
container.innerHTML = '<div class="loading-screen"><div class="spinner"></div><p>Caricamento ticket...</p></div>';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch lookups for filter dropdowns
|
// Fetch lookups for filter dropdowns
|
||||||
@@ -106,15 +108,18 @@ const TicketListView = {
|
|||||||
${(App.lookups.users || []).map(u => `<option value="${u.id}">${u.first_name} ${u.last_name}</option>`).join('')}
|
${(App.lookups.users || []).map(u => `<option value="${u.id}">${u.first_name} ${u.last_name}</option>`).join('')}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="filter-group" style="position:relative;">
|
${(App.lookups.types || []).length > 0 ? `
|
||||||
<span class="filter-label">Cliente</span>
|
<div class="filter-group">
|
||||||
<input type="text" class="form-input filter-select" id="batch-customer-search" placeholder="Cerca cliente..." autocomplete="off" style="width:160px; padding: 4px 8px; font-family: inherit; font-size: 0.85rem;" />
|
<span class="filter-label">Tipo</span>
|
||||||
<input type="hidden" id="batch-customer-user-id" />
|
<select class="filter-select" id="batch-type">
|
||||||
<input type="hidden" id="batch-customer-id" />
|
<option value="">—</option>
|
||||||
<div id="batch-customer-suggestions" class="autocomplete-suggestions" style="display:none; top: 100%; left: 0; width: 280px; z-index: 1001;"></div>
|
${App.lookups.types.map(t => `<option value="${t.id}">${t.name}</option>`).join('')}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
` : ''}
|
||||||
<button class="btn btn-primary btn-sm" id="batch-apply">Applica</button>
|
<button class="btn btn-primary btn-sm" id="batch-apply">Applica</button>
|
||||||
<button class="btn btn-primary btn-sm" id="batch-merge" disabled style="background: var(--accent-secondary); border-color: var(--accent-secondary); margin-left: 8px;">Unisci Selezionati</button>
|
<button class="btn btn-primary btn-sm" id="batch-add-group" disabled style="background: var(--accent-primary); border-color: var(--accent-primary); margin-left: 8px;">Aggiungi a gruppo</button>
|
||||||
|
<button class="btn btn-primary btn-sm" id="batch-merge" disabled style="background: rgba(160, 65, 71, 0.32); border-color: var(--accent-primary); color: var(--text-primary); margin-left: 8px;">Unisci Selezionati</button>
|
||||||
<button class="btn btn-primary btn-sm" id="batch-open-tabs" disabled style="background: var(--accent-primary); border-color: var(--accent-primary); margin-left: 8px;">Apri ticket in schede</button>
|
<button class="btn btn-primary btn-sm" id="batch-open-tabs" disabled style="background: var(--accent-primary); border-color: var(--accent-primary); margin-left: 8px;">Apri ticket in schede</button>
|
||||||
<button class="btn btn-ghost btn-xs" id="batch-cancel">Annulla</button>
|
<button class="btn btn-ghost btn-xs" id="batch-cancel">Annulla</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -306,6 +311,12 @@ const TicketListView = {
|
|||||||
batchApply.addEventListener('click', () => this.applyBatch());
|
batchApply.addEventListener('click', () => this.applyBatch());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Batch add to group
|
||||||
|
const batchAddGroup = document.getElementById('batch-add-group');
|
||||||
|
if (batchAddGroup) {
|
||||||
|
batchAddGroup.addEventListener('click', () => this.addToGroup());
|
||||||
|
}
|
||||||
|
|
||||||
// Batch merge (Issue #7)
|
// Batch merge (Issue #7)
|
||||||
const batchMerge = document.getElementById('batch-merge');
|
const batchMerge = document.getElementById('batch-merge');
|
||||||
if (batchMerge) {
|
if (batchMerge) {
|
||||||
@@ -439,6 +450,8 @@ const TicketListView = {
|
|||||||
if (batchOwner) batchOwner.value = '';
|
if (batchOwner) batchOwner.value = '';
|
||||||
const batchResponsible = document.getElementById('batch-responsible');
|
const batchResponsible = document.getElementById('batch-responsible');
|
||||||
if (batchResponsible) batchResponsible.value = '';
|
if (batchResponsible) batchResponsible.value = '';
|
||||||
|
const batchType = document.getElementById('batch-type');
|
||||||
|
if (batchType) batchType.value = '';
|
||||||
|
|
||||||
this.updateBatchBar();
|
this.updateBatchBar();
|
||||||
});
|
});
|
||||||
@@ -591,6 +604,12 @@ const TicketListView = {
|
|||||||
count.textContent = `${this.selectedIds.size} selezionat${this.selectedIds.size === 1 ? 'o' : 'i'}`;
|
count.textContent = `${this.selectedIds.size} selezionat${this.selectedIds.size === 1 ? 'o' : 'i'}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enable/disable add to group button
|
||||||
|
const addGroupBtn = document.getElementById('batch-add-group');
|
||||||
|
if (addGroupBtn) {
|
||||||
|
addGroupBtn.disabled = this.selectedIds.size === 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Enable/disable merge button
|
// Enable/disable merge button
|
||||||
const mergeBtn = document.getElementById('batch-merge');
|
const mergeBtn = document.getElementById('batch-merge');
|
||||||
if (mergeBtn) {
|
if (mergeBtn) {
|
||||||
@@ -620,6 +639,7 @@ const TicketListView = {
|
|||||||
const batchQueue = document.getElementById('batch-queue')?.value;
|
const batchQueue = document.getElementById('batch-queue')?.value;
|
||||||
const batchOwner = document.getElementById('batch-owner')?.value;
|
const batchOwner = document.getElementById('batch-owner')?.value;
|
||||||
const batchResponsible = document.getElementById('batch-responsible')?.value;
|
const batchResponsible = document.getElementById('batch-responsible')?.value;
|
||||||
|
const batchType = document.getElementById('batch-type')?.value;
|
||||||
const batchCustomerSearch = document.getElementById('batch-customer-search')?.value.trim();
|
const batchCustomerSearch = document.getElementById('batch-customer-search')?.value.trim();
|
||||||
let batchCustomerUserId = document.getElementById('batch-customer-user-id')?.value;
|
let batchCustomerUserId = document.getElementById('batch-customer-user-id')?.value;
|
||||||
let batchCustomerId = document.getElementById('batch-customer-id')?.value;
|
let batchCustomerId = document.getElementById('batch-customer-id')?.value;
|
||||||
@@ -633,6 +653,7 @@ const TicketListView = {
|
|||||||
|
|
||||||
if (batchState) updates.ticket_state_id = parseInt(batchState);
|
if (batchState) updates.ticket_state_id = parseInt(batchState);
|
||||||
if (batchQueue) updates.queue_id = parseInt(batchQueue);
|
if (batchQueue) updates.queue_id = parseInt(batchQueue);
|
||||||
|
if (batchType) updates.type_id = parseInt(batchType);
|
||||||
if (batchOwner) updates.user_id = parseInt(batchOwner);
|
if (batchOwner) updates.user_id = parseInt(batchOwner);
|
||||||
if (batchResponsible) {
|
if (batchResponsible) {
|
||||||
updates.responsible_user_id = parseInt(batchResponsible);
|
updates.responsible_user_id = parseInt(batchResponsible);
|
||||||
@@ -706,10 +727,86 @@ const TicketListView = {
|
|||||||
Toast.success(res.message || 'Ticket uniti con successo');
|
Toast.success(res.message || 'Ticket uniti con successo');
|
||||||
this.selectedIds.clear();
|
this.selectedIds.clear();
|
||||||
this.selectedOrder = [];
|
this.selectedOrder = [];
|
||||||
this.render();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Toast.error('Errore durante l\'unione: ' + err.message);
|
Toast.error('Errore durante l\'unione: ' + err.message);
|
||||||
this.updateBatchBar();
|
this.updateBatchBar();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async addToGroup() {
|
||||||
|
if (this.selectedIds.size === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const groups = await App.api('/api/groups');
|
||||||
|
if (!groups || groups.length === 0) {
|
||||||
|
Toast.warning('non sono presenti gruppi');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show group selection prompt/dialog
|
||||||
|
const groupOptions = groups.map(g => `<option value="${g.id}">${App.escapeHtml(g.nome)}</option>`).join('');
|
||||||
|
const dialogHtml = `
|
||||||
|
<div id="batch-group-modal" style="position: fixed; inset: 0; z-index: 8000; background: rgba(0,0,0,0.5); backdrop-filter: blur(4px); display: flex; align-items: center; justify-content: center;">
|
||||||
|
<div style="background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); width: 400px; max-width: 90vw; padding: var(--space-lg); box-shadow: var(--shadow-lg);">
|
||||||
|
<h4 style="margin: 0 0 var(--space-md) 0; font-size: 1rem; font-weight: 600; color: var(--text-primary);">Aggiungi a Gruppo</h4>
|
||||||
|
<p style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: var(--space-md);">Seleziona il gruppo a cui aggiungere i <strong>${this.selectedIds.size}</strong> ticket selezionati:</p>
|
||||||
|
<select id="batch-group-select" class="form-select" style="width: 100%; margin-bottom: var(--space-lg);">
|
||||||
|
${groupOptions}
|
||||||
|
</select>
|
||||||
|
<div style="display: flex; gap: var(--space-sm); justify-content: flex-end;">
|
||||||
|
<button class="btn btn-ghost btn-sm" id="batch-group-cancel">Annulla</button>
|
||||||
|
<button class="btn btn-primary btn-sm" id="batch-group-confirm">Aggiungi</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Append modal to body
|
||||||
|
const modalContainer = document.createElement('div');
|
||||||
|
modalContainer.innerHTML = dialogHtml;
|
||||||
|
document.body.appendChild(modalContainer);
|
||||||
|
|
||||||
|
const closeModal = () => modalContainer.remove();
|
||||||
|
|
||||||
|
document.getElementById('batch-group-cancel').addEventListener('click', closeModal);
|
||||||
|
document.getElementById('batch-group-confirm').addEventListener('click', async () => {
|
||||||
|
const groupId = document.getElementById('batch-group-select').value;
|
||||||
|
if (!groupId) return;
|
||||||
|
|
||||||
|
const confirmBtn = document.getElementById('batch-group-confirm');
|
||||||
|
confirmBtn.disabled = true;
|
||||||
|
confirmBtn.textContent = 'Aggiunta...';
|
||||||
|
|
||||||
|
let addedCount = 0;
|
||||||
|
let errorsCount = 0;
|
||||||
|
|
||||||
|
for (const ticketId of this.selectedIds) {
|
||||||
|
try {
|
||||||
|
await App.api(`/api/groups/${groupId}/tickets`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ ticket_identifier: ticketId })
|
||||||
|
});
|
||||||
|
addedCount++;
|
||||||
|
} catch (err) {
|
||||||
|
// Conflict (already in group) or other errors
|
||||||
|
errorsCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeModal();
|
||||||
|
|
||||||
|
if (addedCount > 0) {
|
||||||
|
Toast.success(`${addedCount} ticket aggiunti al gruppo!`);
|
||||||
|
this.selectedIds.clear();
|
||||||
|
this.selectedOrder = [];
|
||||||
|
this.render();
|
||||||
|
} else if (errorsCount > 0) {
|
||||||
|
Toast.warning('I ticket selezionati appartengono già a questo gruppo.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error('Errore durante il recupero dei gruppi: ' + err.message);
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+10
-1
@@ -34,11 +34,20 @@ async function fetchTicketsDetails(ticketIds) {
|
|||||||
// GET /api/groups - List all groups with member counts
|
// GET /api/groups - List all groups with member counts
|
||||||
router.get('/', (req, res) => {
|
router.get('/', (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
// Ensure default group exists
|
||||||
|
const defaultGroup = db.prepare('SELECT id FROM ticket_groups WHERE UPPER(nome) = ?').get('CHIUDI A FINE GIORNATA');
|
||||||
|
if (!defaultGroup) {
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO ticket_groups (nome, descrizione)
|
||||||
|
VALUES (?, ?)
|
||||||
|
`).run('CHIUDI A FINE GIORNATA', 'I ticket in questo gruppo verranno chiusi automaticamente alla consuntivazione di fine giornata');
|
||||||
|
}
|
||||||
|
|
||||||
const groups = db.prepare(`
|
const groups = db.prepare(`
|
||||||
SELECT g.*,
|
SELECT g.*,
|
||||||
(SELECT COUNT(*) FROM ticket_group_members WHERE group_id = g.id) AS member_count
|
(SELECT COUNT(*) FROM ticket_group_members WHERE group_id = g.id) AS member_count
|
||||||
FROM ticket_groups g
|
FROM ticket_groups g
|
||||||
ORDER BY g.nome ASC
|
ORDER BY CASE WHEN UPPER(g.nome) = 'CHIUDI A FINE GIORNATA' THEN 1 ELSE 0 END ASC, g.nome ASC
|
||||||
`).all();
|
`).all();
|
||||||
|
|
||||||
res.json(groups);
|
res.json(groups);
|
||||||
|
|||||||
+38
-12
@@ -530,6 +530,8 @@ router.post('/', async (req, res) => {
|
|||||||
// Operator user for create_by (X-Agent-ID header or default to 1)
|
// Operator user for create_by (X-Agent-ID header or default to 1)
|
||||||
const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1;
|
const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1;
|
||||||
|
|
||||||
|
const localNow = getLocalTimestamp();
|
||||||
|
|
||||||
const ticketResult = await client.query(
|
const ticketResult = await client.query(
|
||||||
`INSERT INTO ticket (
|
`INSERT INTO ticket (
|
||||||
tn, title, queue_id, ticket_lock_id, type_id,
|
tn, title, queue_id, ticket_lock_id, type_id,
|
||||||
@@ -550,14 +552,14 @@ router.post('/', async (req, res) => {
|
|||||||
0, 0,
|
0, 0,
|
||||||
0, 0,
|
0, 0,
|
||||||
0,
|
0,
|
||||||
NOW(), $12, NOW(), $12
|
$12, $13, $12, $13
|
||||||
) RETURNING id, tn`,
|
) RETURNING id, tn`,
|
||||||
[
|
[
|
||||||
tn, title, queue_id, lockId, type_id || null,
|
tn, title, queue_id, lockId, type_id || null,
|
||||||
user_id || 1, responsibleUserId,
|
user_id || 1, responsibleUserId,
|
||||||
priority_id, state_id,
|
priority_id, state_id,
|
||||||
customer_id || null, customer_user_id || null,
|
customer_id || null, customer_user_id || null,
|
||||||
operatorId
|
localNow, operatorId
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -578,13 +580,13 @@ router.post('/', async (req, res) => {
|
|||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5,
|
$1, $2, $3, $4, $5,
|
||||||
$6, $7, $8,
|
$6, $7, $8,
|
||||||
NOW(), $9, NOW(), $9
|
$9, $10, $9, $10
|
||||||
)`,
|
)`,
|
||||||
[
|
[
|
||||||
`%%`,
|
`%%`,
|
||||||
historyTypeId, ticketId, type_id || 1, queue_id,
|
historyTypeId, ticketId, type_id || 1, queue_id,
|
||||||
user_id || 1, priority_id, state_id,
|
user_id || 1, priority_id, state_id,
|
||||||
operatorId
|
localNow, operatorId
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -643,9 +645,9 @@ router.post('/', async (req, res) => {
|
|||||||
is_visible_for_customer, search_index_needs_rebuild,
|
is_visible_for_customer, search_index_needs_rebuild,
|
||||||
create_time, create_by, change_time, change_by
|
create_time, create_by, change_time, change_by
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, 0, 1, NOW(), $4, NOW(), $4
|
$1, $2, $3, 0, 1, $4, $5, $4, $5
|
||||||
) RETURNING id`,
|
) RETURNING id`,
|
||||||
[ticketId, senderTypeId, channelId, operatorId]
|
[ticketId, senderTypeId, channelId, localNow, operatorId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const articleId = articleResult.rows[0].id;
|
const articleId = articleResult.rows[0].id;
|
||||||
@@ -660,9 +662,9 @@ router.post('/', async (req, res) => {
|
|||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, '', $3, $4,
|
$1, $2, '', $3, $4,
|
||||||
$5, EXTRACT(EPOCH FROM NOW())::INTEGER,
|
$5, EXTRACT(EPOCH FROM NOW())::INTEGER,
|
||||||
NOW(), $6, NOW(), $6
|
$6, $7, $6, $7
|
||||||
)`,
|
)`,
|
||||||
[articleId, customerFrom, subject || title, finalBody, contentType, operatorId]
|
[articleId, customerFrom, subject || title, finalBody, contentType, localNow, operatorId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// If HTML content, create article_data_mime_attachment for OTRS CE HTML rendering (file-1)
|
// If HTML content, create article_data_mime_attachment for OTRS CE HTML rendering (file-1)
|
||||||
@@ -678,9 +680,9 @@ router.post('/', async (req, res) => {
|
|||||||
create_time, create_by, change_time, change_by
|
create_time, create_by, change_time, change_by
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, 'file-1', $2, 'text/html; charset="utf-8"', '', $3,
|
$1, 'file-1', $2, 'text/html; charset="utf-8"', '', $3,
|
||||||
NOW(), $4, NOW(), $4
|
$4, $5, $4, $5
|
||||||
)`,
|
)`,
|
||||||
[articleId, String(contentSize), base64Body, operatorId]
|
[articleId, String(contentSize), base64Body, localNow, operatorId]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,13 +694,14 @@ router.post('/', async (req, res) => {
|
|||||||
`INSERT INTO article_data_mime_attachment (
|
`INSERT INTO article_data_mime_attachment (
|
||||||
article_id, filename, content_size, content_type, disposition, content,
|
article_id, filename, content_size, content_type, disposition, content,
|
||||||
create_time, create_by, change_time, change_by
|
create_time, create_by, change_time, change_by
|
||||||
) VALUES ($1, $2, $3, $4, 'attachment', $5, NOW(), $6, NOW(), $6)`,
|
) VALUES ($1, $2, $3, $4, 'attachment', $5, $6, $7, $6, $7)`,
|
||||||
[
|
[
|
||||||
articleId,
|
articleId,
|
||||||
att.filename,
|
att.filename,
|
||||||
contentBuffer.length,
|
contentBuffer.length,
|
||||||
att.content_type || 'application/octet-stream',
|
att.content_type || 'application/octet-stream',
|
||||||
att.content, // OTRS CE expects base64 string directly
|
att.content, // OTRS CE expects base64 string directly
|
||||||
|
localNow,
|
||||||
operatorId
|
operatorId
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -1417,6 +1420,7 @@ router.patch('/batch/update', async (req, res) => {
|
|||||||
if (updates.ticket_state_id !== undefined) ticketFields.StateID = updates.ticket_state_id;
|
if (updates.ticket_state_id !== undefined) ticketFields.StateID = updates.ticket_state_id;
|
||||||
if (updates.ticket_priority_id !== undefined) ticketFields.PriorityID = updates.ticket_priority_id;
|
if (updates.ticket_priority_id !== undefined) ticketFields.PriorityID = updates.ticket_priority_id;
|
||||||
if (updates.queue_id !== undefined) ticketFields.QueueID = updates.queue_id;
|
if (updates.queue_id !== undefined) ticketFields.QueueID = updates.queue_id;
|
||||||
|
if (updates.type_id !== undefined) ticketFields.TypeID = updates.type_id;
|
||||||
if (updates.user_id !== undefined) ticketFields.OwnerID = updates.user_id;
|
if (updates.user_id !== undefined) ticketFields.OwnerID = updates.user_id;
|
||||||
if (updates.responsible_user_id !== undefined) ticketFields.ResponsibleID = updates.responsible_user_id;
|
if (updates.responsible_user_id !== undefined) ticketFields.ResponsibleID = updates.responsible_user_id;
|
||||||
if (updates.customer_id !== undefined) ticketFields.CustomerID = updates.customer_id;
|
if (updates.customer_id !== undefined) ticketFields.CustomerID = updates.customer_id;
|
||||||
@@ -1471,7 +1475,7 @@ router.patch('/batch/update', async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
await client.query('BEGIN');
|
await client.query('BEGIN');
|
||||||
|
|
||||||
const allowedFields = ['ticket_state_id', 'ticket_priority_id', 'queue_id', 'user_id', 'responsible_user_id', 'customer_id', 'customer_user_id'];
|
const allowedFields = ['ticket_state_id', 'ticket_priority_id', 'queue_id', 'type_id', 'user_id', 'responsible_user_id', 'customer_id', 'customer_user_id'];
|
||||||
const setClauses = [];
|
const setClauses = [];
|
||||||
const setParams = [];
|
const setParams = [];
|
||||||
let pIdx = 1;
|
let pIdx = 1;
|
||||||
@@ -2185,6 +2189,28 @@ router.post('/auto-time', async (req, res) => {
|
|||||||
[ticketId, articleId, remaining, operatorId]
|
[ticketId, articleId, remaining, operatorId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 6. Close tickets in default group 'CHIUDI A FINE GIORNATA'
|
||||||
|
try {
|
||||||
|
const closeGroup = db.prepare('SELECT id FROM ticket_groups WHERE UPPER(nome) = ?').get('CHIUDI A FINE GIORNATA');
|
||||||
|
if (closeGroup) {
|
||||||
|
const groupMembers = db.prepare('SELECT ticket_id FROM ticket_group_members WHERE group_id = ?').all(closeGroup.id);
|
||||||
|
if (groupMembers.length > 0) {
|
||||||
|
const groupTicketIds = groupMembers.map(m => m.ticket_id);
|
||||||
|
const idPlaceholders = groupTicketIds.map((_, i) => `$${i + 1}`).join(', ');
|
||||||
|
|
||||||
|
// Update ticket state to closed in OTRS DB
|
||||||
|
await client.query(
|
||||||
|
`UPDATE ticket SET ticket_state_id = $${groupTicketIds.length + 1}, change_time = NOW(), change_by = $${groupTicketIds.length + 2} WHERE id IN (${idPlaceholders})`,
|
||||||
|
[...groupTicketIds, stateId, operatorId]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[Auto-Time] Closed ${groupTicketIds.length} tickets from group 'CHIUDI A FINE GIORNATA'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (grpCloseErr) {
|
||||||
|
console.error('[Auto-Time] Error closing tickets from CHIUDI A FINE GIORNATA group:', grpCloseErr);
|
||||||
|
}
|
||||||
|
|
||||||
await client.query('COMMIT');
|
await client.query('COMMIT');
|
||||||
|
|
||||||
// Log activity
|
// Log activity
|
||||||
|
|||||||
Reference in New Issue
Block a user