feat: batch polish — edit/delete positions, ZIP export, exports page, candidate batch links

This commit is contained in:
root
2026-07-29 08:31:35 +00:00
parent 29c0a0c348
commit 040882deb5
5 changed files with 423 additions and 73 deletions

View File

@@ -166,6 +166,22 @@ async function viewCandidate(id) {
</div>
`).join('') || '<p class="text-muted">No certifications extracted</p>'}
</div>
<div class="card">
<h3 style="margin-bottom:12px">Batches (${(data.batches || []).length})</h3>
${(data.batches || []).length ? (data.batches || []).map(b => `
<div class="skill-row" style="margin-bottom:8px">
<div>
<strong>${escapeHtml(b.batch_name || 'Batch')}</strong>
<div class="text-muted text-sm">${escapeHtml(b.position_title || 'Unassigned')} · ${escapeHtml(b.item_status || '')} · Score ${b.match_score || 0}%</div>
</div>
<div class="flex gap-8">
<span class="badge badge-${b.batch_status === 'exported' ? 'blue' : b.batch_status === 'active' ? 'green' : 'orange'}">${escapeHtml(b.batch_status || 'draft')}</span>
${b.generated_cv_path ? `<a class="btn btn-sm btn-outline" href="${b.generated_cv_path}" target="_blank">PDF</a>` : ''}
<button class="btn btn-sm" onclick="closeModal(); document.querySelector('[data-page=\\'batches\\']')?.click(); setTimeout(() => openBatch('${b.batch_id}'), 100);">Open</button>
</div>
</div>
`).join('') : '<p class="text-muted">Not linked to any batches yet.</p>'}
</div>
<div class="card">
<h3 style="margin-bottom:12px">Raw CV Text</h3>
<div style="max-height:400px;overflow-y:auto;background:var(--bg-input);padding:12px;border-radius:6px;white-space:pre-wrap;font-size:13px">${c.raw_cv_text || 'No raw text available'}</div>
@@ -354,26 +370,52 @@ async function deleteTemplate(id) {
async function loadGeneratedCVs() {
const data = await api('/api/generated-cvs');
const list = document.getElementById('generated-list');
if (!data.generated_cvs.length) {
list.innerHTML = '<p class="text-muted">No generated CVs yet. Match and generate from a requirement request.</p>';
const rows = data.generated_cvs || [];
if (!rows.length) {
list.innerHTML = '<p class="text-muted">No generated CVs yet. Approve candidates in a Batch and click Generate CVs.</p>';
return;
}
list.innerHTML = data.generated_cvs.map(g => `
list.innerHTML = rows.map(g => {
const source = g.source || 'legacy';
const status = g.item_status || g.status || 'draft';
const openBtn = g.generated_cv_path
? `<a class="btn btn-sm btn-outline" href="${g.generated_cv_path}" target="_blank">Open PDF</a>`
: `<button class="btn btn-sm btn-outline" onclick="viewGeneratedCV('${g.id}')">View</button>`;
const zipBtn = (source === 'batch' && g.batch_id)
? `<a class="btn btn-sm btn-outline" href="/api/batches/${g.batch_id}/download-zip" target="_blank">Batch ZIP</a>`
: '';
const delBtn = source === 'batch'
? `<button class="btn btn-sm btn-danger" onclick="deleteBatchGenerated('${g.batch_id}', '${g.id}')">Remove</button>`
: `<button class="btn btn-sm btn-danger" onclick="deleteGeneratedCV('${g.id}')">Delete</button>`;
return `
<div class="card">
<div class="flex-between">
<div>
<strong>${g.first_name || ''} ${g.last_name || ''}</strong>
<span class="text-muted"> · ${g.position_title || 'N/A'}</span>
<div class="text-sm text-muted mt-8">Generated: ${formatDate(g.generation_date)} · Score: ${g.match_score || 0}%</div>
<strong>${escapeHtml(g.first_name || '')} ${escapeHtml(g.last_name || '')}</strong>
<span class="text-muted"> · ${escapeHtml(g.position_title || 'N/A')}</span>
<div class="text-sm text-muted mt-8">
Batch: ${escapeHtml(g.batch_name || '—')} · Score: ${g.match_score || 0}% ·
${g.updated_at ? ('Updated: ' + formatDate(g.updated_at)) : (g.created_at ? ('Created: ' + formatDate(g.created_at)) : '')}
</div>
</div>
<div class="flex gap-8">
<span class="badge badge-${g.status === 'approved' ? 'green' : g.status === 'draft' ? 'orange' : g.status === 'reviewed' ? 'blue' : 'red'}">${g.status}</span>
<button class="btn btn-sm btn-outline" onclick="viewGeneratedCV('${g.id}')">View</button>
<button class="btn btn-sm btn-danger" onclick="deleteGeneratedCV('${g.id}')">Delete</button>
<span class="badge badge-${status === 'approved' ? 'green' : status === 'draft' || status === 'proposed' ? 'orange' : status === 'exported' || status === 'reviewed' ? 'blue' : 'red'}">${escapeHtml(status)}</span>
<span class="badge badge-blue">${source === 'batch' ? 'Batch export' : 'Legacy'}</span>
${openBtn}
${zipBtn}
${delBtn}
</div>
</div>
</div>
`).join('');
</div>`;
}).join('');
}
async function deleteBatchGenerated(batchId, itemId) {
if (!confirm('Remove this generated CV from the exports list?')) return;
await api('/api/batches/' + batchId + '/items/' + itemId + '/generated', 'DELETE');
toast('Generated CV removed');
loadGeneratedCVs();
}
async function viewGeneratedCV(id) {