feat: ora è possibile scorrere gli allegati e modificare i pdf, aggiunte personalizzazioni di immagini e forme per poterle richiamare più rapidamente.
This commit is contained in:
1 parent
ec1fb25d4e
commit
719fc09c66
8 files changed
+2168
-71
No files matched your search
@@ -78,6 +78,30 @@ db.exec(`
|
|||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS user_signatures (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
agent_id INTEGER NOT NULL DEFAULT 1,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
image_data TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS user_shapes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
agent_id INTEGER NOT NULL DEFAULT 1,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
shape_type TEXT NOT NULL DEFAULT 'rectangle',
|
||||||
|
fill_color TEXT NOT NULL DEFAULT '#3b82f6',
|
||||||
|
border_color TEXT NOT NULL DEFAULT '#1e3a8a',
|
||||||
|
border_width INTEGER NOT NULL DEFAULT 3,
|
||||||
|
is_favorite INTEGER NOT NULL DEFAULT 1,
|
||||||
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
db.exec(`
|
db.exec(`
|
||||||
CREATE TABLE IF NOT EXISTS ticket_draft_notes (
|
CREATE TABLE IF NOT EXISTS ticket_draft_notes (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
|||||||
@@ -133,6 +133,15 @@
|
|||||||
<span>Gestione Mail</span>
|
<span>Gestione Mail</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="#/config-user" class="nav-link" data-view="config-user" id="nav-config-user">
|
||||||
|
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="3"></circle>
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
|
||||||
|
</svg>
|
||||||
|
<span>Configurazioni Utente</span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a href="#/config-env" class="nav-link" data-view="config-env" id="nav-config-env">
|
<a href="#/config-env" class="nav-link" data-view="config-env" id="nav-config-env">
|
||||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
@@ -249,6 +258,8 @@
|
|||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
|
||||||
<script src="/js/components/toast.js"></script>
|
<script src="/js/components/toast.js"></script>
|
||||||
<script src="/js/components/filters.js"></script>
|
<script src="/js/components/filters.js"></script>
|
||||||
<script src="/js/views/dashboard.js"></script>
|
<script src="/js/views/dashboard.js"></script>
|
||||||
@@ -264,6 +275,7 @@
|
|||||||
<script src="/js/views/timeAccounting.js"></script>
|
<script src="/js/views/timeAccounting.js"></script>
|
||||||
<script src="/js/views/ticketGroups.js"></script>
|
<script src="/js/views/ticketGroups.js"></script>
|
||||||
<script src="/js/views/envConfig.js"></script>
|
<script src="/js/views/envConfig.js"></script>
|
||||||
|
<script src="/js/views/userConfig.js"></script>
|
||||||
<script src="/js/views/noteTemporanee.js"></script>
|
<script src="/js/views/noteTemporanee.js"></script>
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+6
-1
@@ -454,9 +454,14 @@ const App = {
|
|||||||
|
|
||||||
} else if (hash === '#/config-env') {
|
} else if (hash === '#/config-env') {
|
||||||
document.getElementById('nav-config-env')?.classList.add('active');
|
document.getElementById('nav-config-env')?.classList.add('active');
|
||||||
titleEl.textContent = 'Modifica .env';
|
titleEl.textContent = 'Modifica file .env';
|
||||||
EnvConfigView.render();
|
EnvConfigView.render();
|
||||||
|
|
||||||
|
} else if (hash === '#/config-user') {
|
||||||
|
document.getElementById('nav-config-user')?.classList.add('active');
|
||||||
|
titleEl.textContent = 'Configurazioni Utente';
|
||||||
|
UserConfigView.render();
|
||||||
|
|
||||||
} else if (hash === '#/time-accounting') {
|
} else if (hash === '#/time-accounting') {
|
||||||
document.getElementById('nav-time-accounting')?.classList.add('active');
|
document.getElementById('nav-time-accounting')?.classList.add('active');
|
||||||
titleEl.textContent = 'Storico consuntivazione';
|
titleEl.textContent = 'Storico consuntivazione';
|
||||||
|
|||||||
@@ -284,9 +284,13 @@ const EnvConfigView = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
const res = await App.api(`/api/config/env/history/${id}`, {
|
let res;
|
||||||
method: 'DELETE'
|
try {
|
||||||
});
|
res = await App.api(`/api/config/env/history/${id}`, { method: 'DELETE' });
|
||||||
|
} catch (delErr) {
|
||||||
|
// Fallback to POST endpoint if DELETE route returned 404
|
||||||
|
res = await App.api(`/api/config/env/history/${id}/delete`, { method: 'POST' });
|
||||||
|
}
|
||||||
|
|
||||||
Toast.success(res.message || `Versione #${id} eliminata dallo storico!`);
|
Toast.success(res.message || `Versione #${id} eliminata dallo storico!`);
|
||||||
this.history = res.history || [];
|
this.history = res.history || [];
|
||||||
|
|||||||
+1536
-32
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,294 @@
|
|||||||
|
/**
|
||||||
|
* userConfig.js
|
||||||
|
* User Configurations View: Upload, preview and manage custom signature images stored in SQLite.
|
||||||
|
*/
|
||||||
|
const UserConfigView = {
|
||||||
|
async render() {
|
||||||
|
const container = document.getElementById('view-container');
|
||||||
|
container.innerHTML = '<div class="loading-screen"><div class="spinner"></div><p>Caricamento configurazioni utente...</p></div>';
|
||||||
|
|
||||||
|
const titleEl = document.getElementById('page-title');
|
||||||
|
if (titleEl) {
|
||||||
|
titleEl.textContent = 'Configurazioni Utente';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [signatures, shapes] = await Promise.all([
|
||||||
|
App.api('/api/config/user-signatures'),
|
||||||
|
App.api('/api/config/user-shapes')
|
||||||
|
]);
|
||||||
|
this.renderUI(container, signatures, shapes);
|
||||||
|
} catch (err) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="empty-state">
|
||||||
|
<div class="empty-state-icon">⚠️</div>
|
||||||
|
<div class="empty-state-text">Errore caricamento configurazioni utente</div>
|
||||||
|
<div class="empty-state-sub">${App.escapeHtml(err.message)}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
renderUI(container, signatures = [], shapes = []) {
|
||||||
|
container.innerHTML = `
|
||||||
|
<div class="user-config-page">
|
||||||
|
<!-- Section 1: Custom Shapes -->
|
||||||
|
<div class="card" style="margin-bottom: var(--space-lg);">
|
||||||
|
<div class="card-title" style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
|
||||||
|
<span>📐 Forme Personalizzate & Preferiti</span>
|
||||||
|
<span style="font-size:0.8rem; font-weight:normal; color:var(--text-muted);">Configura forme salvate per l'editor PDF</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: var(--bg-tertiary); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 16px; margin-bottom: 20px;">
|
||||||
|
<h4 style="margin-top:0; margin-bottom:12px; font-size:0.95rem; color:var(--text-primary);">Crea Nuova Forma Personalizzata</h4>
|
||||||
|
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap:12px; align-items:flex-end;">
|
||||||
|
<div>
|
||||||
|
<label style="font-size:0.8rem; font-weight:600; color:var(--text-secondary); margin-bottom:4px; display:block;">Nome Forma</label>
|
||||||
|
<input type="text" id="shape-name" class="form-input" placeholder="Es. Evidenziatore Blu, Timbro Approvato..." style="height:34px; font-size:0.85rem;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style="font-size:0.8rem; font-weight:600; color:var(--text-secondary); margin-bottom:4px; display:block;">Tipo Forma</label>
|
||||||
|
<select id="shape-type" class="form-select" style="height:34px; font-size:0.85rem;">
|
||||||
|
<option value="rectangle">🟦 Rettangolo</option>
|
||||||
|
<option value="circle">⚪ Cerchio / Ellisse</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style="font-size:0.8rem; font-weight:600; color:var(--text-secondary); margin-bottom:4px; display:block;">Colore Riempimento</label>
|
||||||
|
<div style="display:flex; align-items:center; gap:6px;">
|
||||||
|
<input type="color" id="shape-fill-color" value="#3b82f6" style="width:34px; height:34px; border:none; border-radius:4px; cursor:pointer; background:none;" />
|
||||||
|
<label style="font-size:0.78rem; color:var(--text-muted); cursor:pointer;"><input type="checkbox" id="shape-transparent-fill" /> Trasparente</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style="font-size:0.8rem; font-weight:600; color:var(--text-secondary); margin-bottom:4px; display:block;">Colore Bordo</label>
|
||||||
|
<input type="color" id="shape-border-color" value="#1e3a8a" style="width:34px; height:34px; border:none; border-radius:4px; cursor:pointer; background:none;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label style="font-size:0.8rem; font-weight:600; color:var(--text-secondary); margin-bottom:4px; display:block;">Spessore Bordo (px)</label>
|
||||||
|
<input type="number" id="shape-border-width" min="0" max="30" value="3" class="form-input" style="height:34px; font-size:0.85rem;" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex; gap:8px;">
|
||||||
|
<button id="btn-save-shape" class="btn btn-primary btn-sm" style="height:34px; padding:0 16px; font-weight:600; display:inline-flex; align-items:center; gap:6px;">
|
||||||
|
⭐ Salva Forma
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shapes Gallery -->
|
||||||
|
<div style="display:grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap:14px;">
|
||||||
|
${shapes.length > 0 ? shapes.map(sh => `
|
||||||
|
<div class="user-shape-card" style="background:var(--bg-card); border:1px solid var(--border-subtle); border-radius:var(--radius-md); padding:12px; display:flex; flex-direction:column; position:relative; box-shadow:var(--shadow-sm);">
|
||||||
|
<div style="position:absolute; top:8px; right:8px; display:flex; gap:4px;">
|
||||||
|
<button class="btn-fav-shape" data-id="${sh.id}" style="background:none; border:none; cursor:pointer; font-size:16px;" title="Aggiungi/Rimuovi Preferito">${sh.is_favorite ? '⭐' : '☆'}</button>
|
||||||
|
<button class="btn-delete-shape" data-id="${sh.id}" style="background:var(--bg-tertiary); border:1px solid var(--border-subtle); color:var(--danger); border-radius:50%; width:24px; height:24px; cursor:pointer; display:flex; align-items:center; justify-content:center; font-size:11px;" title="Elimina Forma">✕</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="width:100%; height:70px; display:flex; align-items:center; justify-content:center; background:var(--bg-tertiary); border-radius:var(--radius-sm); margin-bottom:8px; border:1px dashed var(--border-subtle);">
|
||||||
|
<div style="width:50px; height:34px; border-radius:${sh.shape_type === 'circle' ? '50%' : '3px'}; background:${sh.fill_color === 'transparent' ? 'transparent' : sh.fill_color}; border:${sh.border_width}px solid ${sh.border_color}; box-sizing:border-box;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="font-weight:600; font-size:0.85rem; color:var(--text-primary); margin-bottom:4px;">
|
||||||
|
${App.escapeHtml(sh.name)}
|
||||||
|
</div>
|
||||||
|
<div style="font-size:0.74rem; color:var(--text-muted);">
|
||||||
|
${sh.shape_type === 'circle' ? 'Cerchio' : 'Rettangolo'} • Bordo ${sh.border_width}px
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('') : `
|
||||||
|
<div style="grid-column: 1 / -1; padding:20px; text-align:center; color:var(--text-muted); font-style:italic;">
|
||||||
|
Nessuna forma personalizzata salvata.
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section 2: Signatures -->
|
||||||
|
<div class="card" style="margin-bottom: var(--space-lg);">
|
||||||
|
<div class="card-title" style="display:flex; align-items:center; justify-content:space-between; gap:12px;">
|
||||||
|
<span>✍️ Immagini e Firme Utente</span>
|
||||||
|
<span style="font-size:0.8rem; font-weight:normal; color:var(--text-muted);">Saranno disponibili per l'apposizione rapida sui PDF</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="background: var(--bg-tertiary); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 16px; margin-bottom: 20px;">
|
||||||
|
<h4 style="margin-top:0; margin-bottom:12px; font-size:0.95rem; color:var(--text-primary);">Aggiungi nuova immagine / firma</h4>
|
||||||
|
<div style="display:flex; gap:12px; align-items:flex-end; flex-wrap:wrap;">
|
||||||
|
<div style="flex:1; min-width:220px;">
|
||||||
|
<label style="font-size:0.8rem; font-weight:600; color:var(--text-secondary); margin-bottom:4px; display:block;">Nome Firma / Immagine</label>
|
||||||
|
<input type="text" id="user-sig-name" class="form-input" placeholder="Es. Firma Gabriele Cimaschi, Timbro Aziendale..." style="height:34px; font-size:0.85rem;" />
|
||||||
|
</div>
|
||||||
|
<div style="flex:1; min-width:220px;">
|
||||||
|
<label style="font-size:0.8rem; font-weight:600; color:var(--text-secondary); margin-bottom:4px; display:block;">Seleziona File Immagine (PNG, JPG, SVG)</label>
|
||||||
|
<input type="file" id="user-sig-file" accept="image/*" class="form-input" style="height:34px; padding:3px 8px; font-size:0.8rem;" />
|
||||||
|
</div>
|
||||||
|
<button id="btn-save-user-sig" class="btn btn-primary btn-sm" style="height:34px; padding:0 16px; font-weight:600; display:inline-flex; align-items:center; gap:6px;">
|
||||||
|
➕ Salva in SQLite
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Signatures Gallery Grid -->
|
||||||
|
<div id="user-sig-grid" style="display:grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap:16px;">
|
||||||
|
${signatures.length > 0 ? signatures.map(sig => `
|
||||||
|
<div class="user-sig-card" style="background:var(--bg-card); border:1px solid var(--border-subtle); border-radius:var(--radius-md); padding:12px; display:flex; flex-direction:column; align-items:center; position:relative; box-shadow:var(--shadow-sm);">
|
||||||
|
<button class="btn-delete-sig" data-id="${sig.id}" style="position:absolute; top:8px; right:8px; background:var(--bg-tertiary); border:1px solid var(--border-subtle); color:var(--danger); border-radius:50%; width:26px; height:26px; cursor:pointer; display:flex; align-items:center; justify-content:center; font-size:12px;" title="Elimina Firma">✕</button>
|
||||||
|
<div style="width:100%; height:110px; display:flex; align-items:center; justify-content:center; background:#ffffff; border-radius:var(--radius-sm); border:1px solid var(--border-subtle); padding:8px; margin-bottom:8px;">
|
||||||
|
<img src="${sig.image_data}" alt="${App.escapeHtml(sig.name)}" style="max-width:100%; max-height:100%; object-fit:contain;" />
|
||||||
|
</div>
|
||||||
|
<div style="font-weight:600; font-size:0.85rem; color:var(--text-primary); text-align:center; word-break:break-word;">
|
||||||
|
${App.escapeHtml(sig.name)}
|
||||||
|
</div>
|
||||||
|
<div style="font-size:0.72rem; color:var(--text-muted); margin-top:2px;">
|
||||||
|
${App.formatDateTime(sig.created_at)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`).join('') : `
|
||||||
|
<div style="grid-column: 1 / -1; padding:32px; text-align:center; color:var(--text-muted); font-style:italic;">
|
||||||
|
Nessuna firma salvata. Carica la prima immagine con il modulo qui sopra.
|
||||||
|
</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.bindEvents(container);
|
||||||
|
},
|
||||||
|
|
||||||
|
bindEvents(container) {
|
||||||
|
// Shapes handlers
|
||||||
|
const btnSaveShape = container.querySelector('#btn-save-shape');
|
||||||
|
if (btnSaveShape) {
|
||||||
|
btnSaveShape.addEventListener('click', async () => {
|
||||||
|
const name = container.querySelector('#shape-name').value.trim();
|
||||||
|
const shapeType = container.querySelector('#shape-type').value;
|
||||||
|
const isTransparent = container.querySelector('#shape-transparent-fill').checked;
|
||||||
|
const fillColor = isTransparent ? 'transparent' : container.querySelector('#shape-fill-color').value;
|
||||||
|
const borderColor = container.querySelector('#shape-border-color').value;
|
||||||
|
const borderWidth = parseInt(container.querySelector('#shape-border-width').value || 3, 10);
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
Toast.warning('Inserisci un nome per la forma.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
btnSaveShape.disabled = true;
|
||||||
|
await App.api('/api/config/user-shapes', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
name,
|
||||||
|
shape_type: shapeType,
|
||||||
|
fill_color: fillColor,
|
||||||
|
border_color: borderColor,
|
||||||
|
border_width: borderWidth,
|
||||||
|
is_favorite: 1
|
||||||
|
})
|
||||||
|
});
|
||||||
|
Toast.success('Forma salvata!');
|
||||||
|
this.render();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error('Errore salvataggio forma: ' + err.message);
|
||||||
|
btnSaveShape.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
container.querySelectorAll('.btn-fav-shape').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
try {
|
||||||
|
await App.api(`/api/config/user-shapes/${btn.dataset.id}/toggle-favorite`, { method: 'PUT' });
|
||||||
|
this.render();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error('Errore aggiornamento preferiti: ' + err.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
container.querySelectorAll('.btn-delete-shape').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const ok = await App.confirm('Elimina Forma', 'Eliminare questa forma personalizzata?', { confirmText: 'Elimina', cancelText: 'Annulla' });
|
||||||
|
if (ok) {
|
||||||
|
try {
|
||||||
|
await App.api(`/api/config/user-shapes/${btn.dataset.id}`, { method: 'DELETE' });
|
||||||
|
Toast.success('Forma eliminata!');
|
||||||
|
this.render();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error('Errore eliminazione: ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Signatures handlers
|
||||||
|
const btnSave = container.querySelector('#btn-save-user-sig');
|
||||||
|
const inputName = container.querySelector('#user-sig-name');
|
||||||
|
const inputFile = container.querySelector('#user-sig-file');
|
||||||
|
|
||||||
|
if (btnSave && inputFile) {
|
||||||
|
btnSave.addEventListener('click', async () => {
|
||||||
|
const name = inputName.value.trim();
|
||||||
|
const file = inputFile.files[0];
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
Toast.warning('Inserisci un nome descrittivo per la firma/immagine.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!file) {
|
||||||
|
Toast.warning('Seleziona un file immagine.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = async () => {
|
||||||
|
const base64Data = reader.result;
|
||||||
|
try {
|
||||||
|
btnSave.disabled = true;
|
||||||
|
btnSave.textContent = 'Salvataggio...';
|
||||||
|
|
||||||
|
const res = await App.api('/api/config/user-signatures', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
name,
|
||||||
|
image_data: base64Data
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
Toast.success(res.message || 'Firma salvata!');
|
||||||
|
this.render();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error('Errore durante il salvataggio: ' + err.message);
|
||||||
|
btnSave.disabled = false;
|
||||||
|
btnSave.textContent = '➕ Salva in SQLite';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
container.querySelectorAll('.btn-delete-sig').forEach(btn => {
|
||||||
|
btn.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const id = btn.dataset.id;
|
||||||
|
const ok = await App.confirm('Elimina Firma', 'Sei sicuro di voler eliminare questa firma dal database?', { confirmText: 'Elimina', cancelText: 'Annulla' });
|
||||||
|
if (ok) {
|
||||||
|
try {
|
||||||
|
await App.api(`/api/config/user-signatures/${id}`, { method: 'DELETE' });
|
||||||
|
Toast.success('Firma eliminata!');
|
||||||
|
this.render();
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error('Errore eliminazione: ' + err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
+178
-2
@@ -146,8 +146,8 @@ router.post('/env/restore/:history_id', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// DELETE /api/config/env/history/:id — Delete a specific .env history entry
|
// Helper handler for deleting a specific .env history entry
|
||||||
router.delete('/env/history/:id', async (req, res) => {
|
async function handleDeleteHistory(req, res) {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
|
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
|
||||||
@@ -181,6 +181,182 @@ router.delete('/env/history/:id', async (req, res) => {
|
|||||||
console.error('Error deleting .env history entry:', err);
|
console.error('Error deleting .env history entry:', err);
|
||||||
res.status(500).json({ error: 'Errore durante l\'eliminazione della versione dello storico', message: err.message });
|
res.status(500).json({ error: 'Errore durante l\'eliminazione della versione dello storico', message: err.message });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE & POST /api/config/env/history/:id — Delete a specific .env history entry
|
||||||
|
router.delete('/env/history/:id', handleDeleteHistory);
|
||||||
|
router.post('/env/history/:id/delete', handleDeleteHistory);
|
||||||
|
|
||||||
|
// GET /api/config/user-signatures — Fetch all user signature images
|
||||||
|
router.get('/user-signatures', (req, res) => {
|
||||||
|
try {
|
||||||
|
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
|
||||||
|
const signatures = db.prepare(`
|
||||||
|
SELECT * FROM user_signatures WHERE agent_id = ? ORDER BY id DESC
|
||||||
|
`).all(operatorId);
|
||||||
|
res.json(signatures);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching user signatures:', err);
|
||||||
|
res.status(500).json({ error: 'Errore durante il recupero delle firme utente', message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/config/user-signatures — Add new user signature image
|
||||||
|
router.post('/user-signatures', (req, res) => {
|
||||||
|
try {
|
||||||
|
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
|
||||||
|
const { name, image_data } = req.body;
|
||||||
|
|
||||||
|
if (!name || !image_data) {
|
||||||
|
return res.status(400).json({ error: 'Nome e dati immagine sono obbligatori' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = db.prepare(`
|
||||||
|
INSERT INTO user_signatures (agent_id, name, image_data)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
`).run(operatorId, name.trim(), image_data);
|
||||||
|
|
||||||
|
const signatures = db.prepare(`
|
||||||
|
SELECT * FROM user_signatures WHERE agent_id = ? ORDER BY id DESC
|
||||||
|
`).all(operatorId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Immagine/Firma salvata nel database con successo!',
|
||||||
|
id: info.lastInsertRowid,
|
||||||
|
signatures
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error saving user signature:', err);
|
||||||
|
res.status(500).json({ error: 'Errore durante il salvataggio della firma', message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/config/user-signatures/:id — Delete user signature image
|
||||||
|
router.delete('/user-signatures/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
db.prepare(`DELETE FROM user_signatures WHERE id = ? AND agent_id = ?`).run(id, operatorId);
|
||||||
|
|
||||||
|
const signatures = db.prepare(`
|
||||||
|
SELECT * FROM user_signatures WHERE agent_id = ? ORDER BY id DESC
|
||||||
|
`).all(operatorId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Firma eliminata con successo!',
|
||||||
|
signatures
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting user signature:', err);
|
||||||
|
res.status(500).json({ error: 'Errore durante l\'eliminazione della firma', message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/config/user-shapes — List custom user shapes
|
||||||
|
router.get('/user-shapes', (req, res) => {
|
||||||
|
try {
|
||||||
|
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
|
||||||
|
const shapes = db.prepare(`
|
||||||
|
SELECT * FROM user_shapes WHERE agent_id = ? ORDER BY id DESC
|
||||||
|
`).all(operatorId);
|
||||||
|
res.json(shapes);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching user shapes:', err);
|
||||||
|
res.status(500).json({ error: 'Errore durante il recupero delle forme utente', message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/config/user-shapes — Create a new custom shape
|
||||||
|
router.post('/user-shapes', (req, res) => {
|
||||||
|
try {
|
||||||
|
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
|
||||||
|
const { name, shape_type, fill_color, border_color, border_width, is_favorite } = req.body;
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
return res.status(400).json({ error: 'Il nome della forma è obbligatorio' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = db.prepare(`
|
||||||
|
INSERT INTO user_shapes (agent_id, name, shape_type, fill_color, border_color, border_width, is_favorite)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`).run(
|
||||||
|
operatorId,
|
||||||
|
name.trim(),
|
||||||
|
shape_type || 'rectangle',
|
||||||
|
fill_color || '#3b82f6',
|
||||||
|
border_color || '#1e3a8a',
|
||||||
|
parseInt(border_width || 3, 10),
|
||||||
|
is_favorite !== undefined ? (is_favorite ? 1 : 0) : 1
|
||||||
|
);
|
||||||
|
|
||||||
|
const shapes = db.prepare(`
|
||||||
|
SELECT * FROM user_shapes WHERE agent_id = ? ORDER BY id DESC
|
||||||
|
`).all(operatorId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Forma salvata nei preferiti con successo!',
|
||||||
|
id: info.lastInsertRowid,
|
||||||
|
shapes
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error saving user shape:', err);
|
||||||
|
res.status(500).json({ error: 'Errore durante il salvataggio della forma', message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// PUT /api/config/user-shapes/:id/toggle-favorite — Toggle favorite status
|
||||||
|
router.put('/user-shapes/:id/toggle-favorite', (req, res) => {
|
||||||
|
try {
|
||||||
|
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const shape = db.prepare(`SELECT * FROM user_shapes WHERE id = ? AND agent_id = ?`).get(id, operatorId);
|
||||||
|
if (!shape) {
|
||||||
|
return res.status(404).json({ error: 'Forma non trovata' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const newFav = shape.is_favorite ? 0 : 1;
|
||||||
|
db.prepare(`UPDATE user_shapes SET is_favorite = ? WHERE id = ? AND agent_id = ?`).run(newFav, id, operatorId);
|
||||||
|
|
||||||
|
const shapes = db.prepare(`
|
||||||
|
SELECT * FROM user_shapes WHERE agent_id = ? ORDER BY id DESC
|
||||||
|
`).all(operatorId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
shapes
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error toggling shape favorite status:', err);
|
||||||
|
res.status(500).json({ error: 'Errore durante l\'aggiornamento dei preferiti', message: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// DELETE /api/config/user-shapes/:id — Delete a custom shape
|
||||||
|
router.delete('/user-shapes/:id', (req, res) => {
|
||||||
|
try {
|
||||||
|
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
db.prepare(`DELETE FROM user_shapes WHERE id = ? AND agent_id = ?`).run(id, operatorId);
|
||||||
|
|
||||||
|
const shapes = db.prepare(`
|
||||||
|
SELECT * FROM user_shapes WHERE agent_id = ? ORDER BY id DESC
|
||||||
|
`).all(operatorId);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Forma eliminata con successo!',
|
||||||
|
shapes
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting user shape:', err);
|
||||||
|
res.status(500).json({ error: 'Errore durante l\'eliminazione della forma', message: err.message });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
+111
-33
@@ -457,27 +457,75 @@ router.get('/:id', async (req, res) => {
|
|||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const iconv = require('iconv-lite');
|
||||||
|
|
||||||
|
const decodeTextContent = (rawContent, contentTypeStr = '') => {
|
||||||
|
if (!rawContent) return '';
|
||||||
|
let rawBuf = null;
|
||||||
|
|
||||||
|
if (Buffer.isBuffer(rawContent)) {
|
||||||
|
rawBuf = rawContent;
|
||||||
|
} else if (typeof rawContent === 'string') {
|
||||||
|
const cleaned = rawContent.replace(/\s+/g, '');
|
||||||
|
if (cleaned.length > 20 && cleaned.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(cleaned)) {
|
||||||
|
try {
|
||||||
|
rawBuf = Buffer.from(cleaned, 'base64');
|
||||||
|
} catch (_) {
|
||||||
|
rawBuf = Buffer.from(rawContent, 'utf-8');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rawBuf = Buffer.from(rawContent, 'utf-8');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return String(rawContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect charset from content-type header
|
||||||
|
let charset = 'utf-8';
|
||||||
|
const charsetMatch = (contentTypeStr || '').match(/charset=["']?([^"';\s]+)/i);
|
||||||
|
if (charsetMatch && charsetMatch[1]) {
|
||||||
|
charset = charsetMatch[1].toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
let text = '';
|
||||||
|
try {
|
||||||
|
if (iconv.encodingExists(charset)) {
|
||||||
|
text = iconv.decode(rawBuf, charset);
|
||||||
|
} else {
|
||||||
|
text = rawBuf.toString('utf-8');
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
text = rawBuf.toString('utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If UTF-8 decoding contains replacement character U+FFFD (), retry decoding as win1252/latin1
|
||||||
|
if (text.includes('\uFFFD') || text.includes('')) {
|
||||||
|
try {
|
||||||
|
const latin1Text = iconv.decode(rawBuf, 'windows-1252');
|
||||||
|
if (latin1Text && !latin1Text.includes('\uFFFD')) {
|
||||||
|
text = latin1Text;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repair Double-UTF-8 / Mojibake (e.g., ’ -> ’, ù -> ù, è -> è, à -> à, à -> È)
|
||||||
|
if (/(?:Ã[\s\xa0¡-ÿ]|â€[™œ]|–)/.test(text)) {
|
||||||
|
try {
|
||||||
|
const reEncoded = iconv.encode(text, 'win1252');
|
||||||
|
const repaired = iconv.decode(reEncoded, 'utf-8');
|
||||||
|
if (repaired && !repaired.includes('\uFFFD')) {
|
||||||
|
text = repaired;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
};
|
||||||
|
|
||||||
const htmlBodyMap = {};
|
const htmlBodyMap = {};
|
||||||
for (const row of htmlAttachmentsRes.rows) {
|
for (const row of htmlAttachmentsRes.rows) {
|
||||||
if (row.content) {
|
if (row.content) {
|
||||||
let contentStr = '';
|
const contentStr = decodeTextContent(row.content, row.content_type);
|
||||||
if (Buffer.isBuffer(row.content)) {
|
|
||||||
contentStr = row.content.toString('utf-8');
|
|
||||||
} else if (typeof row.content === 'string') {
|
|
||||||
contentStr = row.content;
|
|
||||||
}
|
|
||||||
|
|
||||||
const cleaned = contentStr.replace(/\s+/g, '');
|
|
||||||
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
||||||
if (cleaned.length > 20 && cleaned.length % 4 === 0 && base64Regex.test(cleaned)) {
|
|
||||||
try {
|
|
||||||
const decoded = Buffer.from(cleaned, 'base64').toString('utf-8');
|
|
||||||
if (decoded && /<(div|p|br|table|html|body|span|img|a|style)\b/i.test(decoded)) {
|
|
||||||
contentStr = decoded;
|
|
||||||
}
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (contentStr && /<(div|p|br|table|html|body|span|img|a|style)\b/i.test(contentStr)) {
|
if (contentStr && /<(div|p|br|table|html|body|span|img|a|style)\b/i.test(contentStr)) {
|
||||||
const fname = (row.filename || '').toLowerCase();
|
const fname = (row.filename || '').toLowerCase();
|
||||||
if (!htmlBodyMap[row.article_id] || fname.startsWith('file-1') || fname.startsWith('file-2')) {
|
if (!htmlBodyMap[row.article_id] || fname.startsWith('file-1') || fname.startsWith('file-2')) {
|
||||||
@@ -488,6 +536,14 @@ router.get('/:id', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
articlesResult.rows.forEach(a => {
|
articlesResult.rows.forEach(a => {
|
||||||
|
// Ensure a.a_body and subject are properly decoded if legacy encoded
|
||||||
|
if (a.a_body) {
|
||||||
|
a.a_body = decodeTextContent(a.a_body, a.a_content_type);
|
||||||
|
}
|
||||||
|
if (a.a_subject) {
|
||||||
|
a.a_subject = decodeTextContent(a.a_subject, 'text/plain; charset=utf-8');
|
||||||
|
}
|
||||||
|
|
||||||
if (htmlBodyMap[a.article_id]) {
|
if (htmlBodyMap[a.article_id]) {
|
||||||
a.html_body = htmlBodyMap[a.article_id];
|
a.html_body = htmlBodyMap[a.article_id];
|
||||||
a.has_html = true;
|
a.has_html = true;
|
||||||
@@ -1821,34 +1877,56 @@ router.get('/attachments/:id', async (req, res) => {
|
|||||||
}
|
}
|
||||||
const attachment = result.rows[0];
|
const attachment = result.rows[0];
|
||||||
|
|
||||||
// Decode base64 if necessary
|
|
||||||
let contentBuffer;
|
let contentBuffer;
|
||||||
if (attachment.content) {
|
if (attachment.content) {
|
||||||
let contentStr = '';
|
|
||||||
if (Buffer.isBuffer(attachment.content)) {
|
if (Buffer.isBuffer(attachment.content)) {
|
||||||
contentStr = attachment.content.toString('utf-8');
|
// Check for raw binary signatures (%PDF, JPEG, PNG, Zip)
|
||||||
} else if (typeof attachment.content === 'string') {
|
const isRawBinary = attachment.content.length > 4 && (
|
||||||
contentStr = attachment.content;
|
(attachment.content[0] === 0x25 && attachment.content[1] === 0x50 && attachment.content[2] === 0x44 && attachment.content[3] === 0x46) || // %PDF
|
||||||
}
|
(attachment.content[0] === 0xFF && attachment.content[1] === 0xD8) || // JPEG
|
||||||
|
(attachment.content[0] === 0x89 && attachment.content[1] === 0x50) || // PNG
|
||||||
|
(attachment.content[0] === 0x50 && attachment.content[1] === 0x4B) // ZIP/DOCX/XLSX
|
||||||
|
);
|
||||||
|
|
||||||
const cleaned = contentStr.replace(/\s+/g, '');
|
if (isRawBinary) {
|
||||||
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
contentBuffer = attachment.content;
|
||||||
if (cleaned.length % 4 === 0 && base64Regex.test(cleaned)) {
|
} else {
|
||||||
try {
|
const str = attachment.content.toString('utf-8');
|
||||||
contentBuffer = Buffer.from(cleaned, 'base64');
|
const cleaned = str.replace(/\s+/g, '');
|
||||||
} catch (e) {
|
if (cleaned.length > 0 && cleaned.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(cleaned)) {
|
||||||
contentBuffer = Buffer.isBuffer(attachment.content) ? attachment.content : Buffer.from(attachment.content);
|
try {
|
||||||
|
contentBuffer = Buffer.from(cleaned, 'base64');
|
||||||
|
} catch (_) {
|
||||||
|
contentBuffer = attachment.content;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contentBuffer = attachment.content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (typeof attachment.content === 'string') {
|
||||||
|
const cleaned = attachment.content.replace(/\s+/g, '');
|
||||||
|
if (cleaned.length > 0 && cleaned.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(cleaned)) {
|
||||||
|
try {
|
||||||
|
contentBuffer = Buffer.from(cleaned, 'base64');
|
||||||
|
} catch (_) {
|
||||||
|
contentBuffer = Buffer.from(attachment.content, 'utf-8');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contentBuffer = Buffer.from(attachment.content, 'utf-8');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
contentBuffer = Buffer.isBuffer(attachment.content) ? attachment.content : Buffer.from(attachment.content);
|
contentBuffer = Buffer.from(attachment.content);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
contentBuffer = Buffer.alloc(0);
|
contentBuffer = Buffer.alloc(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isDownload = req.query.download === '1' || req.query.disposition === 'attachment';
|
||||||
|
const disposition = isDownload ? 'attachment' : 'inline';
|
||||||
const safeFilename = attachment.filename || 'attachment';
|
const safeFilename = attachment.filename || 'attachment';
|
||||||
|
|
||||||
res.setHeader('Content-Type', attachment.content_type || 'application/octet-stream');
|
res.setHeader('Content-Type', attachment.content_type || 'application/octet-stream');
|
||||||
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(safeFilename)}"; filename*=UTF-8''${encodeURIComponent(safeFilename)}`);
|
res.setHeader('Content-Disposition', `${disposition}; filename="${encodeURIComponent(safeFilename)}"; filename*=UTF-8''${encodeURIComponent(safeFilename)}`);
|
||||||
res.send(contentBuffer);
|
res.send(contentBuffer);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Errore download allegato:', err);
|
console.error('Errore download allegato:', err);
|
||||||
|
|||||||
Reference in new issue
Block a user