// ============================================================ // Elegant Work — UI Utilities // Toast, Modal, Confirm, Format helpers // ============================================================ // ── Toast ──────────────────────────────────────────────── const Toast = (() => { const ICONS = { success: ``, error: ``, warning: ``, info: ``, }; function show(message, type = 'success', title = null, duration = 4000) { const container = document.getElementById('toast-container'); if (!container) return; const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.innerHTML = ` ${ICONS[type] || ICONS.info}
${title ? `
${title}
` : ''}
${message}
`; container.appendChild(toast); setTimeout(() => { toast.classList.add('hiding'); setTimeout(() => toast.remove(), 200); }, duration); } return { show }; })(); // ── Modal Helper ───────────────────────────────────────── const Modal = (() => { function open({ id, title, body, footer, size = '' }) { close(); const backdrop = document.createElement('div'); backdrop.className = 'modal-backdrop'; backdrop.id = `modal-backdrop-${id}`; backdrop.innerHTML = ` `; document.body.appendChild(backdrop); // Close on backdrop click backdrop.addEventListener('click', e => { if (e.target === backdrop) close(); }); // Close on Escape document.addEventListener('keydown', function esc(e) { if (e.key === 'Escape') { close(); document.removeEventListener('keydown', esc); } }); return document.getElementById(`modal-${id}`); } function close() { document.querySelectorAll('.modal-backdrop').forEach(el => el.remove()); } async function confirm(message, title = 'Confirm', danger = false) { return new Promise(resolve => { const m = open({ id: 'confirm', title, body: `

${message}

`, footer: ` ` }); window._confirmResolve = resolve; }); } return { open, close, confirm }; })(); // ── Format Helpers ─────────────────────────────────────── const Fmt = (() => { function currency(amount, currency = 'ZAR') { if (amount === null || amount === undefined || amount === '') return '—'; return new Intl.NumberFormat('en-ZA', { style: 'currency', currency }).format(amount); } function date(val, opts = {}) { if (!val) return '—'; const d = new Date(val); if (isNaN(d)) return val; return d.toLocaleDateString('en-ZA', { day: '2-digit', month: 'short', year: 'numeric', ...opts }); } function datetime(val) { if (!val) return '—'; const d = new Date(val); if (isNaN(d)) return val; return d.toLocaleDateString('en-ZA', { day: '2-digit', month: 'short', year: 'numeric' }) + ' ' + d.toLocaleTimeString('en-ZA', { hour: '2-digit', minute: '2-digit' }); } function ago(val) { if (!val) return '—'; const d = new Date(val); const diff = Date.now() - d.getTime(); const mins = Math.floor(diff / 60000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; const days = Math.floor(hrs / 24); if (days < 30) return `${days}d ago`; return date(val); } function capitalize(s) { if (!s) return ''; return s.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()); } function initials(name) { if (!name) return '?'; return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2); } function statusBadge(status, extraClass = '') { const s = (status || '').toLowerCase().replace(/ /g, '_'); return `${capitalize(status)}`; } function priorityBadge(priority) { const map = { low: 'badge-muted', medium: 'badge-info', high: 'badge-warning', critical: 'badge-danger', urgent: 'badge-danger' }; const cls = map[(priority || '').toLowerCase()] || 'badge-muted'; return `${capitalize(priority || '')}`; } function severityBadge(sev) { return `${capitalize(sev || '')}`; } return { currency, date, datetime, ago, capitalize, initials, statusBadge, priorityBadge, severityBadge }; })(); // ── Pagination Renderer ────────────────────────────────── function renderPagination(containerId, pagination, onPageChange) { const el = document.getElementById(containerId); if (!el || !pagination) return; const { total, page, limit, pages } = pagination; const start = total === 0 ? 0 : (page - 1) * limit + 1; const end = Math.min(page * limit, total); let btns = ''; btns += ``; for (let p = Math.max(1, page - 2); p <= Math.min(pages, page + 2); p++) { btns += ``; } btns += ``; el.innerHTML = ` `; } // ── Form Helpers ───────────────────────────────────────── function getFormData(formId) { const form = document.getElementById(formId); if (!form) return {}; const data = {}; new FormData(form).forEach((v, k) => { data[k] = v; }); return data; } function setFormData(formId, data) { const form = document.getElementById(formId); if (!form || !data) return; for (const [key, val] of Object.entries(data)) { const el = form.elements[key]; if (!el) continue; if (el.type === 'checkbox') el.checked = !!val; else el.value = val ?? ''; } } // ── User Avatar HTML ───────────────────────────────────── function avatarHTML(name, size = 'sm') { const s = size === 'lg' ? '40px' : '30px'; const fs = size === 'lg' ? '.875rem' : '.75rem'; return `
${Fmt.initials(name)}
`; } // ── Build a select options string ──────────────────────── function buildOptions(items, valueKey, labelKey, selected = '') { return items.map(i => `` ).join(''); } // ── Debounce ───────────────────────────────────────────── function debounce(fn, delay = 300) { let t; return (...args) => { clearTimeout(t); t = setTimeout(() => fn(...args), delay); }; }