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 === 'dashboard') loadDashboard();
|
||||||
if (page === 'candidates') loadCandidates();
|
if (page === 'candidates') loadCandidates();
|
||||||
if (page === 'templates') loadCarboneTemplates();
|
if (page === 'templates') loadCarboneTemplates();
|
||||||
if (page === 'requirements') loadRequirements();
|
|
||||||
if (page === 'generated') loadGeneratedCVs();
|
if (page === 'generated') loadGeneratedCVs();
|
||||||
if (page === 'batches') loadBatches();
|
if (page === 'batches') loadBatches();
|
||||||
});
|
});
|
||||||
@@ -343,194 +342,6 @@ async function deleteTemplate(id) {
|
|||||||
loadTemplates();
|
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
|
// GENERATED CVs
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
<li><a href="#" class="nav-link" data-page="candidates">Candidates</a></li>
|
<li><a href="#" class="nav-link" data-page="candidates">Candidates</a></li>
|
||||||
<li><a href="#" class="nav-link" data-page="templates">Templates</a></li>
|
<li><a href="#" class="nav-link" data-page="templates">Templates</a></li>
|
||||||
<li><a href="#" class="nav-link" data-page="batches">Batches</a></li>
|
<li><a href="#" class="nav-link" data-page="batches">Batches</a></li>
|
||||||
<li><a href="#" class="nav-link" data-page="requirements">Requirements</a></li>
|
|
||||||
<li><a href="#" class="nav-link" data-page="generated">Generated CVs</a></li>
|
<li><a href="#" class="nav-link" data-page="generated">Generated CVs</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -117,15 +116,6 @@
|
|||||||
<div id="batch-detail-view" style="display:none"></div>
|
<div id="batch-detail-view" style="display:none"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- REQUIREMENTS -->
|
|
||||||
<div id="page-requirements" class="page">
|
|
||||||
<h2 style="margin-bottom:20px">Requirement Requests</h2>
|
|
||||||
<div class="flex mb-16 gap-8">
|
|
||||||
<button class="btn" onclick="showRequirementModal()">+ New Requirement</button>
|
|
||||||
</div>
|
|
||||||
<div id="requirements-list"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- GENERATED CVs -->
|
<!-- GENERATED CVs -->
|
||||||
<div id="page-generated" class="page">
|
<div id="page-generated" class="page">
|
||||||
<h2 style="margin-bottom:20px">Generated CVs</h2>
|
<h2 style="margin-bottom:20px">Generated CVs</h2>
|
||||||
@@ -150,7 +140,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="/static/app.js?v=17"></script>
|
<script src="/static/app.js?v=18"></script>
|
||||||
<script src="/static/carbone.js?v=16"></script>
|
<script src="/static/carbone.js?v=16"></script>
|
||||||
<script src="/static/batches.js?v=2"></script>
|
<script src="/static/batches.js?v=2"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user