tutto di troppo: fix visualizzazione email. style: riviste icone invio ed elimina, postato aggiungi ticket al grppo e informazioni laterali da a cc.
This commit is contained in:
1 parent
9ae0cec0ca
commit
cf500aabb3
7 files changed
+743
-85
No files matched your search
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* ShortcutsView — Gestione Shortcuts e Scorciatoie da Tastiera
|
||||
* Visualizza e permette la personalizzazione delle scorciatoie da tastiera.
|
||||
*/
|
||||
const ShortcutsView = {
|
||||
editingId: null,
|
||||
|
||||
render() {
|
||||
const container = document.getElementById('view-container');
|
||||
const shortcuts = this.getShortcutsList();
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="view-header" style="padding:var(--space-md) var(--space-xl); display:flex; align-items:center; justify-content:space-between; flex-wrap:wrap; gap:var(--space-sm); border-bottom:1px solid var(--border-subtle);">
|
||||
<div style="display:flex; align-items:center; gap:var(--space-sm);">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:22px; height:22px; color:var(--accent-primary);">
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||
<path d="M6 8h.01M10 8h.01M14 8h.01M18 8h.01M6 12h.01M10 12h.01M14 12h.01M18 12h.01M8 16h8" />
|
||||
</svg>
|
||||
<span style="font-weight:700; font-size:1.1rem;">Shortcuts e Scorciatoie da Tastiera</span>
|
||||
</div>
|
||||
<button id="sc-reset-all" class="btn btn-ghost btn-sm" style="font-size:0.82rem;">
|
||||
🔄 Ripristina Predefiniti
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style="padding:var(--space-lg) var(--space-xl); max-width:960px;">
|
||||
<p style="color:var(--text-secondary); font-size:0.88rem; margin-bottom:var(--space-lg); line-height:1.5;">
|
||||
Personalizza o abilita/disabilita le scorciatoie da tastiera rapide per velocizzare la gestione dei ticket e delle consuntivazioni.
|
||||
</p>
|
||||
|
||||
<div style="display:flex; flex-direction:column; gap:var(--space-md);">
|
||||
${shortcuts.map(sc => this.buildShortcutRow(sc)).join('')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal per la registrazione di una nuova combinazione -->
|
||||
<div id="sc-modal" class="modal" style="display:none;">
|
||||
<div class="modal-content" style="max-width:440px; text-align:center;">
|
||||
<h3 style="margin-bottom:var(--space-xs); font-size:1.1rem;" id="sc-modal-title">Modifica Shortcut</h3>
|
||||
<p style="font-size:0.85rem; color:var(--text-muted); margin-bottom:var(--space-lg);">
|
||||
Premi la combinazione di tasti desiderata sulla tastiera (es. <kbd style="background:var(--bg-tertiary); padding:2px 6px; border-radius:4px; border:1px solid var(--border-subtle);">Ctrl</kbd> + <kbd style="background:var(--bg-tertiary); padding:2px 6px; border-radius:4px; border:1px solid var(--border-subtle);">Invio</kbd>).
|
||||
</p>
|
||||
|
||||
<div id="sc-key-recorder" tabindex="0" style="padding:var(--space-lg); background:var(--bg-tertiary); border:2px dashed var(--accent-primary); border-radius:var(--radius-md); font-size:1.2rem; font-weight:700; color:var(--accent-primary); outline:none; cursor:pointer; min-height:70px; display:flex; align-items:center; justify-content:center;">
|
||||
Premi i tasti...
|
||||
</div>
|
||||
|
||||
<div style="display:flex; justify-content:flex-end; gap:var(--space-sm); margin-top:var(--space-lg);">
|
||||
<button id="sc-modal-cancel" class="btn btn-ghost btn-sm">Annulla</button>
|
||||
<button id="sc-modal-save" class="btn btn-primary btn-sm" disabled>Conferma</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.bindEvents();
|
||||
},
|
||||
|
||||
getShortcutsList() {
|
||||
const list = [
|
||||
{
|
||||
id: 'create_ticket_open',
|
||||
name: 'Crea Nuovo Ticket',
|
||||
description: 'Apre la pagina di creazione di un nuovo ticket',
|
||||
context: 'Globale',
|
||||
defaultCombo: { ctrl: true, alt: true, shift: false, key: 'n', enabled: true }
|
||||
},
|
||||
{
|
||||
id: 'create_ticket_submit',
|
||||
name: 'Invia Nuovo Ticket',
|
||||
description: 'Invia il modulo e crea un nuovo ticket',
|
||||
context: 'Pagina Nuovo Ticket',
|
||||
defaultCombo: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true }
|
||||
},
|
||||
{
|
||||
id: 'quick_edit_save',
|
||||
name: 'Salva Modifica Rapida',
|
||||
description: 'Salva le modifiche apportate nei campi di Modifica Rapida',
|
||||
context: 'Pagina Dettaglio Ticket (Modifica Rapida)',
|
||||
defaultCombo: { ctrl: true, alt: false, shift: false, key: 'Enter', enabled: true }
|
||||
}
|
||||
];
|
||||
|
||||
return list.map(item => {
|
||||
const current = App.getShortcut(item.id);
|
||||
return {
|
||||
...item,
|
||||
currentCombo: current
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
buildShortcutRow(sc) {
|
||||
const combo = sc.currentCombo;
|
||||
const label = App.formatShortcutLabel(combo);
|
||||
const keysHtml = label.split(' + ').map(k => `
|
||||
<kbd style="background:var(--bg-tertiary); color:var(--text-primary); border:1px solid var(--border-subtle); border-bottom-width:2px; padding:3px 8px; border-radius:4px; font-family:monospace; font-weight:700; font-size:0.85rem; box-shadow:0 1px 2px rgba(0,0,0,0.15);">
|
||||
${App.escapeHtml(k)}
|
||||
</kbd>
|
||||
`).join(' + ');
|
||||
|
||||
return `
|
||||
<div class="card" style="display:flex; align-items:center; justify-content:space-between; padding:var(--space-md) var(--space-lg); gap:var(--space-md); border:1px solid var(--border-subtle); flex-wrap:wrap;">
|
||||
<div style="flex:1; min-width:240px;">
|
||||
<div style="font-weight:700; font-size:0.95rem; color:var(--text-primary); display:flex; align-items:center; gap:var(--space-xs);">
|
||||
${App.escapeHtml(sc.name)}
|
||||
<span class="badge" style="background:var(--bg-tertiary); color:var(--text-muted); font-size:0.72rem; font-weight:600; padding:2px 8px; border-radius:10px;">
|
||||
${App.escapeHtml(sc.context)}
|
||||
</span>
|
||||
</div>
|
||||
<div style="font-size:0.82rem; color:var(--text-secondary); margin-top:3px;">
|
||||
${App.escapeHtml(sc.description)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display:flex; align-items:center; gap:var(--space-lg);">
|
||||
<div style="display:flex; align-items:center; gap:4px;">
|
||||
${combo.enabled ? keysHtml : '<span style="color:var(--text-muted); font-size:0.82rem; font-style:italic;">Disabilitato</span>'}
|
||||
</div>
|
||||
|
||||
<div style="display:flex; align-items:center; gap:var(--space-xs);">
|
||||
<button class="btn btn-ghost btn-sm sc-edit-btn" data-id="${sc.id}" style="height:32px; padding:4px 10px; font-size:0.82rem;">
|
||||
✏️ Modifica
|
||||
</button>
|
||||
<label class="html-toggle-container" style="margin:0; cursor:pointer;" title="Abilita/Disabilita shortcut">
|
||||
<input type="checkbox" class="sc-toggle-input" data-id="${sc.id}" ${combo.enabled ? 'checked' : ''} />
|
||||
<span style="font-size:0.8rem; color:var(--text-secondary);">${combo.enabled ? 'Attivo' : 'Off'}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
bindEvents() {
|
||||
// Reset all
|
||||
document.getElementById('sc-reset-all')?.addEventListener('click', () => {
|
||||
if (confirm('Ripristinare tutte le scorciatoie da tastiera ai valori predefiniti?')) {
|
||||
localStorage.removeItem('otrs_shortcuts');
|
||||
Toast.success('Shortcuts ripristinati ai valori predefiniti');
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
|
||||
// Toggles
|
||||
document.querySelectorAll('.sc-toggle-input').forEach(toggle => {
|
||||
toggle.addEventListener('change', (e) => {
|
||||
const id = e.target.dataset.id;
|
||||
const current = App.getShortcut(id);
|
||||
current.enabled = e.target.checked;
|
||||
App.saveCustomShortcut(id, current);
|
||||
Toast.success(`Shortcut ${current.enabled ? 'abilitato' : 'disabilitato'}`);
|
||||
this.render();
|
||||
});
|
||||
});
|
||||
|
||||
// Edit button
|
||||
let recordedCombo = null;
|
||||
const modal = document.getElementById('sc-modal');
|
||||
const recorder = document.getElementById('sc-key-recorder');
|
||||
const saveBtn = document.getElementById('sc-modal-save');
|
||||
const cancelBtn = document.getElementById('sc-modal-cancel');
|
||||
|
||||
document.querySelectorAll('.sc-edit-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
this.editingId = btn.dataset.id;
|
||||
const item = this.getShortcutsList().find(x => x.id === this.editingId);
|
||||
document.getElementById('sc-modal-title').textContent = `Modifica: ${item ? item.name : ''}`;
|
||||
recordedCombo = null;
|
||||
recorder.innerHTML = 'Premi i tasti sulla tastiera...';
|
||||
saveBtn.disabled = true;
|
||||
modal.style.display = 'flex';
|
||||
recorder.focus();
|
||||
});
|
||||
});
|
||||
|
||||
cancelBtn?.addEventListener('click', () => {
|
||||
modal.style.display = 'none';
|
||||
});
|
||||
|
||||
recorder?.addEventListener('keydown', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
// Ignore lone modifier presses
|
||||
if (['Control', 'Alt', 'Shift', 'Meta'].includes(e.key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ctrl = e.ctrlKey || e.metaKey;
|
||||
const alt = e.altKey;
|
||||
const shift = e.shiftKey;
|
||||
let key = e.key;
|
||||
|
||||
if (key === ' ') key = 'Space';
|
||||
|
||||
recordedCombo = { ctrl, alt, shift, key, enabled: true };
|
||||
const label = App.formatShortcutLabel(recordedCombo);
|
||||
recorder.innerHTML = label.split(' + ').map(k => `
|
||||
<kbd style="background:var(--bg-secondary); color:var(--accent-primary); border:1px solid var(--accent-primary); padding:4px 10px; border-radius:6px; font-family:monospace; margin:0 2px;">
|
||||
${App.escapeHtml(k)}
|
||||
</kbd>
|
||||
`).join(' + ');
|
||||
|
||||
saveBtn.disabled = false;
|
||||
});
|
||||
|
||||
saveBtn?.addEventListener('click', () => {
|
||||
if (this.editingId && recordedCombo) {
|
||||
App.saveCustomShortcut(this.editingId, recordedCombo);
|
||||
Toast.success('Combinazione salvata!');
|
||||
modal.style.display = 'none';
|
||||
this.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -747,5 +747,18 @@ const TicketCreateView = {
|
||||
el.addEventListener('input', () => this.saveState());
|
||||
el.addEventListener('change', () => this.saveState());
|
||||
});
|
||||
|
||||
// Shortcut to submit ticket
|
||||
const createFormEl = document.querySelector('.create-form');
|
||||
if (createFormEl) {
|
||||
createFormEl.addEventListener('keydown', (e) => {
|
||||
if (App.isShortcutTriggered(e, 'create_ticket_submit')) {
|
||||
e.preventDefault();
|
||||
if (submitBtn && !submitBtn.disabled) {
|
||||
submitBtn.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
+229
-64
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
const TicketDetailView = {
|
||||
ticketId: null,
|
||||
htmlMode: false,
|
||||
htmlMode: true,
|
||||
originalValues: {},
|
||||
noteAttachments: [],
|
||||
|
||||
@@ -13,13 +13,15 @@ const TicketDetailView = {
|
||||
const body = this.noteQuill ? this.noteQuill.root.innerHTML.trim() : '';
|
||||
const subject = document.getElementById('note-subject') ? document.getElementById('note-subject').value.trim() : '';
|
||||
const time_unit = document.getElementById('note-time-units') ? document.getElementById('note-time-units').value.trim() : '';
|
||||
const ticket_state_id = document.getElementById('note-ticket-state') ? document.getElementById('note-ticket-state').value : '';
|
||||
|
||||
if ((body !== '<p><br></p>' && body !== '') || subject !== '' || time_unit !== '' || this.noteAttachments.length > 0) {
|
||||
if ((body !== '<p><br></p>' && body !== '') || subject !== '' || time_unit !== '' || this.noteAttachments.length > 0 || ticket_state_id !== '') {
|
||||
App.saveDraft(this.ticketId, {
|
||||
type: 'note',
|
||||
body,
|
||||
subject,
|
||||
time_unit,
|
||||
ticket_state_id,
|
||||
attachments: [...this.noteAttachments]
|
||||
});
|
||||
} else {
|
||||
@@ -27,6 +29,42 @@ const TicketDetailView = {
|
||||
}
|
||||
},
|
||||
|
||||
extractLinks(htmlOrText) {
|
||||
if (!htmlOrText) return [];
|
||||
const links = [];
|
||||
const seen = new Set();
|
||||
|
||||
// 1. Extract <a href="..."> tags
|
||||
const anchorRegex = /<a\s+(?:[^>]*?\s+)?href=["']([^"']+)["'][^>]*>(.*?)<\/a>/gi;
|
||||
let match;
|
||||
while ((match = anchorRegex.exec(htmlOrText)) !== null) {
|
||||
let url = match[1].trim().replace(/&/g, '&');
|
||||
let text = match[2].replace(/<[^>]+>/g, '').trim();
|
||||
if (!text) text = url;
|
||||
|
||||
if (/^(javascript|mailto|tel|cid|#|\/api\/tickets\/attachments)/i.test(url)) {
|
||||
continue;
|
||||
}
|
||||
if (!seen.has(url)) {
|
||||
seen.add(url);
|
||||
links.push({ url, text });
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Extract plain HTTP/HTTPS URLs from text
|
||||
const plainUrlRegex = /(https?:\/\/[^\s<"'>]+)/gi;
|
||||
while ((match = plainUrlRegex.exec(htmlOrText)) !== null) {
|
||||
let url = match[1].trim().replace(/[.,;)]+$/, '');
|
||||
if (/^\/api\/tickets\/attachments/i.test(url)) continue;
|
||||
if (!seen.has(url)) {
|
||||
seen.add(url);
|
||||
links.push({ url, text: url });
|
||||
}
|
||||
}
|
||||
|
||||
return links;
|
||||
},
|
||||
|
||||
updateNoteAttachmentList() {
|
||||
const listEl = document.getElementById('note-file-list');
|
||||
if (!listEl) return;
|
||||
@@ -158,8 +196,8 @@ const TicketDetailView = {
|
||||
<div class="ticket-detail-main">
|
||||
<!-- Header -->
|
||||
<div class="card">
|
||||
<div class="ticket-header">
|
||||
<div class="ticket-header-info">
|
||||
<div class="ticket-header" style="display:flex; justify-content:space-between; align-items:flex-start; gap:16px; flex-wrap:wrap;">
|
||||
<div class="ticket-header-info" style="flex:1; min-width:280px;">
|
||||
<div class="ticket-number" style="display:inline-flex; align-items:center; gap:6px;">
|
||||
<span class="copy-ticket-btn" data-tn="${ticket.tn}" style="cursor: pointer; font-size: 0.85rem;" title="Copia numero ticket">📋</span>
|
||||
#${ticket.tn}
|
||||
@@ -177,6 +215,34 @@ const TicketDetailView = {
|
||||
${ticket.lock_name === 'lock' ? '<span class="badge" style="background:var(--warning-bg);color:var(--warning);">🔒 Bloccato</span>' : ''}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gruppi Ticket sul lato destro dell'intestazione in due righe -->
|
||||
<div id="ticket-groups-panel" style="display:flex; flex-direction:column; align-items:flex-end; gap:6px; font-size:0.8rem;">
|
||||
<div id="ticket-groups-list" style="display:flex; align-items:center; gap:8px; flex-wrap:wrap; justify-content:flex-end;">
|
||||
${groupsData.asMaster.length === 0 && groupsData.asMember.length === 0 ? `
|
||||
<span style="font-size:0.78rem; color:var(--text-muted);">Nessun gruppo associato</span>
|
||||
` : ''}
|
||||
${groupsData.asMaster.map(g => `
|
||||
<span style="font-size:0.8rem;">
|
||||
<span style="color:var(--accent-primary); font-weight:bold;">👑 Master in:</span>
|
||||
<a href="#/tickets/groups" onclick="localStorage.setItem('otrs_selected_group_id', ${g.id})" style="color:var(--text-primary); text-decoration:none; border-bottom:1px dashed var(--text-muted);">${App.escapeHtml(g.nome)}</a>
|
||||
</span>
|
||||
`).join('')}
|
||||
${groupsData.asMember.map(g => `
|
||||
<span style="font-size:0.8rem;">
|
||||
<span style="color:var(--text-secondary); font-weight:bold;">🔗 Membro di:</span>
|
||||
<a href="#/tickets/groups" onclick="localStorage.setItem('otrs_selected_group_id', ${g.id})" style="color:var(--text-primary); text-decoration:none; border-bottom:1px dashed var(--text-muted);">${App.escapeHtml(g.nome)}</a>
|
||||
</span>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<div style="display:flex; align-items:center; gap:4px; margin-top:2px;">
|
||||
<select class="form-select" id="group-association-select" style="padding:4px 20px 4px 8px; font-size:0.78rem; height:28px; margin:0;">
|
||||
<option value="">-- Seleziona --</option>
|
||||
</select>
|
||||
<button class="btn btn-primary btn-sm" id="btn-associate-to-group" style="height:28px; padding:0 8px; font-size:0.75rem;">+ Aggiungi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -241,10 +307,6 @@ const TicketDetailView = {
|
||||
<input type="hidden" id="qe-customer-id" data-field="customer_id" value="${ticket.customer_id || ''}" />
|
||||
<div id="qe-customer-suggestions" class="autocomplete-suggestions" style="display:none; top: 100%;"></div>
|
||||
</div>
|
||||
<div class="quick-edit-field">
|
||||
<label class="quick-edit-label">Tempo (minuti)</label>
|
||||
<input type="number" step="any" min="0" class="form-input" id="qe-time-unit" placeholder="minuti" style="padding: 6px 12px; height: 32px; font-size: 0.85rem;" />
|
||||
</div>
|
||||
<div class="quick-edit-field full-width">
|
||||
<label class="quick-edit-label">Oggetto Nota (opzionale)</label>
|
||||
<input type="text" class="form-input quick-edit-input" id="qe-note-subject" placeholder="Oggetto della nota..." style="padding: 6px 12px; height: 32px; font-size: 0.85rem;" />
|
||||
@@ -253,7 +315,11 @@ const TicketDetailView = {
|
||||
<label class="quick-edit-label">Nota / Commento</label>
|
||||
<textarea class="quick-edit-textarea" id="qe-note" placeholder="Inserisci una nota..."></textarea>
|
||||
</div>
|
||||
<div style="margin-top:var(--space-md);display:flex;gap:var(--space-sm);justify-content:flex-end;grid-column:1 / -1;">
|
||||
<div style="margin-top:var(--space-md);display:flex;gap:var(--space-sm);align-items:center;justify-content:flex-end;grid-column:1 / -1;flex-wrap:wrap;">
|
||||
<div style="display:flex;align-items:center;gap:var(--space-xs);margin-right:auto;">
|
||||
<label for="qe-time-unit" style="font-size:0.85rem;font-weight:600;color:var(--text-secondary);white-space:nowrap;">Tempo (minuti):</label>
|
||||
<input type="number" step="any" min="0" class="form-input" id="qe-time-unit" placeholder="minuti" style="padding:4px 8px; height:32px; font-size:0.85rem; width:110px;" />
|
||||
</div>
|
||||
<button class="btn btn-ghost btn-sm" id="qe-reset">Reset</button>
|
||||
<button class="btn btn-primary btn-sm" id="qe-save" disabled>Salva Modifiche</button>
|
||||
</div>
|
||||
@@ -262,7 +328,17 @@ const TicketDetailView = {
|
||||
|
||||
<!-- Add Note Form -->
|
||||
<div class="add-note-form" style="margin-bottom: var(--space-lg);">
|
||||
<div class="card-title">Aggiungi Nota</div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom: var(--space-md); flex-wrap:wrap; gap:var(--space-sm);">
|
||||
<div class="card-title" style="margin-bottom:0;">Aggiungi Nota</div>
|
||||
<div style="display:flex; align-items:center; gap:var(--space-xs);">
|
||||
<label for="note-ticket-state" style="font-size:0.85rem; font-weight:600; color:var(--text-secondary);">Stato ticket:</label>
|
||||
<select id="note-ticket-state" class="quick-edit-select" style="height:32px; padding:4px 8px; font-size:0.85rem; margin-bottom:0; width:auto;">
|
||||
${(App.lookups.states || []).map(s =>
|
||||
`<option value="${s.id}" ${s.id === ticket.ticket_state_id ? 'selected' : ''}>${App.escapeHtml(s.name)}</option>`
|
||||
).join('')}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:var(--space-md); margin-bottom:var(--space-md);">
|
||||
<input type="text" class="note-subject-input" id="note-subject" placeholder="Oggetto (opzionale)" style="flex:1; margin-bottom:0;" />
|
||||
</div>
|
||||
@@ -304,34 +380,82 @@ const TicketDetailView = {
|
||||
</div>
|
||||
<div class="articles-timeline">
|
||||
${articles.length > 0 ? articles.map(a => {
|
||||
const hasHtml = (a.a_content_type || '').toLowerCase().includes('html') || a.a_body.includes('</') || a.a_body.includes('/>');
|
||||
const hasHtml = a.has_html || (a.a_content_type || '').toLowerCase().includes('html') || /<(div|p|br|table|html|body|span|img|a|style)\b/i.test(a.html_body || a.a_body || '');
|
||||
|
||||
let processedBody = a.a_body || '';
|
||||
let processedBody = (hasHtml && a.html_body) ? a.html_body : (a.a_body || '');
|
||||
const articleAttachments = (attachments || []).filter(att => att.article_id === a.article_id);
|
||||
const usedAttachmentIds = new Set();
|
||||
|
||||
if (hasHtml) {
|
||||
if (hasHtml && this.htmlMode) {
|
||||
articleAttachments.forEach(att => {
|
||||
let replaced = false;
|
||||
if (att.content_id) {
|
||||
const cleanCid = att.content_id.replace(/[<>]/g, '').trim();
|
||||
if (cleanCid) {
|
||||
const escapedCid = cleanCid.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
const regex = new RegExp(`cid:<?${escapedCid}>?`, 'gi');
|
||||
processedBody = processedBody.replace(regex, `/api/tickets/attachments/${att.id}`);
|
||||
if (regex.test(processedBody)) {
|
||||
processedBody = processedBody.replace(regex, `/api/tickets/attachments/${att.id}`);
|
||||
replaced = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (att.filename) {
|
||||
const escapedFilename = att.filename.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
const regexFilename = new RegExp(`cid:<?${escapedFilename}>?`, 'gi');
|
||||
processedBody = processedBody.replace(regexFilename, `/api/tickets/attachments/${att.id}`);
|
||||
if (regexFilename.test(processedBody)) {
|
||||
processedBody = processedBody.replace(regexFilename, `/api/tickets/attachments/${att.id}`);
|
||||
replaced = true;
|
||||
}
|
||||
const regexSrc = new RegExp(`src=["']${escapedFilename}["']`, 'gi');
|
||||
if (regexSrc.test(processedBody)) {
|
||||
processedBody = processedBody.replace(regexSrc, `src="/api/tickets/attachments/${att.id}"`);
|
||||
replaced = true;
|
||||
}
|
||||
|
||||
const nameWithoutExt = att.filename.replace(/\.[^/.]+$/, '');
|
||||
if (nameWithoutExt && nameWithoutExt.length > 3) {
|
||||
const escapedBase = nameWithoutExt.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||
const regexBase = new RegExp(`cid:<?${escapedBase}[^"'>\\s]*>?`, 'gi');
|
||||
if (regexBase.test(processedBody)) {
|
||||
processedBody = processedBody.replace(regexBase, `/api/tickets/attachments/${att.id}`);
|
||||
replaced = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (att.disposition === 'inline' || replaced) {
|
||||
usedAttachmentIds.add(att.id);
|
||||
}
|
||||
});
|
||||
|
||||
if (!processedBody.includes('<base')) {
|
||||
if (processedBody.includes('<head>')) {
|
||||
processedBody = processedBody.replace('<head>', '<head><base target="_blank">');
|
||||
} else {
|
||||
processedBody = `<base target="_blank">${processedBody}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const displayBody = this.htmlMode && hasHtml
|
||||
? `<iframe srcdoc="${processedBody.replace(/"/g, '"')}" style="width:100%; border:none; background:var(--bg-card); border-radius:var(--radius-md); min-height:220px; font-family:inherit; color-scheme: dark;"></iframe>`
|
||||
: `<div class="article-body">${App.escapeHtml(a.a_body || '')}</div>`;
|
||||
const displayBody = (this.htmlMode && hasHtml)
|
||||
? `<iframe id="article-iframe-${a.article_id}" srcdoc="${processedBody.replace(/"/g, '"')}"
|
||||
onload="try{this.style.height=(this.contentWindow.document.body.scrollHeight+30)+'px';}catch(e){}"
|
||||
style="width:100%; border:none; background:#ffffff; color:#222222; border-radius:var(--radius-md); min-height:160px; padding:0; display:block;">
|
||||
</iframe>`
|
||||
: `<div class="article-body" style="white-space:pre-wrap; font-family:inherit;">${App.linkifyText(App.escapeHtml(a.a_body || ''))}</div>`;
|
||||
|
||||
const visibleAttachments = articleAttachments.filter(att => att.filename !== 'file-1' && att.filename !== 'file-2');
|
||||
const visibleAttachments = articleAttachments.filter(att => {
|
||||
const fname = (att.filename || '').toLowerCase();
|
||||
if (fname.startsWith('file-1') || fname.startsWith('file-2') || fname.startsWith('file-3')) {
|
||||
return false;
|
||||
}
|
||||
if (usedAttachmentIds.has(att.id)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const extractedLinks = this.extractLinks(processedBody || a.a_body || '');
|
||||
|
||||
return `
|
||||
<div class="article-card sender-${(a.sender_type || 'system').toLowerCase()}">
|
||||
@@ -341,8 +465,12 @@ const TicketDetailView = {
|
||||
<span style="font-size: 0.72rem; color: var(--text-muted); font-family: monospace; margin-right: var(--space-xs);">ID: ${a.article_id}</span>
|
||||
<span class="article-from">${App.escapeHtml(a.a_from || a.creator_first + ' ' + a.creator_last || 'Sistema')}</span>
|
||||
${a.channel_name ? `<span style="font-size:0.72rem;color:var(--text-muted);">via ${a.channel_name}</span>` : ''}
|
||||
<button class="btn-delete-article" data-article-id="${a.article_id}" style="background:none; border:none; cursor:pointer; font-size:0.85rem; padding: 2px; margin-left: var(--space-xs); display:inline-flex; align-items:center; opacity: 0.6; transition: opacity 0.2s;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.6" title="Elimina Articolo">🗑️</button>
|
||||
<button class="btn-email-article" data-article-id="${a.article_id}" style="background:none; border:none; cursor:pointer; font-size:0.85rem; padding: 2px; margin-left: var(--space-xs); display:inline-flex; align-items:center; opacity: 0.6; transition: opacity 0.2s;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.6" title="Rispondi via email (Quota questo articolo)">✉️</button>
|
||||
<button class="btn-delete-article" data-article-id="${a.article_id}" style="background:var(--bg-tertiary); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); cursor:pointer; padding:3px 6px; margin-left:var(--space-xs); display:inline-flex; align-items:center; justify-content:center; color:var(--danger); transition:all 0.15s ease; opacity:1;" title="Elimina Articolo">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px; height:14px;"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>
|
||||
</button>
|
||||
<button class="btn-email-article" data-article-id="${a.article_id}" style="background:var(--bg-tertiary); border:1px solid var(--border-subtle); border-radius:var(--radius-sm); cursor:pointer; padding:3px 6px; margin-left:var(--space-xs); display:inline-flex; align-items:center; justify-content:center; color:var(--accent-secondary); transition:all 0.15s ease; opacity:1;" title="Rispondi via email (Quota questo articolo)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px; height:14px;"><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>
|
||||
</button>
|
||||
</div>
|
||||
<div style="display:flex; gap: var(--space-sm); align-items:center;">
|
||||
<div class="time-edit-container" data-article-id="${a.article_id}" style="display:inline-flex; align-items:center; gap:4px; position:relative;">
|
||||
@@ -373,20 +501,68 @@ const TicketDetailView = {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${a.a_subject ? `<div class="article-subject">${App.escapeHtml(a.a_subject)}</div>` : ''}
|
||||
${displayBody}
|
||||
${a.a_subject ? `<div class="article-subject" style="font-weight:700; font-size:0.95rem; color:var(--text-primary); margin-bottom:var(--space-xs);">${App.escapeHtml(a.a_subject)}</div>` : ''}
|
||||
|
||||
${visibleAttachments.length > 0 ? `
|
||||
<div class="article-attachments">
|
||||
${visibleAttachments.map(att => `
|
||||
<a href="/api/tickets/attachments/${att.id}" class="attachment-badge" target="_blank" download="${att.filename}" data-is-image="${(att.content_type || '').startsWith('image/')}">
|
||||
<span>📎</span>
|
||||
<strong>${App.escapeHtml(att.filename)}</strong>
|
||||
<span class="attachment-size">(${Math.round(att.content_size / 1024)} KB)</span>
|
||||
</a>
|
||||
`).join('')}
|
||||
<div class="article-content-layout" style="display: flex; gap: var(--space-md); align-items: flex-start; flex-wrap: wrap;">
|
||||
<div class="article-body-main" style="flex: 1; min-width: 280px; width: 0;">
|
||||
${displayBody}
|
||||
|
||||
${visibleAttachments.length > 0 ? `
|
||||
<div class="article-attachments">
|
||||
${visibleAttachments.map(att => `
|
||||
<a href="/api/tickets/attachments/${att.id}" class="attachment-badge" target="_blank" download="${att.filename}" data-is-image="${(att.content_type || '').startsWith('image/')}">
|
||||
<span>📎</span>
|
||||
<strong>${App.escapeHtml(att.filename)}</strong>
|
||||
<span class="attachment-size">(${Math.round(att.content_size / 1024)} KB)</span>
|
||||
</a>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Scheda a lato per ogni mail con Da: A: Cc: e lista Link -->
|
||||
<div class="article-email-side-card" style="width: 260px; min-width: 220px; background: var(--bg-tertiary); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); padding: 12px; font-size: 0.78rem; align-self: flex-start; box-shadow: var(--shadow-sm);">
|
||||
<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; gap: 4px; font-size: 0.8rem;">
|
||||
<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>
|
||||
Dettagli Mail
|
||||
</div>
|
||||
<div style="margin-bottom: 6px; word-break: break-all; line-height: 1.35;">
|
||||
<strong style="color: var(--text-muted);">Da:</strong>
|
||||
<span style="color: var(--text-primary); font-family: inherit;">${App.escapeHtml(a.a_from || '—')}</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 6px; word-break: break-all; line-height: 1.35;">
|
||||
<strong style="color: var(--text-muted);">A:</strong>
|
||||
<span style="color: var(--text-primary); font-family: inherit;">${App.escapeHtml(a.a_to || '—')}</span>
|
||||
</div>
|
||||
${a.a_cc ? `
|
||||
<div style="margin-bottom: 6px; word-break: break-all; line-height: 1.35;">
|
||||
<strong style="color: var(--text-muted);">Cc:</strong>
|
||||
<span style="color: var(--text-primary); font-family: inherit;">${App.escapeHtml(a.a_cc)}</span>
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<div style="font-weight: 700; color: var(--text-secondary); margin-top: 12px; 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;">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;color:var(--accent-primary);"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
|
||||
Link contenuti
|
||||
</span>
|
||||
<span class="badge" style="font-size: 0.7rem; padding: 1px 6px; background: var(--bg-card);">${extractedLinks.length}</span>
|
||||
</div>
|
||||
|
||||
${extractedLinks.length > 0 ? `
|
||||
<div class="article-links-minilist" style="display: flex; flex-direction: column; gap: 6px; max-height: 200px; overflow-y: auto; padding-right: 2px;">
|
||||
${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-card); padding: 4px 6px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle); transition: border-color 0.15s ease;">
|
||||
<span style="flex-shrink:0; font-size: 0.7rem;">🌐</span>
|
||||
<span style="overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; word-break: break-word;">${App.escapeHtml(link.text || link.url)}</span>
|
||||
</a>
|
||||
`).join('')}
|
||||
</div>
|
||||
` : `
|
||||
<div style="color: var(--text-muted); font-size: 0.75rem; font-style: italic;">Nessun link presente</div>
|
||||
`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('') : `
|
||||
@@ -497,37 +673,6 @@ const TicketDetailView = {
|
||||
</div>
|
||||
` : ''}
|
||||
|
||||
<!-- Panel per i Gruppi Ticket -->
|
||||
<div class="sidebar-panel" id="ticket-groups-panel">
|
||||
<div class="sidebar-panel-title">Gruppi Ticket</div>
|
||||
<div id="ticket-groups-list" style="margin-bottom: var(--space-sm);">
|
||||
${groupsData.asMaster.length === 0 && groupsData.asMember.length === 0 ? `
|
||||
<div style="font-size:0.78rem; color:var(--text-muted); padding:4px 0;">Nessun gruppo associato</div>
|
||||
` : ''}
|
||||
${groupsData.asMaster.map(g => `
|
||||
<div style="font-size:0.8rem; margin-bottom:4px;">
|
||||
<span style="color:var(--accent-primary); font-weight:bold;">👑 Master in:</span>
|
||||
<a href="#/tickets/groups" onclick="localStorage.setItem('otrs_selected_group_id', ${g.id})" style="color:var(--text-primary); text-decoration:none; border-bottom:1px dashed var(--text-muted);">${App.escapeHtml(g.nome)}</a>
|
||||
</div>
|
||||
`).join('')}
|
||||
${groupsData.asMember.map(g => `
|
||||
<div style="font-size:0.8rem; margin-bottom:4px;">
|
||||
<span style="color:var(--text-secondary); font-weight:bold;">🔗 Membro di:</span>
|
||||
<a href="#/tickets/groups" onclick="localStorage.setItem('otrs_selected_group_id', ${g.id})" style="color:var(--text-primary); text-decoration:none; border-bottom:1px dashed var(--text-muted);">${App.escapeHtml(g.nome)}</a>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
|
||||
<div style="border-top:1px solid var(--border-subtle); padding-top:var(--space-xs); margin-top:var(--space-xs);">
|
||||
<label class="quick-edit-label">Associa a Gruppo</label>
|
||||
<div style="display:flex; gap:4px; margin-top:4px;">
|
||||
<select class="form-select" id="group-association-select" style="padding:4px 20px 4px 8px; font-size:0.78rem; height:28px; margin:0; flex:1;">
|
||||
<option value="">-- Seleziona --</option>
|
||||
</select>
|
||||
<button class="btn btn-primary btn-sm" id="btn-associate-to-group" style="height:28px; padding:0 8px; font-size:0.75rem;">+ Aggiungi</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -579,6 +724,9 @@ const TicketDetailView = {
|
||||
if (document.getElementById('note-time-units')) {
|
||||
document.getElementById('note-time-units').value = noteDraft.time_unit || '';
|
||||
}
|
||||
if (document.getElementById('note-ticket-state') && noteDraft.ticket_state_id) {
|
||||
document.getElementById('note-ticket-state').value = noteDraft.ticket_state_id;
|
||||
}
|
||||
this.noteAttachments = noteDraft.attachments || [];
|
||||
this.updateNoteAttachmentList();
|
||||
}
|
||||
@@ -891,6 +1039,19 @@ const TicketDetailView = {
|
||||
}
|
||||
});
|
||||
|
||||
// Shortcut for Quick Edit save
|
||||
const quickEditContainer = container.querySelector('.quick-edit');
|
||||
if (quickEditContainer) {
|
||||
quickEditContainer.addEventListener('keydown', (e) => {
|
||||
if (App.isShortcutTriggered(e, 'quick_edit_save')) {
|
||||
e.preventDefault();
|
||||
if (saveBtn && !saveBtn.disabled) {
|
||||
saveBtn.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Send note
|
||||
const noteSendBtn = document.getElementById('note-send');
|
||||
noteSendBtn.addEventListener('click', async () => {
|
||||
@@ -925,6 +1086,9 @@ const TicketDetailView = {
|
||||
return;
|
||||
}
|
||||
|
||||
const noteStateSelect = document.getElementById('note-ticket-state');
|
||||
const ticket_state_id = noteStateSelect ? noteStateSelect.value : null;
|
||||
|
||||
try {
|
||||
noteSendBtn.disabled = true;
|
||||
noteSendBtn.innerHTML = '<div class="spinner" style="width:14px;height:14px;border-width:2px;"></div> Invio...';
|
||||
@@ -935,6 +1099,7 @@ const TicketDetailView = {
|
||||
subject,
|
||||
body,
|
||||
time_unit,
|
||||
ticket_state_id: ticket_state_id ? parseInt(ticket_state_id) : undefined,
|
||||
attachments: this.noteAttachments.length > 0 ? this.noteAttachments : undefined
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -46,7 +46,25 @@ const TimeAccountingView = {
|
||||
date: this.selectedDate,
|
||||
});
|
||||
|
||||
const data = await App.api(`/api/tickets/time-accounting/summary?${params}`);
|
||||
let data = await App.api(`/api/tickets/time-accounting/summary?${params}`);
|
||||
|
||||
// If non-working days are hidden, ensure selectedDate is a valid working day
|
||||
if (!this.showNonWorkingDays) {
|
||||
const workDays = (data && data.work_days) || [1, 2, 3, 4, 5];
|
||||
const parts = this.selectedDate.split('-').map(Number);
|
||||
const d = new Date(parts[0], parts[1] - 1, parts[2]);
|
||||
let shifted = false;
|
||||
while (workDays.length > 0 && !workDays.includes(d.getDay())) {
|
||||
d.setDate(d.getDate() - 1);
|
||||
shifted = true;
|
||||
}
|
||||
if (shifted) {
|
||||
this.selectedDate = this.formatLocalISO(d);
|
||||
params.set('date', this.selectedDate);
|
||||
data = await App.api(`/api/tickets/time-accounting/summary?${params}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.summaryData = data;
|
||||
|
||||
// Auto-select first article if current selection is invalid
|
||||
@@ -192,22 +210,43 @@ const TimeAccountingView = {
|
||||
const targetMins = (data && data.target_minutes) || 480;
|
||||
const workDays = (data && data.work_days) || [1, 2, 3, 4, 5];
|
||||
|
||||
// Generate 7 days before and 7 days after selected date (15 days total)
|
||||
// Generate 2 full weeks of days (15 calendar days if showing all, or 10-11 working days if hiding non-working days)
|
||||
const dateList = [];
|
||||
if (this.showNonWorkingDays) {
|
||||
for (let offset = -7; offset <= 7; offset++) {
|
||||
const d = new Date(selected);
|
||||
d.setDate(d.getDate() + offset);
|
||||
dateList.push(d);
|
||||
}
|
||||
} else {
|
||||
const before = [];
|
||||
let dPrev = new Date(selected);
|
||||
while (before.length < 5) {
|
||||
dPrev.setDate(dPrev.getDate() - 1);
|
||||
if (workDays.includes(dPrev.getDay())) {
|
||||
before.unshift(new Date(dPrev));
|
||||
}
|
||||
}
|
||||
const current = new Date(selected);
|
||||
const after = [];
|
||||
let dNext = new Date(selected);
|
||||
while (after.length < 5) {
|
||||
dNext.setDate(dNext.getDate() + 1);
|
||||
if (workDays.includes(dNext.getDay())) {
|
||||
after.push(new Date(dNext));
|
||||
}
|
||||
}
|
||||
dateList.push(...before, current, ...after);
|
||||
}
|
||||
|
||||
const days = [];
|
||||
for (let offset = -7; offset <= 7; offset++) {
|
||||
const d = new Date(selected);
|
||||
d.setDate(d.getDate() + offset);
|
||||
for (const d of dateList) {
|
||||
const isoStr = this.formatLocalISO(d);
|
||||
const dayOfWeek = d.getDay(); // 0 = Sun, 1 = Mon ... 6 = Sat
|
||||
const isWorkDay = workDays.includes(dayOfWeek);
|
||||
const isSelected = isoStr === this.selectedDate;
|
||||
const isToday = isoStr === todayStr;
|
||||
|
||||
// Filter out non-working days if toggle is off, unless it's currently selected
|
||||
if (!isWorkDay && !this.showNonWorkingDays && !isSelected) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const dayNum = d.getDate();
|
||||
const weekdayName = weekdays[dayOfWeek];
|
||||
const monthName = months[d.getMonth()];
|
||||
@@ -413,9 +452,7 @@ const TimeAccountingView = {
|
||||
document.getElementById('ta-btn-toggle-nonwork')?.addEventListener('click', () => {
|
||||
this.showNonWorkingDays = !this.showNonWorkingDays;
|
||||
localStorage.setItem('otrs_ta_show_non_working_days', this.showNonWorkingDays);
|
||||
const container = document.getElementById('view-container');
|
||||
container.innerHTML = this.buildHtml(this.summaryData);
|
||||
this.bindEvents();
|
||||
this.loadData();
|
||||
});
|
||||
|
||||
// Day pill clicks
|
||||
@@ -428,18 +465,24 @@ const TimeAccountingView = {
|
||||
|
||||
// Previous day button
|
||||
document.getElementById('ta-day-prev')?.addEventListener('click', () => {
|
||||
const workDays = (this.summaryData && this.summaryData.work_days) || [1, 2, 3, 4, 5];
|
||||
const parts = this.selectedDate.split('-').map(Number);
|
||||
const d = new Date(parts[0], parts[1] - 1, parts[2]);
|
||||
d.setDate(d.getDate() - 1);
|
||||
do {
|
||||
d.setDate(d.getDate() - 1);
|
||||
} while (!this.showNonWorkingDays && workDays.length > 0 && !workDays.includes(d.getDay()));
|
||||
this.selectedDate = this.formatLocalISO(d);
|
||||
this.loadData();
|
||||
});
|
||||
|
||||
// Next day button
|
||||
document.getElementById('ta-day-next')?.addEventListener('click', () => {
|
||||
const workDays = (this.summaryData && this.summaryData.work_days) || [1, 2, 3, 4, 5];
|
||||
const parts = this.selectedDate.split('-').map(Number);
|
||||
const d = new Date(parts[0], parts[1] - 1, parts[2]);
|
||||
d.setDate(d.getDate() + 1);
|
||||
do {
|
||||
d.setDate(d.getDate() + 1);
|
||||
} while (!this.showNonWorkingDays && workDays.length > 0 && !workDays.includes(d.getDay()));
|
||||
this.selectedDate = this.formatLocalISO(d);
|
||||
this.loadData();
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user