vari piccoli fix. Importati js e css per eliminare i warning e correzioni W3c.

This commit is contained in:
Gabriele Cimaschi committed 2026-09-15 17:01:01 +02:00
1 parent ae458256d5
commit a1b6d9333c
13 files changed
+872 -48

No files matched your search

+31 -4
View File
@@ -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
View File
@@ -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>
+5 -5
View File
@@ -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>
+1 -1
View File
@@ -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>
+600 -32
View File
@@ -120,7 +120,7 @@ const TicketDetailView = {
} }
}); });
} }
} catch (_) {} } catch (_) { }
}); });
// 2. Gather all attachment badges (Images, PDFs, Word, Excel, etc.) // 2. Gather all attachment badges (Images, PDFs, Word, Excel, etc.)
@@ -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
@@ -658,8 +746,8 @@ const TicketDetailView = {
if (sigs && sigs.length > 0) { if (sigs && sigs.length > 0) {
activeSignature = sigs.find(s => s.is_default) || sigs[0]; activeSignature = sigs.find(s => s.is_default) || sigs[0];
} }
}).catch(() => {}); }).catch(() => { });
} catch (_) {} } catch (_) { }
const toolSelectBtn = document.getElementById('pdf-tool-select'); const toolSelectBtn = document.getElementById('pdf-tool-select');
const toolDrawBtn = document.getElementById('pdf-tool-draw'); const toolDrawBtn = document.getElementById('pdf-tool-draw');
@@ -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++) {
@@ -720,7 +821,7 @@ const TicketDetailView = {
let allSignatures = []; let allSignatures = [];
try { try {
allSignatures = await App.api('/api/config/user-signatures'); allSignatures = await App.api('/api/config/user-signatures');
} catch (_) {} } catch (_) { }
if (!activeSignature && allSignatures.length > 0) { if (!activeSignature && allSignatures.length > 0) {
activeSignature = allSignatures.find(s => s.is_default) || allSignatures[0]; activeSignature = allSignatures.find(s => s.is_default) || allSignatures[0];
@@ -823,7 +924,7 @@ const TicketDetailView = {
let allShapes = []; let allShapes = [];
try { try {
allShapes = await App.api('/api/config/user-shapes'); allShapes = await App.api('/api/config/user-shapes');
} catch (_) {} } catch (_) { }
const btnRect = toolShapeBtn.getBoundingClientRect(); const btnRect = toolShapeBtn.getBoundingClientRect();
shapePopup = document.createElement('div'); shapePopup = document.createElement('div');
@@ -1095,7 +1196,7 @@ const TicketDetailView = {
try { try {
const textContent = await page.getTextContent(); const textContent = await page.getTextContent();
renderManualTextLayer(textContent, textLayerDiv, viewport); renderManualTextLayer(textContent, textLayerDiv, viewport);
} catch (_) {} } catch (_) { }
} }
} }
@@ -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) => {
@@ -1355,7 +1477,7 @@ const TicketDetailView = {
]); ]);
signatures = sigRes || []; signatures = sigRes || [];
favoriteShapes = (shapeRes || []).filter(s => s.is_favorite); favoriteShapes = (shapeRes || []).filter(s => s.is_favorite);
} catch (_) {} } catch (_) { }
let menuOverlay = document.getElementById('pdf-context-menu-popup'); let menuOverlay = document.getElementById('pdf-context-menu-popup');
if (menuOverlay) menuOverlay.remove(); if (menuOverlay) menuOverlay.remove();
@@ -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;
@@ -2183,8 +2348,8 @@ const TicketDetailView = {
if (sigs && sigs.length > 0) { if (sigs && sigs.length > 0) {
activeSignature = sigs.find(s => s.is_default) || sigs[0]; activeSignature = sigs.find(s => s.is_default) || sigs[0];
} }
}).catch(() => {}); }).catch(() => { });
} catch (_) {} } catch (_) { }
const pageObjects = []; const pageObjects = [];
drawCanvas.pageObjects = pageObjects; drawCanvas.pageObjects = pageObjects;
@@ -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) {
@@ -3226,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>
@@ -3273,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>
@@ -4149,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 => {
@@ -4325,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);
}, },
}; };
+1 -1
View File
@@ -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>
+14
View File
@@ -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
+16
View File
@@ -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
+22
View File
@@ -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
+3
View File
@@ -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
+10
View File
@@ -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*/
+112
View File
@@ -1958,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;
+52
View File
@@ -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();