366 lines
15 KiB
JavaScript
366 lines
15 KiB
JavaScript
// Migration workflow: options modal → progress → results view
|
|
|
|
import { api } from './api.js';
|
|
import { state, setState } from './state.js';
|
|
import { escHtml, formatDateTime, downloadCsv } from './utils.js';
|
|
import { navigate } from './router.js';
|
|
import { refreshTemplates } from './templates.js';
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
|
|
function getSettings() {
|
|
try { return JSON.parse(localStorage.getItem('migrator_settings')) || {}; }
|
|
catch { return {}; }
|
|
}
|
|
|
|
// ── Options modal ──────────────────────────────────────────────────────────
|
|
|
|
export function showOptionsModal(ids) {
|
|
if (!ids || !ids.length) return;
|
|
const settings = getSettings();
|
|
const names = ids.map(id => {
|
|
const t = state.templates.find(t => t.adobe_id === id);
|
|
return t ? t.name : id;
|
|
});
|
|
|
|
const existing = document.getElementById('migration-modal');
|
|
if (existing) existing.remove();
|
|
|
|
const wrapper = document.createElement('div');
|
|
wrapper.id = 'migration-modal';
|
|
wrapper.innerHTML = `
|
|
<div class="modal-backdrop"></div>
|
|
<div class="modal-box">
|
|
<div class="modal-header">
|
|
<span class="modal-title">Migration Options</span>
|
|
<button class="modal-close" id="mm-close">✕</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="callout info" style="margin-bottom:16px">
|
|
<span class="callout-icon">📋</span>
|
|
<span>Migrating <strong>${ids.length}</strong> template${ids.length > 1 ? 's' : ''}:
|
|
${names.slice(0, 3).map(n => `<em>${escHtml(n)}</em>`).join(', ')}${names.length > 3 ? `… +${names.length - 3} more` : ''}</span>
|
|
</div>
|
|
|
|
<div class="options-panel">
|
|
<div class="options-title">Options</div>
|
|
|
|
<div class="option-row">
|
|
<button class="toggle" id="opt-dry-run" role="switch" aria-checked="false"></button>
|
|
<div class="option-body">
|
|
<div class="option-label">Dry Run</div>
|
|
<div class="option-desc">Validate and preview without creating Docusign templates</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="option-row">
|
|
<button class="toggle" id="opt-overwrite" role="switch"
|
|
aria-checked="${settings.defaultOverwrite ? 'true' : 'false'}"
|
|
class="toggle ${settings.defaultOverwrite ? 'on' : ''}"></button>
|
|
<div class="option-body">
|
|
<div class="option-label">Overwrite Existing</div>
|
|
<div class="option-desc">Update Docusign templates that already exist (default: skip)</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="option-row">
|
|
<button class="toggle on" id="opt-include-docs" role="switch" aria-checked="true"></button>
|
|
<div class="option-body">
|
|
<div class="option-label">Include Documents</div>
|
|
<div class="option-desc">Embed PDFs in the Docusign template payload</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="option-row" style="align-items:center">
|
|
<div class="option-body">
|
|
<div class="option-label">Target Folder <span class="form-label-sub">(optional)</span></div>
|
|
<input type="text" class="form-input" id="opt-folder"
|
|
placeholder="e.g. Migrated Templates" style="margin-top:6px" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn btn-secondary" id="mm-cancel">Cancel</button>
|
|
<button class="btn btn-primary" id="mm-run">Run Migration →</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
document.body.appendChild(wrapper);
|
|
|
|
// Wire toggles
|
|
wrapper.querySelectorAll('.toggle').forEach(btn => {
|
|
if (settings.defaultOverwrite && btn.id === 'opt-overwrite') btn.classList.add('on');
|
|
btn.addEventListener('click', () => {
|
|
btn.classList.toggle('on');
|
|
btn.setAttribute('aria-checked', btn.classList.contains('on'));
|
|
});
|
|
});
|
|
|
|
document.getElementById('mm-close').onclick = () => wrapper.remove();
|
|
document.getElementById('mm-cancel').onclick = () => wrapper.remove();
|
|
document.getElementById('mm-run').onclick = () => _startMigration(ids, wrapper);
|
|
}
|
|
|
|
// ── Start migration ────────────────────────────────────────────────────────
|
|
|
|
async function _startMigration(ids, wrapper) {
|
|
const dryRun = document.getElementById('opt-dry-run')?.classList.contains('on') || false;
|
|
const overwrite = document.getElementById('opt-overwrite')?.classList.contains('on') || false;
|
|
const includeDocs = !(document.getElementById('opt-include-docs')?.classList.contains('on') === false);
|
|
const folder = document.getElementById('opt-folder')?.value.trim() || undefined;
|
|
|
|
// Replace modal body with progress view
|
|
const body = wrapper.querySelector('.modal-body');
|
|
const footer = wrapper.querySelector('.modal-footer');
|
|
wrapper.querySelector('.modal-title').textContent = dryRun ? 'Dry Run' : 'Migrating…';
|
|
footer.innerHTML = ''; // hide footer during migration
|
|
|
|
const names = ids.map(id => {
|
|
const t = state.templates.find(t => t.adobe_id === id);
|
|
return { id, name: t ? t.name : id };
|
|
});
|
|
|
|
body.innerHTML = `
|
|
<div class="migration-progress">
|
|
<div class="progress-wrap">
|
|
<div class="progress-label">
|
|
<span id="prog-label">Starting…</span>
|
|
<span id="prog-count">0 / ${ids.length}</span>
|
|
</div>
|
|
<div class="progress-bar"><div class="progress-fill" id="prog-bar" style="width:0%"></div></div>
|
|
</div>
|
|
<div class="progress-template-list" id="prog-list">
|
|
${names.map(n => `
|
|
<div class="progress-template-row" id="prog-row-${escHtml(n.id)}">
|
|
<div class="progress-template-name">${escHtml(n.name)}</div>
|
|
<span class="progress-template-status spinner spinner-sm"></span>
|
|
</div>`).join('')}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
try {
|
|
const jobData = await api.migrate.batch({
|
|
source_template_ids: ids,
|
|
target_folder: folder,
|
|
options: { dry_run: dryRun, overwrite_if_exists: overwrite, include_documents: includeDocs },
|
|
});
|
|
|
|
const jobId = jobData.job_id;
|
|
await pollJob(jobId, (progress) => {
|
|
const { completed, total } = progress.progress || { completed: 0, total: ids.length };
|
|
const pct = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
document.getElementById('prog-bar') && (document.getElementById('prog-bar').style.width = pct + '%');
|
|
document.getElementById('prog-count')&& (document.getElementById('prog-count').textContent = `${completed} / ${total}`);
|
|
document.getElementById('prog-label')&& (document.getElementById('prog-label').textContent = `Migrating… ${pct}%`);
|
|
|
|
// Update per-template icons as results come in
|
|
(progress.results || []).forEach(r => {
|
|
const row = document.getElementById(`prog-row-${r.adobe_template_id}`);
|
|
if (!row) return;
|
|
const statusEl = row.querySelector('.progress-template-status');
|
|
if (!statusEl) return;
|
|
statusEl.className = 'progress-template-status';
|
|
if (r.status === 'success') statusEl.textContent = r.action === 'created' ? '✅' : r.action === 'dry_run' ? '🔍' : '✏️';
|
|
else if (r.status === 'skipped') statusEl.textContent = '⏭';
|
|
else if (r.status === 'blocked') statusEl.textContent = '🚫';
|
|
else statusEl.textContent = '❌';
|
|
});
|
|
});
|
|
|
|
// Migration done — show "View Results" button
|
|
document.getElementById('prog-label') && (document.getElementById('prog-label').textContent = 'Done!');
|
|
footer.innerHTML = `
|
|
<button class="btn btn-secondary" id="mm-close-done">Close</button>
|
|
<button class="btn btn-primary" id="mm-view-results">View Results →</button>
|
|
`;
|
|
document.getElementById('mm-close-done').onclick = () => wrapper.remove();
|
|
document.getElementById('mm-view-results').onclick = () => {
|
|
wrapper.remove();
|
|
navigate('#/results');
|
|
};
|
|
|
|
// Refresh template list
|
|
await refreshTemplates();
|
|
state.selectedIds.clear();
|
|
|
|
} catch (err) {
|
|
body.innerHTML += `<div class="callout error" style="margin-top:12px">
|
|
<span class="callout-icon">❌</span> Migration failed: ${escHtml(err.message)}
|
|
</div>`;
|
|
footer.innerHTML = `<button class="btn btn-secondary" id="mm-err-close">Close</button>`;
|
|
document.getElementById('mm-err-close').onclick = () => wrapper.remove();
|
|
}
|
|
}
|
|
|
|
// ── Poll batch job ─────────────────────────────────────────────────────────
|
|
|
|
export async function pollJob(jobId, onProgress) {
|
|
const POLL_MS = 2000;
|
|
const MAX_WAIT = 300000; // 5 minutes
|
|
const started = Date.now();
|
|
|
|
return new Promise((resolve, reject) => {
|
|
const tick = async () => {
|
|
try {
|
|
const data = await api.migrate.batchStatus(jobId);
|
|
if (onProgress) onProgress(data);
|
|
|
|
if (data.status === 'done' || data.status === 'complete') {
|
|
setState('lastMigrationResults', data);
|
|
resolve(data);
|
|
} else if (data.status === 'failed') {
|
|
reject(new Error('Migration job failed'));
|
|
} else if (Date.now() - started > MAX_WAIT) {
|
|
reject(new Error('Migration timed out'));
|
|
} else {
|
|
setTimeout(tick, POLL_MS);
|
|
}
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
};
|
|
tick();
|
|
});
|
|
}
|
|
|
|
// ── Results view ───────────────────────────────────────────────────────────
|
|
|
|
export function renderResults() {
|
|
const outlet = document.getElementById('router-outlet');
|
|
const results = state.lastMigrationResults;
|
|
|
|
if (!results) {
|
|
outlet.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">📊</div>
|
|
<div class="empty-state-title">No migration results yet</div>
|
|
<div class="empty-state-sub">Run a migration from the <a href="#/templates" style="color:var(--cobalt)">Templates</a> view to see results here.</div>
|
|
</div>`;
|
|
return;
|
|
}
|
|
|
|
const templateResults = results.results || [];
|
|
const summary = {
|
|
created: templateResults.filter(r => r.action === 'created').length,
|
|
updated: templateResults.filter(r => r.action === 'updated').length,
|
|
skipped: templateResults.filter(r => r.status === 'skipped').length,
|
|
blocked: templateResults.filter(r => r.status === 'blocked').length,
|
|
errors: templateResults.filter(r => r.status === 'error').length,
|
|
dry_run: templateResults.filter(r => r.status === 'dry_run').length,
|
|
};
|
|
|
|
const migratedIds = templateResults
|
|
.filter(r => r.status === 'success')
|
|
.map(r => r.adobe_template_id);
|
|
|
|
outlet.innerHTML = `
|
|
<div class="page-header">
|
|
<div>
|
|
<div class="page-title">Migration Results</div>
|
|
<div class="page-subtitle">${formatDateTime(results.completed_at || new Date().toISOString())}</div>
|
|
</div>
|
|
<div class="page-actions">
|
|
<button class="btn btn-secondary btn-sm" id="btn-export-results">⬇ Export CSV</button>
|
|
${migratedIds.length ? `<button class="btn btn-primary btn-sm" id="btn-verify-results">Verify Templates →</button>` : ''}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary stat cards -->
|
|
<div class="stat-grid" style="grid-template-columns:repeat(${summary.dry_run ? 6 : 5},1fr)">
|
|
<div class="stat-card green">
|
|
<div class="stat-label">Created</div>
|
|
<div class="stat-value">${summary.created}</div>
|
|
</div>
|
|
<div class="stat-card blue">
|
|
<div class="stat-label">Updated</div>
|
|
<div class="stat-value">${summary.updated}</div>
|
|
</div>
|
|
<div class="stat-card gray">
|
|
<div class="stat-label">Skipped</div>
|
|
<div class="stat-value">${summary.skipped}</div>
|
|
</div>
|
|
<div class="stat-card red">
|
|
<div class="stat-label">Blocked</div>
|
|
<div class="stat-value">${summary.blocked}</div>
|
|
</div>
|
|
<div class="stat-card red">
|
|
<div class="stat-label">Errors</div>
|
|
<div class="stat-value">${summary.errors}</div>
|
|
</div>
|
|
${summary.dry_run ? `<div class="stat-card gray"><div class="stat-label">Dry Run</div><div class="stat-value">${summary.dry_run}</div></div>` : ''}
|
|
</div>
|
|
|
|
<!-- Per-template results -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<span class="card-title">Per-Template Results</span>
|
|
</div>
|
|
<div id="results-list" style="padding:8px 16px">
|
|
${templateResults.map(r => _resultRow(r)).join('')}
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display:flex;gap:8px;margin-top:8px">
|
|
<a href="#/templates" class="btn btn-secondary btn-sm">← Back to Templates</a>
|
|
</div>
|
|
`;
|
|
|
|
// Expand/collapse result rows
|
|
document.querySelectorAll('.result-header').forEach(hdr => {
|
|
hdr.addEventListener('click', () => {
|
|
hdr.parentElement.classList.toggle('open');
|
|
});
|
|
});
|
|
|
|
// Export CSV
|
|
document.getElementById('btn-export-results')?.addEventListener('click', () => {
|
|
downloadCsv('migration-results.csv', templateResults.map(r => ({
|
|
name: r.adobe_template_name || r.adobe_template_id,
|
|
adobe_id: r.adobe_template_id,
|
|
docusign_id: r.docusign_template_id || '',
|
|
status: r.status,
|
|
action: r.action || '',
|
|
warnings: (r.warnings || []).join('; '),
|
|
})));
|
|
});
|
|
|
|
// Verify button
|
|
document.getElementById('btn-verify-results')?.addEventListener('click', () => {
|
|
import('./verification.js').then(m => {
|
|
setState('verifyIds', migratedIds);
|
|
navigate('#/verify');
|
|
});
|
|
});
|
|
}
|
|
|
|
function _resultRow(r) {
|
|
const icon = r.status === 'success'
|
|
? (r.action === 'created' ? '✅' : r.action === 'dry_run' ? '🔍' : '✏️')
|
|
: (r.status === 'skipped' ? '⏭' : r.status === 'blocked' ? '🚫' : '❌');
|
|
|
|
const statusBadge = r.status === 'success'
|
|
? `<span class="badge badge-green">${r.action || 'success'}</span>`
|
|
: `<span class="badge badge-${r.status === 'skipped' ? 'gray' : 'red'}">${r.status}</span>`;
|
|
|
|
const warnings = r.warnings || [];
|
|
|
|
return `
|
|
<div class="result-row">
|
|
<div class="result-header">
|
|
<span class="result-icon">${icon}</span>
|
|
<span class="result-name">${escHtml(r.adobe_template_name || r.adobe_template_id)}</span>
|
|
${statusBadge}
|
|
${r.docusign_template_id ? `<span class="ds-pill" title="${escHtml(r.docusign_template_id)}">DS: ${escHtml(r.docusign_template_id.slice(0,8))}…</span>` : ''}
|
|
${warnings.length ? `<span class="result-meta">⚠ ${warnings.length} warning${warnings.length > 1 ? 's' : ''}</span>` : ''}
|
|
</div>
|
|
${warnings.length || r.error ? `
|
|
<div class="result-body">
|
|
${warnings.map(w => `<div class="result-warn-item"><span class="ri">⚠</span>${escHtml(w)}</div>`).join('')}
|
|
${r.error ? `<div class="result-warn-item" style="color:var(--error)"><span class="ri">❌</span>${escHtml(r.error)}</div>` : ''}
|
|
</div>` : ''}
|
|
</div>
|
|
`;
|
|
}
|