297 lines
12 KiB
JavaScript
297 lines
12 KiB
JavaScript
// 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="callout info" style="margin-bottom:14px">
|
||
<span class="callout-icon">ℹ️</span>
|
||
This test send uses the same recipient name and email for every recipient role in the template. That is expected for a basic verification run.
|
||
</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);
|
||
}
|
||
}
|