// Templates view — filterable table with readiness badges + template detail import { api } from './api.js'; import { state, setState, updateDerivedState } from './state.js'; import { escHtml, formatDate, formatRelative, debounce } from './utils.js'; import { navigate } from './router.js'; // ── Readiness badge ──────────────────────────────────────────────────────── function readiness(t) { if (t.blockers && t.blockers.length > 0) { return { key: 'blocked', label: 'Blocked', cls: 'badge-blocked' }; } if (t.status === 'migrated') { return t.warnings && t.warnings.length > 0 ? { key: 'migrated-warn', label: 'Migrated', cls: 'badge-migrated' } : { key: 'migrated', label: 'Migrated', cls: 'badge-migrated' }; } if (t.status === 'needs_update') { return { key: 'needs-update', label: 'Needs Update', cls: 'badge-needs-update' }; } if (t.warnings && t.warnings.length > 0) { return { key: 'caveats', label: 'Caveats', cls: 'badge-caveats' }; } return { key: 'ready', label: 'Ready', cls: 'badge-ready' }; } // ── Refresh templates from API ───────────────────────────────────────────── export async function refreshTemplates() { if (!state.auth.adobe || !state.auth.docusign) { setState('templates', []); updateDerivedState(); return; } try { const data = await api.templates.status(); setState('templates', data.templates || []); updateDerivedState(); } catch (e) { console.warn('refreshTemplates failed:', e.message); } } // ── Templates list view ──────────────────────────────────────────────────── let _filter = { search: '', status: 'all' }; let _sort = { col: 'name', dir: 'asc' }; export async function renderTemplates() { const outlet = document.getElementById('router-outlet'); // Fetch if not loaded if (!state.templates.length && state.auth.adobe && state.auth.docusign) { outlet.innerHTML = `
`; await refreshTemplates(); } _render(); } function _render() { const outlet = document.getElementById('router-outlet'); const templates = _applyFilter(state.templates); const counts = _statusCounts(state.templates); const anySelected = state.selectedIds.size > 0; outlet.innerHTML = ` ${!state.auth.adobe || !state.auth.docusign ? `
ℹ️ Connect both Adobe Sign and Docusign in the top bar to load templates.
` : ''}
${_filterTab('all', `All ${counts.total}`)} ${_filterTab('not_migrated', `Not Migrated ${counts.not_migrated}`)} ${_filterTab('migrated', `Migrated ${counts.migrated}`)} ${_filterTab('needs_update', `Needs Update ${counts.needs_update}`)}
${_filterTab2('blocked', `Blocked ${counts.blocked}`)} ${_filterTab2('caveats', `Caveats ${counts.caveats}`)}
${state.selectedIds.size} template(s) selected
${_th('name', 'Template Name')} ${_th('readiness', 'Readiness')} ${_th('warnings', 'Issues')} ${_th('adobe_modified','Last Modified')} ${_th('status', 'DS Status')} ${templates.length ? templates.map(t => _templateRow(t)).join('') : `` }
Actions
📄
${state.templates.length ? 'No templates match your filter' : 'No templates found'}
${state.templates.length ? 'Try clearing the search or filter.' : 'Connect Adobe Sign to load templates.'}
`; _bindEvents(); } function _filterTab(key, label) { return ``; } function _filterTab2(key, label) { // readiness-based filters return ``; } function _th(col, label) { const dir = _sort.col === col ? (_sort.dir === 'asc' ? 'sort-asc' : 'sort-desc') : ''; return `${label}`; } function _templateRow(t) { const r = readiness(t); const selected = state.selectedIds.has(t.adobe_id); const warnCount = (t.warnings || []).length; const blockCount = (t.blockers || []).length; const issueClass = blockCount > 0 ? 'blocked' : (warnCount > 0 ? 'has-issues' : 'no-issues'); const issueLabel = blockCount > 0 ? `🚫 ${blockCount} blocker${blockCount > 1 ? 's' : ''}` : (warnCount > 0 ? `⚠ ${warnCount} warning${warnCount > 1 ? 's' : ''}` : '✓ Clean'); return `
${escHtml(t.adobe_id)}
${r.label} ${issueLabel} ${formatRelative(t.adobe_modified)} ${t.docusign_id ? `In Docusign` : `Not Migrated`}
`; } function _statusCounts(templates) { return { total: templates.length, not_migrated: templates.filter(t => t.status === 'not_migrated').length, migrated: templates.filter(t => t.status === 'migrated').length, needs_update: templates.filter(t => t.status === 'needs_update').length, blocked: templates.filter(t => t.blockers && t.blockers.length > 0).length, caveats: templates.filter(t => (!t.blockers || !t.blockers.length) && t.warnings && t.warnings.length > 0).length, }; } function _applyFilter(templates) { let list = [...templates]; // Text search if (_filter.search) { const q = _filter.search.toLowerCase(); list = list.filter(t => t.name.toLowerCase().includes(q)); } // Status / readiness filter if (_filter.status !== 'all') { if (_filter.status === 'blocked') { list = list.filter(t => t.blockers && t.blockers.length > 0); } else if (_filter.status === 'caveats') { list = list.filter(t => (!t.blockers || !t.blockers.length) && t.warnings && t.warnings.length > 0); } else { list = list.filter(t => t.status === _filter.status); } } // Sorting list.sort((a, b) => { let va = a[_sort.col] || ''; let vb = b[_sort.col] || ''; if (_sort.col === 'readiness') { va = readiness(a).key; vb = readiness(b).key; } if (_sort.col === 'warnings') { va = (a.blockers||[]).length + (a.warnings||[]).length; vb = (b.blockers||[]).length + (b.warnings||[]).length; } if (typeof va === 'number') return _sort.dir === 'asc' ? va - vb : vb - va; return _sort.dir === 'asc' ? String(va).localeCompare(String(vb)) : String(vb).localeCompare(String(va)); }); return list; } // ── Event wiring ─────────────────────────────────────────────────────────── function _bindEvents() { // Search const searchEl = document.getElementById('template-search'); if (searchEl) { searchEl.addEventListener('input', debounce(e => { _filter.search = e.target.value; _render(); }, 250)); } // Filter tabs document.querySelectorAll('.filter-tab').forEach(btn => { btn.addEventListener('click', () => { _filter.status = btn.dataset.filter; _render(); }); }); // Sort headers document.querySelectorAll('th.sortable').forEach(th => { th.addEventListener('click', () => { const col = th.dataset.col; if (_sort.col === col) { _sort.dir = _sort.dir === 'asc' ? 'desc' : 'asc'; } else { _sort.col = col; _sort.dir = 'asc'; } _render(); }); }); // Checkboxes document.getElementById('select-all')?.addEventListener('change', e => { const ids = _applyFilter(state.templates).map(t => t.adobe_id); if (e.target.checked) { ids.forEach(id => state.selectedIds.add(id)); } else { ids.forEach(id => state.selectedIds.delete(id)); } _render(); }); document.querySelectorAll('.row-cb').forEach(cb => { cb.addEventListener('change', e => { const id = cb.dataset.id; if (e.target.checked) state.selectedIds.add(id); else state.selectedIds.delete(id); _render(); }); }); // Migrate selected document.getElementById('btn-migrate-selected')?.addEventListener('click', () => { _launchMigration([...state.selectedIds]); }); document.getElementById('btn-clear-selection')?.addEventListener('click', () => { state.selectedIds.clear(); _render(); }); // Migrate individual document.querySelectorAll('.btn-migrate-one').forEach(btn => { btn.addEventListener('click', () => _launchMigration([btn.dataset.id])); }); // View detail document.querySelectorAll('.btn-view-detail, .tpl-name-link').forEach(el => { el.addEventListener('click', () => navigate(`#/templates/${el.dataset.id}`)); }); // Refresh document.getElementById('btn-refresh-templates')?.addEventListener('click', async () => { await refreshTemplates(); _render(); }); } async function _launchMigration(ids) { if (!ids.length) return; const { showOptionsModal } = await import('./migration.js'); showOptionsModal(ids); } // ── Template detail view ─────────────────────────────────────────────────── export async function renderTemplateDetail(adobeId) { const outlet = document.getElementById('router-outlet'); const t = state.templates.find(t => t.adobe_id === adobeId); if (!t) { outlet.innerHTML = `
🔍
Template not found
← Back to Templates
`; return; } const r = readiness(t); outlet.innerHTML = `
Overview
Issues ${(t.blockers||[]).length + (t.warnings||[]).length > 0 ? `${(t.blockers||[]).length + (t.warnings||[]).length}` : ''}
Migration History
`; document.getElementById('detail-migrate-btn')?.addEventListener('click', () => { import('./migration.js').then(m => m.showOptionsModal([adobeId])); }); _bindDetailTabs(t); _renderDetailTab(t, 'overview'); } function _bindDetailTabs(t) { document.querySelectorAll('#detail-tabs .tab').forEach(tab => { tab.addEventListener('click', () => { document.querySelectorAll('#detail-tabs .tab').forEach(x => x.classList.remove('active')); tab.classList.add('active'); _renderDetailTab(t, tab.dataset.tab); }); }); } function _renderDetailTab(t, tabKey) { const content = document.getElementById('detail-tab-content'); if (tabKey === 'overview') { content.innerHTML = `
Template Overview
Adobe Sign ID ${escHtml(t.adobe_id)}
Last Modified ${formatDate(t.adobe_modified)}
Migration Status ${t.status.replace('_', ' ')}
${t.docusign_id ? `
Docusign Template ID ${escHtml(t.docusign_id)}
` : ''}
`; } else if (tabKey === 'issues') { const blockers = t.blockers || []; const warnings = t.warnings || []; if (!blockers.length && !warnings.length) { content.innerHTML = `
No issues found. This template is ready to migrate.
`; } else { content.innerHTML = ` ${blockers.length ? `
🚫 Blockers (${blockers.length})
${blockers.map(b => `
BLOCKER
${escHtml(b)}
`).join('')}
` : ''} ${warnings.length ? `
⚠ Warnings (${warnings.length})
${warnings.map(w => `
WARNING
${escHtml(w)}
`).join('')}
` : ''}`; } } else if (tabKey === 'history') { api.migrate.history().then(data => { const records = (data.history || []).filter(r => r.adobe_template_id === t.adobe_id || r.adobe_template_name === t.name ); if (!records.length) { content.innerHTML = `
ℹ️No migration history for this template yet.
`; } else { content.innerHTML = `
${[...records].reverse().map(r => ` `).join('')}
TimeActionStatusDocusign ID
${(r.timestamp||'').slice(0,19).replace('T',' ')} ${escHtml(r.action||'—')} ${r.status} ${escHtml(r.docusign_template_id||'—')}
`; } }).catch(() => { content.innerHTML = `
Failed to load history.
`; }); } }