adobe-to-docusign-migrator/web/static/js/verification.js

293 lines
11 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Verification view — send test envelopes to confirm migrated templates work
import { api } from './api.js';
import { state } from './state.js';
import { escHtml, formatDateTime } from './utils.js';
const POLL_MS = 30_000; // DocuSign rate-limit guidance: no more than once per 15 min in prod
const POLL_TIMEOUT = 300_000; // 5 minutes — treat as manual quick-test only; prod should use DS Connect
const _envelopes = {}; // { adobeId: { envelopeId, status, sentAt, completedAt, polling } }
function getSettings() {
try { return JSON.parse(localStorage.getItem('migrator_settings')) || {}; }
catch { return {}; }
}
export async function renderVerification(preloadedIds = null) {
const outlet = document.getElementById('router-outlet');
const ids = preloadedIds || state.verifyIds || null;
// Candidate templates: recently migrated (from state) or all migrated
const candidates = (state.templates || []).filter(t =>
t.status === 'migrated' || t.status === 'needs_update' || t.docusign_id
);
if (!state.auth.docusign) {
outlet.innerHTML = `
<div class="page-header"><div><div class="page-title">Verification</div></div></div>
<div class="callout info"><span class="callout-icon"></span>Connect Docusign to send verification envelopes.</div>`;
return;
}
if (!candidates.length) {
outlet.innerHTML = `
<div class="page-header"><div><div class="page-title">Verification</div></div></div>
<div class="callout info"><span class="callout-icon"></span>
No migrated templates yet. Run a migration first, then return here to verify.
</div>`;
return;
}
_renderVerifyView(candidates);
}
function _renderVerifyView(candidates) {
const outlet = document.getElementById('router-outlet');
const settings = getSettings();
outlet.innerHTML = `
<div class="page-header">
<div>
<div class="page-title">Verification</div>
<div class="page-subtitle">Send test envelopes to confirm templates work end-to-end</div>
</div>
</div>
<div class="callout info" style="margin-bottom:20px">
<span class="callout-icon"></span>
Verification sends a real Docusign envelope using each template. Test envelopes should be voided after use.
Configure default recipient in <a href="#/settings" style="color:var(--cobalt)">Settings</a>.
</div>
<div class="card">
<div class="card-header">
<span class="card-title">Migrated Templates</span>
<span style="font-size:12px;color:var(--text-muted)">${candidates.length} templates</span>
</div>
<div class="table-wrap">
<table id="verify-table">
<thead>
<tr>
<th>Template</th>
<th>Docusign ID</th>
<th>Verification Status</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
${candidates.map(t => _verifyRow(t, settings)).join('')}
</tbody>
</table>
</div>
</div>
`;
// Wire Send Test buttons
document.querySelectorAll('.btn-send-test').forEach(btn => {
btn.addEventListener('click', () => {
const adobeId = btn.dataset.id;
const t = candidates.find(t => t.adobe_id === adobeId);
if (t) _showSendDialog(t, settings);
});
});
// Wire Void buttons
document.querySelectorAll('.btn-void-envelope').forEach(btn => {
btn.addEventListener('click', () => {
_voidEnvelope(btn.dataset.id, btn.dataset.envelopeid);
});
});
}
function _verifyRow(t, settings) {
const env = _envelopes[t.adobe_id];
let statusCell = '<span class="badge badge-gray">Not Tested</span>';
let actionsCell = `<button class="btn btn-primary btn-sm btn-send-test" data-id="${escHtml(t.adobe_id)}">Send Test</button>`;
if (env) {
if (env.status === 'completed') {
statusCell = '<span class="badge badge-green">✓ Verified</span>';
actionsCell = `<button class="btn btn-secondary btn-xs btn-void-envelope"
data-id="${escHtml(t.adobe_id)}" data-envelopeid="${escHtml(env.envelopeId)}">Void</button>`;
} else if (env.status === 'sent' || env.status === 'delivered') {
statusCell = `<span class="badge badge-blue"><span class="spinner spinner-sm"></span> ${env.status}</span>`;
actionsCell = `<button class="btn btn-secondary btn-xs btn-void-envelope"
data-id="${escHtml(t.adobe_id)}" data-envelopeid="${escHtml(env.envelopeId)}">Void</button>`;
} else if (env.status === 'voided') {
statusCell = '<span class="badge badge-gray">Voided</span>';
actionsCell = `<button class="btn btn-primary btn-sm btn-send-test" data-id="${escHtml(t.adobe_id)}">Send Again</button>`;
} else if (env.status === 'timeout') {
statusCell = '<span class="badge badge-amber">Timed Out</span>';
actionsCell = `<button class="btn btn-primary btn-sm btn-send-test" data-id="${escHtml(t.adobe_id)}">Send Again</button>`;
} else {
statusCell = `<span class="badge badge-amber">${escHtml(env.status || 'pending')}</span>`;
}
}
return `
<tr id="verify-row-${escHtml(t.adobe_id)}">
<td>
<div class="table-name">${escHtml(t.name)}</div>
<div class="table-sub">${escHtml(t.adobe_id)}</div>
</td>
<td class="mono" style="font-size:11px">${t.docusign_id ? escHtml(t.docusign_id.slice(0,12)) + '…' : '—'}</td>
<td id="verify-status-${escHtml(t.adobe_id)}">${statusCell}</td>
<td id="verify-actions-${escHtml(t.adobe_id)}">${actionsCell}</td>
</tr>
`;
}
function _showSendDialog(t, settings) {
const existing = document.getElementById('send-dialog');
if (existing) existing.remove();
const wrapper = document.createElement('div');
wrapper.id = 'send-dialog';
wrapper.innerHTML = `
<div class="modal-backdrop"></div>
<div class="modal-box modal-sm">
<div class="modal-header">
<span class="modal-title">Send Test Envelope</span>
<button class="modal-close" id="sd-close">✕</button>
</div>
<div class="modal-body">
<div style="font-size:13px;color:var(--text-muted);margin-bottom:14px">
Template: <strong>${escHtml(t.name)}</strong>
</div>
<div class="form-group">
<label class="form-label" for="sd-name">Recipient Name</label>
<input type="text" class="form-input" id="sd-name"
value="${escHtml(settings.testRecipientName || '')}"
placeholder="Test Recipient" />
</div>
<div class="form-group">
<label class="form-label" for="sd-email">Recipient Email</label>
<input type="email" class="form-input" id="sd-email"
value="${escHtml(settings.testRecipientEmail || '')}"
placeholder="test@example.com" />
<div class="form-error" id="sd-error"></div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="sd-cancel">Cancel</button>
<button class="btn btn-primary" id="sd-send">Send Test →</button>
</div>
</div>
`;
document.body.appendChild(wrapper);
document.getElementById('sd-close').onclick = () => wrapper.remove();
document.getElementById('sd-cancel').onclick = () => wrapper.remove();
document.getElementById('sd-send').onclick = () => _sendEnvelope(t, wrapper);
}
async function _sendEnvelope(t, wrapper) {
const name = document.getElementById('sd-name')?.value.trim();
const email = document.getElementById('sd-email')?.value.trim();
const errorEl = document.getElementById('sd-error');
if (!name || !email) {
if (errorEl) errorEl.textContent = 'Recipient name and email are required.';
return;
}
if (errorEl) errorEl.textContent = '';
const sendBtn = document.getElementById('sd-send');
sendBtn.disabled = true;
sendBtn.textContent = 'Sending…';
try {
const data = await api.verify.send(t.docusign_id || t.adobe_id, name, email);
wrapper.remove();
_envelopes[t.adobe_id] = { envelopeId: data.envelope_id, status: 'sent', sentAt: new Date().toISOString() };
_updateVerifyRow(t.adobe_id);
_startPolling(t.adobe_id, data.envelope_id);
} catch (e) {
if (errorEl) errorEl.textContent = 'Send failed: ' + e.message;
sendBtn.disabled = false;
sendBtn.textContent = 'Send Test →';
}
}
function _startPolling(adobeId, envelopeId) {
const env = _envelopes[adobeId];
if (!env || env.polling) return;
env.polling = true;
const deadline = Date.now() + POLL_TIMEOUT;
const poll = async () => {
try {
const data = await api.verify.status(envelopeId);
if (!_envelopes[adobeId]) return;
_envelopes[adobeId].status = data.status;
_envelopes[adobeId].completedAt = data.completed_at;
_updateVerifyRow(adobeId);
if (data.status === 'completed' || data.status === 'voided') {
_envelopes[adobeId].polling = false;
} else if (Date.now() >= deadline) {
_envelopes[adobeId].polling = false;
_envelopes[adobeId].status = 'timeout';
_updateVerifyRow(adobeId);
} else {
setTimeout(poll, POLL_MS);
}
} catch (e) {
console.warn('Polling error:', e.message);
}
};
setTimeout(poll, POLL_MS);
}
function _updateVerifyRow(adobeId) {
const t = (state.templates || []).find(t => t.adobe_id === adobeId);
const env = _envelopes[adobeId];
if (!t || !env) return;
const statusEl = document.getElementById(`verify-status-${adobeId}`);
const actionsEl = document.getElementById(`verify-actions-${adobeId}`);
if (!statusEl) return;
if (env.status === 'completed') {
statusEl.innerHTML = '<span class="badge badge-green">✓ Verified</span>';
actionsEl.innerHTML = `<button class="btn btn-secondary btn-xs btn-void-envelope"
data-id="${escHtml(adobeId)}" data-envelopeid="${escHtml(env.envelopeId)}">Void</button>`;
} else if (env.status === 'sent' || env.status === 'delivered') {
statusEl.innerHTML = `<span class="badge badge-blue"><span class="spinner spinner-sm"></span> ${env.status}</span>`;
actionsEl.innerHTML = `<button class="btn btn-secondary btn-xs btn-void-envelope"
data-id="${escHtml(adobeId)}" data-envelopeid="${escHtml(env.envelopeId)}">Void</button>`;
} else if (env.status === 'voided') {
statusEl.innerHTML = '<span class="badge badge-gray">Voided</span>';
actionsEl.innerHTML = `<button class="btn btn-primary btn-sm btn-send-test" data-id="${escHtml(adobeId)}">Send Again</button>`;
} else if (env.status === 'timeout') {
statusEl.innerHTML = '<span class="badge badge-amber">Timed Out</span>';
actionsEl.innerHTML = `<button class="btn btn-primary btn-sm btn-send-test" data-id="${escHtml(adobeId)}">Send Again</button>`;
}
// Re-wire newly injected buttons
actionsEl.querySelectorAll('.btn-void-envelope').forEach(btn => {
btn.onclick = () => _voidEnvelope(btn.dataset.id, btn.dataset.envelopeid);
});
actionsEl.querySelectorAll('.btn-send-test').forEach(btn => {
btn.onclick = () => {
const settings = getSettings();
_showSendDialog(t, settings);
};
});
}
async function _voidEnvelope(adobeId, envelopeId) {
if (!confirm('Void this test envelope?')) return;
try {
await api.verify.void(envelopeId);
if (_envelopes[adobeId]) {
_envelopes[adobeId].status = 'voided';
_envelopes[adobeId].polling = false;
}
_updateVerifyRow(adobeId);
} catch (e) {
alert('Failed to void envelope: ' + e.message);
}
}