refactor: remove Requirements page — superseded by Batches
This commit is contained in:
189
static/app.js
189
static/app.js
@@ -17,7 +17,6 @@ document.querySelectorAll('.nav-link').forEach(link => {
|
||||
if (page === 'dashboard') loadDashboard();
|
||||
if (page === 'candidates') loadCandidates();
|
||||
if (page === 'templates') loadCarboneTemplates();
|
||||
if (page === 'requirements') loadRequirements();
|
||||
if (page === 'generated') loadGeneratedCVs();
|
||||
if (page === 'batches') loadBatches();
|
||||
});
|
||||
@@ -343,194 +342,6 @@ async function deleteTemplate(id) {
|
||||
loadTemplates();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// REQUIREMENTS
|
||||
// ============================================================
|
||||
async function loadRequirements() {
|
||||
const data = await api('/api/requirements');
|
||||
const list = document.getElementById('requirements-list');
|
||||
if (!data.requirements.length) {
|
||||
list.innerHTML = '<p class="text-muted">No requirements yet. Create one to start matching candidates.</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = data.requirements.map(r => `
|
||||
<div class="card">
|
||||
<div class="flex-between">
|
||||
<div>
|
||||
<strong>${r.title}</strong>
|
||||
${r.customer_name ? '<span class="text-muted"> · ' + r.customer_name + '</span>' : ''}
|
||||
<div class="text-muted text-sm mt-8">${r.description || ''}</div>
|
||||
<div class="text-sm mt-8">${(r.requirements || []).length} position(s) requested</div>
|
||||
</div>
|
||||
<div class="flex gap-8">
|
||||
<button class="btn btn-sm" onclick="viewRequirement('${r.id}')">View</button>
|
||||
<button class="btn btn-sm btn-outline" onclick="matchRequirement('${r.id}')">Match</button>
|
||||
<button class="btn btn-sm btn-green" onclick="generateForRequirement('${r.id}')">Generate CVs</button>
|
||||
<button class="btn btn-sm btn-danger" onclick="deleteRequirement('${r.id}')">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function showRequirementModal() {
|
||||
const body = `
|
||||
<div class="form-group">
|
||||
<label>Request Title</label>
|
||||
<input type="text" id="req-title" placeholder="e.g. Q3 2026 Staff Augmentation">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Customer Name</label>
|
||||
<input type="text" id="req-customer" placeholder="Client name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Description</label>
|
||||
<textarea id="req-desc" placeholder="Overall description of the requirement"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Positions Required (JSON)</label>
|
||||
<p class="text-muted text-sm mb-16">Define each position with required skills and years of experience.</p>
|
||||
<textarea id="req-positions" style="min-height:300px;font-family:monospace;font-size:13px" placeholder='[
|
||||
{
|
||||
"position_title": "Senior .NET Developer",
|
||||
"quantity": 2,
|
||||
"experience_years": 5,
|
||||
"skills": [
|
||||
{"name": "C#", "min_years": 4, "required": true},
|
||||
{"name": ".NET Core", "min_years": 3, "required": true},
|
||||
{"name": "Azure", "min_years": 2, "required": false}
|
||||
],
|
||||
"education": "Bachelor degree in Computer Science or related",
|
||||
"description": "Backend developer with cloud experience"
|
||||
},
|
||||
{
|
||||
"position_title": "Frontend Developer",
|
||||
"quantity": 1,
|
||||
"experience_years": 3,
|
||||
"skills": [
|
||||
{"name": "React", "min_years": 2, "required": true},
|
||||
{"name": "TypeScript", "min_years": 2, "required": true}
|
||||
]
|
||||
}
|
||||
]'></textarea>
|
||||
</div>
|
||||
<button class="btn" onclick="createRequirement()">Create Requirement</button>
|
||||
`;
|
||||
showModal(body, 'Create Requirement Request');
|
||||
}
|
||||
|
||||
async function createRequirement() {
|
||||
const positions = JSON.parse(document.getElementById('req-positions').value || '[]');
|
||||
await api('/api/requirements', 'POST', {
|
||||
title: document.getElementById('req-title').value,
|
||||
customer_name: document.getElementById('req-customer').value,
|
||||
description: document.getElementById('req-desc').value,
|
||||
requirements: positions
|
||||
});
|
||||
closeModal();
|
||||
toast('Requirement created');
|
||||
loadRequirements();
|
||||
}
|
||||
|
||||
async function viewRequirement(id) {
|
||||
const data = await api('/api/requirements/' + id);
|
||||
const positions = data.requirements || [];
|
||||
const body = `
|
||||
<h2>${data.title}</h2>
|
||||
${data.customer_name ? '<p class="text-muted">Customer: ' + data.customer_name + '</p>' : ''}
|
||||
${data.description ? '<p class="mt-8">' + data.description + '</p>' : ''}
|
||||
<h3 style="margin-top:20px">Positions (${positions.length})</h3>
|
||||
${positions.map((p, i) => `
|
||||
<div class="req-position mt-16">
|
||||
<strong>${p.position_title || 'Position ' + (i+1)}</strong>
|
||||
${p.quantity ? '<span class="badge badge-blue">Qty: ' + p.quantity + '</span>' : ''}
|
||||
${p.experience_years ? '<span class="badge badge-orange">Min ' + p.experience_years + ' yrs</span>' : ''}
|
||||
${p.description ? '<p class="text-sm mt-8">' + p.description + '</p>' : ''}
|
||||
${(p.skills || []).map(s => `
|
||||
<div class="text-sm" style="margin:4px 0">
|
||||
${s.required ? '🔒' : '⚪'} ${s.name} ${s.min_years ? '(min ' + s.min_years + ' yrs)' : ''}
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`).join('')}
|
||||
${data.generated_cvs && data.generated_cvs.length ? `
|
||||
<h3 style="margin-top:20px">Generated CVs (${data.generated_cvs.length})</h3>
|
||||
${data.generated_cvs.map(g => `
|
||||
<div class="skill-row">
|
||||
<span>${g.position_title || 'N/A'} — ${g.first_name || ''} ${g.last_name || ''}</span>
|
||||
<span class="badge badge-${g.status === 'approved' ? 'green' : g.status === 'draft' ? 'orange' : 'red'}">${g.status}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
` : ''}
|
||||
`;
|
||||
showModal(body, 'Requirement Details');
|
||||
}
|
||||
|
||||
async function matchRequirement(id) {
|
||||
const list = document.getElementById('requirements-list');
|
||||
// Show loading
|
||||
const oldHTML = list.innerHTML;
|
||||
list.innerHTML = '<div class="loading"><span class="spinner"></span> Matching candidates with AI... this may take a while</div>';
|
||||
|
||||
try {
|
||||
const data = await api('/api/requirements/' + id + '/match', 'POST', {});
|
||||
const results = data.matches.filter(m => !m.error);
|
||||
results.sort((a, b) => (b.match_score || 0) - (a.match_score || 0));
|
||||
|
||||
const body = `
|
||||
<h2>Match Results</h2>
|
||||
<p class="text-muted">${results.length} candidates matched</p>
|
||||
<div style="margin-top:16px">
|
||||
${results.map(m => `
|
||||
<div class="match-item">
|
||||
<div>
|
||||
<strong>${m.candidate_name || 'Unknown'}</strong>
|
||||
<div class="text-sm text-muted">${m.position_title || ''}</div>
|
||||
<div class="text-sm">
|
||||
Match: ${m.matching_skills?.join(', ') || 'none'} |
|
||||
Missing: ${m.missing_skills?.join(', ') || 'none'}
|
||||
</div>
|
||||
<div class="text-sm text-muted mt-8">${m.reasoning || ''}</div>
|
||||
</div>
|
||||
<div class="match-score" style="color: ${m.match_score >= 70 ? 'var(--green)' : m.match_score >= 40 ? 'var(--orange)' : 'var(--red)'}">${m.match_score || 0}%</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
showModal(body, 'Match Results');
|
||||
list.innerHTML = oldHTML;
|
||||
} catch(e) {
|
||||
list.innerHTML = oldHTML;
|
||||
toast('Match failed: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function generateForRequirement(id) {
|
||||
if (!confirm('Generate tailored CVs for all parsed candidates? This may take several minutes.')) return;
|
||||
const list = document.getElementById('requirements-list');
|
||||
const oldHTML = list.innerHTML;
|
||||
list.innerHTML = '<div class="loading"><span class="spinner"></span> Generating CVs with AI... this may take several minutes</div>';
|
||||
|
||||
try {
|
||||
const data = await api('/api/requirements/' + id + '/generate', 'POST', {});
|
||||
closeModal();
|
||||
list.innerHTML = oldHTML;
|
||||
toast(`Generated ${data.generated.length} CVs`);
|
||||
loadGeneratedCVs();
|
||||
document.querySelector('[data-page="generated"]').click();
|
||||
} catch(e) {
|
||||
list.innerHTML = oldHTML;
|
||||
toast('Generation failed: ' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteRequirement(id) {
|
||||
if (!confirm('Delete this requirement and all generated CVs?')) return;
|
||||
await api('/api/requirements/' + id, 'DELETE');
|
||||
toast('Requirement deleted');
|
||||
loadRequirements();
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// GENERATED CVs
|
||||
// ============================================================
|
||||
|
||||
Reference in New Issue
Block a user