Compare commits
6
Commits
a15ab7e89d
...
a1b6d9333c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1b6d9333c | ||
|
|
ae458256d5 | ||
|
|
d4ef03b6f4 | ||
|
|
1bc3d8bf8a | ||
|
|
c33a7a4666 | ||
|
|
44c89aa92f |
No files matched your search
+30
-3
@@ -950,6 +950,7 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
overflow: visible !important;
|
overflow: visible !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-tooltip {
|
.queue-tooltip {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -970,6 +971,7 @@ body {
|
|||||||
transition: opacity 0.05s ease, visibility 0.05s ease;
|
transition: opacity 0.05s ease, visibility 0.05s ease;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-cell:hover .queue-tooltip {
|
.queue-cell:hover .queue-tooltip {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -2477,10 +2479,12 @@ body {
|
|||||||
box-shadow: 0 0 15px rgba(239, 68, 68, 0.4), inset 0 0 15px rgba(239, 68, 68, 0.2);
|
box-shadow: 0 0 15px rgba(239, 68, 68, 0.4), inset 0 0 15px rgba(239, 68, 68, 0.2);
|
||||||
border-color: rgba(239, 68, 68, 0.6);
|
border-color: rgba(239, 68, 68, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
box-shadow: 0 0 30px rgba(239, 68, 68, 0.8), inset 0 0 30px rgba(239, 68, 68, 0.4);
|
box-shadow: 0 0 30px rgba(239, 68, 68, 0.8), inset 0 0 30px rgba(239, 68, 68, 0.4);
|
||||||
border-color: rgba(239, 68, 68, 1);
|
border-color: rgba(239, 68, 68, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 15px rgba(239, 68, 68, 0.4), inset 0 0 15px rgba(239, 68, 68, 0.2);
|
box-shadow: 0 0 15px rgba(239, 68, 68, 0.4), inset 0 0 15px rgba(239, 68, 68, 0.2);
|
||||||
border-color: rgba(239, 68, 68, 0.6);
|
border-color: rgba(239, 68, 68, 0.6);
|
||||||
@@ -2502,11 +2506,13 @@ body {
|
|||||||
box-shadow: 0 0 5px rgba(16, 112, 202, 0.4);
|
box-shadow: 0 0 5px rgba(16, 112, 202, 0.4);
|
||||||
border-color: rgba(16, 112, 202, 0.5);
|
border-color: rgba(16, 112, 202, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
box-shadow: 0 0 15px rgba(16, 112, 202, 0.8), inset 0 0 5px rgba(16, 112, 202, 0.3);
|
box-shadow: 0 0 15px rgba(16, 112, 202, 0.8), inset 0 0 5px rgba(16, 112, 202, 0.3);
|
||||||
border-color: rgba(16, 112, 202, 0.9);
|
border-color: rgba(16, 112, 202, 0.9);
|
||||||
background: rgba(16, 112, 202, 0.15);
|
background: rgba(16, 112, 202, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 5px rgba(16, 112, 202, 0.4);
|
box-shadow: 0 0 5px rgba(16, 112, 202, 0.4);
|
||||||
border-color: rgba(16, 112, 202, 0.5);
|
border-color: rgba(16, 112, 202, 0.5);
|
||||||
@@ -2548,11 +2554,13 @@ body {
|
|||||||
box-shadow: 0 0 5px rgba(239, 68, 68, 0.4);
|
box-shadow: 0 0 5px rgba(239, 68, 68, 0.4);
|
||||||
border-color: rgba(239, 68, 68, 0.6);
|
border-color: rgba(239, 68, 68, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
box-shadow: 0 0 15px rgba(239, 68, 68, 0.8), inset 0 0 5px rgba(239, 68, 68, 0.3);
|
box-shadow: 0 0 15px rgba(239, 68, 68, 0.8), inset 0 0 5px rgba(239, 68, 68, 0.3);
|
||||||
border-color: rgba(239, 68, 68, 0.9);
|
border-color: rgba(239, 68, 68, 0.9);
|
||||||
background: rgba(239, 68, 68, 0.15) !important;
|
background: rgba(239, 68, 68, 0.15) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
box-shadow: 0 0 5px rgba(239, 68, 68, 0.4);
|
box-shadow: 0 0 5px rgba(239, 68, 68, 0.4);
|
||||||
border-color: rgba(239, 68, 68, 0.6);
|
border-color: rgba(239, 68, 68, 0.6);
|
||||||
@@ -2571,11 +2579,13 @@ body {
|
|||||||
/* ---- Ticket Tab Bar System ---- */
|
/* ---- Ticket Tab Bar System ---- */
|
||||||
.tabs-bar {
|
.tabs-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap; /* Wraps to new line if too many tabs */
|
flex-wrap: wrap;
|
||||||
|
/* Wraps to new line if too many tabs */
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
background: var(--bg-secondary);
|
background: var(--bg-secondary);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item {
|
.tab-item {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -2584,7 +2594,8 @@ body {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
border: none;
|
border: none;
|
||||||
border-right: 1px solid var(--border-subtle);
|
border-right: 1px solid var(--border-subtle);
|
||||||
border-radius: 0 !important; /* Square corners */
|
border-radius: 0 !important;
|
||||||
|
/* Square corners */
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
@@ -2593,15 +2604,19 @@ body {
|
|||||||
transition: all var(--transition-fast);
|
transition: all var(--transition-fast);
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item:hover {
|
.tab-item:hover {
|
||||||
background: var(--bg-tertiary);
|
background: var(--bg-tertiary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item.active {
|
.tab-item.active {
|
||||||
background: var(--bg-primary); /* Blend with main content background */
|
background: var(--bg-primary);
|
||||||
|
/* Blend with main content background */
|
||||||
color: var(--accent-primary);
|
color: var(--accent-primary);
|
||||||
border-bottom: 2px solid var(--accent-primary);
|
border-bottom: 2px solid var(--accent-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item .tab-close {
|
.tab-item .tab-close {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -2613,6 +2628,7 @@ body {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
transition: all 0.15s;
|
transition: all 0.15s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tab-item .tab-close:hover {
|
.tab-item .tab-close:hover {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
@@ -2622,14 +2638,17 @@ body {
|
|||||||
#view-container.ticket-view-active {
|
#view-container.ticket-view-active {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#view-container.ticket-view-active .back-link {
|
#view-container.ticket-view-active .back-link {
|
||||||
margin: var(--space-md) var(--space-xl) var(--space-xs);
|
margin: var(--space-md) var(--space-xl) var(--space-xs);
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
#view-container.ticket-view-active .ticket-detail {
|
#view-container.ticket-view-active .ticket-detail {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#view-container.ticket-view-active .ticket-detail .card {
|
#view-container.ticket-view-active .ticket-detail .card {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
border-left: none !important;
|
border-left: none !important;
|
||||||
@@ -2651,6 +2670,7 @@ body {
|
|||||||
gap: 2px;
|
gap: 2px;
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect-item {
|
.multiselect-item {
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
@@ -2660,14 +2680,17 @@ body {
|
|||||||
transition: background-color 0.15s, color 0.15s;
|
transition: background-color 0.15s, color 0.15s;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect-item:hover {
|
.multiselect-item:hover {
|
||||||
background-color: var(--bg-hover, rgba(0, 0, 0, 0.05));
|
background-color: var(--bg-hover, rgba(0, 0, 0, 0.05));
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect-item.selected {
|
.multiselect-item.selected {
|
||||||
background-color: var(--accent-primary);
|
background-color: var(--accent-primary);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-dark .multiselect-item:hover,
|
.theme-dark .multiselect-item:hover,
|
||||||
.theme-rosso .multiselect-item:hover,
|
.theme-rosso .multiselect-item:hover,
|
||||||
.theme-naturale .multiselect-item:hover,
|
.theme-naturale .multiselect-item:hover,
|
||||||
@@ -2676,6 +2699,7 @@ body {
|
|||||||
.theme-fairytale .multiselect-item:hover {
|
.theme-fairytale .multiselect-item:hover {
|
||||||
background-color: rgba(255, 255, 255, 0.08);
|
background-color: rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-config-row {
|
.line-config-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
@@ -2686,14 +2710,17 @@ body {
|
|||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
transition: border-color var(--transition-fast);
|
transition: border-color var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-config-row:hover {
|
.line-config-row:hover {
|
||||||
border-color: var(--border-light);
|
border-color: var(--border-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-config-row .line-name {
|
.line-config-row .line-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-config-row .line-actions {
|
.line-config-row .line-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
|
|||||||
+5
-5
@@ -11,7 +11,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap"
|
||||||
rel="stylesheet">
|
rel="stylesheet">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.snow.css" rel="stylesheet">
|
<link href="/vendor/quill.snow.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/css/style.css">
|
<link rel="stylesheet" href="/css/style.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -258,10 +258,10 @@
|
|||||||
<div class="toast-container" id="toast-container"></div>
|
<div class="toast-container" id="toast-container"></div>
|
||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js"></script>
|
<script src="/vendor/quill.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="/vendor/chart.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
<script src="/vendor/pdf.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
|
<script src="/vendor/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>
|
||||||
|
|||||||
+24
-2
@@ -64,8 +64,10 @@ const App = {
|
|||||||
create_ticket_open: { ctrl: true, alt: true, shift: false, key: 'n', enabled: true },
|
create_ticket_open: { ctrl: true, alt: true, shift: false, key: 'n', enabled: true },
|
||||||
create_ticket_submit: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true },
|
create_ticket_submit: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true },
|
||||||
quick_edit_save: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true },
|
quick_edit_save: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true },
|
||||||
|
send_email_submit: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true },
|
||||||
pdf_save_edited: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true },
|
pdf_save_edited: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true },
|
||||||
ricerca_spudorata_open: { ctrl: true, alt: false, shift: true, key: 'f', enabled: true },
|
ricerca_spudorata_open: { ctrl: true, alt: false, shift: true, key: 'f', enabled: true },
|
||||||
|
ticket_list_open: { ctrl: true, alt: false, shift: true, key: 'Backspace', enabled: true },
|
||||||
};
|
};
|
||||||
return defaults[id] || { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true };
|
return defaults[id] || { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true };
|
||||||
},
|
},
|
||||||
@@ -126,6 +128,21 @@ const App = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async assignAndOpenTab(id, tn, title) {
|
||||||
|
const activeAgentId = parseInt(localStorage.getItem('activeAgentId') || '1', 10);
|
||||||
|
try {
|
||||||
|
await this.api(`/api/tickets/${id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify({ user_id: activeAgentId }),
|
||||||
|
});
|
||||||
|
Toast.success('Ticket assegnato all\'agente attivo e aperto in scheda!');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error assigning ticket:', err);
|
||||||
|
Toast.error('Errore durante l\'assegnazione del ticket: ' + err.message);
|
||||||
|
}
|
||||||
|
this.openTab(id, tn, title);
|
||||||
|
},
|
||||||
|
|
||||||
addTabWithoutRedirect(id, tn, title) {
|
addTabWithoutRedirect(id, tn, title) {
|
||||||
const exists = this.tabs.find(t => String(t.id) === String(id));
|
const exists = this.tabs.find(t => String(t.id) === String(id));
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
@@ -262,7 +279,7 @@ const App = {
|
|||||||
toggleClearBtn();
|
toggleClearBtn();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global keyboard shortcuts (e.g. Crea Nuovo Ticket, Ricerca Spudorata)
|
// Global keyboard shortcuts (e.g. Crea Nuovo Ticket, Ricerca Spudorata, Torna a Elenco Ticket)
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (this.isShortcutTriggered(e, 'create_ticket_open')) {
|
if (this.isShortcutTriggered(e, 'create_ticket_open')) {
|
||||||
if (window.location.hash !== '#/tickets/new') {
|
if (window.location.hash !== '#/tickets/new') {
|
||||||
@@ -280,8 +297,13 @@ const App = {
|
|||||||
searchInput.select();
|
searchInput.select();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (this.isShortcutTriggered(e, 'ticket_list_open')) {
|
||||||
|
if (window.location.hash !== '#/tickets' && window.location.hash !== '') {
|
||||||
|
e.preventDefault();
|
||||||
|
window.location.hash = '#/tickets';
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
}, true);
|
||||||
|
|
||||||
// Check DB connection
|
// Check DB connection
|
||||||
this.checkConnection();
|
this.checkConnection();
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ const Filters = {
|
|||||||
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
|
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="multiselect-popover" id="state-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; width: 220px; background:var(--bg-card); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); box-shadow:var(--shadow-md); z-index:1002; padding:var(--space-xs); margin-top:4px;">
|
<div class="multiselect-popover" id="state-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; width: 220px; background:var(--bg-card); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); box-shadow:var(--shadow-md); z-index:1002; padding:var(--space-xs); margin-top:4px;">
|
||||||
<input type="text" class="form-input ms-search" placeholder="Cerca..." style="width:100%; padding:4px 8px; font-size:0.78rem; margin-bottom:6px; box-sizing:border-box;" autocomplete="off" />
|
<input type="text" id="state-ms-search-input" name="state_search" class="form-input ms-search" placeholder="Cerca..." style="width:100%; padding:4px 8px; font-size:0.78rem; margin-bottom:6px; box-sizing:border-box;" autocomplete="off" />
|
||||||
<div class="ms-items-container" style="display:flex; flex-direction:column; gap:2px; padding-bottom:6px; border-bottom:1px solid var(--border-subtle);">
|
<div class="ms-items-container" style="display:flex; flex-direction:column; gap:2px; padding-bottom:6px; border-bottom:1px solid var(--border-subtle);">
|
||||||
${(lookups.states || []).map(s => {
|
${(lookups.states || []).map(s => {
|
||||||
const selectedList = String(this.state.state_id || '').split(',').filter(Boolean);
|
const selectedList = String(this.state.state_id || '').split(',').filter(Boolean);
|
||||||
@@ -243,7 +243,7 @@ const Filters = {
|
|||||||
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
|
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="multiselect-popover" id="queue-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; min-width: 480px; width: max-content; max-width: 85vw; background:var(--bg-card); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); box-shadow:var(--shadow-md); z-index:1002; padding:var(--space-xs); margin-top:4px;">
|
<div class="multiselect-popover" id="queue-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; min-width: 480px; width: max-content; max-width: 85vw; background:var(--bg-card); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); box-shadow:var(--shadow-md); z-index:1002; padding:var(--space-xs); margin-top:4px;">
|
||||||
<input type="text" class="form-input ms-search" placeholder="Cerca coda..." style="width:100%; padding:4px 8px; font-size:0.78rem; margin-bottom:6px; box-sizing:border-box;" autocomplete="off" />
|
<input type="text" id="queue-ms-search-input" name="queue_search" class="form-input ms-search" placeholder="Cerca coda..." style="width:100%; padding:4px 8px; font-size:0.78rem; margin-bottom:6px; box-sizing:border-box;" autocomplete="off" />
|
||||||
<div class="ms-items-container" style="display:flex; flex-direction:column; gap:2px; max-height:300px; overflow-y:auto; padding-bottom:6px; border-bottom:1px solid var(--border-subtle);">
|
<div class="ms-items-container" style="display:flex; flex-direction:column; gap:2px; max-height:300px; overflow-y:auto; padding-bottom:6px; border-bottom:1px solid var(--border-subtle);">
|
||||||
${(lookups.queues || []).map(q => {
|
${(lookups.queues || []).map(q => {
|
||||||
const selectedList = String(this.state.queue_id || '').split(',').filter(Boolean);
|
const selectedList = String(this.state.queue_id || '').split(',').filter(Boolean);
|
||||||
@@ -271,7 +271,7 @@ const Filters = {
|
|||||||
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
|
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="multiselect-popover" id="priority-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; width: 200px; background:var(--bg-card); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); box-shadow:var(--shadow-md); z-index:1002; padding:var(--space-xs); margin-top:4px;">
|
<div class="multiselect-popover" id="priority-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; width: 200px; background:var(--bg-card); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); box-shadow:var(--shadow-md); z-index:1002; padding:var(--space-xs); margin-top:4px;">
|
||||||
<input type="text" class="form-input ms-search" placeholder="Cerca priorità..." style="width:100%; padding:4px 8px; font-size:0.78rem; margin-bottom:6px; box-sizing:border-box;" autocomplete="off" />
|
<input type="text" id="priority-ms-search-input" name="priority_search" class="form-input ms-search" placeholder="Cerca priorità..." style="width:100%; padding:4px 8px; font-size:0.78rem; margin-bottom:6px; box-sizing:border-box;" autocomplete="off" />
|
||||||
<div class="ms-items-container" style="display:flex; flex-direction:column; gap:2px; max-height:180px; overflow-y:auto; padding-bottom:6px; border-bottom:1px solid var(--border-subtle);">
|
<div class="ms-items-container" style="display:flex; flex-direction:column; gap:2px; max-height:180px; overflow-y:auto; padding-bottom:6px; border-bottom:1px solid var(--border-subtle);">
|
||||||
${(lookups.priorities || []).map(p => {
|
${(lookups.priorities || []).map(p => {
|
||||||
const selectedList = String(this.state.priority_id || '').split(',').filter(Boolean);
|
const selectedList = String(this.state.priority_id || '').split(',').filter(Boolean);
|
||||||
@@ -299,7 +299,7 @@ const Filters = {
|
|||||||
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
|
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="multiselect-popover" id="owner-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; width: 240px; background:var(--bg-card); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); box-shadow:var(--shadow-md); z-index:1002; padding:var(--space-xs); margin-top:4px;">
|
<div class="multiselect-popover" id="owner-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; width: 240px; background:var(--bg-card); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); box-shadow:var(--shadow-md); z-index:1002; padding:var(--space-xs); margin-top:4px;">
|
||||||
<input type="text" class="form-input ms-search" placeholder="Cerca owner..." style="width:100%; padding:4px 8px; font-size:0.78rem; margin-bottom:6px; box-sizing:border-box;" autocomplete="off" />
|
<input type="text" id="owner-ms-search-input" name="owner_search" class="form-input ms-search" placeholder="Cerca owner..." style="width:100%; padding:4px 8px; font-size:0.78rem; margin-bottom:6px; box-sizing:border-box;" autocomplete="off" />
|
||||||
<div class="ms-items-container" style="display:flex; flex-direction:column; gap:2px; max-height:180px; overflow-y:auto; padding-bottom:6px; border-bottom:1px solid var(--border-subtle);">
|
<div class="ms-items-container" style="display:flex; flex-direction:column; gap:2px; max-height:180px; overflow-y:auto; padding-bottom:6px; border-bottom:1px solid var(--border-subtle);">
|
||||||
${(lookups.users || []).map(u => {
|
${(lookups.users || []).map(u => {
|
||||||
const selectedList = String(this.state.user_id || '').split(',').filter(Boolean);
|
const selectedList = String(this.state.user_id || '').split(',').filter(Boolean);
|
||||||
@@ -326,7 +326,7 @@ const Filters = {
|
|||||||
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
|
<span style="font-size:0.6rem; color:var(--text-muted); margin-left:6px;">▼</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="multiselect-popover" id="customer-user-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; width: 280px; background:var(--bg-card); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); box-shadow:var(--shadow-md); z-index:1002; padding:var(--space-xs); margin-top:4px;">
|
<div class="multiselect-popover" id="customer-user-multiselect-popover" style="display:none; position:absolute; left:0; top:100%; width: 280px; background:var(--bg-card); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); box-shadow:var(--shadow-md); z-index:1002; padding:var(--space-xs); margin-top:4px;">
|
||||||
<input type="text" id="customer-user-search-input" placeholder="Cerca utente..." style="width:100%; padding:6px 8px; font-size:0.8rem; background:var(--bg-tertiary); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); margin-bottom:6px; box-sizing:border-box; color:var(--text-primary); font-family:inherit;" autocomplete="off" />
|
<input type="text" id="customer-user-search-input" name="customer_user_search" placeholder="Cerca utente..." style="width:100%; padding:6px 8px; font-size:0.8rem; background:var(--bg-tertiary); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); margin-bottom:6px; box-sizing:border-box; color:var(--text-primary); font-family:inherit;" autocomplete="off" />
|
||||||
<div id="customer-user-items-container" style="display:flex; flex-direction:column; gap:4px; max-height:220px; overflow-y:auto; padding-bottom:6px; border-bottom:1px solid var(--border-subtle);">
|
<div id="customer-user-items-container" style="display:flex; flex-direction:column; gap:4px; max-height:220px; overflow-y:auto; padding-bottom:6px; border-bottom:1px solid var(--border-subtle);">
|
||||||
<!-- Populated dynamically -->
|
<!-- Populated dynamically -->
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -559,7 +559,22 @@ const EmailCompose = (() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Send
|
// Send
|
||||||
document.getElementById('ec-send').addEventListener('click', () => sendEmail(toTagsCtrl, ccTagsCtrl, bccTagsCtrl));
|
const handleEmailSend = () => sendEmail(toTagsCtrl, ccTagsCtrl, bccTagsCtrl);
|
||||||
|
document.getElementById('ec-send').addEventListener('click', handleEmailSend);
|
||||||
|
|
||||||
|
// Keyboard shortcut handler for email compose modal
|
||||||
|
const handleEmailKeydown = (e) => {
|
||||||
|
if (!document.getElementById('email-compose-overlay')) {
|
||||||
|
document.removeEventListener('keydown', handleEmailKeydown, true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (App.isShortcutTriggered(e, 'send_email_submit')) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
handleEmailSend();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleEmailKeydown, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function processFiles(files) {
|
function processFiles(files) {
|
||||||
|
|||||||
@@ -431,7 +431,7 @@ const RicercaSpudorataView = {
|
|||||||
const limitSelectHtml = `
|
const limitSelectHtml = `
|
||||||
<div style="display:inline-flex; align-items:center; gap:var(--space-xs); font-size:0.8rem; color:var(--text-secondary);">
|
<div style="display:inline-flex; align-items:center; gap:var(--space-xs); font-size:0.8rem; color:var(--text-secondary);">
|
||||||
<span>Righe:</span>
|
<span>Righe:</span>
|
||||||
<select class="form-select spudorata-per-page-select" style="padding: 2px 24px 2px 6px; font-size: 0.75rem; height: 26px; min-width: 65px; margin: 0; background-position: right 6px center; border-color: var(--border-light);">
|
<select id="spudorata-per-page-select-${isTop ? 'top' : 'bottom'}" name="spudorata_per_page_${isTop ? 'top' : 'bottom'}" class="form-select spudorata-per-page-select" style="padding: 2px 24px 2px 6px; font-size: 0.75rem; height: 26px; min-width: 65px; margin: 0; background-position: right 6px center; border-color: var(--border-light);">
|
||||||
<option value="10" ${perPage === 10 ? 'selected' : ''}>10</option>
|
<option value="10" ${perPage === 10 ? 'selected' : ''}>10</option>
|
||||||
<option value="20" ${perPage === 20 ? 'selected' : ''}>20</option>
|
<option value="20" ${perPage === 20 ? 'selected' : ''}>20</option>
|
||||||
<option value="50" ${perPage === 50 ? 'selected' : ''}>50</option>
|
<option value="50" ${perPage === 50 ? 'selected' : ''}>50</option>
|
||||||
|
|||||||
@@ -72,6 +72,13 @@ const ShortcutsView = {
|
|||||||
context: 'Globale',
|
context: 'Globale',
|
||||||
defaultCombo: { ctrl: true, alt: false, shift: true, key: 'f', enabled: true }
|
defaultCombo: { ctrl: true, alt: false, shift: true, key: 'f', enabled: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'ticket_list_open',
|
||||||
|
name: 'Torna a Elenco Ticket',
|
||||||
|
description: 'Naviga direttamente alla pagina principale dell\'elenco ticket',
|
||||||
|
context: 'Globale',
|
||||||
|
defaultCombo: { ctrl: true, alt: false, shift: true, key: 'Backspace', enabled: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'create_ticket_submit',
|
id: 'create_ticket_submit',
|
||||||
name: 'Invia Nuovo Ticket',
|
name: 'Invia Nuovo Ticket',
|
||||||
@@ -86,6 +93,13 @@ const ShortcutsView = {
|
|||||||
context: 'Pagina Dettaglio Ticket',
|
context: 'Pagina Dettaglio Ticket',
|
||||||
defaultCombo: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true }
|
defaultCombo: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'send_email_submit',
|
||||||
|
name: 'Invia Email',
|
||||||
|
description: 'Invia l\'email quando la finestra di composizione email è aperta',
|
||||||
|
context: 'Modal Composizione Email',
|
||||||
|
defaultCombo: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'pdf_save_edited',
|
id: 'pdf_save_edited',
|
||||||
name: 'Salva Immagine / Allegato / PDF Modificato',
|
name: 'Salva Immagine / Allegato / PDF Modificato',
|
||||||
|
|||||||
+646
-28
@@ -246,6 +246,13 @@ const TicketDetailView = {
|
|||||||
|
|
||||||
<input type="color" id="img-color-picker" value="#ef4444" title="Colore primario" style="width:24px; height:24px; border:none; border-radius:50%; cursor:pointer; background:none;" />
|
<input type="color" id="img-color-picker" value="#ef4444" title="Colore primario" style="width:24px; height:24px; border:none; border-radius:50%; cursor:pointer; background:none;" />
|
||||||
|
|
||||||
|
<div id="img-highlight-controls" style="display: none; align-items: center; gap: 4px; padding-left: 4px; border-left: 1px solid rgba(255,255,255,0.2);" title="Intensità Opacità">
|
||||||
|
<label style="font-size:0.7rem; color:#cbd5e1; display:flex; align-items:center; gap:3px;">
|
||||||
|
Opacità: <input type="range" id="img-highlight-opacity" min="0.01" max="1.0" step="0.01" value="0.15" style="width:65px; cursor:pointer;" />
|
||||||
|
<span id="img-highlight-opacity-val" style="font-size:0.7rem; font-family:monospace; color:#38bdf8; min-width:28px;">15%</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="img-shape-controls" style="display: none; align-items: center; gap: 4px; padding-left: 4px; border-left: 1px solid rgba(255,255,255,0.2);">
|
<div id="img-shape-controls" style="display: none; align-items: center; gap: 4px; padding-left: 4px; border-left: 1px solid rgba(255,255,255,0.2);">
|
||||||
<select id="img-shape-type" style="height: 24px; background: #0f172a; color: #fff; border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; font-size: 0.74rem; padding: 0 4px; cursor: pointer;">
|
<select id="img-shape-type" style="height: 24px; background: #0f172a; color: #fff; border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; font-size: 0.74rem; padding: 0 4px; cursor: pointer;">
|
||||||
<option value="rectangle">🟦 Rettangolo</option>
|
<option value="rectangle">🟦 Rettangolo</option>
|
||||||
@@ -321,6 +328,13 @@ const TicketDetailView = {
|
|||||||
<!-- Main Color Picker -->
|
<!-- Main Color Picker -->
|
||||||
<input type="color" id="pdf-color-picker" value="#ef4444" title="Colore primario" style="width:26px; height:26px; padding:0; border:none; border-radius:50%; cursor:pointer; background:none;" />
|
<input type="color" id="pdf-color-picker" value="#ef4444" title="Colore primario" style="width:26px; height:26px; padding:0; border:none; border-radius:50%; cursor:pointer; background:none;" />
|
||||||
|
|
||||||
|
<div id="pdf-highlight-controls" style="display: none; align-items: center; gap: 4px; padding-left: 6px; border-left: 1px solid rgba(255,255,255,0.2);" title="Intensità Opacità">
|
||||||
|
<label style="font-size:0.72rem; color:#cbd5e1; display:flex; align-items:center; gap:4px;">
|
||||||
|
Opacità: <input type="range" id="pdf-highlight-opacity" min="0.01" max="1.0" step="0.01" value="0.15" style="width:70px; cursor:pointer;" />
|
||||||
|
<span id="pdf-highlight-opacity-val" style="font-size:0.72rem; font-family:monospace; color:#38bdf8; min-width:30px;">15%</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Shape controls container -->
|
<!-- Shape controls container -->
|
||||||
<div id="pdf-shape-controls" style="display: none; align-items: center; gap: 6px; padding-left: 6px; border-left: 1px solid rgba(255,255,255,0.2);">
|
<div id="pdf-shape-controls" style="display: none; align-items: center; gap: 6px; padding-left: 6px; border-left: 1px solid rgba(255,255,255,0.2);">
|
||||||
<select id="pdf-shape-type" style="height: 26px; background: #0f172a; color: #fff; border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; font-size: 0.76rem; padding: 0 4px; cursor: pointer;">
|
<select id="pdf-shape-type" style="height: 26px; background: #0f172a; color: #fff; border: 1px solid rgba(255,255,255,0.3); border-radius: 4px; font-size: 0.76rem; padding: 0 4px; cursor: pointer;">
|
||||||
@@ -365,7 +379,26 @@ const TicketDetailView = {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
} else if (current.type === 'text') {
|
} else if (current.type === 'text') {
|
||||||
previewHtml = `<iframe src="${current.src}" style="width: 84vw; height: 78vh; border: none; border-radius: 6px; background: #ffffff; box-shadow: 0 10px 40px rgba(0,0,0,0.6);"></iframe>`;
|
const attId = (current.src.match(/\/attachments\/(\d+)/) || [])[1];
|
||||||
|
previewHtml = `
|
||||||
|
<div id="text-viewer-container" style="position: relative; width: 84vw; height: 78vh; background: var(--bg-card); border-radius: 8px; box-shadow: 0 10px 40px rgba(0,0,0,0.6); display: flex; flex-direction: column; overflow: hidden; border: 1px solid var(--border-subtle);">
|
||||||
|
<div id="text-toolbar" style="height: 48px; min-height: 48px; background: rgba(15, 23, 42, 0.95); border-bottom: 1px solid rgba(255, 255, 255, 0.15); display: flex; align-items: center; justify-content: space-between; padding: 0 16px; color: #ffffff; font-size: 0.82rem; user-select: none; gap: 12px; flex-wrap: wrap;">
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
<span style="font-weight: 700; color: #ffffff;">📝 Documento di Testo (Modificabile)</span>
|
||||||
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; gap: 10px;">
|
||||||
|
${attId ? `
|
||||||
|
<button id="text-save-btn" class="btn btn-primary btn-sm" style="height:30px; padding:0 14px; font-size:0.8rem; font-weight:600; display:inline-flex; align-items:center; gap:6px; background:var(--accent-primary); color:#ffffff; border:none; border-radius:6px; cursor:pointer;" title="Salva le modifiche apportate al file di testo">
|
||||||
|
💾 Salva Modifiche
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex: 1; position: relative; padding: 12px; background: #0f172a; display: flex;">
|
||||||
|
<textarea id="text-editor-textarea" style="flex: 1; width: 100%; height: 100%; background: #090d16; color: #e2e8f0; font-family: consolas, Monaco, monospace, sans-serif; font-size: 0.88rem; line-height: 1.5; border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; padding: 14px; resize: none; outline: none;" placeholder="Caricamento contenuto di testo..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
} else {
|
} else {
|
||||||
const icon = this.getFileIcon(current.filename);
|
const icon = this.getFileIcon(current.filename);
|
||||||
previewHtml = `
|
previewHtml = `
|
||||||
@@ -380,13 +413,18 @@ const TicketDetailView = {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p style="font-size: 0.85rem; color: var(--text-secondary); margin: 0; line-height: 1.4;">
|
<p style="font-size: 0.85rem; color: var(--text-secondary); margin: 0; line-height: 1.4;">
|
||||||
Per modificare o visualizzare questo file con l'applicazione predefinita del tuo sistema (Word, Excel, ecc.), usa il pulsante qui sotto.
|
Scegli se aprire il file con l'applicazione predefinita del tuo computer (Word, Excel, PowerPoint, ecc.) oppure scaricarne una copia locale.
|
||||||
</p>
|
</p>
|
||||||
<a href="${current.downloadUrl}" download="${App.escapeHtml(current.filename)}" target="_blank" class="btn btn-primary" style="padding: 10px 22px; font-size: 0.9rem; font-weight: 600; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; border-radius: var(--radius-md); box-shadow: var(--shadow-sm);">
|
<div style="display:flex; gap:10px; flex-wrap:wrap; justify-content:center;">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:18px; height:18px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
<button id="doc-open-system-btn" class="btn btn-primary" style="padding: 10px 18px; font-size: 0.88rem; font-weight: 600; display: inline-flex; align-items: center; gap: 8px; border-radius: var(--radius-md); border:none; cursor:pointer;">
|
||||||
Apri nell'app / Scarica
|
🚀 Apri con app di sistema
|
||||||
|
</button>
|
||||||
|
<a href="${current.downloadUrl}" download="${App.escapeHtml(current.filename)}" target="_blank" class="btn btn-ghost" style="padding: 10px 18px; font-size: 0.88rem; font-weight: 600; display: inline-flex; align-items: center; gap: 8px; text-decoration: none; border-radius: var(--radius-md); border:1px solid var(--border-subtle);">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px; height:16px;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||||
|
Scarica copia
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,6 +447,54 @@ const TicketDetailView = {
|
|||||||
this.initPdfJsViewer(current.src);
|
this.initPdfJsViewer(current.src);
|
||||||
} else if (current.type === 'image') {
|
} else if (current.type === 'image') {
|
||||||
this.initImageEditor(current.src, current.filename);
|
this.initImageEditor(current.src, current.filename);
|
||||||
|
} else if (current.type === 'text') {
|
||||||
|
const textarea = document.getElementById('text-editor-textarea');
|
||||||
|
const saveBtn = document.getElementById('text-save-btn');
|
||||||
|
const attId = (current.src.match(/\/attachments\/(\d+)/) || [])[1];
|
||||||
|
|
||||||
|
if (textarea) {
|
||||||
|
fetch(current.src)
|
||||||
|
.then(r => r.text())
|
||||||
|
.then(txt => { textarea.value = txt; })
|
||||||
|
.catch(err => { textarea.value = 'Errore nel caricamento del file: ' + err.message; });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveBtn && attId && textarea) {
|
||||||
|
saveBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
saveBtn.disabled = true;
|
||||||
|
saveBtn.innerHTML = '💾 Salvataggio...';
|
||||||
|
await App.api(`/api/tickets/attachments/${attId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify({ content: textarea.value })
|
||||||
|
});
|
||||||
|
Toast.success('File di testo salvato con successo!');
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error('Errore durante il salvataggio: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
saveBtn.disabled = false;
|
||||||
|
saveBtn.innerHTML = '💾 Salva Modifiche';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const openSysBtn = document.getElementById('doc-open-system-btn');
|
||||||
|
const attId = (current.src.match(/\/attachments\/(\d+)/) || [])[1];
|
||||||
|
if (openSysBtn && attId) {
|
||||||
|
openSysBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
openSysBtn.disabled = true;
|
||||||
|
openSysBtn.innerHTML = '🚀 Apertura in corso...';
|
||||||
|
await App.api(`/api/tickets/attachments/${attId}/open`, { method: 'POST' });
|
||||||
|
Toast.success('File aperto con l\'applicazione di sistema!');
|
||||||
|
} catch (err) {
|
||||||
|
Toast.error('Impossibile aprire il file: ' + err.message);
|
||||||
|
} finally {
|
||||||
|
openSysBtn.disabled = false;
|
||||||
|
openSysBtn.innerHTML = '🚀 Apri con app di sistema';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
overlay.focus();
|
overlay.focus();
|
||||||
@@ -650,6 +736,8 @@ const TicketDetailView = {
|
|||||||
|
|
||||||
let currentTool = 'draw'; // 'draw' | 'text' | 'highlight' | 'shape' | 'signature'
|
let currentTool = 'draw'; // 'draw' | 'text' | 'highlight' | 'shape' | 'signature'
|
||||||
let currentColor = '#ef4444';
|
let currentColor = '#ef4444';
|
||||||
|
let currentHighlightOpacity = 0.15;
|
||||||
|
let mousePos = null;
|
||||||
let activeSignature = null;
|
let activeSignature = null;
|
||||||
|
|
||||||
// Pre-fetch signatures to select default signature
|
// Pre-fetch signatures to select default signature
|
||||||
@@ -668,10 +756,20 @@ const TicketDetailView = {
|
|||||||
const toolShapeBtn = document.getElementById('pdf-tool-shape');
|
const toolShapeBtn = document.getElementById('pdf-tool-shape');
|
||||||
const toolSigBtn = document.getElementById('pdf-tool-signature');
|
const toolSigBtn = document.getElementById('pdf-tool-signature');
|
||||||
const shapeControls = document.getElementById('pdf-shape-controls');
|
const shapeControls = document.getElementById('pdf-shape-controls');
|
||||||
|
const highlightControls = document.getElementById('pdf-highlight-controls');
|
||||||
|
const highlightOpacityInput = document.getElementById('pdf-highlight-opacity');
|
||||||
|
const highlightOpacityVal = document.getElementById('pdf-highlight-opacity-val');
|
||||||
const colorPicker = document.getElementById('pdf-color-picker');
|
const colorPicker = document.getElementById('pdf-color-picker');
|
||||||
const toolClearBtn = document.getElementById('pdf-tool-clear');
|
const toolClearBtn = document.getElementById('pdf-tool-clear');
|
||||||
const saveEditedBtn = document.getElementById('pdf-save-edited');
|
const saveEditedBtn = document.getElementById('pdf-save-edited');
|
||||||
|
|
||||||
|
if (highlightOpacityInput && highlightOpacityVal) {
|
||||||
|
highlightOpacityInput.oninput = (e) => {
|
||||||
|
currentHighlightOpacity = parseFloat(e.target.value);
|
||||||
|
highlightOpacityVal.textContent = `${Math.round(currentHighlightOpacity * 100)}%`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const setActiveTool = (tool, activeBtn) => {
|
const setActiveTool = (tool, activeBtn) => {
|
||||||
currentTool = tool;
|
currentTool = tool;
|
||||||
const allToolBtns = [toolSelectBtn, toolDrawBtn, toolTextBtn, toolHighlightBtn, toolShapeBtn, toolSigBtn];
|
const allToolBtns = [toolSelectBtn, toolDrawBtn, toolTextBtn, toolHighlightBtn, toolShapeBtn, toolSigBtn];
|
||||||
@@ -688,6 +786,9 @@ const TicketDetailView = {
|
|||||||
if (shapeControls) {
|
if (shapeControls) {
|
||||||
shapeControls.style.display = tool === 'shape' ? 'flex' : 'none';
|
shapeControls.style.display = tool === 'shape' ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
|
if (highlightControls) {
|
||||||
|
highlightControls.style.display = (tool === 'draw' || tool === 'highlight') ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
// Adjust pointer-events for text selection layer vs annotation canvas
|
// Adjust pointer-events for text selection layer vs annotation canvas
|
||||||
for (let num = 1; num <= totalPages; num++) {
|
for (let num = 1; num <= totalPages; num++) {
|
||||||
@@ -1131,13 +1232,13 @@ const TicketDetailView = {
|
|||||||
drawCtx.strokeStyle = obj.color;
|
drawCtx.strokeStyle = obj.color;
|
||||||
drawCtx.lineWidth = obj.size;
|
drawCtx.lineWidth = obj.size;
|
||||||
|
|
||||||
if (obj.brushType === 'highlight') {
|
if (obj.brushType === 'highlight' || obj.type === 'highlight') {
|
||||||
drawCtx.globalCompositeOperation = 'multiply';
|
drawCtx.globalCompositeOperation = 'multiply';
|
||||||
drawCtx.globalAlpha = 0.55;
|
drawCtx.globalAlpha = obj.opacity !== undefined ? obj.opacity : 0.15;
|
||||||
drawCtx.lineCap = 'square';
|
drawCtx.lineCap = 'square';
|
||||||
} else {
|
} else {
|
||||||
drawCtx.globalCompositeOperation = 'source-over';
|
drawCtx.globalCompositeOperation = 'source-over';
|
||||||
drawCtx.globalAlpha = obj.opacity || 1.0;
|
drawCtx.globalAlpha = obj.opacity !== undefined ? obj.opacity : 1.0;
|
||||||
drawCtx.lineCap = 'round';
|
drawCtx.lineCap = 'round';
|
||||||
}
|
}
|
||||||
drawCtx.lineJoin = 'round';
|
drawCtx.lineJoin = 'round';
|
||||||
@@ -1264,6 +1365,27 @@ const TicketDetailView = {
|
|||||||
drawCtx.restore();
|
drawCtx.restore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Draw brush size circle indicator under mouse cursor when drawing/highlighting
|
||||||
|
if (mousePos && (currentTool === 'draw' || currentTool === 'highlight')) {
|
||||||
|
const currentSize = (currentTool === 'highlight' || currentBrushType === 'highlight') ? (currentBrushSize || 20) : (currentBrushType === 'brush' ? (currentBrushSize || 8) : (currentBrushSize || 3));
|
||||||
|
const radius = Math.max(2, currentSize / 2);
|
||||||
|
|
||||||
|
drawCtx.save();
|
||||||
|
drawCtx.globalCompositeOperation = 'source-over';
|
||||||
|
drawCtx.beginPath();
|
||||||
|
drawCtx.arc(mousePos.x, mousePos.y, radius, 0, Math.PI * 2);
|
||||||
|
drawCtx.strokeStyle = 'rgba(0, 0, 0, 0.7)';
|
||||||
|
drawCtx.lineWidth = 1.5;
|
||||||
|
drawCtx.stroke();
|
||||||
|
|
||||||
|
drawCtx.beginPath();
|
||||||
|
drawCtx.arc(mousePos.x, mousePos.y, radius, 0, Math.PI * 2);
|
||||||
|
drawCtx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
|
||||||
|
drawCtx.lineWidth = 1;
|
||||||
|
drawCtx.stroke();
|
||||||
|
drawCtx.restore();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getObjectHandles = (obj) => {
|
const getObjectHandles = (obj) => {
|
||||||
@@ -1396,8 +1518,16 @@ const TicketDetailView = {
|
|||||||
|
|
||||||
<div style="margin-bottom: 10px; display:flex; gap:8px; align-items:center;">
|
<div style="margin-bottom: 10px; display:flex; gap:8px; align-items:center;">
|
||||||
<div style="flex:1;">
|
<div style="flex:1;">
|
||||||
<label style="font-size:0.75rem; color:#94a3b8; display:block; margin-bottom:2px;">Dimensione tratto</label>
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:2px;">
|
||||||
<input type="range" id="ctx-brush-size" min="1" max="40" value="${currentBrushSize}" style="width:100%; cursor:pointer;" />
|
<label style="font-size:0.75rem; color:#94a3b8;">Dimensione tratto</label>
|
||||||
|
<span id="ctx-brush-val-badge" style="font-size:0.72rem; font-family:monospace; color:#38bdf8; font-weight:700;">${currentBrushSize}px</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; gap:6px;">
|
||||||
|
<input type="range" id="ctx-brush-size" min="1" max="40" value="${currentBrushSize}" style="flex:1; cursor:pointer;" />
|
||||||
|
<div id="ctx-brush-preview-box" title="Anteprima dimensione reale" style="width:26px; height:26px; border-radius:4px; background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.2); display:flex; align-items:center; justify-content:center; flex-shrink:0;">
|
||||||
|
<div id="ctx-brush-preview-circle" style="width:${Math.max(2, currentBrushSize)}px; height:${Math.max(2, currentBrushSize)}px; max-width:22px; max-height:22px; border-radius:50%; background:${currentColor}; transition:all 0.1s ease;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label style="font-size:0.75rem; color:#94a3b8; display:block; margin-bottom:2px;">Colore</label>
|
<label style="font-size:0.75rem; color:#94a3b8; display:block; margin-bottom:2px;">Colore</label>
|
||||||
@@ -1496,6 +1626,13 @@ const TicketDetailView = {
|
|||||||
} else if (type === 'shape') {
|
} else if (type === 'shape') {
|
||||||
setActiveTool('shape', toolShapeBtn);
|
setActiveTool('shape', toolShapeBtn);
|
||||||
}
|
}
|
||||||
|
const valBadge = menuOverlay.querySelector('#ctx-brush-val-badge');
|
||||||
|
if (valBadge) valBadge.textContent = `${currentBrushSize}px`;
|
||||||
|
const pCircle = menuOverlay.querySelector('#ctx-brush-preview-circle');
|
||||||
|
if (pCircle) {
|
||||||
|
pCircle.style.width = `${Math.max(2, currentBrushSize)}px`;
|
||||||
|
pCircle.style.height = `${Math.max(2, currentBrushSize)}px`;
|
||||||
|
}
|
||||||
if (sizeInput) sizeInput.value = currentBrushSize;
|
if (sizeInput) sizeInput.value = currentBrushSize;
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -1507,6 +1644,15 @@ const TicketDetailView = {
|
|||||||
const val = parseInt(eVal.target.value, 10);
|
const val = parseInt(eVal.target.value, 10);
|
||||||
currentBrushSize = val;
|
currentBrushSize = val;
|
||||||
|
|
||||||
|
const valBadge = menuOverlay.querySelector('#ctx-brush-val-badge');
|
||||||
|
if (valBadge) valBadge.textContent = `${val}px`;
|
||||||
|
|
||||||
|
const pCircle = menuOverlay.querySelector('#ctx-brush-preview-circle');
|
||||||
|
if (pCircle) {
|
||||||
|
pCircle.style.width = `${Math.max(2, val)}px`;
|
||||||
|
pCircle.style.height = `${Math.max(2, val)}px`;
|
||||||
|
}
|
||||||
|
|
||||||
// Update shape border width input in toolbar if shape tool is active
|
// Update shape border width input in toolbar if shape tool is active
|
||||||
const shapeBorderWidthEl = document.getElementById('pdf-shape-border-width');
|
const shapeBorderWidthEl = document.getElementById('pdf-shape-border-width');
|
||||||
if (shapeBorderWidthEl) {
|
if (shapeBorderWidthEl) {
|
||||||
@@ -1526,6 +1672,8 @@ const TicketDetailView = {
|
|||||||
ctxColorPicker.onchange = (eVal) => {
|
ctxColorPicker.onchange = (eVal) => {
|
||||||
eVal.stopPropagation();
|
eVal.stopPropagation();
|
||||||
currentColor = eVal.target.value;
|
currentColor = eVal.target.value;
|
||||||
|
const pCircle = menuOverlay.querySelector('#ctx-brush-preview-circle');
|
||||||
|
if (pCircle) pCircle.style.background = currentColor;
|
||||||
if (colorPicker) colorPicker.value = currentColor;
|
if (colorPicker) colorPicker.value = currentColor;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1611,6 +1759,10 @@ const TicketDetailView = {
|
|||||||
|
|
||||||
drawCanvas.onmousedown = (e) => {
|
drawCanvas.onmousedown = (e) => {
|
||||||
if (e.button !== 0) return; // Only primary click
|
if (e.button !== 0) return; // Only primary click
|
||||||
|
|
||||||
|
const existingPopup = document.getElementById('pdf-context-menu-popup') || document.getElementById('img-context-menu-popup');
|
||||||
|
if (existingPopup) existingPopup.remove();
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const rect = drawCanvas.getBoundingClientRect();
|
const rect = drawCanvas.getBoundingClientRect();
|
||||||
@@ -1847,7 +1999,9 @@ const TicketDetailView = {
|
|||||||
// Drawing mode (pencil / brush / highlight)
|
// Drawing mode (pencil / brush / highlight)
|
||||||
isDrawingStroke = true;
|
isDrawingStroke = true;
|
||||||
currentStrokePoints = [{ x, y }];
|
currentStrokePoints = [{ x, y }];
|
||||||
const opacity = (currentTool === 'highlight' || currentBrushType === 'highlight') ? 0.15 : 1.0;
|
const opacity = (currentTool === 'highlight' || currentBrushType === 'highlight')
|
||||||
|
? currentHighlightOpacity
|
||||||
|
: (currentTool === 'draw' ? currentHighlightOpacity : 1.0);
|
||||||
const size = (currentTool === 'highlight' || currentBrushType === 'highlight') ? (currentBrushSize || 20) : (currentBrushType === 'brush' ? (currentBrushSize || 8) : (currentBrushSize || 3));
|
const size = (currentTool === 'highlight' || currentBrushType === 'highlight') ? (currentBrushSize || 20) : (currentBrushType === 'brush' ? (currentBrushSize || 8) : (currentBrushSize || 3));
|
||||||
|
|
||||||
const newStrokeObj = {
|
const newStrokeObj = {
|
||||||
@@ -1872,6 +2026,7 @@ const TicketDetailView = {
|
|||||||
const rect = drawCanvas.getBoundingClientRect();
|
const rect = drawCanvas.getBoundingClientRect();
|
||||||
const x = e.clientX - rect.left;
|
const x = e.clientX - rect.left;
|
||||||
const y = e.clientY - rect.top;
|
const y = e.clientY - rect.top;
|
||||||
|
mousePos = { x, y };
|
||||||
|
|
||||||
if (isResizingObj && selectedObject) {
|
if (isResizingObj && selectedObject) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -1990,7 +2145,14 @@ const TicketDetailView = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const hoverObj = hitTestObject(x, y);
|
const hoverObj = hitTestObject(x, y);
|
||||||
drawCanvas.style.cursor = hoverObj ? 'move' : (currentTool === 'select' ? 'default' : 'crosshair');
|
if (hoverObj) {
|
||||||
|
drawCanvas.style.cursor = 'move';
|
||||||
|
} else if (currentTool === 'select') {
|
||||||
|
drawCanvas.style.cursor = 'default';
|
||||||
|
} else {
|
||||||
|
// Custom subtle dot cursor so the outer stroke-size circle is the main visual indicator
|
||||||
|
drawCanvas.style.cursor = 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'8\' height=\'8\' viewBox=\'0 0 8 8\'><circle cx=\'4\' cy=\'4\' r=\'1.5\' fill=\'%23ffffff\' stroke=\'%23000000\' stroke-width=\'0.8\'/></svg>") 4 4, crosshair';
|
||||||
|
}
|
||||||
|
|
||||||
// Live preview ghost for active signature under cursor
|
// Live preview ghost for active signature under cursor
|
||||||
if (currentTool === 'signature' && activeSignature && !isDrawingStroke && !isDraggingObj && !isResizingObj) {
|
if (currentTool === 'signature' && activeSignature && !isDrawingStroke && !isDraggingObj && !isResizingObj) {
|
||||||
@@ -2019,14 +2181,14 @@ const TicketDetailView = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDrawingStroke) return;
|
if (isDrawingStroke) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
currentStrokePoints.push({ x, y });
|
currentStrokePoints.push({ x, y });
|
||||||
|
|
||||||
// Update stroke bounding box
|
// Update stroke bounding box
|
||||||
if (isDrawingStroke && pageObjects.length > 0) {
|
if (pageObjects.length > 0) {
|
||||||
const activeStrokeObj = pageObjects[pageObjects.length - 1];
|
const activeStrokeObj = pageObjects[pageObjects.length - 1];
|
||||||
if (activeStrokeObj && activeStrokeObj.type === 'stroke') {
|
if (activeStrokeObj && activeStrokeObj.type === 'stroke') {
|
||||||
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
||||||
@@ -2042,6 +2204,7 @@ const TicketDetailView = {
|
|||||||
activeStrokeObj.height = maxY - minY;
|
activeStrokeObj.height = maxY - minY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
redrawPageObjects();
|
redrawPageObjects();
|
||||||
};
|
};
|
||||||
@@ -2086,6 +2249,7 @@ const TicketDetailView = {
|
|||||||
|
|
||||||
drawCanvas.onmouseup = stopDrawing;
|
drawCanvas.onmouseup = stopDrawing;
|
||||||
drawCanvas.onmouseleave = (e) => {
|
drawCanvas.onmouseleave = (e) => {
|
||||||
|
mousePos = null;
|
||||||
stopDrawing(e);
|
stopDrawing(e);
|
||||||
redrawPageObjects();
|
redrawPageObjects();
|
||||||
};
|
};
|
||||||
@@ -2174,6 +2338,7 @@ const TicketDetailView = {
|
|||||||
|
|
||||||
let currentTool = 'draw';
|
let currentTool = 'draw';
|
||||||
let currentColor = '#ef4444';
|
let currentColor = '#ef4444';
|
||||||
|
let currentHighlightOpacity = 0.15;
|
||||||
let currentBrushType = 'pencil';
|
let currentBrushType = 'pencil';
|
||||||
let currentBrushSize = 3;
|
let currentBrushSize = 3;
|
||||||
let activeSignature = null;
|
let activeSignature = null;
|
||||||
@@ -2225,6 +2390,8 @@ const TicketDetailView = {
|
|||||||
baseCtx.restore();
|
baseCtx.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mousePos = null;
|
||||||
|
|
||||||
const redrawPageObjects = () => {
|
const redrawPageObjects = () => {
|
||||||
drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height);
|
drawCtx.clearRect(0, 0, drawCanvas.width, drawCanvas.height);
|
||||||
pageObjects.forEach(obj => {
|
pageObjects.forEach(obj => {
|
||||||
@@ -2236,13 +2403,13 @@ const TicketDetailView = {
|
|||||||
obj.points.forEach(p => drawCtx.lineTo(p.x, p.y));
|
obj.points.forEach(p => drawCtx.lineTo(p.x, p.y));
|
||||||
drawCtx.strokeStyle = obj.color;
|
drawCtx.strokeStyle = obj.color;
|
||||||
drawCtx.lineWidth = obj.size;
|
drawCtx.lineWidth = obj.size;
|
||||||
if (obj.brushType === 'highlight') {
|
if (obj.brushType === 'highlight' || obj.type === 'highlight') {
|
||||||
drawCtx.globalCompositeOperation = 'multiply';
|
drawCtx.globalCompositeOperation = 'multiply';
|
||||||
drawCtx.globalAlpha = 0.55;
|
drawCtx.globalAlpha = obj.opacity !== undefined ? obj.opacity : 0.15;
|
||||||
drawCtx.lineCap = 'square';
|
drawCtx.lineCap = 'square';
|
||||||
} else {
|
} else {
|
||||||
drawCtx.globalCompositeOperation = 'source-over';
|
drawCtx.globalCompositeOperation = 'source-over';
|
||||||
drawCtx.globalAlpha = obj.opacity || 1.0;
|
drawCtx.globalAlpha = obj.opacity !== undefined ? obj.opacity : 1.0;
|
||||||
drawCtx.lineCap = 'round';
|
drawCtx.lineCap = 'round';
|
||||||
}
|
}
|
||||||
drawCtx.lineJoin = 'round';
|
drawCtx.lineJoin = 'round';
|
||||||
@@ -2365,6 +2532,27 @@ const TicketDetailView = {
|
|||||||
drawCtx.restore();
|
drawCtx.restore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Draw brush size circle indicator under mouse cursor when drawing/highlighting
|
||||||
|
if (mousePos && (currentTool === 'draw' || currentTool === 'highlight')) {
|
||||||
|
const currentSize = (currentTool === 'highlight' || currentBrushType === 'highlight') ? (currentBrushSize || 20) : (currentBrushType === 'brush' ? (currentBrushSize || 8) : (currentBrushSize || 3));
|
||||||
|
const radius = Math.max(2, currentSize / 2);
|
||||||
|
|
||||||
|
drawCtx.save();
|
||||||
|
drawCtx.globalCompositeOperation = 'source-over';
|
||||||
|
drawCtx.beginPath();
|
||||||
|
drawCtx.arc(mousePos.x, mousePos.y, radius, 0, Math.PI * 2);
|
||||||
|
drawCtx.strokeStyle = 'rgba(0, 0, 0, 0.7)';
|
||||||
|
drawCtx.lineWidth = 1.5;
|
||||||
|
drawCtx.stroke();
|
||||||
|
|
||||||
|
drawCtx.beginPath();
|
||||||
|
drawCtx.arc(mousePos.x, mousePos.y, radius, 0, Math.PI * 2);
|
||||||
|
drawCtx.strokeStyle = 'rgba(255, 255, 255, 0.9)';
|
||||||
|
drawCtx.lineWidth = 1;
|
||||||
|
drawCtx.stroke();
|
||||||
|
drawCtx.restore();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getObjectHandles = (obj) => {
|
const getObjectHandles = (obj) => {
|
||||||
@@ -2461,10 +2649,20 @@ const TicketDetailView = {
|
|||||||
const toolShapeBtn = document.getElementById('img-tool-shape');
|
const toolShapeBtn = document.getElementById('img-tool-shape');
|
||||||
const toolSigBtn = document.getElementById('img-tool-signature');
|
const toolSigBtn = document.getElementById('img-tool-signature');
|
||||||
const shapeControls = document.getElementById('img-shape-controls');
|
const shapeControls = document.getElementById('img-shape-controls');
|
||||||
|
const highlightControls = document.getElementById('img-highlight-controls');
|
||||||
|
const highlightOpacityInput = document.getElementById('img-highlight-opacity');
|
||||||
|
const highlightOpacityVal = document.getElementById('img-highlight-opacity-val');
|
||||||
const colorPicker = document.getElementById('img-color-picker');
|
const colorPicker = document.getElementById('img-color-picker');
|
||||||
const toolClearBtn = document.getElementById('img-tool-clear');
|
const toolClearBtn = document.getElementById('img-tool-clear');
|
||||||
const saveEditedBtn = document.getElementById('img-save-edited');
|
const saveEditedBtn = document.getElementById('img-save-edited');
|
||||||
|
|
||||||
|
if (highlightOpacityInput && highlightOpacityVal) {
|
||||||
|
highlightOpacityInput.oninput = (e) => {
|
||||||
|
currentHighlightOpacity = parseFloat(e.target.value);
|
||||||
|
highlightOpacityVal.textContent = `${Math.round(currentHighlightOpacity * 100)}%`;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const setActiveTool = (tool, activeBtn) => {
|
const setActiveTool = (tool, activeBtn) => {
|
||||||
currentTool = tool;
|
currentTool = tool;
|
||||||
const allToolBtns = [toolSelectBtn, toolDrawBtn, toolTextBtn, toolHighlightBtn, toolShapeBtn, toolSigBtn];
|
const allToolBtns = [toolSelectBtn, toolDrawBtn, toolTextBtn, toolHighlightBtn, toolShapeBtn, toolSigBtn];
|
||||||
@@ -2481,6 +2679,9 @@ const TicketDetailView = {
|
|||||||
if (shapeControls) {
|
if (shapeControls) {
|
||||||
shapeControls.style.display = tool === 'shape' ? 'flex' : 'none';
|
shapeControls.style.display = tool === 'shape' ? 'flex' : 'none';
|
||||||
}
|
}
|
||||||
|
if (highlightControls) {
|
||||||
|
highlightControls.style.display = (tool === 'draw' || tool === 'highlight') ? 'flex' : 'none';
|
||||||
|
}
|
||||||
drawCanvas.style.pointerEvents = tool === 'select' ? 'none' : 'auto';
|
drawCanvas.style.pointerEvents = tool === 'select' ? 'none' : 'auto';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2522,6 +2723,10 @@ const TicketDetailView = {
|
|||||||
// Mouse Events on drawCanvas
|
// Mouse Events on drawCanvas
|
||||||
drawCanvas.onmousedown = (e) => {
|
drawCanvas.onmousedown = (e) => {
|
||||||
if (e.button !== 0) return;
|
if (e.button !== 0) return;
|
||||||
|
|
||||||
|
const existingPopup = document.getElementById('img-context-menu-popup') || document.getElementById('pdf-context-menu-popup');
|
||||||
|
if (existingPopup) existingPopup.remove();
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const rect = drawCanvas.getBoundingClientRect();
|
const rect = drawCanvas.getBoundingClientRect();
|
||||||
@@ -2625,7 +2830,9 @@ const TicketDetailView = {
|
|||||||
|
|
||||||
isDrawingStroke = true;
|
isDrawingStroke = true;
|
||||||
currentStrokePoints = [{ x, y }];
|
currentStrokePoints = [{ x, y }];
|
||||||
const opacity = (currentTool === 'highlight' || currentBrushType === 'highlight') ? 0.15 : 1.0;
|
const opacity = (currentTool === 'highlight' || currentBrushType === 'highlight')
|
||||||
|
? currentHighlightOpacity
|
||||||
|
: (currentTool === 'draw' ? currentHighlightOpacity : 1.0);
|
||||||
const size = (currentTool === 'highlight' || currentBrushType === 'highlight') ? (currentBrushSize || 20) : (currentBrushType === 'brush' ? (currentBrushSize || 8) : (currentBrushSize || 3));
|
const size = (currentTool === 'highlight' || currentBrushType === 'highlight') ? (currentBrushSize || 20) : (currentBrushType === 'brush' ? (currentBrushSize || 8) : (currentBrushSize || 3));
|
||||||
|
|
||||||
const newStrokeObj = {
|
const newStrokeObj = {
|
||||||
@@ -2643,6 +2850,7 @@ const TicketDetailView = {
|
|||||||
const rect = drawCanvas.getBoundingClientRect();
|
const rect = drawCanvas.getBoundingClientRect();
|
||||||
const x = e.clientX - rect.left;
|
const x = e.clientX - rect.left;
|
||||||
const y = e.clientY - rect.top;
|
const y = e.clientY - rect.top;
|
||||||
|
mousePos = { x, y };
|
||||||
|
|
||||||
if (isResizingObj && selectedObject) {
|
if (isResizingObj && selectedObject) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -2681,9 +2889,29 @@ const TicketDetailView = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isDrawingStroke) return;
|
if (isDrawingStroke) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
currentStrokePoints.push({ x, y });
|
currentStrokePoints.push({ x, y });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedObject) {
|
||||||
|
const hIdx = hitTestHandle(selectedObject, x, y);
|
||||||
|
if (hIdx !== -1) {
|
||||||
|
drawCanvas.style.cursor = 'nwse-resize';
|
||||||
|
redrawPageObjects();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hoverObj = hitTestObject(x, y);
|
||||||
|
if (hoverObj) {
|
||||||
|
drawCanvas.style.cursor = 'move';
|
||||||
|
} else if (currentTool === 'select') {
|
||||||
|
drawCanvas.style.cursor = 'default';
|
||||||
|
} else {
|
||||||
|
// Custom subtle dot cursor so the outer stroke-size circle is the main visual indicator
|
||||||
|
drawCanvas.style.cursor = 'url("data:image/svg+xml;utf8,<svg xmlns=\'http://www.w3.org/2000/svg\' width=\'8\' height=\'8\' viewBox=\'0 0 8 8\'><circle cx=\'4\' cy=\'4\' r=\'1.5\' fill=\'%23ffffff\' stroke=\'%23000000\' stroke-width=\'0.8\'/></svg>") 4 4, crosshair';
|
||||||
|
}
|
||||||
|
|
||||||
redrawPageObjects();
|
redrawPageObjects();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2713,7 +2941,300 @@ const TicketDetailView = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
drawCanvas.onmouseup = stopDrawing;
|
drawCanvas.onmouseup = stopDrawing;
|
||||||
drawCanvas.onmouseleave = stopDrawing;
|
drawCanvas.onmouseleave = () => {
|
||||||
|
mousePos = null;
|
||||||
|
stopDrawing();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Ctrl + Right Click Context Menu Handler for Image Editor
|
||||||
|
drawCanvas.oncontextmenu = async (e) => {
|
||||||
|
if (e.ctrlKey || e.metaKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const rect = drawCanvas.getBoundingClientRect();
|
||||||
|
const clickX = e.clientX - rect.left;
|
||||||
|
const clickY = e.clientY - rect.top;
|
||||||
|
|
||||||
|
// Fetch signatures and shapes from SQLite
|
||||||
|
let signatures = [];
|
||||||
|
let favoriteShapes = [];
|
||||||
|
try {
|
||||||
|
const [sigRes, shapeRes] = await Promise.all([
|
||||||
|
App.api('/api/config/user-signatures'),
|
||||||
|
App.api('/api/config/user-shapes')
|
||||||
|
]);
|
||||||
|
signatures = sigRes || [];
|
||||||
|
favoriteShapes = (shapeRes || []).filter(s => s.is_favorite);
|
||||||
|
} catch (_) { }
|
||||||
|
|
||||||
|
let menuOverlay = document.getElementById('img-context-menu-popup');
|
||||||
|
if (menuOverlay) menuOverlay.remove();
|
||||||
|
|
||||||
|
menuOverlay = document.createElement('div');
|
||||||
|
menuOverlay.id = 'img-context-menu-popup';
|
||||||
|
menuOverlay.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
left: ${e.clientX}px;
|
||||||
|
top: ${e.clientY}px;
|
||||||
|
z-index: 20000;
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.8);
|
||||||
|
color: #ffffff;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
min-width: 260px;
|
||||||
|
user-select: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
menuOverlay.innerHTML = `
|
||||||
|
<div style="font-weight: 700; margin-bottom: 8px; color: var(--accent-primary); border-bottom: 1px solid rgba(255,255,255,0.1); padding-bottom: 4px; display: flex; align-items: center; justify-content: space-between;">
|
||||||
|
<span>⚙️ Strumenti Rapidi Immagine</span>
|
||||||
|
<span style="cursor:pointer; opacity:0.7;" id="ctx-close">✕</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<label style="font-size:0.75rem; color:#94a3b8; display:block; margin-bottom:4px;">Tipo di Strumento</label>
|
||||||
|
<div style="display:flex; gap:4px; flex-wrap:wrap;">
|
||||||
|
<button class="ctx-btn ${currentTool === 'draw' && currentBrushType === 'pencil' ? 'active' : ''}" data-type="pencil" style="flex:1; min-width:60px; padding:4px; font-size:0.75rem; background:${currentTool === 'draw' && currentBrushType === 'pencil' ? 'var(--accent-primary)' : 'rgba(255,255,255,0.1)'}; color:#fff; border:none; border-radius:4px; cursor:pointer;">✏️ Matita</button>
|
||||||
|
<button class="ctx-btn ${currentTool === 'draw' && currentBrushType === 'brush' ? 'active' : ''}" data-type="brush" style="flex:1; min-width:60px; padding:4px; font-size:0.75rem; background:${currentTool === 'draw' && currentBrushType === 'brush' ? 'var(--accent-primary)' : 'rgba(255,255,255,0.1)'}; color:#fff; border:none; border-radius:4px; cursor:pointer;">🖌️ Pennello</button>
|
||||||
|
<button class="ctx-btn ${currentTool === 'highlight' ? 'active' : ''}" data-type="highlight" style="flex:1; min-width:65px; padding:4px; font-size:0.75rem; background:${currentTool === 'highlight' ? 'var(--accent-primary)' : 'rgba(255,255,255,0.1)'}; color:#fff; border:none; border-radius:4px; cursor:pointer;">🖍️ Evidenzia</button>
|
||||||
|
<button class="ctx-btn ${currentTool === 'shape' ? 'active' : ''}" data-type="shape" style="flex:1; min-width:60px; padding:4px; font-size:0.75rem; background:${currentTool === 'shape' ? 'var(--accent-primary)' : 'rgba(255,255,255,0.1)'}; color:#fff; border:none; border-radius:4px; cursor:pointer;">📐 Forme</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 10px; display:flex; gap:8px; align-items:center;">
|
||||||
|
<div style="flex:1;">
|
||||||
|
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:2px;">
|
||||||
|
<label style="font-size:0.75rem; color:#94a3b8;">Dimensione tratto</label>
|
||||||
|
<span id="ctx-brush-val-badge" style="font-size:0.72rem; font-family:monospace; color:#38bdf8; font-weight:700;">${currentBrushSize}px</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; gap:6px;">
|
||||||
|
<input type="range" id="ctx-brush-size" min="1" max="40" value="${currentBrushSize}" style="flex:1; cursor:pointer;" />
|
||||||
|
<div id="ctx-brush-preview-box" title="Anteprima dimensione reale" style="width:26px; height:26px; border-radius:4px; background:rgba(255,255,255,0.06); border:1px solid rgba(255,255,255,0.2); display:flex; align-items:center; justify-content:center; flex-shrink:0;">
|
||||||
|
<div id="ctx-brush-preview-circle" style="width:${Math.max(2, currentBrushSize)}px; height:${Math.max(2, currentBrushSize)}px; max-width:22px; max-height:22px; border-radius:50%; background:${currentColor}; transition:all 0.1s ease;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label style="font-size:0.75rem; color:#94a3b8; display:block; margin-bottom:2px;">Colore</label>
|
||||||
|
<input type="color" id="ctx-color" value="${currentColor}" style="width:26px; height:26px; border:none; border-radius:50%; cursor:pointer; background:none;" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Favorite Shapes Section -->
|
||||||
|
<div style="border-top: 1px solid rgba(255,255,255,0.1); padding-top: 8px; margin-bottom: 8px;">
|
||||||
|
<label style="font-size:0.75rem; color:#94a3b8; display:block; margin-bottom:6px;">📐 Forme Preferite Salvate</label>
|
||||||
|
${favoriteShapes.length > 0 ? `
|
||||||
|
<div style="max-height: 100px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px;">
|
||||||
|
${favoriteShapes.map(sh => `
|
||||||
|
<button class="ctx-shape-btn" data-id="${sh.id}" style="display:flex; align-items:center; gap:8px; width:100%; background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.15); color:#fff; padding:4px 8px; border-radius:4px; cursor:pointer; text-align:left; font-size:0.76rem;">
|
||||||
|
<span style="display:inline-block; width:16px; height:12px; border-radius:${sh.shape_type === 'circle' ? '50%' : '2px'}; background:${sh.fill_color === 'transparent' ? 'transparent' : sh.fill_color}; border:${Math.min(sh.border_width, 3)}px solid ${sh.border_color}; box-sizing:border-box;"></span>
|
||||||
|
<span style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${App.escapeHtml(sh.name)}</span>
|
||||||
|
</button>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
` : `
|
||||||
|
<div style="font-size:0.74rem; color:#94a3b8; font-style:italic;">Nessuna forma preferita salvata.</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="border-top: 1px solid rgba(255,255,255,0.1); padding-top: 8px;">
|
||||||
|
<label style="font-size:0.75rem; color:#94a3b8; display:block; margin-bottom:6px;">✍️ Inserisci Firma / Immagine Utente</label>
|
||||||
|
${signatures.length > 0 ? `
|
||||||
|
<div style="max-height: 120px; overflow-y: auto; display: flex; flex-direction: column; gap: 4px;">
|
||||||
|
${signatures.map(s => `
|
||||||
|
<button class="ctx-sig-btn" data-id="${s.id}" style="display:flex; align-items:center; gap:8px; width:100%; background:rgba(255,255,255,0.08); border:1px solid rgba(255,255,255,0.15); color:#fff; padding:4px 8px; border-radius:4px; cursor:pointer; text-align:left; font-size:0.76rem;">
|
||||||
|
<img src="${s.image_data}" style="width:28px; height:20px; object-fit:contain; background:#fff; border-radius:2px; padding:1px;" />
|
||||||
|
<span style="overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">${App.escapeHtml(s.name)}</span>
|
||||||
|
</button>
|
||||||
|
`).join('')}
|
||||||
|
</div>
|
||||||
|
` : `
|
||||||
|
<div style="font-size:0.74rem; color:#94a3b8; font-style:italic;">Nessuna firma salvata in SQLite.</div>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(menuOverlay);
|
||||||
|
|
||||||
|
// Clamp positioning to prevent overflowing window boundaries
|
||||||
|
const menuRect = menuOverlay.getBoundingClientRect();
|
||||||
|
const winW = window.innerWidth;
|
||||||
|
const winH = window.innerHeight;
|
||||||
|
|
||||||
|
let finalLeft = e.clientX;
|
||||||
|
let finalTop = e.clientY;
|
||||||
|
|
||||||
|
if (finalLeft + menuRect.width > winW - 12) {
|
||||||
|
finalLeft = winW - menuRect.width - 12;
|
||||||
|
}
|
||||||
|
if (finalLeft < 12) {
|
||||||
|
finalLeft = 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (finalTop + menuRect.height > winH - 12) {
|
||||||
|
finalTop = winH - menuRect.height - 12;
|
||||||
|
}
|
||||||
|
if (finalTop < 12) {
|
||||||
|
finalTop = 12;
|
||||||
|
}
|
||||||
|
|
||||||
|
menuOverlay.style.left = `${finalLeft}px`;
|
||||||
|
menuOverlay.style.top = `${finalTop}px`;
|
||||||
|
|
||||||
|
// Menu Event Listeners
|
||||||
|
const closeCtx = () => { if (menuOverlay) menuOverlay.remove(); };
|
||||||
|
menuOverlay.querySelector('#ctx-close').onclick = closeCtx;
|
||||||
|
|
||||||
|
menuOverlay.querySelectorAll('.ctx-btn').forEach(btn => {
|
||||||
|
btn.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const type = btn.dataset.type;
|
||||||
|
menuOverlay.querySelectorAll('.ctx-btn').forEach(b => {
|
||||||
|
b.style.background = 'rgba(255,255,255,0.1)';
|
||||||
|
b.classList.remove('active');
|
||||||
|
});
|
||||||
|
btn.style.background = 'var(--accent-primary)';
|
||||||
|
btn.classList.add('active');
|
||||||
|
|
||||||
|
if (type === 'pencil') {
|
||||||
|
currentBrushType = 'pencil';
|
||||||
|
currentBrushSize = 3;
|
||||||
|
setActiveTool('draw', toolDrawBtn);
|
||||||
|
} else if (type === 'brush') {
|
||||||
|
currentBrushType = 'brush';
|
||||||
|
currentBrushSize = 8;
|
||||||
|
setActiveTool('draw', toolDrawBtn);
|
||||||
|
} else if (type === 'highlight') {
|
||||||
|
currentBrushType = 'highlight';
|
||||||
|
currentBrushSize = 20;
|
||||||
|
setActiveTool('highlight', toolHighlightBtn);
|
||||||
|
} else if (type === 'shape') {
|
||||||
|
setActiveTool('shape', toolShapeBtn);
|
||||||
|
}
|
||||||
|
const valBadge = menuOverlay.querySelector('#ctx-brush-val-badge');
|
||||||
|
if (valBadge) valBadge.textContent = `${currentBrushSize}px`;
|
||||||
|
const pCircle = menuOverlay.querySelector('#ctx-brush-preview-circle');
|
||||||
|
if (pCircle) {
|
||||||
|
pCircle.style.width = `${Math.max(2, currentBrushSize)}px`;
|
||||||
|
pCircle.style.height = `${Math.max(2, currentBrushSize)}px`;
|
||||||
|
}
|
||||||
|
if (sizeInput) sizeInput.value = currentBrushSize;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const sizeInput = menuOverlay.querySelector('#ctx-brush-size');
|
||||||
|
if (sizeInput) {
|
||||||
|
sizeInput.oninput = (eVal) => {
|
||||||
|
eVal.stopPropagation();
|
||||||
|
const val = parseInt(eVal.target.value, 10);
|
||||||
|
currentBrushSize = val;
|
||||||
|
|
||||||
|
const valBadge = menuOverlay.querySelector('#ctx-brush-val-badge');
|
||||||
|
if (valBadge) valBadge.textContent = `${val}px`;
|
||||||
|
|
||||||
|
const pCircle = menuOverlay.querySelector('#ctx-brush-preview-circle');
|
||||||
|
if (pCircle) {
|
||||||
|
pCircle.style.width = `${Math.max(2, val)}px`;
|
||||||
|
pCircle.style.height = `${Math.max(2, val)}px`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const shapeBorderWidthEl = document.getElementById('img-shape-border-width');
|
||||||
|
if (shapeBorderWidthEl) {
|
||||||
|
shapeBorderWidthEl.value = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedObject && selectedObject.type === 'shape') {
|
||||||
|
selectedObject.borderWidth = val;
|
||||||
|
redrawPageObjects();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const ctxColorPicker = menuOverlay.querySelector('#ctx-color');
|
||||||
|
if (ctxColorPicker) {
|
||||||
|
ctxColorPicker.onchange = (eVal) => {
|
||||||
|
eVal.stopPropagation();
|
||||||
|
currentColor = eVal.target.value;
|
||||||
|
const pCircle = menuOverlay.querySelector('#ctx-brush-preview-circle');
|
||||||
|
if (pCircle) pCircle.style.background = currentColor;
|
||||||
|
if (colorPicker) colorPicker.value = currentColor;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
menuOverlay.querySelectorAll('.ctx-shape-btn').forEach(btn => {
|
||||||
|
btn.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const shId = btn.dataset.id;
|
||||||
|
const sh = favoriteShapes.find(s => s.id == shId);
|
||||||
|
if (sh) {
|
||||||
|
const defaultW = 130;
|
||||||
|
const defaultH = 85;
|
||||||
|
const newObj = {
|
||||||
|
id: Date.now(),
|
||||||
|
type: 'shape',
|
||||||
|
shapeType: sh.shape_type,
|
||||||
|
x: clickX - defaultW / 2,
|
||||||
|
y: clickY - defaultH / 2,
|
||||||
|
width: defaultW,
|
||||||
|
height: defaultH,
|
||||||
|
fillColor: sh.fill_color,
|
||||||
|
borderColor: sh.border_color,
|
||||||
|
borderWidth: sh.border_width
|
||||||
|
};
|
||||||
|
pageObjects.push(newObj);
|
||||||
|
selectedObject = newObj;
|
||||||
|
redrawPageObjects();
|
||||||
|
Toast.success(`Forma preferita "${sh.name}" inserita!`);
|
||||||
|
closeCtx();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
menuOverlay.querySelectorAll('.ctx-sig-btn').forEach(btn => {
|
||||||
|
btn.onclick = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const sId = btn.dataset.id;
|
||||||
|
const s = signatures.find(sig => sig.id == sId);
|
||||||
|
if (s) {
|
||||||
|
const img = new Image();
|
||||||
|
img.onload = () => {
|
||||||
|
const targetWidth = Math.min(220, img.width || 180);
|
||||||
|
const targetHeight = (img.height / img.width) * targetWidth || 80;
|
||||||
|
const newObj = {
|
||||||
|
id: Date.now(),
|
||||||
|
type: 'image',
|
||||||
|
img: img,
|
||||||
|
x: clickX - targetWidth / 2,
|
||||||
|
y: clickY - targetHeight / 2,
|
||||||
|
width: targetWidth,
|
||||||
|
height: targetHeight
|
||||||
|
};
|
||||||
|
pageObjects.push(newObj);
|
||||||
|
selectedObject = newObj;
|
||||||
|
redrawPageObjects();
|
||||||
|
Toast.success(`Firma "${s.name}" apposta!`);
|
||||||
|
closeCtx();
|
||||||
|
};
|
||||||
|
img.src = s.image_data;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleOutsideClick = (eClick) => {
|
||||||
|
if (menuOverlay && !menuOverlay.contains(eClick.target)) {
|
||||||
|
closeCtx();
|
||||||
|
document.removeEventListener('mousedown', handleOutsideClick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
setTimeout(() => {
|
||||||
|
document.addEventListener('mousedown', handleOutsideClick);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getOutputFilename(originalName) {
|
getOutputFilename(originalName) {
|
||||||
@@ -2906,6 +3427,11 @@ const TicketDetailView = {
|
|||||||
const hasEmailDraft = App.getDraft(id, 'email');
|
const hasEmailDraft = App.getDraft(id, 'email');
|
||||||
const emailBtnText = hasEmailDraft ? 'Continua mail' : 'Invia Email';
|
const emailBtnText = hasEmailDraft ? 'Continua mail' : 'Invia Email';
|
||||||
|
|
||||||
|
const ticketRealAttachments = (attachments || []).filter(att => {
|
||||||
|
const fname = (att.filename || '').toLowerCase();
|
||||||
|
return !fname.startsWith('file-1') && !fname.startsWith('file-2') && !fname.startsWith('file-3');
|
||||||
|
});
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<a class="back-link" onclick="window.location.hash = App.lastListView || '#/tickets'">
|
<a class="back-link" onclick="window.location.hash = App.lastListView || '#/tickets'">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
|
||||||
@@ -2924,9 +3450,18 @@ const TicketDetailView = {
|
|||||||
-
|
-
|
||||||
</div>
|
</div>
|
||||||
<h2 class="ticket-detail-title" style="display:inline-flex; align-items:center; gap:6px; flex-wrap:wrap;">
|
<h2 class="ticket-detail-title" style="display:inline-flex; align-items:center; gap:6px; flex-wrap:wrap;">
|
||||||
<span class="copy-ticket-btn" data-tn="${App.escapeHtml(ticket.title || '')}" style="cursor: pointer; font-size: 0.85rem;" title="Copia titolo ticket">📋</span>
|
<span class="copy-full-ticket-title" data-full="${ticket.tn} - ${App.escapeHtml(ticket.title || '(senza titolo)')}" style="cursor: pointer;" title="Clicca per copiare: ${ticket.tn} - ${App.escapeHtml(ticket.title || '')}">
|
||||||
${App.escapeHtml(ticket.title || '(senza titolo)')}
|
${App.escapeHtml(ticket.title || '(senza titolo)')}
|
||||||
|
</span>
|
||||||
<button class="open-tab-btn" data-id="${ticket.id}" data-tn="${ticket.tn}" data-title="${App.escapeHtml(ticket.title || '(senza titolo)')}" onclick="App.openTab(${ticket.id}, '${ticket.tn}', this.dataset.title); event.stopPropagation();" title="Apri in scheda" style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:var(--accent-primary); color:#fff; border:none; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; margin-left:6px; cursor:pointer; line-height:16px;">+</button>
|
<button class="open-tab-btn" data-id="${ticket.id}" data-tn="${ticket.tn}" data-title="${App.escapeHtml(ticket.title || '(senza titolo)')}" onclick="App.openTab(${ticket.id}, '${ticket.tn}', this.dataset.title); event.stopPropagation();" title="Apri in scheda" style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:var(--accent-primary); color:#fff; border:none; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; margin-left:6px; cursor:pointer; line-height:16px;">+</button>
|
||||||
|
<button class="assign-open-tab-btn" data-id="${ticket.id}" data-tn="${ticket.tn}" data-title="${App.escapeHtml(ticket.title || '(senza titolo)')}" onclick="App.assignAndOpenTab(${ticket.id}, '${ticket.tn}', this.dataset.title); event.stopPropagation();" title="Assegna a me ed apri in scheda" style="display:inline-flex; align-items:center; justify-content:center; height:20px; padding:0 8px; background:var(--accent-primary); color:#fff; border:none; border-radius:var(--radius-full); font-size:11px; font-weight:600; font-family:inherit; margin-left:6px; cursor:pointer; gap:3px;">
|
||||||
|
👤+ Assegna & Scheda
|
||||||
|
</button>
|
||||||
|
${ticketRealAttachments.length > 0 ? `
|
||||||
|
<button class="header-open-attachments-btn" style="display:inline-flex; align-items:center; justify-content:center; height:20px; padding:0 8px; background:var(--bg-tertiary); color:var(--accent-primary); border:1px solid var(--border-subtle); border-radius:var(--radius-full); font-size:11px; font-weight:600; font-family:inherit; margin-left:6px; cursor:pointer; gap:4px;" title="Visualizza allegati ticket (${ticketRealAttachments.length})">
|
||||||
|
📎 ${ticketRealAttachments.length}
|
||||||
|
</button>
|
||||||
|
` : ''}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="ticket-meta-badges">
|
<div class="ticket-meta-badges">
|
||||||
<span class="badge badge-state" data-state-type="${(ticket.state_type || '').toLowerCase()}">${ticket.state_name}</span>
|
<span class="badge badge-state" data-state-type="${(ticket.state_type || '').toLowerCase()}">${ticket.state_name}</span>
|
||||||
@@ -3212,7 +3747,7 @@ const TicketDetailView = {
|
|||||||
ℹ️
|
ℹ️
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="article-info-popover" style="position: absolute; top: calc(100% + 4px); right: 0; left: auto; width: 340px; background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 12px; font-size: 0.78rem; box-shadow: var(--shadow-lg); z-index: 120; display: none; text-align: left;">
|
<div class="article-info-popover" style="position: absolute; top: calc(100% + 4px); right: 0; left: auto; width: 340px; max-height: 360px; overflow-y: auto; overscroll-behavior: contain; background: var(--bg-card); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 12px; font-size: 0.78rem; box-shadow: var(--shadow-lg); z-index: 120; display: none; text-align: left; user-select: text; -webkit-user-select: text;">
|
||||||
<div style="font-weight: 700; color: var(--text-secondary); margin-bottom: 8px; border-bottom: 1px solid var(--border-subtle); padding-bottom: 4px; display: flex; align-items: center; justify-content: space-between; font-size: 0.8rem;">
|
<div style="font-weight: 700; color: var(--text-secondary); margin-bottom: 8px; border-bottom: 1px solid var(--border-subtle); padding-bottom: 4px; display: flex; align-items: center; justify-content: space-between; font-size: 0.8rem;">
|
||||||
<span style="display: flex; align-items: center; gap: 4px;">
|
<span style="display: flex; align-items: center; gap: 4px;">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;color:var(--accent-primary);"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;color:var(--accent-primary);"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
|
||||||
@@ -3259,7 +3794,7 @@ const TicketDetailView = {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
${extractedLinks.length > 0 ? `
|
${extractedLinks.length > 0 ? `
|
||||||
<div class="article-links-minilist" style="display: flex; flex-direction: column; gap: 6px; max-height: 180px; overflow-y: auto; padding-right: 2px;">
|
<div class="article-links-minilist" style="display: flex; flex-direction: column; gap: 6px; max-height: 180px; overflow-y: auto; overscroll-behavior: contain; padding-right: 4px;">
|
||||||
${extractedLinks.map(link => `
|
${extractedLinks.map(link => `
|
||||||
<a href="${App.escapeHtml(link.url)}" target="_blank" rel="noopener noreferrer" title="${App.escapeHtml(link.url)}" style="color: var(--accent-primary); text-decoration: none; word-break: break-all; font-size: 0.75rem; display: flex; align-items: flex-start; gap: 4px; line-height: 1.3; background: var(--bg-tertiary); padding: 4px 6px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);">
|
<a href="${App.escapeHtml(link.url)}" target="_blank" rel="noopener noreferrer" title="${App.escapeHtml(link.url)}" style="color: var(--accent-primary); text-decoration: none; word-break: break-all; font-size: 0.75rem; display: flex; align-items: flex-start; gap: 4px; line-height: 1.3; background: var(--bg-tertiary); padding: 4px 6px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);">
|
||||||
<span style="flex-shrink:0; font-size: 0.7rem;">🌐</span>
|
<span style="flex-shrink:0; font-size: 0.7rem;">🌐</span>
|
||||||
@@ -3573,7 +4108,12 @@ const TicketDetailView = {
|
|||||||
queueSearchInput.addEventListener('input', () => {
|
queueSearchInput.addEventListener('input', () => {
|
||||||
clearTimeout(queueDebounce);
|
clearTimeout(queueDebounce);
|
||||||
const q = queueSearchInput.value.trim();
|
const q = queueSearchInput.value.trim();
|
||||||
|
const origQueueName = (this.originalValues['queue_name'] || '').trim();
|
||||||
|
if (q !== origQueueName) {
|
||||||
if (queueIdInput) queueIdInput.value = '';
|
if (queueIdInput) queueIdInput.value = '';
|
||||||
|
} else {
|
||||||
|
if (queueIdInput) queueIdInput.value = String(this.originalValues['queue_id'] || '');
|
||||||
|
}
|
||||||
checkChanges();
|
checkChanges();
|
||||||
|
|
||||||
queueDebounce = setTimeout(async () => {
|
queueDebounce = setTimeout(async () => {
|
||||||
@@ -3695,9 +4235,14 @@ const TicketDetailView = {
|
|||||||
saveBtn.addEventListener('click', async () => {
|
saveBtn.addEventListener('click', async () => {
|
||||||
const queueSearch = queueSearchInput ? queueSearchInput.value.trim() : '';
|
const queueSearch = queueSearchInput ? queueSearchInput.value.trim() : '';
|
||||||
if (queueSearchInput && !queueIdInput.value) {
|
if (queueSearchInput && !queueIdInput.value) {
|
||||||
|
const origName = (this.originalValues['queue_name'] || '').trim();
|
||||||
|
if (queueSearch === origName && this.originalValues['queue_id']) {
|
||||||
|
queueIdInput.value = String(this.originalValues['queue_id']);
|
||||||
|
} else {
|
||||||
Toast.warning('Seleziona una coda valida dall\'elenco.');
|
Toast.warning('Seleziona una coda valida dall\'elenco.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const customerSearch = customerSearchInput ? customerSearchInput.value.trim() : '';
|
const customerSearch = customerSearchInput ? customerSearchInput.value.trim() : '';
|
||||||
if (customerSearch && customerUserIdInput && !customerUserIdInput.value) {
|
if (customerSearch && customerUserIdInput && !customerUserIdInput.value) {
|
||||||
@@ -3751,14 +4296,16 @@ const TicketDetailView = {
|
|||||||
|
|
||||||
// 2. Perform article note creation if note fields are populated
|
// 2. Perform article note creation if note fields are populated
|
||||||
if (hasNoteContent) {
|
if (hasNoteContent) {
|
||||||
if (!noteBody && noteSubject) {
|
if (!noteSubject && !noteBody) {
|
||||||
|
noteSubject = 'consuntivazione';
|
||||||
|
noteBody = '<p>consuntivazione</p>';
|
||||||
|
} else if (!noteBody && noteSubject) {
|
||||||
noteBody = `<p>${App.escapeHtml(noteSubject)}</p>`;
|
noteBody = `<p>${App.escapeHtml(noteSubject)}</p>`;
|
||||||
}
|
} else if (noteBody && !noteSubject) {
|
||||||
if (noteBody && !noteSubject) {
|
|
||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
tempDiv.innerHTML = noteBody;
|
tempDiv.innerHTML = noteBody;
|
||||||
const plainText = tempDiv.textContent || tempDiv.innerText || '';
|
const plainText = tempDiv.textContent || tempDiv.innerText || '';
|
||||||
noteSubject = plainText.trim().substring(0, 50) || 'Nota interna';
|
noteSubject = plainText.trim().substring(0, 50) || 'consuntivazione';
|
||||||
}
|
}
|
||||||
|
|
||||||
const resNote = await App.api(`/api/tickets/${this.ticketId}/articles`, {
|
const resNote = await App.api(`/api/tickets/${this.ticketId}/articles`, {
|
||||||
@@ -4123,11 +4670,50 @@ const TicketDetailView = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Article Info Popover (ℹ️) listeners & Copy email address buttons
|
||||||
|
document.querySelectorAll('.article-info-popover-wrapper').forEach(wrapper => {
|
||||||
|
const btnInfo = wrapper.querySelector('.btn-article-info');
|
||||||
|
const popover = wrapper.querySelector('.article-info-popover');
|
||||||
|
const card = wrapper.closest('.article-card');
|
||||||
|
const iframe = card ? card.querySelector('iframe') : null;
|
||||||
|
|
||||||
|
if (btnInfo && popover) {
|
||||||
|
btnInfo.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const isOpen = popover.style.display === 'block';
|
||||||
|
|
||||||
|
// Close all popovers and restore pointer-events on all iframes
|
||||||
|
document.querySelectorAll('.article-info-popover, .time-editor-popover').forEach(p => p.style.display = 'none');
|
||||||
|
document.querySelectorAll('.article-card iframe').forEach(f => f.style.pointerEvents = 'auto');
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
popover.style.display = 'block';
|
||||||
|
if (iframe) iframe.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
popover.addEventListener('click', (e) => e.stopPropagation());
|
||||||
|
popover.addEventListener('wheel', (e) => e.stopPropagation(), { passive: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.btn-copy-email-addr').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const text = btn.dataset.copyText;
|
||||||
|
if (text) {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
Toast.success('Indirizzo copiato negli appunti!');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Close popovers clicking outside
|
// Close popovers clicking outside
|
||||||
document.addEventListener('click', () => {
|
document.addEventListener('click', () => {
|
||||||
document.querySelectorAll('.time-editor-popover').forEach(p => {
|
document.querySelectorAll('.time-editor-popover, .article-info-popover').forEach(p => {
|
||||||
p.style.display = 'none';
|
p.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
document.querySelectorAll('.article-card iframe').forEach(f => f.style.pointerEvents = 'auto');
|
||||||
});
|
});
|
||||||
// Delete Article Event Listeners
|
// Delete Article Event Listeners
|
||||||
document.querySelectorAll('.btn-delete-article').forEach(btn => {
|
document.querySelectorAll('.btn-delete-article').forEach(btn => {
|
||||||
@@ -4259,6 +4845,30 @@ const TicketDetailView = {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.copy-full-ticket-title').forEach(el => {
|
||||||
|
el.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const fullText = el.dataset.full;
|
||||||
|
if (fullText) {
|
||||||
|
navigator.clipboard.writeText(fullText).then(() => {
|
||||||
|
Toast.success('TN e Oggetto copiati negli appunti!');
|
||||||
|
}).catch(err => {
|
||||||
|
Toast.error('Errore durante la copia: ' + err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const headerAttBtn = document.querySelector('.header-open-attachments-btn');
|
||||||
|
if (headerAttBtn) {
|
||||||
|
headerAttBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const firstBadge = document.querySelector('.attachment-badge');
|
||||||
|
const firstSrc = firstBadge ? firstBadge.href : '';
|
||||||
|
this.openImageLightbox(firstSrc);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Keydown listener for Ticket Detail shortcut (Save Quick Edit / Send Note)
|
// Keydown listener for Ticket Detail shortcut (Save Quick Edit / Send Note)
|
||||||
const handleTicketDetailShortcut = (e) => {
|
const handleTicketDetailShortcut = (e) => {
|
||||||
if (!document.getElementById('qe-save')) {
|
if (!document.getElementById('qe-save')) {
|
||||||
@@ -4275,5 +4885,13 @@ const TicketDetailView = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener('keydown', handleTicketDetailShortcut, true);
|
document.addEventListener('keydown', handleTicketDetailShortcut, true);
|
||||||
|
|
||||||
|
// Set focus on State dropdown (#qe-state) on page load so Tab navigation starts from State
|
||||||
|
setTimeout(() => {
|
||||||
|
const qeStateEl = document.getElementById('qe-state');
|
||||||
|
if (qeStateEl) {
|
||||||
|
qeStateEl.focus();
|
||||||
|
}
|
||||||
|
}, 50);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -371,7 +371,7 @@ const TicketListView = {
|
|||||||
const limitSelectHtml = `
|
const limitSelectHtml = `
|
||||||
<div style="display:inline-flex; align-items:center; gap:var(--space-xs); font-size:0.8rem; color:var(--text-secondary); margin-right:var(--space-md);">
|
<div style="display:inline-flex; align-items:center; gap:var(--space-xs); font-size:0.8rem; color:var(--text-secondary); margin-right:var(--space-md);">
|
||||||
<span>Righe:</span>
|
<span>Righe:</span>
|
||||||
<select class="form-select ticket-per-page-select" style="padding: 2px 24px 2px 6px; font-size: 0.75rem; height: 26px; min-width: 65px; margin: 0; background-position: right 6px center; border-color: var(--border-light);">
|
<select id="ticket-per-page-select-${isTop ? 'top' : 'bottom'}" name="per_page_${isTop ? 'top' : 'bottom'}" class="form-select ticket-per-page-select" style="padding: 2px 24px 2px 6px; font-size: 0.75rem; height: 26px; min-width: 65px; margin: 0; background-position: right 6px center; border-color: var(--border-light);">
|
||||||
<option value="10" ${per_page === 10 ? 'selected' : ''}>10</option>
|
<option value="10" ${per_page === 10 ? 'selected' : ''}>10</option>
|
||||||
<option value="20" ${per_page === 20 ? 'selected' : ''}>20</option>
|
<option value="20" ${per_page === 20 ? 'selected' : ''}>20</option>
|
||||||
<option value="50" ${per_page === 50 ? 'selected' : ''}>50</option>
|
<option value="50" ${per_page === 50 ? 'selected' : ''}>50</option>
|
||||||
|
|||||||
Vendored
+14
@@ -0,0 +1,14 @@
|
|||||||
|
/*!
|
||||||
|
* Chart.js v4.5.1
|
||||||
|
* https://www.chartjs.org
|
||||||
|
* (c) 2025 Chart.js Contributors
|
||||||
|
* Released under the MIT License
|
||||||
|
*/
|
||||||
|
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Chart=e()}(this,(function(){"use strict";var t=Object.freeze({__proto__:null,get Colors(){return Jo},get Decimation(){return ta},get Filler(){return ba},get Legend(){return Ma},get SubTitle(){return Pa},get Title(){return ka},get Tooltip(){return Na}});function e(){}const i=(()=>{let t=0;return()=>t++})();function s(t){return null==t}function n(t){if(Array.isArray&&Array.isArray(t))return!0;const e=Object.prototype.toString.call(t);return"[object"===e.slice(0,7)&&"Array]"===e.slice(-6)}function o(t){return null!==t&&"[object Object]"===Object.prototype.toString.call(t)}function a(t){return("number"==typeof t||t instanceof Number)&&isFinite(+t)}function r(t,e){return a(t)?t:e}function l(t,e){return void 0===t?e:t}const h=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100:+t/e,c=(t,e)=>"string"==typeof t&&t.endsWith("%")?parseFloat(t)/100*e:+t;function d(t,e,i){if(t&&"function"==typeof t.call)return t.apply(i,e)}function u(t,e,i,s){let a,r,l;if(n(t))if(r=t.length,s)for(a=r-1;a>=0;a--)e.call(i,t[a],a);else for(a=0;a<r;a++)e.call(i,t[a],a);else if(o(t))for(l=Object.keys(t),r=l.length,a=0;a<r;a++)e.call(i,t[l[a]],l[a])}function f(t,e){let i,s,n,o;if(!t||!e||t.length!==e.length)return!1;for(i=0,s=t.length;i<s;++i)if(n=t[i],o=e[i],n.datasetIndex!==o.datasetIndex||n.index!==o.index)return!1;return!0}function g(t){if(n(t))return t.map(g);if(o(t)){const e=Object.create(null),i=Object.keys(t),s=i.length;let n=0;for(;n<s;++n)e[i[n]]=g(t[i[n]]);return e}return t}function p(t){return-1===["__proto__","prototype","constructor"].indexOf(t)}function m(t,e,i,s){if(!p(t))return;const n=e[t],a=i[t];o(n)&&o(a)?x(n,a,s):e[t]=g(a)}function x(t,e,i){const s=n(e)?e:[e],a=s.length;if(!o(t))return t;const r=(i=i||{}).merger||m;let l;for(let e=0;e<a;++e){if(l=s[e],!o(l))continue;const n=Object.keys(l);for(let e=0,s=n.length;e<s;++e)r(n[e],t,l,i)}return t}function b(t,e){return x(t,e,{merger:_})}function _(t,e,i){if(!p(t))return;const s=e[t],n=i[t];o(s)&&o(n)?b(s,n):Object.prototype.hasOwnProperty.call(e,t)||(e[t]=g(n))}const y={"":t=>t,x:t=>t.x,y:t=>t.y};function v(t){const e=t.split("."),i=[];let s="";for(const t of e)s+=t,s.endsWith("\\")?s=s.slice(0,-1)+".":(i.push(s),s="");return i}function M(t,e){const i=y[e]||(y[e]=function(t){const e=v(t);return t=>{for(const i of e){if(""===i)break;t=t&&t[i]}return t}}(e));return i(t)}function w(t){return t.charAt(0).toUpperCase()+t.slice(1)}const k=t=>void 0!==t,S=t=>"function"==typeof t,P=(t,e)=>{if(t.size!==e.size)return!1;for(const i of t)if(!e.has(i))return!1;return!0};function D(t){return"mouseup"===t.type||"click"===t.type||"contextmenu"===t.type}const C=Math.PI,O=2*C,A=O+C,T=Number.POSITIVE_INFINITY,L=C/180,E=C/2,R=C/4,I=2*C/3,z=Math.log10,F=Math.sign;function V(t,e,i){return Math.abs(t-e)<i}function B(t){const e=Math.round(t);t=V(t,e,t/1e3)?e:t;const i=Math.pow(10,Math.floor(z(t))),s=t/i;return(s<=1?1:s<=2?2:s<=5?5:10)*i}function W(t){const e=[],i=Math.sqrt(t);let s;for(s=1;s<i;s++)t%s==0&&(e.push(s),e.push(t/s));return i===(0|i)&&e.push(i),e.sort(((t,e)=>t-e)).pop(),e}function N(t){return!function(t){return"symbol"==typeof t||"object"==typeof t&&null!==t&&!(Symbol.toPrimitive in t||"toString"in t||"valueOf"in t)}(t)&&!isNaN(parseFloat(t))&&isFinite(t)}function H(t,e){const i=Math.round(t);return i-e<=t&&i+e>=t}function j(t,e,i){let s,n,o;for(s=0,n=t.length;s<n;s++)o=t[s][i],isNaN(o)||(e.min=Math.min(e.min,o),e.max=Math.max(e.max,o))}function $(t){return t*(C/180)}function Y(t){return t*(180/C)}function U(t){if(!a(t))return;let e=1,i=0;for(;Math.round(t*e)/e!==t;)e*=10,i++;return i}function X(t,e){const i=e.x-t.x,s=e.y-t.y,n=Math.sqrt(i*i+s*s);let o=Math.atan2(s,i);return o<-.5*C&&(o+=O),{angle:o,distance:n}}function q(t,e){return Math.sqrt(Math.pow(e.x-t.x,2)+Math.pow(e.y-t.y,2))}function K(t,e){return(t-e+A)%O-C}function G(t){return(t%O+O)%O}function J(t,e,i,s){const n=G(t),o=G(e),a=G(i),r=G(o-n),l=G(a-n),h=G(n-o),c=G(n-a);return n===o||n===a||s&&o===a||r>l&&h<c}function Z(t,e,i){return Math.max(e,Math.min(i,t))}function Q(t){return Z(t,-32768,32767)}function tt(t,e,i,s=1e-6){return t>=Math.min(e,i)-s&&t<=Math.max(e,i)+s}function et(t,e,i){i=i||(i=>t[i]<e);let s,n=t.length-1,o=0;for(;n-o>1;)s=o+n>>1,i(s)?o=s:n=s;return{lo:o,hi:n}}const it=(t,e,i,s)=>et(t,i,s?s=>{const n=t[s][e];return n<i||n===i&&t[s+1][e]===i}:s=>t[s][e]<i),st=(t,e,i)=>et(t,i,(s=>t[s][e]>=i));function nt(t,e,i){let s=0,n=t.length;for(;s<n&&t[s]<e;)s++;for(;n>s&&t[n-1]>i;)n--;return s>0||n<t.length?t.slice(s,n):t}const ot=["push","pop","shift","splice","unshift"];function at(t,e){t._chartjs?t._chartjs.listeners.push(e):(Object.defineProperty(t,"_chartjs",{configurable:!0,enumerable:!1,value:{listeners:[e]}}),ot.forEach((e=>{const i="_onData"+w(e),s=t[e];Object.defineProperty(t,e,{configurable:!0,enumerable:!1,value(...eLine truncated
|
||||||
|
/*!
|
||||||
|
* @kurkle/color v0.3.2
|
||||||
|
* https://github.com/kurkle/color#readme
|
||||||
|
* (c) 2023 Jukka Kurkela
|
||||||
|
* Released under the MIT License
|
||||||
|
*/function _t(t){return t+.5|0}const yt=(t,e,i)=>Math.max(Math.min(t,i),e);function vt(t){return yt(_t(2.55*t),0,255)}function Mt(t){return yt(_t(255*t),0,255)}function wt(t){return yt(_t(t/2.55)/100,0,1)}function kt(t){return yt(_t(100*t),0,100)}const St={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Pt=[..."0123456789ABCDEF"],Dt=t=>Pt[15&t],Ct=t=>Pt[(240&t)>>4]+Pt[15&t],Ot=t=>(240&t)>>4==(15&t);function At(t){var e=(t=>Ot(t.r)&&Ot(t.g)&&Ot(t.b)&&Ot(t.a))(t)?Dt:Ct;return t?"#"+e(t.r)+e(t.g)+e(t.b)+((t,e)=>t<255?e(t):"")(t.a,e):void 0}const Tt=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Lt(t,e,i){const s=e*Math.min(i,1-i),n=(e,n=(e+t/30)%12)=>i-s*Math.max(Math.min(n-3,9-n,1),-1);return[n(0),n(8),n(4)]}function Et(t,e,i){const s=(s,n=(s+t/60)%6)=>i-i*e*Math.max(Math.min(n,4-n,1),0);return[s(5),s(3),s(1)]}function Rt(t,e,i){const s=Lt(t,1,.5);let n;for(e+i>1&&(n=1/(e+i),e*=n,i*=n),n=0;n<3;n++)s[n]*=1-e-i,s[n]+=e;return s}function It(t){const e=t.r/255,i=t.g/255,s=t.b/255,n=Math.max(e,i,s),o=Math.min(e,i,s),a=(n+o)/2;let r,l,h;return n!==o&&(h=n-o,l=a>.5?h/(2-n-o):h/(n+o),r=function(t,e,i,s,n){return t===n?(e-i)/s+(e<i?6:0):e===n?(i-t)/s+2:(t-e)/s+4}(e,i,s,h,n),r=60*r+.5),[0|r,l||0,a]}function zt(t,e,i,s){return(Array.isArray(e)?t(e[0],e[1],e[2]):t(e,i,s)).map(Mt)}function Ft(t,e,i){return zt(Lt,t,e,i)}function Vt(t){return(t%360+360)%360}function Bt(t){const e=Tt.exec(t);let i,s=255;if(!e)return;e[5]!==i&&(s=e[6]?vt(+e[5]):Mt(+e[5]));const n=Vt(+e[2]),o=+e[3]/100,a=+e[4]/100;return i="hwb"===e[1]?function(t,e,i){return zt(Rt,t,e,i)}(n,o,a):"hsv"===e[1]?function(t,e,i){return zt(Et,t,e,i)}(n,o,a):Ft(n,o,a),{r:i[0],g:i[1],b:i[2],a:s}}const Wt={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},Nt={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};let Ht;function jt(t){Ht||(Ht=function(){const t={},e=Object.keys(Nt),i=Object.keys(Wt);let s,n,o,a,r;for(s=0;s<e.length;s++){for(a=r=e[s],n=0;n<i.length;n++)o=i[n],r=r.replace(o,Wt[o]);o=parseInt(Nt[a],16),t[r]=[o>>16&255,o>>8&255,255&o]}return t}(),Ht.transparent=[0,0,0,0]);const e=Ht[t.toLowerCase()];return e&&{r:e[0],g:e[1],b:e[2],a:4===e.length?e[3]:255}}const $t=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;const Yt=t=>t<=.0031308?12.92*t:1.055*Math.pow(t,1/2.4)-.055,Ut=t=>t<=.04045?t/12.92:Math.pow((t+.055)/1.055,2.4);function Xt(t,e,i){if(t){let s=It(t);s[e]=Math.max(0,Math.min(s[e]+s[e]*i,0===e?360:1)),Line truncated
|
||||||
|
//# sourceMappingURL=chart.umd.min.js.map
|
||||||
Vendored
+16
@@ -0,0 +1,16 @@
|
|||||||
|
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).PDFLib={})}(this,(function(t){"use strict";
|
||||||
|
/*! *****************************************************************************
|
||||||
|
Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
||||||
|
this file except in compliance with the License. You may obtain a copy of the
|
||||||
|
License at http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED
|
||||||
|
WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE,
|
||||||
|
MERCHANTABLITY OR NON-INFRINGEMENT.
|
||||||
|
|
||||||
|
See the Apache Version 2.0 License for specific language governing permissions
|
||||||
|
and limitations under the License.
|
||||||
|
***************************************************************************** */var e=function(t,n){return(e=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,e){t.__proto__=e}||function(t,e){for(var n in e)e.hasOwnProperty(n)&&(t[n]=e[n])})(t,n)};function n(t,n){function r(){this.constructor=t}e(t,n),t.prototype=null===n?Object.create(n):(r.prototype=n.prototype,new r)}var r=function(){return(r=Object.assign||function(t){for(var e,n=1,r=arguments.length;n<r;n++)for(var i in e=arguments[n])Object.prototype.hasOwnProperty.call(e,i)&&(t[i]=e[i]);return t}).apply(this,arguments)};function i(t,e,n,r){return new(n||(n=Promise))((function(i,o){function a(t){try{l(r.next(t))}catch(t){o(t)}}function s(t){try{l(r.throw(t))}catch(t){o(t)}}function l(t){var e;t.done?i(t.value):(e=t.value,e instanceof n?e:new n((function(t){t(e)}))).then(a,s)}l((r=r.apply(t,e||[])).next())}))}function o(t,e){var n,r,i,o,a={label:0,sent:function(){if(1&i[0])throw i[1];return i[1]},trys:[],ops:[]};return o={next:s(0),throw:s(1),return:s(2)},"function"==typeof Symbol&&(o[Symbol.iterator]=function(){return this}),o;function s(o){return function(s){return function(o){if(n)throw new TypeError("Generator is already executing.");for(;a;)try{if(n=1,r&&(i=2&o[0]?r.return:o[0]?r.throw||((i=r.return)&&i.call(r),0):r.next)&&!(i=i.call(r,o[1])).done)return i;switch(r=0,i&&(o=[2&o[0],i.value]),o[0]){case 0:case 1:i=o;break;case 4:return a.label++,{value:o[1],done:!1};case 5:a.label++,r=o[1],o=[0];continue;case 7:o=a.ops.pop(),a.trys.pop();continue;default:if(!(i=a.trys,(i=i.length>0&&i[i.length-1])||6!==o[0]&&2!==o[0])){a=0;continue}if(3===o[0]&&(!i||o[1]>i[0]&&o[1]<i[3])){a.label=o[1];break}if(6===o[0]&&a.label<i[1]){a.label=i[1],i=o;break}if(i&&a.label<i[2]){a.label=i[2],a.ops.push(o);break}i[2]&&a.ops.pop(),a.trys.pop();continue}o=e.call(t,a)}catch(t){o=[6,t],r=0}finally{n=i=0}if(5&o[0])throw o[1];return{value:o[0]?o[1]:void 0,done:!0}}([o,s])}}}function a(){for(var t=0,e=0,n=arguments.length;e<n;e++)t+=arguments[e].length;var r=Array(t),i=0;for(e=0;e<n;e++)for(var o=arguments[e],a=0,s=o.length;a<s;a++,i++)r[i]=o[a];return r}for(var s="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",l=new Uint8Array(256),h=0;h<s.length;h++)l[s.charCodeAt(h)]=h;var u,c=function(t){for(var e="",n=t.length,r=0;r<n;r+=3)e+=s[t[r]>>2],e+=s[(3&t[r])<<4|t[r+1]>>4],e+=s[(15&t[r+1])<<2|t[r+2]>>6],e+=s[63&t[r+2]];return n%3==2?e=e.substring(0,e.length-1)+"=":n%3==1&&(e=e.substring(0,e.length-2)+"=="),e},d=function(t){var e,n,r,i,o,a=.75*t.length,s=t.length,h=0;"="===t[t.length-1]&&(a--,"="===t[t.length-2]&&a--);var u=new Uint8Array(a);for(e=0;e<s;e+=4)n=l[t.charCodeAt(e)],r=l[t.charCodeAt(e+1)],i=l[t.charCodeAt(e+2)],o=l[t.charCodeAt(e+3)],u[h++]=n<<2|r>>4,u[h++]=(15&r)<<4|i>>2,u[h++]=(3&i)<<6|63&o;return u},f=/^(data)?:?([\w\/\+]+)?;?(charset=[\w-]+|base64)?.*,/i,p=function(t){var e=t.trim(),n=e.substring(0,100).match(f);if(!n)return d(e);var r=n[0],i=e.substring(r.length);return d(i)},g=function(t){return t.charCodeAt(0)},v=function(t){return t.codePointAt(0)},y=function(t,e){return x(t.toString(16),e,"0").toUpperCase()},m=function(t){return y(t,2)},b=function(t){return String.fromCharCode(t)},w=function(t){return b(parseInt(t,16))},x=function(t,e,n){for(var r="",i=0,o=e-t.length;i<o;i++)r+=n;return r+t},k=function(t,e,n){for(var r=t.length,i=0;i<r;i++)e[n++]=t.charCodeAt(i);return r},F=function(t){return t.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")},S=function(t){return t.replace(/\t|\u0085|\u2028|\u2029/g," ").replace(/[\b\v]/g,"")},C=["\\n","\\f","\\r","\\u000B"],A=function(t){return/^[\n\f\r\u000B]$/.test(t)},T=function(t){return t.split(/[\n\f\r\u000B]/)},P=function(t){return t.replace(/[\n\f\r\u000B]/g," ")},z=function(t,e){var n,r=t.charCodeAt(e),i=e+1,o=1;return r>=55296&&r<=56319&&t.length>i&&(n=t.charCodeAt(i))>=56320&&n<=57343&&(o=2),[t.slice(e,e+o),o]},R=function(t){for(var e=[],n=0,r=t.length;n<r;){var i=z(t,n),o=i[0],a=i[1];e.push(o),n+=a}return e},O=function(t,e,n,r){for(var i=function(t){for(var e=C.join("|"),n=["$"],r=0,i=t.length;r<i;r++){var o=t[r];if(A(o))throw new TypeError("`wordBreak` must not include "+e);n.push(""===o?".":F(o))}var a=n.join("|");return new RegExp("("+e+")|((.*?)("+a+"))","gm")}(e),o=S(t).match(i),a="",s=0,l=[],h=function(){""!==a&&l.push(a),a="",s=0},u=0,c=o.length;u<c;u++){var d=o[u];if(A(d))h();else{var f=r(d);s+f>n&&h(),a+=d,s+=f}}return h(),l},D=/^D:(\d\d\d\d)(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([+\-Z])?(\d\d)?'?(\d\d)?'?$/,B=function(t){var e=t.match(D);if(e){var n=e[1],r=e[2],i=void 0===r?"01":r,o=e[3],a=void 0===o?"01":o,s=e[4],l=void 0===s?"00":s,h=e[5],u=void 0===h?"00":h,c=e[6],d=void 0===c?"00":c,f=e[7],p=void 0===f?"Z":f,g=e[8],v=void 0===g?"00":g,y=e[9];return new Date(n+"-"+i+"-"+a+"T"+l+":"+u+":"+d+("Z"===p?"Z":""+p+v+":"+(void 0===y?"00":y)))}},N=function(t,e){for(var n,r,i=0;i<t.length;){var o=t.substring(i).match(e);if(!o)return{match:r,pos:i};r=o,i+=(null!==(n=o.index)&&void 0Line truncated
|
||||||
|
//# sourceMappingURL=pdf-lib.min.js.map
|
||||||
Vendored
+22
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* @licstart The following is the entire license notice for the
|
||||||
|
* JavaScript code in this page
|
||||||
|
*
|
||||||
|
* Copyright 2023 Mozilla Foundation
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* @licend The above is the entire license notice for the
|
||||||
|
* JavaScript code in this page
|
||||||
|
*/
|
||||||
|
!function webpackUniversalModuleDefinition(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=t.pdfjsLib=e():"function"==typeof define&&define.amd?define("pdfjs-dist/build/pdf",[],(()=>t.pdfjsLib=e())):"object"==typeof exports?exports["pdfjs-dist/build/pdf"]=t.pdfjsLib=e():t["pdfjs-dist/build/pdf"]=t.pdfjsLib=e()}(globalThis,(()=>(()=>{"use strict";var __webpack_modules__=[,(t,e)=>{Object.defineProperty(e,"__esModule",{value:!0});e.VerbosityLevel=e.Util=e.UnknownErrorException=e.UnexpectedResponseException=e.TextRenderingMode=e.RenderingIntentFlag=e.PromiseCapability=e.PermissionFlag=e.PasswordResponses=e.PasswordException=e.PageActionEventType=e.OPS=e.MissingPDFException=e.MAX_IMAGE_SIZE_TO_CACHE=e.LINE_FACTOR=e.LINE_DESCENT_FACTOR=e.InvalidPDFException=e.ImageKind=e.IDENTITY_MATRIX=e.FormatError=e.FeatureTest=e.FONT_IDENTITY_MATRIX=e.DocumentActionEventType=e.CMapCompressionType=e.BaseException=e.BASELINE_FACTOR=e.AnnotationType=e.AnnotationReplyType=e.AnnotationPrefix=e.AnnotationMode=e.AnnotationFlag=e.AnnotationFieldFlag=e.AnnotationEditorType=e.AnnotationEditorPrefix=e.AnnotationEditorParamsType=e.AnnotationBorderStyleType=e.AnnotationActionEventType=e.AbortException=void 0;e.assert=function assert(t,e){t||unreachable(e)};e.bytesToString=bytesToString;e.createValidAbsoluteUrl=function createValidAbsoluteUrl(t,e=null,i=null){if(!t)return null;try{if(i&&"string"==typeof t){if(i.addDefaultProtocol&&t.startsWith("www.")){const e=t.match(/\./g);e?.length>=2&&(t=`http://${t}`)}if(i.tryConvertEncoding)try{t=stringToUTF8String(t)}catch{}}const s=e?new URL(t,e):new URL(t);if(function _isValidProtocol(t){switch(t?.protocol){case"http:":case"https:":case"ftp:":case"mailto:":case"tel:":return!0;default:return!1}}(s))return s}catch{}return null};e.getModificationDate=function getModificationDate(t=new Date){return[t.getUTCFullYear().toString(),(t.getUTCMonth()+1).toString().padStart(2,"0"),t.getUTCDate().toString().padStart(2,"0"),t.getUTCHours().toString().padStart(2,"0"),t.getUTCMinutes().toString().padStart(2,"0"),t.getUTCSeconds().toString().padStart(2,"0")].join("")};e.getUuid=function getUuid(){if("undefined"!=typeof crypto&&"function"==typeof crypto?.randomUUID)return crypto.randomUUID();const t=new Uint8Array(32);if("undefined"!=typeof crypto&&"function"==typeof crypto?.getRandomValues)crypto.getRandomValues(t);else for(let e=0;e<32;e++)t[e]=Math.floor(255*Math.random());return bytesToString(t)};e.getVerbosityLevel=function getVerbosityLevel(){return n};e.info=function info(t){n>=s.INFOS&&console.log(`Info: ${t}`)};e.isArrayBuffer=function isArrayBuffer(t){return"object"==typeof t&&void 0!==t?.byteLength};e.isArrayEqual=function isArrayEqual(t,e){if(t.length!==e.length)return!1;for(let i=0,s=t.length;i<s;i++)if(t[i]!==e[i])return!1;return!0};e.isNodeJS=void 0;e.normalizeUnicode=function normalizeUnicode(t){if(!l){l=/([\u00a0\u00b5\u037e\u0eb3\u2000-\u200a\u202f\u2126\ufb00-\ufb04\ufb06\ufb20-\ufb36\ufb38-\ufb3c\ufb3e\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufba1\ufba4-\ufba9\ufbae-\ufbb1\ufbd3-\ufbdc\ufbde-\ufbe7\ufbea-\ufbf8\ufbfc-\ufbfd\ufc00-\ufc5d\ufc64-\ufcf1\ufcf5-\ufd3d\ufd88\ufdf4\ufdfa-\ufdfb\ufe71\ufe77\ufe79\ufe7b\ufe7d]+)|(\ufb05+)/gu;h=new Map([["ſt","ſt"]])}return t.replaceAll(l,((t,e,i)=>e?e.normalize("NFKC"):h.get(i)))};e.objectFromMap=function objectFromMap(t){const e=Object.create(null);for(const[i,s]of t)e[i]=s;return e};e.objectSize=function objectSize(t){return Object.keys(t).length};e.setVerbosityLevel=function setVerbosityLevel(t){Number.isInteger(t)&&(n=t)};e.shadow=shadow;e.string32=function string32(t){return String.fromCharCode(t>>24&255,t>>16&255,t>>8&255,255&t)};e.stringToBytes=stringToBytes;e.stringToPDFString=function stringToPDFString(t){if(t[0]>="ï"){let e;"þ"===t[0]&&"ÿ"===t[1]?e="utf-16be":"ÿ"===t[0]&&"þ"===t[1]?e="utf-16le":"ï"===t[0]&&"»"===t[1]&&"¿"===t[2]&&(e="utf-8");if(e)try{const i=new TextDecoder(e,{fatal:!0}),s=stringToBytes(t);return i.decode(s)}catch(t){warn(`stringToPDFString: "${t}".`)}}const e=[];for(let i=0,s=t.length;i<s;i++){const s=o[t.charCodeAt(i)];e.push(s?String.fromCharCode(s):t.charAt(i))}return e.join("")};e.stringToUTF8String=stringToUTF8String;e.unreachable=unreachable;e.utf8StringToString=function utf8StringToString(t){return unescape(encodeURIComponent(t))};e.warn=warn;const i=!("object"!=typeof process||process+""!="[object process]"||process.versions.nw||process.versions.electron&&process.type&&"browser"!==process.type);e.isNodeJS=i;e.IDENTITY_MATRIX=[1,0,0,1,0,0];e.FONT_IDENTITY_MATRIX=[.001,0,0,.001,0,0];e.MAX_IMAGE_SIZE_TO_CACHE=1e7;e.LINE_FACTOR=1.35;e.LINE_DESCENT_FACTOR=.35;e.BASELINE_FACTOR=.25925925925925924;e.RenderingIntentFlag={ANY:1,DISPLAY:2,PRINT:4,SAVE:8,ANNOTATIONS_FORMS:16,ANNOTATIONS_STORAGE:32,ANNOTATIONS_DISABLE:64,OPLIST:256};e.AnnotationMode={DISABLE:0,ENABLE:1,ENABLE_FORMS:2,ENABLE_STORAGE:3};e.AnnotationEditorPrefix="pdfjs_internal_editor_";e.AnnotationEditorType={DISABLE:-1,NONE:0,FREETEXT:3,STLine truncated
|
||||||
Vendored
+3
@@ -0,0 +1,3 @@
|
|||||||
|
/*! For license information please see quill.js.LICENSE.txt */
|
||||||
|
!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Quill=e():t.Quill=e()}(self,(function(){return function(){var t={9698:function(t,e,n){"use strict";n.d(e,{Ay:function(){return c},Ji:function(){return d},mG:function(){return h},zo:function(){return u}});var r=n(6003),i=n(5232),s=n.n(i),o=n(3036),l=n(4850),a=n(5508);class c extends r.BlockBlot{cache={};delta(){return null==this.cache.delta&&(this.cache.delta=h(this)),this.cache.delta}deleteAt(t,e){super.deleteAt(t,e),this.cache={}}formatAt(t,e,n,i){e<=0||(this.scroll.query(n,r.Scope.BLOCK)?t+e===this.length()&&this.format(n,i):super.formatAt(t,Math.min(e,this.length()-t-1),n,i),this.cache={})}insertAt(t,e,n){if(null!=n)return super.insertAt(t,e,n),void(this.cache={});if(0===e.length)return;const r=e.split("\n"),i=r.shift();i.length>0&&(t<this.length()-1||null==this.children.tail?super.insertAt(Math.min(t,this.length()-1),i):this.children.tail.insertAt(this.children.tail.length(),i),this.cache={});let s=this;r.reduce(((t,e)=>(s=s.split(t,!0),s.insertAt(0,e),e.length)),t+i.length)}insertBefore(t,e){const{head:n}=this.children;super.insertBefore(t,e),n instanceof o.A&&n.remove(),this.cache={}}length(){return null==this.cache.length&&(this.cache.length=super.length()+1),this.cache.length}moveChildren(t,e){super.moveChildren(t,e),this.cache={}}optimize(t){super.optimize(t),this.cache={}}path(t){return super.path(t,!0)}removeChild(t){super.removeChild(t),this.cache={}}split(t){let e=arguments.length>1&&void 0!==arguments[1]&&arguments[1];if(e&&(0===t||t>=this.length()-1)){const e=this.clone();return 0===t?(this.parent.insertBefore(e,this),this):(this.parent.insertBefore(e,this.next),e)}const n=super.split(t,e);return this.cache={},n}}c.blotName="block",c.tagName="P",c.defaultChild=o.A,c.allowedChildren=[o.A,l.A,r.EmbedBlot,a.A];class u extends r.EmbedBlot{attach(){super.attach(),this.attributes=new r.AttributorStore(this.domNode)}delta(){return(new(s())).insert(this.value(),{...this.formats(),...this.attributes.values()})}format(t,e){const n=this.scroll.query(t,r.Scope.BLOCK_ATTRIBUTE);null!=n&&this.attributes.attribute(n,e)}formatAt(t,e,n,r){this.format(n,r)}insertAt(t,e,n){if(null!=n)return void super.insertAt(t,e,n);const r=e.split("\n"),i=r.pop(),s=r.map((t=>{const e=this.scroll.create(c.blotName);return e.insertAt(0,t),e})),o=this.split(t);s.forEach((t=>{this.parent.insertBefore(t,o)})),i&&this.parent.insertBefore(this.scroll.create("text",i),o)}}function h(t){let e=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];return t.descendants(r.LeafBlot).reduce(((t,n)=>0===n.length()?t:t.insert(n.value(),d(n,{},e))),new(s())).insert("\n",d(t))}function d(t){let e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=!(arguments.length>2&&void 0!==arguments[2])||arguments[2];return null==t?e:("formats"in t&&"function"==typeof t.formats&&(e={...e,...t.formats()},n&&delete e["code-token"]),null==t.parent||"scroll"===t.parent.statics.blotName||t.parent.statics.scope!==t.statics.scope?e:d(t.parent,e,n))}u.scope=r.Scope.BLOCK_BLOT},3036:function(t,e,n){"use strict";var r=n(6003);class i extends r.EmbedBlot{static value(){}optimize(){(this.prev||this.next)&&this.remove()}length(){return 0}value(){return""}}i.blotName="break",i.tagName="BR",e.A=i},580:function(t,e,n){"use strict";var r=n(6003);class i extends r.ContainerBlot{}e.A=i},4541:function(t,e,n){"use strict";var r=n(6003),i=n(5508);class s extends r.EmbedBlot{static blotName="cursor";static className="ql-cursor";static tagName="span";static CONTENTS="\ufeff";static value(){}constructor(t,e,n){super(t,e),this.selection=n,this.textNode=document.createTextNode(s.CONTENTS),this.domNode.appendChild(this.textNode),this.savedLength=0}detach(){null!=this.parent&&this.parent.removeChild(this)}format(t,e){if(0!==this.savedLength)return void super.format(t,e);let n=this,i=0;for(;null!=n&&n.statics.scope!==r.Scope.BLOCK_BLOT;)i+=n.offset(n.parent),n=n.parent;null!=n&&(this.savedLength=s.CONTENTS.length,n.optimize(),n.formatAt(i,s.CONTENTS.length,t,e),this.savedLength=0)}index(t,e){return t===this.textNode?0:super.index(t,e)}length(){return this.savedLength}position(){return[this.textNode,this.textNode.data.length]}remove(){super.remove(),this.parent=null}restore(){if(this.selection.composing||null==this.parent)return null;const t=this.selection.getNativeRange();for(;null!=this.domNode.lastChild&&this.domNode.lastChild!==this.textNode;)this.domNode.parentNode.insertBefore(this.domNode.lastChild,this.domNode);const e=this.prev instanceof i.A?this.prev:null,n=e?e.length():0,r=this.next instanceof i.A?this.next:null,o=r?r.text:"",{textNode:l}=this,a=l.data.split(s.CONTENTS).join("");let c;if(l.data=s.CONTENTS,e)c=e,(a||r)&&(e.insertAt(e.length(),a+o),r&&r.remove());else if(r)c=r,r.insertAt(0,a);else{const t=document.createTextNode(a);c=this.scroll.create(t),this.parent.insertBefore(c,this)}if(this.reLine truncated
|
||||||
|
//# sourceMappingURL=quill.js.map
|
||||||
Vendored
+10
@@ -0,0 +1,10 @@
|
|||||||
|
/*!
|
||||||
|
* Quill Editor v2.0.2
|
||||||
|
* https://quilljs.com
|
||||||
|
* Copyright (c) 2017-2024, Slab
|
||||||
|
* Copyright (c) 2014, Jason Chen
|
||||||
|
* Copyright (c) 2013, salesforce.com
|
||||||
|
*/
|
||||||
|
.ql-container{box-sizing:border-box;font-family:Helvetica,Arial,sans-serif;font-size:13px;height:100%;margin:0;position:relative}.ql-container.ql-disabled .ql-tooltip{visibility:hidden}.ql-container:not(.ql-disabled) li[data-list=checked] > .ql-ui,.ql-container:not(.ql-disabled) li[data-list=unchecked] > .ql-ui{cursor:pointer}.ql-clipboard{left:-100000px;height:1px;overflow-y:hidden;position:absolute;top:50%}.ql-clipboard p{margin:0;padding:0}.ql-editor{box-sizing:border-box;counter-reset:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;line-height:1.42;height:100%;outline:none;overflow-y:auto;padding:12px 15px;tab-size:4;-moz-tab-size:4;text-align:left;white-space:pre-wrap;word-wrap:break-word}.ql-editor > *{cursor:text}.ql-editor p,.ql-editor ol,.ql-editor pre,.ql-editor blockquote,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{margin:0;padding:0}@supports (counter-set:none){.ql-editor p,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{counter-set:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor p,.ql-editor h1,.ql-editor h2,.ql-editor h3,.ql-editor h4,.ql-editor h5,.ql-editor h6{counter-reset:list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor table{border-collapse:collapse}.ql-editor td{border:1px solid #000;padding:2px 5px}.ql-editor ol{padding-left:1.5em}.ql-editor li{list-style-type:none;padding-left:1.5em;position:relative}.ql-editor li > .ql-ui:before{display:inline-block;margin-left:-1.5em;margin-right:.3em;text-align:right;white-space:nowrap;width:1.2em}.ql-editor li[data-list=checked] > .ql-ui,.ql-editor li[data-list=unchecked] > .ql-ui{color:#777}.ql-editor li[data-list=bullet] > .ql-ui:before{content:'\2022'}.ql-editor li[data-list=checked] > .ql-ui:before{content:'\2611'}.ql-editor li[data-list=unchecked] > .ql-ui:before{content:'\2610'}@supports (counter-set:none){.ql-editor li[data-list]{counter-set:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list]{counter-reset:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered]{counter-increment:list-0}.ql-editor li[data-list=ordered] > .ql-ui:before{content:counter(list-0, decimal) '. '}.ql-editor li[data-list=ordered].ql-indent-1{counter-increment:list-1}.ql-editor li[data-list=ordered].ql-indent-1 > .ql-ui:before{content:counter(list-1, lower-alpha) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-1{counter-set:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-1{counter-reset:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-2{counter-increment:list-2}.ql-editor li[data-list=ordered].ql-indent-2 > .ql-ui:before{content:counter(list-2, lower-roman) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-2{counter-set:list-3 list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-2{counter-reset:list-3 list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-3{counter-increment:list-3}.ql-editor li[data-list=ordered].ql-indent-3 > .ql-ui:before{content:counter(list-3, decimal) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-3{counter-set:list-4 list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-3{counter-reset:list-4 list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-4{counter-increment:list-4}.ql-editor li[data-list=ordered].ql-indent-4 > .ql-ui:before{content:counter(list-4, lower-alpha) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-4{counter-set:list-5 list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-4{counter-reset:list-5 list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-5{counter-increment:list-5}.ql-editor li[data-list=ordered].ql-indent-5 > .ql-ui:before{content:counter(list-5, lower-roman) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-5{counter-set:list-6 list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-5{counter-reset:list-6 list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-6{counter-increment:list-6}.ql-editor li[data-list=ordered].ql-indent-6 > .ql-ui:before{content:counter(list-6, decimal) '. '}@supports (counter-set:none){.ql-editor li[data-list].ql-indent-6{counter-set:list-7 list-8 list-9}}@supports not (counter-set:none){.ql-editor li[data-list].ql-indent-6{counter-reset:list-7 list-8 list-9}}.ql-editor li[data-list=ordered].ql-indent-7{counter-increment:list-7}.ql-editor li[data-list=ordered].ql-indent-7 > .ql-ui:before{content:coLine truncated
|
||||||
|
|
||||||
|
/*# sourceMappingURL=quill.snow.css.map*/
|
||||||
+115
-2
@@ -916,8 +916,8 @@ router.patch('/:id', async (req, res) => {
|
|||||||
if (updates.type_id !== undefined) ticketFields.TypeID = updates.type_id;
|
if (updates.type_id !== undefined) ticketFields.TypeID = updates.type_id;
|
||||||
if (updates.title !== undefined) ticketFields.Title = updates.title;
|
if (updates.title !== undefined) ticketFields.Title = updates.title;
|
||||||
if (updates.ticket_lock_id !== undefined) ticketFields.LockID = updates.ticket_lock_id;
|
if (updates.ticket_lock_id !== undefined) ticketFields.LockID = updates.ticket_lock_id;
|
||||||
if (updates.customer_id !== undefined) ticketFields.CustomerID = updates.customer_id;
|
if (updates.customer_id !== undefined && updates.customer_id !== null && updates.customer_id !== '') ticketFields.CustomerID = updates.customer_id;
|
||||||
if (updates.customer_user_id !== undefined) ticketFields.CustomerUser = updates.customer_user_id;
|
if (updates.customer_user_id !== undefined && updates.customer_user_id !== null && updates.customer_user_id !== '') ticketFields.CustomerUser = updates.customer_user_id;
|
||||||
|
|
||||||
// If ticketFields is empty but we have a note or timeUnit to log, supply a dummy field (StateID) so Ticket is not empty
|
// If ticketFields is empty but we have a note or timeUnit to log, supply a dummy field (StateID) so Ticket is not empty
|
||||||
if (Object.keys(ticketFields).length === 0 && (hasNote || (!isNaN(timeUnit) && timeUnit > 0))) {
|
if (Object.keys(ticketFields).length === 0 && (hasNote || (!isNaN(timeUnit) && timeUnit > 0))) {
|
||||||
@@ -986,6 +986,7 @@ router.patch('/:id', async (req, res) => {
|
|||||||
const client = await pool.connect();
|
const client = await pool.connect();
|
||||||
try {
|
try {
|
||||||
await client.query('BEGIN');
|
await client.query('BEGIN');
|
||||||
|
const localNow = getLocalTimestamp();
|
||||||
|
|
||||||
// If state is changing, check if the target state is a closed state type to auto-unlock
|
// If state is changing, check if the target state is a closed state type to auto-unlock
|
||||||
if (updates.ticket_state_id && updates.ticket_state_id !== current.ticket_state_id) {
|
if (updates.ticket_state_id && updates.ticket_state_id !== current.ticket_state_id) {
|
||||||
@@ -1957,6 +1958,118 @@ router.get('/attachments/:id', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// PUT /api/tickets/attachments/:id — Update text attachment content
|
||||||
|
router.put('/attachments/:id', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { content } = req.body;
|
||||||
|
if (typeof content !== 'string') {
|
||||||
|
return res.status(400).json({ error: 'Contenuto non valido' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkRes = await pool.query(
|
||||||
|
`SELECT filename, content_type FROM article_data_mime_attachment WHERE id = $1`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
if (checkRes.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'Allegato non trovato' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentBuffer = Buffer.from(content, 'utf-8');
|
||||||
|
await pool.query(
|
||||||
|
`UPDATE article_data_mime_attachment SET content = $1, content_size = $2 WHERE id = $3`,
|
||||||
|
[contentBuffer, contentBuffer.length, id]
|
||||||
|
);
|
||||||
|
res.json({ message: 'Allegato di testo aggiornato con successo' });
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Errore aggiornamento allegato:', err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// POST /api/tickets/attachments/:id/open — Open attachment using OS default app (Word, Excel, etc.)
|
||||||
|
router.post('/attachments/:id/open', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const result = await pool.query(
|
||||||
|
`SELECT filename, content_type, content FROM article_data_mime_attachment WHERE id = $1`,
|
||||||
|
[id]
|
||||||
|
);
|
||||||
|
if (result.rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'Allegato non trovato' });
|
||||||
|
}
|
||||||
|
const attachment = result.rows[0];
|
||||||
|
|
||||||
|
let contentBuffer;
|
||||||
|
if (attachment.content) {
|
||||||
|
if (Buffer.isBuffer(attachment.content)) {
|
||||||
|
const isRawBinary = attachment.content.length > 4 && (
|
||||||
|
(attachment.content[0] === 0x25 && attachment.content[1] === 0x50 && attachment.content[2] === 0x44 && attachment.content[3] === 0x46) ||
|
||||||
|
(attachment.content[0] === 0xFF && attachment.content[1] === 0xD8) ||
|
||||||
|
(attachment.content[0] === 0x89 && attachment.content[1] === 0x50) ||
|
||||||
|
(attachment.content[0] === 0x50 && attachment.content[1] === 0x4B)
|
||||||
|
);
|
||||||
|
if (isRawBinary) {
|
||||||
|
contentBuffer = attachment.content;
|
||||||
|
} else {
|
||||||
|
const str = attachment.content.toString('utf-8');
|
||||||
|
const cleaned = str.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 = 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 {
|
||||||
|
contentBuffer = Buffer.from(attachment.content);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contentBuffer = Buffer.alloc(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const os = require('os');
|
||||||
|
const { exec } = require('child_process');
|
||||||
|
|
||||||
|
const tempDir = path.join(os.tmpdir(), 'otrs_turbo_attachments');
|
||||||
|
if (!fs.existsSync(tempDir)) {
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const safeName = (attachment.filename || `allegato_${id}`).replace(/[^a-zA-Z0-9._-]/g, '_');
|
||||||
|
const filePath = path.join(tempDir, `${id}_${safeName}`);
|
||||||
|
fs.writeFileSync(filePath, contentBuffer);
|
||||||
|
|
||||||
|
let command = `"" "${filePath}"`;
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
command = `start "" "${filePath}"`;
|
||||||
|
} else if (process.platform === 'darwin') {
|
||||||
|
command = `open "${filePath}"`;
|
||||||
|
} else {
|
||||||
|
command = `xdg-open "${filePath}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
exec(command, (err) => {
|
||||||
|
if (err) {
|
||||||
|
console.error('Error opening file via OS default app:', err);
|
||||||
|
return res.status(500).json({ error: 'Impossibile aprire il file con l\'applicazione di sistema: ' + err.message });
|
||||||
|
}
|
||||||
|
res.json({ message: 'File aperto con l\'applicazione predefinita di sistema', filePath });
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Errore apertura allegato:', err);
|
||||||
|
res.status(500).json({ error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// POST /api/tickets/merge — Merge tickets (Issue #7)
|
// POST /api/tickets/merge — Merge tickets (Issue #7)
|
||||||
router.post('/merge', async (req, res) => {
|
router.post('/merge', async (req, res) => {
|
||||||
const { targetId, sourceIds } = req.body;
|
const { targetId, sourceIds } = req.body;
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const https = require('https');
|
||||||
|
|
||||||
|
const vendorDir = path.join(__dirname, '..', 'public', 'vendor');
|
||||||
|
if (!fs.existsSync(vendorDir)) {
|
||||||
|
fs.mkdirSync(vendorDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = [
|
||||||
|
{ url: 'https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.snow.css', dest: 'quill.snow.css' },
|
||||||
|
{ url: 'https://cdn.jsdelivr.net/npm/quill@2.0.2/dist/quill.js', dest: 'quill.js' },
|
||||||
|
{ url: 'https://cdn.jsdelivr.net/npm/chart.js', dest: 'chart.js' },
|
||||||
|
{ url: 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js', dest: 'pdf.min.js' },
|
||||||
|
{ url: 'https://cdn.jsdelivr.net/npm/pdf-lib@1.17.1/dist/pdf-lib.min.js', dest: 'pdf-lib.min.js' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function download(url, destPath) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
https.get(url, (res) => {
|
||||||
|
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||||
|
return download(res.headers.location, destPath).then(resolve).catch(reject);
|
||||||
|
}
|
||||||
|
if (res.statusCode !== 200) {
|
||||||
|
return reject(new Error(`Failed to download ${url}: status ${res.statusCode}`));
|
||||||
|
}
|
||||||
|
const file = fs.createWriteStream(destPath);
|
||||||
|
res.pipe(file);
|
||||||
|
file.on('finish', () => {
|
||||||
|
file.close(() => {
|
||||||
|
console.log(`Downloaded: ${path.basename(destPath)} (${fs.statSync(destPath).size} bytes)`);
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}).on('error', (err) => {
|
||||||
|
reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
for (const f of files) {
|
||||||
|
const destPath = path.join(vendorDir, f.dest);
|
||||||
|
try {
|
||||||
|
await download(f.url, destPath);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error downloading ${f.dest}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
Reference in new issue
Block a user