/**
* OTRS Turbo — Core Application
* SPA router, API client, lookup cache, and utility functions.
*/
const App = {
lookups: {
queues: [],
states: [],
priorities: [],
users: [],
types: [],
},
lookupsLoaded: false,
demotivationalPhrases: [],
motivationalPhrases: [],
drafts: {},
tabs: [],
lastListView: '#/tickets',
loadTabs() {
try {
const saved = localStorage.getItem('otrs_turbo_tabs');
if (saved) this.tabs = JSON.parse(saved);
const savedDrafts = localStorage.getItem('otrs_turbo_drafts');
if (savedDrafts) this.drafts = JSON.parse(savedDrafts);
} catch (e) {}
this.renderTabs();
},
saveTabs() {
localStorage.setItem('otrs_turbo_tabs', JSON.stringify(this.tabs));
this.renderTabs();
},
saveDraft(ticketId, draft) {
if (!this.drafts[ticketId]) this.drafts[ticketId] = {};
this.drafts[ticketId][draft.type] = draft;
localStorage.setItem('otrs_turbo_drafts', JSON.stringify(this.drafts));
},
getDraft(ticketId, type) {
return this.drafts[ticketId] ? this.drafts[ticketId][type] : null;
},
clearDraft(ticketId, type) {
if (this.drafts[ticketId] && this.drafts[ticketId][type]) {
delete this.drafts[ticketId][type];
if (Object.keys(this.drafts[ticketId]).length === 0) {
delete this.drafts[ticketId];
}
localStorage.setItem('otrs_turbo_drafts', JSON.stringify(this.drafts));
this.renderTabs();
}
},
openTab(id, tn, title) {
const exists = this.tabs.find(t => String(t.id) === String(id));
if (!exists) {
this.tabs.push({ id, tn, title });
this.saveTabs();
}
},
addTabWithoutRedirect(id, tn, title) {
const exists = this.tabs.find(t => String(t.id) === String(id));
if (!exists) {
this.tabs.push({ id, tn, title });
this.saveTabs();
} else {
if (title && exists.title !== title) {
exists.title = title;
this.saveTabs();
} else {
this.renderTabs();
}
}
},
closeTab(id, e) {
if (e) e.stopPropagation();
this.tabs = this.tabs.filter(t => String(t.id) !== String(id));
this.saveTabs();
// Clear drafts for closed tab
delete this.drafts[id];
localStorage.setItem('otrs_turbo_drafts', JSON.stringify(this.drafts));
const hash = window.location.hash;
if (hash === `#/tickets/${id}`) {
if (this.tabs.length > 0) {
window.location.hash = `#/tickets/${this.tabs[this.tabs.length - 1].id}`;
} else {
window.location.hash = '#/tickets';
}
}
},
renderTabs() {
const bar = document.getElementById('tabs-bar');
if (!bar) return;
if (this.tabs.length === 0) {
bar.style.display = 'none';
this.updateHeaderHeight();
return;
}
bar.style.display = 'flex';
const currentHash = window.location.hash;
bar.innerHTML = this.tabs.map(t => {
const isActive = currentHash === `#/tickets/${t.id}`;
const hasEmailDraft = this.getDraft(t.id, 'email');
const emailIconHtml = hasEmailDraft ? `✉️` : '';
const displayTitle = t.title ? (t.title.length > 25 ? t.title.substring(0, 22) + '...' : t.title) : `#${t.tn}`;
return `
${emailIconHtml}
${App.escapeHtml(displayTitle)}
`;
}).join('');
this.updateHeaderHeight();
},
updateHeaderHeight() {
const topbar = document.getElementById('topbar');
if (topbar) {
const height = topbar.offsetHeight;
document.documentElement.style.setProperty('--topbar-total-height', `${height}px`);
}
},
get currentAgentId() {
return parseInt(localStorage.getItem('activeAgentId') || '1', 10);
},
/** Initialize the application */
init() {
this.initTheme();
this.loadDemotivationalPhrases();
this.loadMotivationalPhrases();
this.loadTabs();
Toast.init();
// Hash-based SPA router
window.addEventListener('hashchange', () => this.route());
// Global search
const searchInput = document.getElementById('global-search');
const searchClearBtn = document.getElementById('global-search-clear');
if (searchInput) {
const toggleClearBtn = () => {
if (searchClearBtn) {
searchClearBtn.style.display = searchInput.value.trim() ? 'flex' : 'none';
}
};
let timeout;
searchInput.addEventListener('input', () => {
toggleClearBtn();
clearTimeout(timeout);
timeout = setTimeout(() => {
const hash = window.location.hash;
if (hash.startsWith('#/tickets') && !hash.includes('/new') && !hash.match(/#\/tickets\/\d+/)) {
TicketListView.currentPage = 1;
TicketListView.render();
} else {
// Navigate to ticket list with search
window.location.hash = '#/tickets';
}
}, 350);
});
searchInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const hash = window.location.hash;
if (!hash.startsWith('#/tickets') || hash.includes('/new') || hash.match(/#\/tickets\/\d+/)) {
window.location.hash = '#/tickets';
} else {
TicketListView.currentPage = 1;
TicketListView.render();
}
}
});
if (searchClearBtn) {
searchClearBtn.addEventListener('click', () => {
searchInput.value = '';
searchClearBtn.style.display = 'none';
searchInput.focus();
const hash = window.location.hash;
if (hash.startsWith('#/tickets') && !hash.includes('/new') && !hash.match(/#\/tickets\/\d+/)) {
TicketListView.currentPage = 1;
TicketListView.render();
} else {
window.location.hash = '#/tickets';
}
});
}
// Initial state of clear button
toggleClearBtn();
}
// Check DB connection
this.checkConnection();
// Init active agent selector
this.initAgentSelector();
// Bind refresh lookups button
const refreshBtn = document.getElementById('refresh-lookups-btn');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => this.refreshLookups());
}
// Initial route
window.addEventListener('resize', () => this.updateHeaderHeight());
setTimeout(() => this.updateHeaderHeight(), 100);
if (!window.location.hash || window.location.hash === '#/') {
window.location.hash = '#/dashboard';
} else {
this.route();
}
},
/** Route based on current hash */
route() {
const fullHash = window.location.hash || '#/dashboard';
const hash = fullHash.split('?')[0];
const titleEl = document.getElementById('page-title');
if (hash === '#/tickets' || hash === '#/tickets/my') {
this.lastListView = hash;
}
// Save draft for previous ticket before routing
if (typeof TicketDetailView !== 'undefined' && TicketDetailView.ticketId) {
TicketDetailView.saveDraft();
if (typeof EmailCompose !== 'undefined') {
EmailCompose.saveDraft();
const overlay = document.getElementById('email-compose-overlay');
if (overlay) overlay.remove();
}
}
this.renderTabs();
this.updateHeaderHeight();
const container = document.getElementById('view-container');
if (container) {
if (hash.match(/^#\/tickets\/(\d+)$/)) {
container.classList.add('ticket-view-active');
} else {
container.classList.remove('ticket-view-active');
}
}
// Update active nav link
document.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
});
if (hash === '#/dashboard') {
document.getElementById('nav-dashboard')?.classList.add('active');
titleEl.textContent = 'Dashboard';
DashboardView.render();
} else if (hash === '#/tickets') {
document.getElementById('nav-tickets')?.classList.add('active');
titleEl.textContent = 'Ticket';
TicketListView.render();
} else if (hash === '#/tickets/my') {
document.getElementById('nav-tickets-my')?.classList.add('active');
titleEl.textContent = 'Ticket a mio carico';
TicketListView.render();
} else if (hash === '#/tickets/new') {
document.getElementById('nav-new-ticket')?.classList.add('active');
titleEl.textContent = 'Nuovo Ticket';
TicketCreateView.render();
} else if (hash === '#/tickets/bulk') {
document.getElementById('nav-bulk-tickets')?.classList.add('active');
titleEl.textContent = 'Apertura Massiva Ticket';
TicketBulkView.render();
} else if (hash === '#/tickets/groups') {
document.getElementById('nav-ticket-groups')?.classList.add('active');
titleEl.textContent = 'Gruppi ticket';
TicketGroupsView.render();
} else if (hash === '#/activity') {
document.getElementById('nav-activity')?.classList.add('active');
titleEl.textContent = 'Storico Attività';
ActivityLogView.render();
} else if (hash === '#/mail-management') {
document.getElementById('nav-mail-management')?.classList.add('active');
titleEl.textContent = 'Gestione Mail';
MailManagementView.render();
} else if (hash.match(/^#\/tickets\/(\d+)$/)) {
const id = hash.match(/^#\/tickets\/(\d+)$/)[1];
document.getElementById('nav-tickets')?.classList.add('active');
titleEl.textContent = `Ticket #${id}`;
TicketDetailView.render(id);
} else {
// Fallback to dashboard
window.location.hash = '#/dashboard';
}
},
/** API fetch wrapper */
async api(url, options = {}) {
const activeAgentId = localStorage.getItem('activeAgentId') || '1';
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
'X-Agent-ID': activeAgentId,
},
};
const headers = { ...defaultOptions.headers, ...(options.headers || {}) };
const response = await fetch(url, { ...defaultOptions, ...options, headers });
if (!response.ok) {
const errData = await response.json().catch(() => ({}));
throw new Error(errData.error || errData.message || `HTTP ${response.status}`);
}
return response.json();
},
/** Ensure lookup data is loaded (cached) */
async ensureLookups(forceRefresh = false) {
if (this.lookupsLoaded && !forceRefresh) return;
// Check localStorage cache first if not forcing refresh
if (!forceRefresh) {
const cached = localStorage.getItem('otrs_lookups');
if (cached) {
try {
this.lookups = JSON.parse(cached);
if (!this.lookups.config || this.lookups.config.autoTimeMinHour === undefined || !this.lookups.customerUsers || !this.lookups.customer_users_version_1) {
throw new Error('Outdated config cache (missing autoTimeMinHour or customerUsers)');
}
this.lookupsLoaded = true;
return;
} catch (e) {
console.warn('Failed to parse cached lookups, reloading...', e.message);
}
}
}
try {
const [queues, states, priorities, users, types, config, customerUsers] = await Promise.all([
this.api('/api/queues'),
this.api('/api/states'),
this.api('/api/priorities'),
this.api('/api/users'),
this.api('/api/types'),
this.api('/api/config').catch(() => ({ defaultAgentLogin: '' })),
this.api('/api/customer-users/search?q=').catch(() => []),
]);
this.lookups = { queues, states, priorities, users, types, config, customerUsers, customer_users_version_1: true };
localStorage.setItem('otrs_lookups', JSON.stringify(this.lookups));
this.lookupsLoaded = true;
} catch (err) {
console.error('Failed to load lookups:', err);
throw err;
}
},
/** Force refresh of lookups */
async refreshLookups() {
const btn = document.getElementById('refresh-lookups-btn');
const origHtml = btn ? btn.innerHTML : '';
if (btn) {
btn.disabled = true;
btn.innerHTML = '';
}
try {
// Sync LDAP customer users to local DB cache
await this.api('/api/customer-users/sync', { method: 'POST' });
await this.ensureLookups(true);
await this.initAgentSelector();
Toast.success('Dati locali (code, utenti, ecc.) aggiornati con successo!');
// If we are on a view that needs lookups, we can re-render it
const hash = window.location.hash;
if (hash === '#/tickets/bulk') {
TicketBulkView.render();
} else if (hash === '#/tickets/new') {
TicketCreateView.render();
}
} catch (err) {
Toast.error('Errore durante l\'aggiornamento: ' + err.message);
} finally {
if (btn) {
btn.disabled = false;
btn.innerHTML = origHtml;
}
}
},
/** Check database connection */
async checkConnection() {
const dot = document.getElementById('connection-status');
const text = document.getElementById('connection-text');
try {
await this.api('/api/queues');
dot.classList.add('connected');
dot.classList.remove('error');
text.textContent = 'DB connesso';
} catch (err) {
dot.classList.add('error');
dot.classList.remove('connected');
text.textContent = 'DB non raggiungibile';
Toast.error('Impossibile connettersi al database OTRS');
}
},
/** Escape HTML to prevent XSS */
escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str || '';
return div.innerHTML;
},
/** Format date for display */
formatDate(dateStr) {
if (!dateStr) return '—';
try {
const d = new Date(dateStr);
return d.toLocaleDateString('it-IT', { day: '2-digit', month: '2-digit', year: 'numeric' });
} catch {
return dateStr;
}
},
/** Format date+time for display */
formatDateTime(dateStr) {
if (!dateStr) return '—';
try {
const d = new Date(dateStr);
return d.toLocaleDateString('it-IT', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit',
});
} catch {
return dateStr;
}
},
/** Initialize active agent dropdown */
async initAgentSelector() {
const select = document.getElementById('active-agent-select');
if (!select) return;
try {
// Ensure lookups are loaded
await this.ensureLookups();
// Populate select dropdown
select.innerHTML = (this.lookups.users || []).map(u =>
``
).join('');
// Load saved agent ID or default to the config-specified default agent, or first available
const savedAgentId = localStorage.getItem('activeAgentId');
const defaultAgentLogin = this.lookups.config ? this.lookups.config.defaultAgentLogin : null;
const defaultAgent = defaultAgentLogin
? (this.lookups.users || []).find(u => u.login === defaultAgentLogin)
: null;
if (savedAgentId && (this.lookups.users || []).some(u => String(u.id) === String(savedAgentId))) {
select.value = savedAgentId;
} else if (defaultAgent) {
select.value = defaultAgent.id;
localStorage.setItem('activeAgentId', select.value);
} else if ((this.lookups.users || []).length > 0) {
select.value = this.lookups.users[0].id;
localStorage.setItem('activeAgentId', select.value);
}
// Handle dropdown change event
select.addEventListener('change', () => {
localStorage.setItem('activeAgentId', select.value);
Toast.success(`Agente attivo cambiato: ${select.options[select.selectedIndex].text}`);
this.updateDailyTimer();
this.route();
});
// Initial update
this.updateDailyTimer();
} catch (err) {
console.error('Failed to init agent selector:', err);
}
},
/** Map priority name to a 1-5 index for styling */
priorityIndex(name) {
if (!name) return 3;
const lower = name.toLowerCase();
if (lower.includes('very low') || lower.includes('1')) return 1;
if (lower.includes('low') || lower.includes('2')) return 2;
if (lower.includes('normal') || lower.includes('3')) return 3;
if (lower.includes('high') && !lower.includes('very') || lower.includes('4')) return 4;
if (lower.includes('very high') || lower.includes('5')) return 5;
return 3;
},
/** Initialize and handle theme selection */
initTheme() {
const themeSelect = document.getElementById('theme-select');
const savedTheme = localStorage.getItem('app-theme') || 'light';
// Apply the saved theme class to body
this.applyThemeClass(savedTheme);
if (themeSelect) {
themeSelect.value = savedTheme;
themeSelect.addEventListener('change', () => {
const selectedTheme = themeSelect.value;
this.applyThemeClass(selectedTheme);
localStorage.setItem('app-theme', selectedTheme);
Toast.success(`Tema cambiato in: ${themeSelect.options[themeSelect.selectedIndex].text}`);
});
}
},
/** Helper to apply theme classes to document.body */
applyThemeClass(theme) {
// Remove any existing theme- classes
document.body.className = document.body.className
.split(' ')
.filter(c => !c.startsWith('theme-'))
.join(' ');
if (theme !== 'light') {
document.body.classList.add(`theme-${theme}`);
}
},
/** Load demotivational phrases from SQLite cache */
async loadDemotivationalPhrases() {
try {
const res = await fetch('/api/dashboard/phrases?tipo=demotivational');
if (res.ok) {
this.demotivationalPhrases = await res.json();
}
} catch (e) {
console.warn('Failed to load demotivational phrases:', e);
}
},
/** Load motivational phrases from SQLite cache */
async loadMotivationalPhrases() {
try {
const res = await fetch('/api/dashboard/phrases?tipo=motivational');
if (res.ok) {
this.motivationalPhrases = await res.json();
}
} catch (e) {
console.warn('Failed to load motivational phrases:', e);
}
},
/** Fetch daily time accounting and update sidebar display */
async updateDailyTimer() {
const timerEl = document.getElementById('daily-timer');
if (!timerEl) return;
try {
await this.ensureLookups();
const targetTime = this.lookups.config?.dailyTargetTime || 480;
const data = await this.api('/api/users/time-today');
const todayTime = typeof data.totalToday === 'number' ? data.totalToday : 0;
const remaining = Math.max(0, targetTime - todayTime);
const mathematicalPercentage = Math.round((todayTime / targetTime) * 100);
const hasOverperformance = mathematicalPercentage > 100;
const percentage = Math.min(100, mathematicalPercentage);
const brandEl = document.querySelector('.sidebar-brand');
if (brandEl) {
if (hasOverperformance) {
brandEl.classList.add('glow');
} else {
brandEl.classList.remove('glow');
}
}
if (timerEl) {
if (hasOverperformance) {
timerEl.classList.add('overperformance-alarm');
} else {
timerEl.classList.remove('overperformance-alarm');
}
}
const minTimeStr = this.lookups.config?.autoTimeMinHour || '18:00';
const [minHour, minMin] = minTimeStr.split(':').map(x => parseInt(x, 10));
const now = new Date();
const currentHour = now.getHours();
const currentMin = now.getMinutes();
let isPastTime = false;
if (currentHour > minHour) {
isPastTime = true;
} else if (currentHour === minHour && currentMin >= minMin) {
isPastTime = true;
}
const triggerAction = async (e) => {
e.preventDefault();
e.stopPropagation();
const confirmed = await this.confirm(
'Consuntivazione Automatica',
`Sei sicuro di voler effettuare la consuntivazione automatica di ${remaining} minuti rimanenti di oggi? Verrà creato un ticket chiuso a tuo carico.`
);
if (!confirmed) return;
try {
Toast.success('Consuntivazione in corso...');
const res = await this.api('/api/tickets/auto-time', { method: 'POST' });
this.updateDailyTimer();
this.route();
await this.alert('Consuntivazione Completata', res.message || 'La consuntivazione automatica è stata completata con successo.');
} catch (err) {
Toast.error('Errore consuntivazione automatica: ' + err.message);
}
};
if (isPastTime && remaining > 0) {
if (brandEl) {
brandEl.classList.add('clickable-auto-time');
brandEl.setAttribute('title', `Consuntivazione Automatica fine giornata (${remaining} m)`);
brandEl.onclick = triggerAction;
}
if (timerEl) {
timerEl.classList.add('clickable-auto-time');
//timerEl.setAttribute('title', `Consuntivazione Automatica fine giornata (${remaining} m). Clicca per eseguire.`);
timerEl.onclick = triggerAction;
}
} else {
if (brandEl) {
brandEl.classList.remove('clickable-auto-time');
brandEl.removeAttribute('title');
brandEl.onclick = null;
}
if (timerEl) {
timerEl.classList.remove('clickable-auto-time');
timerEl.removeAttribute('title');
timerEl.onclick = null;
}
}
let phrase = "";
const phraseThreshold = this.lookups.config?.phraseThreshold || 70;
const isDemotivational = percentage >= phraseThreshold;
if (isDemotivational) {
if (this.demotivationalPhrases.length > 0) {
const daySeed = new Date().getDate() + todayTime;
const idx = Math.floor(Math.abs(Math.sin(daySeed) * this.demotivationalPhrases.length));
phrase = this.demotivationalPhrases[idx % this.demotivationalPhrases.length];
} else {
phrase = "Hai fatto fin troppo lavoro per oggi. Smetti.";
}
} else {
if (this.motivationalPhrases.length > 0) {
const daySeed = new Date().getDate() + todayTime;
const idx = Math.floor(Math.abs(Math.sin(daySeed) * this.motivationalPhrases.length));
phrase = this.motivationalPhrases[idx % this.motivationalPhrases.length];
} else {
phrase = "Continua così! Stai andando alla grande.";
}
}
timerEl.innerHTML = `
⏱ ${todayTime} / ${targetTime} m
|
rimanenti: ${remaining} m
`;
// Update sidebar counts as well
this.updateSidebarBadges();
} catch (err) {
console.warn('Failed to update daily timer:', err);
}
},
/** Fetch dashboard stats to update sidebar counts */
async updateSidebarBadges() {
try {
const stats = await this.api('/api/dashboard/stats');
const badge = document.getElementById('open-ticket-count');
if (badge) {
badge.textContent = stats.total_open > 0 ? stats.total_open : '';
}
const myBadge = document.getElementById('my-ticket-count');
if (myBadge) {
myBadge.textContent = stats.total_my_open > 0 ? stats.total_my_open : '';
}
} catch (e) {
console.warn('Failed to update sidebar badges:', e);
}
},
/** Custom confirm dialog in the center of the screen */
confirm(title, message, options = {}) {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100vw';
overlay.style.height = '100vh';
overlay.style.background = 'rgba(0, 0, 0, 0.6)';
overlay.style.backdropFilter = 'blur(4px)';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.zIndex = '99999';
overlay.style.opacity = '0';
overlay.style.transition = 'opacity 0.2s ease';
const card = document.createElement('div');
card.style.background = 'var(--bg-card, #1e1e2e)';
card.style.border = '1px solid var(--border-subtle, #313244)';
card.style.borderRadius = 'var(--radius-lg, 12px)';
card.style.padding = 'var(--space-lg, 24px)';
card.style.width = '100%';
card.style.maxWidth = '400px';
card.style.boxShadow = 'var(--shadow-lg, 0 10px 30px rgba(0,0,0,0.5))';
card.style.transform = 'scale(0.9)';
card.style.transition = 'transform 0.2s ease';
card.className = 'confirm-dialog-card';
card.innerHTML = `
${title}
${message}
`;
overlay.appendChild(card);
document.body.appendChild(overlay);
// Trigger animations
requestAnimationFrame(() => {
overlay.style.opacity = '1';
card.style.transform = 'scale(1)';
});
const cleanUp = (result) => {
overlay.style.opacity = '0';
card.style.transform = 'scale(0.9)';
setTimeout(() => {
overlay.remove();
resolve(result);
}, 200);
};
const btnCancel = card.querySelector('#confirm-btn-cancel');
const btnOk = card.querySelector('#confirm-btn-ok');
btnCancel.addEventListener('click', () => cleanUp(false));
btnOk.addEventListener('click', () => cleanUp(true));
// Close on backdrop click
overlay.addEventListener('click', (e) => {
if (e.target === overlay) cleanUp(false);
});
});
},
/** Custom alert dialog in the center of the screen */
alert(title, message, options = {}) {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100vw';
overlay.style.height = '100vh';
overlay.style.background = 'rgba(0, 0, 0, 0.6)';
overlay.style.backdropFilter = 'blur(4px)';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.zIndex = '99999';
overlay.style.opacity = '0';
overlay.style.transition = 'opacity 0.2s ease';
const card = document.createElement('div');
card.style.background = 'var(--bg-card, #1e1e2e)';
card.style.border = '1px solid var(--border-subtle, #313244)';
card.style.borderRadius = 'var(--radius-lg, 12px)';
card.style.padding = 'var(--space-lg, 24px)';
card.style.width = '100%';
card.style.maxWidth = '400px';
card.style.boxShadow = 'var(--shadow-lg, 0 10px 30px rgba(0,0,0,0.5))';
card.style.transform = 'scale(0.9)';
card.style.transition = 'transform 0.2s ease';
card.className = 'alert-dialog-card';
card.innerHTML = `
${title}
${message}
`;
overlay.appendChild(card);
document.body.appendChild(overlay);
requestAnimationFrame(() => {
overlay.style.opacity = '1';
card.style.transform = 'scale(1)';
});
const cleanUp = () => {
overlay.style.opacity = '0';
card.style.transform = 'scale(0.9)';
setTimeout(() => {
overlay.remove();
resolve();
}, 200);
};
card.querySelector('#alert-btn-ok').addEventListener('click', cleanUp);
overlay.addEventListener('click', (e) => {
if (e.target === overlay) cleanUp();
});
});
},
/** Custom prompt dialog in the center of the screen */
prompt(title, message, options = {}) {
return new Promise((resolve) => {
const overlay = document.createElement('div');
overlay.style.position = 'fixed';
overlay.style.top = '0';
overlay.style.left = '0';
overlay.style.width = '100vw';
overlay.style.height = '100vh';
overlay.style.background = 'rgba(0, 0, 0, 0.6)';
overlay.style.backdropFilter = 'blur(4px)';
overlay.style.display = 'flex';
overlay.style.alignItems = 'center';
overlay.style.justifyContent = 'center';
overlay.style.zIndex = '99999';
overlay.style.opacity = '0';
overlay.style.transition = 'opacity 0.2s ease';
const card = document.createElement('div');
card.style.background = 'var(--bg-card, #1e1e2e)';
card.style.border = '1px solid var(--border-subtle, #313244)';
card.style.borderRadius = 'var(--radius-lg, 12px)';
card.style.padding = 'var(--space-lg, 24px)';
card.style.width = '100%';
card.style.maxWidth = '400px';
card.style.boxShadow = 'var(--shadow-lg, 0 10px 30px rgba(0,0,0,0.5))';
card.style.transform = 'scale(0.9)';
card.style.transition = 'transform 0.2s ease';
card.className = 'prompt-dialog-card';
card.innerHTML = `
${title}
${message}
`;
overlay.appendChild(card);
document.body.appendChild(overlay);
const input = card.querySelector('#prompt-input-field');
requestAnimationFrame(() => {
overlay.style.opacity = '1';
card.style.transform = 'scale(1)';
setTimeout(() => {
if (input) input.focus();
}, 50);
});
const cleanUp = (resultValue) => {
overlay.style.opacity = '0';
card.style.transform = 'scale(0.9)';
setTimeout(() => {
overlay.remove();
resolve(resultValue);
}, 200);
};
const btnCancel = card.querySelector('#prompt-btn-cancel');
const btnOk = card.querySelector('#prompt-btn-ok');
btnCancel.addEventListener('click', () => cleanUp(null));
btnOk.addEventListener('click', () => {
const val = input ? input.value : '';
cleanUp(val);
});
if (input) {
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
btnOk.click();
} else if (e.key === 'Escape') {
btnCancel.click();
}
});
}
overlay.addEventListener('click', (e) => {
if (e.target === overlay) cleanUp(null);
});
});
},
};
// Start the app when DOM is ready
document.addEventListener('DOMContentLoaded', () => App.init());