tool per ticket automatici
This commit is contained in:
@@ -1984,3 +1984,58 @@ body {
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-size: 0.72rem;
|
font-size: 0.72rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Upload list & item styling */
|
||||||
|
.upload-file-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-xs);
|
||||||
|
margin-top: var(--space-sm);
|
||||||
|
margin-bottom: var(--space-md);
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-light);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--space-xs) var(--space-sm);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-name {
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
margin-right: var(--space-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-remove {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--error);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 4px;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: opacity var(--transition-fast);
|
||||||
|
}
|
||||||
|
|
||||||
|
.upload-file-remove:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-row .badge-attachments-count {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: -6px;
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
const TicketBulkView = {
|
const TicketBulkView = {
|
||||||
rowCount: 0,
|
rowCount: 0,
|
||||||
activeRequests: false,
|
activeRequests: false,
|
||||||
|
savedRows: null,
|
||||||
|
rowAttachments: {},
|
||||||
|
|
||||||
async render() {
|
async render() {
|
||||||
const container = document.getElementById('view-container');
|
const container = document.getElementById('view-container');
|
||||||
@@ -196,6 +198,10 @@ const TicketBulkView = {
|
|||||||
</td>
|
</td>
|
||||||
<td style="text-align:center;">
|
<td style="text-align:center;">
|
||||||
<div class="row-actions">
|
<div class="row-actions">
|
||||||
|
<button type="button" class="btn btn-ghost btn-xs" id="bulk-attach-btn-${id}" title="Allega file" style="position:relative; display:inline-flex; align-items:center; gap:2px; height:24px; padding:0 6px;">
|
||||||
|
📎 <span class="badge-attachments-count" id="bulk-attach-badge-${id}" style="display:none; font-size:10px;">0</span>
|
||||||
|
</button>
|
||||||
|
<input type="file" id="bulk-file-input-${id}" multiple style="display:none;" />
|
||||||
<button class="btn btn-ghost btn-xs" id="bulk-expand-btn-${id}" title="Opzioni avanzate">
|
<button class="btn btn-ghost btn-xs" id="bulk-expand-btn-${id}" title="Opzioni avanzate">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;">
|
||||||
<path d="M12 5v14M5 12h14"/>
|
<path d="M12 5v14M5 12h14"/>
|
||||||
@@ -238,6 +244,48 @@ const TicketBulkView = {
|
|||||||
container.appendChild(tr);
|
container.appendChild(tr);
|
||||||
container.appendChild(trExp);
|
container.appendChild(trExp);
|
||||||
|
|
||||||
|
// Initialize attachments for this row
|
||||||
|
this.rowAttachments[id] = savedData ? (savedData.attachments || []) : [];
|
||||||
|
|
||||||
|
// Bind attachment click and input change
|
||||||
|
const attachBtn = tr.querySelector(`#bulk-attach-btn-${id}`);
|
||||||
|
const fileInput = tr.querySelector(`#bulk-file-input-${id}`);
|
||||||
|
const attachBadge = tr.querySelector(`#bulk-attach-badge-${id}`);
|
||||||
|
|
||||||
|
const updateBadge = () => {
|
||||||
|
const count = (this.rowAttachments[id] || []).length;
|
||||||
|
if (attachBadge) {
|
||||||
|
attachBadge.textContent = count;
|
||||||
|
attachBadge.style.display = count > 0 ? 'inline-block' : 'none';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (attachBtn && fileInput) {
|
||||||
|
attachBtn.addEventListener('click', () => fileInput.click());
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
const files = Array.from(e.target.files);
|
||||||
|
for (const file of files) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const base64Data = reader.result.split(',')[1];
|
||||||
|
if (!this.rowAttachments[id]) {
|
||||||
|
this.rowAttachments[id] = [];
|
||||||
|
}
|
||||||
|
this.rowAttachments[id].push({
|
||||||
|
filename: file.name,
|
||||||
|
content: base64Data,
|
||||||
|
content_type: file.type
|
||||||
|
});
|
||||||
|
updateBadge();
|
||||||
|
this.saveState();
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
fileInput.value = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Populate with savedData if available, otherwise set default values
|
// Populate with savedData if available, otherwise set default values
|
||||||
if (savedData) {
|
if (savedData) {
|
||||||
tr.querySelector(`#bulk-state-${id}`).value = savedData.state_id;
|
tr.querySelector(`#bulk-state-${id}`).value = savedData.state_id;
|
||||||
@@ -367,6 +415,7 @@ const TicketBulkView = {
|
|||||||
input.addEventListener('change', () => this.saveState());
|
input.addEventListener('change', () => this.saveState());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
updateBadge();
|
||||||
this.saveState();
|
this.saveState();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -430,7 +479,8 @@ const TicketBulkView = {
|
|||||||
company_display,
|
company_display,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
statusDotClass,
|
statusDotClass,
|
||||||
statusDotTitle
|
statusDotTitle,
|
||||||
|
attachments: this.rowAttachments[id] || []
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
this.savedRows = data;
|
this.savedRows = data;
|
||||||
@@ -658,7 +708,8 @@ const TicketBulkView = {
|
|||||||
customer_id: customerId || undefined,
|
customer_id: customerId || undefined,
|
||||||
customer_user_id: customerUserId || undefined,
|
customer_user_id: customerUserId || undefined,
|
||||||
subject: subject || undefined,
|
subject: subject || undefined,
|
||||||
body: body || undefined
|
body: body || undefined,
|
||||||
|
attachments: (this.rowAttachments[id] || []).length > 0 ? this.rowAttachments[id] : undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -707,6 +758,7 @@ const TicketBulkView = {
|
|||||||
if (failCount === 0) {
|
if (failCount === 0) {
|
||||||
Toast.success(`Tutti i ${successCount} ticket sono stati creati con successo!`);
|
Toast.success(`Tutti i ${successCount} ticket sono stati creati con successo!`);
|
||||||
this.savedRows = [];
|
this.savedRows = [];
|
||||||
|
this.rowAttachments = {};
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.hash = '#/tickets';
|
window.location.hash = '#/tickets';
|
||||||
}, 1500);
|
}, 1500);
|
||||||
|
|||||||
@@ -4,6 +4,27 @@
|
|||||||
*/
|
*/
|
||||||
const TicketCreateView = {
|
const TicketCreateView = {
|
||||||
savedState: null,
|
savedState: null,
|
||||||
|
attachments: [],
|
||||||
|
|
||||||
|
updateAttachmentList() {
|
||||||
|
const listEl = document.getElementById('create-file-list');
|
||||||
|
if (!listEl) return;
|
||||||
|
listEl.innerHTML = this.attachments.map((att, idx) => `
|
||||||
|
<div class="upload-file-item" data-index="${idx}">
|
||||||
|
<span class="upload-file-name">📎 ${App.escapeHtml(att.filename)} (${Math.round(att.content.length * 0.75 / 1024)} KB)</span>
|
||||||
|
<button type="button" class="upload-file-remove" data-index="${idx}">×</button>
|
||||||
|
</div>
|
||||||
|
`).join('');
|
||||||
|
|
||||||
|
// Bind remove clicks
|
||||||
|
listEl.querySelectorAll('.upload-file-remove').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const idx = parseInt(btn.dataset.index, 10);
|
||||||
|
this.attachments.splice(idx, 1);
|
||||||
|
this.updateAttachmentList();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
saveState() {
|
saveState() {
|
||||||
const stateEl = document.getElementById('create-state');
|
const stateEl = document.getElementById('create-state');
|
||||||
@@ -175,6 +196,9 @@ const TicketCreateView = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Attachments List -->
|
||||||
|
<div class="upload-file-list" id="create-file-list"></div>
|
||||||
|
|
||||||
<div class="form-actions" style="display:flex; justify-content:space-between; align-items:center;">
|
<div class="form-actions" style="display:flex; justify-content:space-between; align-items:center;">
|
||||||
<div>
|
<div>
|
||||||
<button type="button" class="btn btn-ghost btn-sm" id="toggle-advanced" style="display: flex; align-items: center; gap: var(--space-xs); padding: 6px 12px; height: auto; margin:0;">
|
<button type="button" class="btn btn-ghost btn-sm" id="toggle-advanced" style="display: flex; align-items: center; gap: var(--space-xs); padding: 6px 12px; height: auto; margin:0;">
|
||||||
@@ -182,7 +206,9 @@ const TicketCreateView = {
|
|||||||
Opzioni Avanzate
|
Opzioni Avanzate
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex; gap:var(--space-sm);">
|
<div style="display:flex; gap:var(--space-sm); align-items:center;">
|
||||||
|
<input type="file" id="create-attachments" multiple style="display:none;" />
|
||||||
|
<button type="button" class="btn btn-ghost" id="btn-add-attachments">📎 Allega file</button>
|
||||||
<button class="btn btn-ghost" onclick="history.back()">Annulla</button>
|
<button class="btn btn-ghost" onclick="history.back()">Annulla</button>
|
||||||
<button class="btn btn-primary" id="create-submit">
|
<button class="btn btn-primary" id="create-submit">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;">
|
||||||
@@ -582,6 +608,7 @@ const TicketCreateView = {
|
|||||||
customer_user_id: customerUserId || undefined,
|
customer_user_id: customerUserId || undefined,
|
||||||
subject: document.getElementById('create-subject').value.trim() || undefined,
|
subject: document.getElementById('create-subject').value.trim() || undefined,
|
||||||
body: document.getElementById('create-body').value.trim() || undefined,
|
body: document.getElementById('create-body').value.trim() || undefined,
|
||||||
|
attachments: this.attachments.length > 0 ? this.attachments : undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -595,6 +622,7 @@ const TicketCreateView = {
|
|||||||
|
|
||||||
Toast.success(`Ticket #${result.tn} creato!`);
|
Toast.success(`Ticket #${result.tn} creato!`);
|
||||||
this.savedState = null; // Clear cached state on success
|
this.savedState = null; // Clear cached state on success
|
||||||
|
this.attachments = []; // Clear attachments array
|
||||||
|
|
||||||
// Navigate to the new ticket
|
// Navigate to the new ticket
|
||||||
window.location.hash = `#/tickets/${result.id}`;
|
window.location.hash = `#/tickets/${result.id}`;
|
||||||
@@ -611,6 +639,32 @@ const TicketCreateView = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Attachments Upload Handlers
|
||||||
|
const attachBtn = document.getElementById('btn-add-attachments');
|
||||||
|
const fileInput = document.getElementById('create-attachments');
|
||||||
|
|
||||||
|
if (attachBtn && fileInput) {
|
||||||
|
attachBtn.addEventListener('click', () => fileInput.click());
|
||||||
|
|
||||||
|
fileInput.addEventListener('change', (e) => {
|
||||||
|
const files = Array.from(e.target.files);
|
||||||
|
for (const file of files) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
const base64Data = reader.result.split(',')[1];
|
||||||
|
this.attachments.push({
|
||||||
|
filename: file.name,
|
||||||
|
content: base64Data,
|
||||||
|
content_type: file.type
|
||||||
|
});
|
||||||
|
this.updateAttachmentList();
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
fileInput.value = ''; // Reset input to allow re-selecting the same file
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Save state on any input/change in the form
|
// Save state on any input/change in the form
|
||||||
document.querySelectorAll('.create-form input, .create-form select, .create-form textarea').forEach(el => {
|
document.querySelectorAll('.create-form input, .create-form select, .create-form textarea').forEach(el => {
|
||||||
el.addEventListener('input', () => this.saveState());
|
el.addEventListener('input', () => this.saveState());
|
||||||
|
|||||||
+45
-6
@@ -244,7 +244,7 @@ router.post('/', async (req, res) => {
|
|||||||
const {
|
const {
|
||||||
title, queue_id, state_id, priority_id, type_id,
|
title, queue_id, state_id, priority_id, type_id,
|
||||||
user_id, customer_id, customer_user_id, body, subject,
|
user_id, customer_id, customer_user_id, body, subject,
|
||||||
responsible_user_id
|
responsible_user_id, attachments
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
await client.query('BEGIN');
|
await client.query('BEGIN');
|
||||||
@@ -328,11 +328,28 @@ router.post('/', async (req, res) => {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create initial article if body is provided
|
// Create initial article if body or attachments are provided
|
||||||
if (body) {
|
if (body || (attachments && attachments.length > 0)) {
|
||||||
// Get sender type ID for "agent"
|
// Determine sender type (customer vs agent) and sender name/email
|
||||||
|
let senderTypeName = 'agent';
|
||||||
|
let customerFrom = 'OTRS Turbo Agent';
|
||||||
|
|
||||||
|
if (customer_user_id) {
|
||||||
|
const custRes = await client.query(
|
||||||
|
`SELECT email, first_name, last_name FROM customer_user WHERE login = $1`,
|
||||||
|
[customer_user_id]
|
||||||
|
);
|
||||||
|
if (custRes.rows.length > 0) {
|
||||||
|
const c = custRes.rows[0];
|
||||||
|
customerFrom = `${c.first_name} ${c.last_name} <${c.email}>`;
|
||||||
|
senderTypeName = 'customer';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get sender type ID
|
||||||
const senderResult = await client.query(
|
const senderResult = await client.query(
|
||||||
`SELECT id FROM article_sender_type WHERE name = 'agent'`
|
`SELECT id FROM article_sender_type WHERE name = $1`,
|
||||||
|
[senderTypeName]
|
||||||
);
|
);
|
||||||
const senderTypeId = senderResult.rows.length > 0 ? senderResult.rows[0].id : 1;
|
const senderTypeId = senderResult.rows.length > 0 ? senderResult.rows[0].id : 1;
|
||||||
|
|
||||||
@@ -354,6 +371,7 @@ router.post('/', async (req, res) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const articleId = articleResult.rows[0].id;
|
const articleId = articleResult.rows[0].id;
|
||||||
|
const finalBody = body || 'File allegati in creazione';
|
||||||
|
|
||||||
await client.query(
|
await client.query(
|
||||||
`INSERT INTO article_data_mime (
|
`INSERT INTO article_data_mime (
|
||||||
@@ -365,8 +383,29 @@ router.post('/', async (req, res) => {
|
|||||||
'text/plain; charset=utf-8', EXTRACT(EPOCH FROM NOW())::INTEGER,
|
'text/plain; charset=utf-8', EXTRACT(EPOCH FROM NOW())::INTEGER,
|
||||||
NOW(), $5, NOW(), $5
|
NOW(), $5, NOW(), $5
|
||||||
)`,
|
)`,
|
||||||
[articleId, 'OTRS Turbo Agent', subject || title, body, operatorId]
|
[articleId, customerFrom, subject || title, finalBody, operatorId]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Insert attachments if any
|
||||||
|
if (attachments && Array.isArray(attachments)) {
|
||||||
|
for (const att of attachments) {
|
||||||
|
const contentBuffer = Buffer.from(att.content, 'base64');
|
||||||
|
await client.query(
|
||||||
|
`INSERT INTO article_data_mime_attachment (
|
||||||
|
article_id, filename, content_size, content_type, disposition, content,
|
||||||
|
create_time, create_by, change_time, change_by
|
||||||
|
) VALUES ($1, $2, $3, $4, 'attachment', $5, NOW(), $6, NOW(), $6)`,
|
||||||
|
[
|
||||||
|
articleId,
|
||||||
|
att.filename,
|
||||||
|
contentBuffer.length,
|
||||||
|
att.content_type || 'application/octet-stream',
|
||||||
|
contentBuffer,
|
||||||
|
operatorId
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await client.query('COMMIT');
|
await client.query('COMMIT');
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
|
||||||
|
// Load environment variables if .env exists
|
||||||
|
try {
|
||||||
|
const dotenvPath = path.resolve(__dirname, '../.env');
|
||||||
|
if (fs.existsSync(dotenvPath)) {
|
||||||
|
require('dotenv').config({ path: dotenvPath });
|
||||||
|
}
|
||||||
|
} catch (err) {}
|
||||||
|
|
||||||
|
const port = process.env.PORT || 3000;
|
||||||
|
const baseUrl = `http://localhost:${port}`;
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
if (args.length < 2) {
|
||||||
|
console.log('\nUso: node scripts/bulk-injector.js <file-csv> <intervallo-secondi>\n');
|
||||||
|
console.log('Esempio: node scripts/bulk-injector.js tickets-sample.csv 5\n');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const csvPath = path.resolve(args[0]);
|
||||||
|
const intervalSeconds = parseFloat(args[1]);
|
||||||
|
|
||||||
|
if (!fs.existsSync(csvPath)) {
|
||||||
|
console.error(`Errore: il file CSV non esiste al percorso: ${csvPath}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNaN(intervalSeconds) || intervalSeconds <= 0) {
|
||||||
|
console.error('Errore: specificare un intervallo in secondi valido (maggiore di 0)');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and parse CSV
|
||||||
|
const content = fs.readFileSync(csvPath, 'utf-8');
|
||||||
|
const lines = content.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0);
|
||||||
|
if (lines.length < 2) {
|
||||||
|
console.error('Errore: il file CSV deve contenere almeno l\'intestazione e una riga di dati.');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const headerLine = lines[0];
|
||||||
|
const delimiter = headerLine.includes(';') ? ';' : ',';
|
||||||
|
const headers = headerLine.split(delimiter).map(h => h.trim().replace(/^["']|["']$/g, ''));
|
||||||
|
|
||||||
|
const rows = [];
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const cols = lines[i].split(delimiter).map(c => c.trim().replace(/^["']|["']$/g, ''));
|
||||||
|
const row = {};
|
||||||
|
headers.forEach((header, idx) => {
|
||||||
|
row[header] = cols[idx] || '';
|
||||||
|
});
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start execution flow
|
||||||
|
async function start() {
|
||||||
|
console.log(`\n============================================================`);
|
||||||
|
console.log(`⚙️ OTRS Turbo - CSV Bulk Injector`);
|
||||||
|
console.log(`============================================================`);
|
||||||
|
console.log(`📂 File CSV caricato: ${path.basename(csvPath)} (${rows.length} righe)`);
|
||||||
|
console.log(`⏱️ Intervallo: ${intervalSeconds} secondi`);
|
||||||
|
console.log(`🌐 Server target: ${baseUrl}`);
|
||||||
|
|
||||||
|
// Fetch ticket types lookup
|
||||||
|
let ticketTypes = [];
|
||||||
|
try {
|
||||||
|
const typesRes = await fetch(`${baseUrl}/api/types`);
|
||||||
|
if (typesRes.ok) {
|
||||||
|
ticketTypes = await typesRes.json();
|
||||||
|
console.log(`📊 Tipi ticket caricati dal server: ${ticketTypes.length}`);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`⚠️ Impossibile pre-caricare i tipi di ticket (verrà usato l'ID diretto): ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`ℹ️ Premi Ctrl+C per arrestare il processo.`);
|
||||||
|
console.log(`============================================================\n`);
|
||||||
|
|
||||||
|
let currentIndex = 0;
|
||||||
|
|
||||||
|
async function injectNext() {
|
||||||
|
const row = rows[currentIndex];
|
||||||
|
console.log(`[${new Date().toLocaleTimeString()}] Invio riga ${currentIndex + 1}/${rows.length}: "${row.title || 'Senza titolo'}"...`);
|
||||||
|
|
||||||
|
// Determine type_id from CSV 'type' or 'type_id'
|
||||||
|
let type_id = undefined;
|
||||||
|
if (row.type) {
|
||||||
|
const parsedId = parseInt(row.type, 10);
|
||||||
|
if (!isNaN(parsedId)) {
|
||||||
|
type_id = parsedId;
|
||||||
|
} else {
|
||||||
|
const found = ticketTypes.find(t => t.name.toLowerCase() === row.type.toLowerCase());
|
||||||
|
if (found) {
|
||||||
|
type_id = found.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (row.type_id) {
|
||||||
|
type_id = parseInt(row.type_id, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build payload
|
||||||
|
const payload = {
|
||||||
|
title: row.title || 'Ticket da CSV',
|
||||||
|
queue_id: row.queue_id ? parseInt(row.queue_id, 10) : 1,
|
||||||
|
state_id: row.state_id ? parseInt(row.state_id, 10) : 1,
|
||||||
|
priority_id: row.priority_id ? parseInt(row.priority_id, 10) : 3,
|
||||||
|
type_id,
|
||||||
|
user_id: row.user_id ? parseInt(row.user_id, 10) : undefined,
|
||||||
|
responsible_user_id: row.responsible_user_id ? parseInt(row.responsible_user_id, 10) : undefined,
|
||||||
|
customer_id: row.customer_id || undefined,
|
||||||
|
customer_user_id: row.customer_user_id || undefined,
|
||||||
|
subject: row.subject || undefined,
|
||||||
|
body: row.body || undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`${baseUrl}/api/tickets`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-agent-id': '1'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resData = await response.json();
|
||||||
|
const ticketId = resData.id;
|
||||||
|
const tn = resData.tn;
|
||||||
|
console.log(` -> Successo: Creato Ticket #${tn} (ID: ${ticketId})`);
|
||||||
|
|
||||||
|
// Check for create_time retrodating
|
||||||
|
if (row.create_time) {
|
||||||
|
console.log(` -> Richiesta retrodatazione a: ${row.create_time}`);
|
||||||
|
|
||||||
|
// 1. Retrodate ticket
|
||||||
|
const ticketRetRes = await fetch(`${baseUrl}/api/tickets/${ticketId}/retrodata-ticket`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ create_time: row.create_time })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ticketRetRes.ok) {
|
||||||
|
console.log(` -> Ticket retrodatato.`);
|
||||||
|
} else {
|
||||||
|
console.error(` -> Errore retrodatazione ticket: ${await ticketRetRes.text()}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Fetch articles to retrodate
|
||||||
|
const detailRes = await fetch(`${baseUrl}/api/tickets/${ticketId}`);
|
||||||
|
if (detailRes.ok) {
|
||||||
|
const detail = await detailRes.json();
|
||||||
|
const articles = detail.articles || [];
|
||||||
|
for (const art of articles) {
|
||||||
|
const artRetRes = await fetch(`${baseUrl}/api/tickets/articles/${art.article_id}/retrodata-article`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ create_time: row.create_time })
|
||||||
|
});
|
||||||
|
if (artRetRes.ok) {
|
||||||
|
console.log(` -> Articolo ID ${art.article_id} retrodatato.`);
|
||||||
|
} else {
|
||||||
|
console.error(` -> Errore retrodatazione articolo ID ${art.article_id}: ${await artRetRes.text()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(` ❌ Errore iniezione riga: ${err.message}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cycle to next row
|
||||||
|
currentIndex = (currentIndex + 1) % rows.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start immediately and schedule next executions
|
||||||
|
injectNext();
|
||||||
|
setInterval(injectNext, intervalSeconds * 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
start();
|
||||||
@@ -12,7 +12,7 @@ const PORT = process.env.PORT || 3000;
|
|||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json());
|
app.use(express.json({ limit: '50mb' }));
|
||||||
|
|
||||||
// Serve static frontend
|
// Serve static frontend
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
title,queue_id,state_id,priority_id,type,customer_user_id,subject,body,create_time
|
||||||
|
"Errori di mille tipi",1,1,1,1,"email","oggetto","corpo","1900-01-01 00:00"
|
||||||
|
Reference in New Issue
Block a user