michael-agent 1.0.2__py3-none-any.whl → 1.0.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- michael_agent/dashboard/static/styles.css +311 -0
- michael_agent/dashboard/templates/career_portal.html +482 -0
- michael_agent/dashboard/templates/dashboard.html +807 -0
- michael_agent/dashboard/templates/jd_creation.html +318 -0
- michael_agent/dashboard/templates/resume_scoring.html +1032 -0
- michael_agent/dashboard/templates/upload_resume.html +411 -0
- {michael_agent-1.0.2.dist-info → michael_agent-1.0.4.dist-info}/METADATA +1 -1
- {michael_agent-1.0.2.dist-info → michael_agent-1.0.4.dist-info}/RECORD +10 -4
- {michael_agent-1.0.2.dist-info → michael_agent-1.0.4.dist-info}/WHEEL +0 -0
- {michael_agent-1.0.2.dist-info → michael_agent-1.0.4.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1032 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<title>Resume Scoring - SmartRecruitAgent</title>
|
7
|
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
8
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
|
9
|
+
<link rel="stylesheet" href="/static/styles.css">
|
10
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
11
|
+
</head>
|
12
|
+
<body>
|
13
|
+
<header class="bg-dark text-white p-3">
|
14
|
+
<div class="container">
|
15
|
+
<div class="d-flex justify-content-between align-items-center">
|
16
|
+
<h1 class="h3 mb-0">SmartRecruitAgent - Resume Scoring</h1>
|
17
|
+
<div>
|
18
|
+
<a href="/career-portal" class="btn btn-outline-light me-2">Career Portal</a>
|
19
|
+
<a href="/" class="btn btn-outline-light">Dashboard</a>
|
20
|
+
</div>
|
21
|
+
</div>
|
22
|
+
</div>
|
23
|
+
</header>
|
24
|
+
|
25
|
+
<div class="container mt-4">
|
26
|
+
<div class="row">
|
27
|
+
<!-- Filters and Job Selection -->
|
28
|
+
<div class="col-md-3">
|
29
|
+
<div class="card mb-4">
|
30
|
+
<div class="card-header bg-primary text-white">
|
31
|
+
Job Description
|
32
|
+
</div>
|
33
|
+
<div class="card-body">
|
34
|
+
<div class="mb-3">
|
35
|
+
<label for="job-select" class="form-label">Select Job</label>
|
36
|
+
<select class="form-select" id="job-select">
|
37
|
+
<option value="" selected>Loading jobs...</option>
|
38
|
+
</select>
|
39
|
+
</div>
|
40
|
+
<div id="job-details-container" class="d-none">
|
41
|
+
<h5 id="job-title" class="mt-3"></h5>
|
42
|
+
<p id="job-location" class="mb-1 text-muted"></p>
|
43
|
+
<div class="mb-2">
|
44
|
+
<span class="badge bg-info me-1" id="job-type"></span>
|
45
|
+
<span class="badge bg-secondary" id="job-level"></span>
|
46
|
+
</div>
|
47
|
+
<p class="mb-2 small">Required Skills:</p>
|
48
|
+
<div id="required-skills-container" class="mb-3 d-flex flex-wrap gap-1"></div>
|
49
|
+
</div>
|
50
|
+
</div>
|
51
|
+
</div>
|
52
|
+
|
53
|
+
<div class="card mb-4">
|
54
|
+
<div class="card-header bg-primary text-white">
|
55
|
+
Filters
|
56
|
+
</div>
|
57
|
+
<div class="card-body">
|
58
|
+
<div class="mb-3">
|
59
|
+
<label for="score-filter" class="form-label">Minimum Score</label>
|
60
|
+
<input type="range" class="form-range" id="score-filter" min="0" max="100" value="50">
|
61
|
+
<div class="d-flex justify-content-between">
|
62
|
+
<span>0</span>
|
63
|
+
<span id="score-value">50</span>
|
64
|
+
<span>100</span>
|
65
|
+
</div>
|
66
|
+
</div>
|
67
|
+
<div class="mb-3">
|
68
|
+
<label class="form-label d-block">Status</label>
|
69
|
+
<div class="form-check form-check-inline">
|
70
|
+
<input class="form-check-input" type="checkbox" id="status-new" value="new" checked>
|
71
|
+
<label class="form-check-label" for="status-new">New</label>
|
72
|
+
</div>
|
73
|
+
<div class="form-check form-check-inline">
|
74
|
+
<input class="form-check-input" type="checkbox" id="status-reviewed" value="reviewed" checked>
|
75
|
+
<label class="form-check-label" for="status-reviewed">Reviewed</label>
|
76
|
+
</div>
|
77
|
+
</div>
|
78
|
+
<div class="d-grid">
|
79
|
+
<button id="apply-filters" class="btn btn-primary">Apply Filters</button>
|
80
|
+
</div>
|
81
|
+
</div>
|
82
|
+
</div>
|
83
|
+
|
84
|
+
<div class="card">
|
85
|
+
<div class="card-header bg-primary text-white">
|
86
|
+
Statistics
|
87
|
+
</div>
|
88
|
+
<div class="card-body">
|
89
|
+
<canvas id="score-distribution-chart"></canvas>
|
90
|
+
<div class="mt-3 text-center">
|
91
|
+
<div class="d-flex justify-content-between">
|
92
|
+
<div>
|
93
|
+
<h6 class="mb-0" id="total-resumes">0</h6>
|
94
|
+
<small class="text-muted">Total</small>
|
95
|
+
</div>
|
96
|
+
<div>
|
97
|
+
<h6 class="mb-0 text-success" id="qualified-resumes">0</h6>
|
98
|
+
<small class="text-muted">Qualified</small>
|
99
|
+
</div>
|
100
|
+
<div>
|
101
|
+
<h6 class="mb-0 text-danger" id="unqualified-resumes">0</h6>
|
102
|
+
<small class="text-muted">Unqualified</small>
|
103
|
+
</div>
|
104
|
+
</div>
|
105
|
+
</div>
|
106
|
+
</div>
|
107
|
+
</div>
|
108
|
+
</div>
|
109
|
+
|
110
|
+
<!-- Resume List and Details -->
|
111
|
+
<div class="col-md-9">
|
112
|
+
<div class="card mb-4">
|
113
|
+
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
114
|
+
<span>Scored Resumes</span>
|
115
|
+
<div>
|
116
|
+
<button class="btn btn-sm btn-light" id="export-csv">Export CSV</button>
|
117
|
+
<button class="btn btn-sm btn-light ms-2" id="refresh-data">Refresh</button>
|
118
|
+
</div>
|
119
|
+
</div>
|
120
|
+
<div class="card-body">
|
121
|
+
<div class="table-responsive">
|
122
|
+
<table class="table table-hover">
|
123
|
+
<thead>
|
124
|
+
<tr>
|
125
|
+
<th>Candidate</th>
|
126
|
+
<th>Date</th>
|
127
|
+
<th>Score</th>
|
128
|
+
<th>Skills Match</th>
|
129
|
+
<th>Status</th>
|
130
|
+
<th>Actions</th>
|
131
|
+
</tr>
|
132
|
+
</thead>
|
133
|
+
<tbody id="resumes-table-body">
|
134
|
+
<tr>
|
135
|
+
<td colspan="6" class="text-center">Select a job to view candidates</td>
|
136
|
+
</tr>
|
137
|
+
</tbody>
|
138
|
+
</table>
|
139
|
+
</div>
|
140
|
+
</div>
|
141
|
+
</div>
|
142
|
+
|
143
|
+
<div class="card" id="resume-details-card" style="display: none;">
|
144
|
+
<div class="card-header bg-info text-white d-flex justify-content-between align-items-center">
|
145
|
+
<span>Candidate Details</span>
|
146
|
+
<button type="button" class="btn-close btn-close-white" id="close-resume-details"></button>
|
147
|
+
</div>
|
148
|
+
<div class="card-body">
|
149
|
+
<div class="row">
|
150
|
+
<div class="col-md-8">
|
151
|
+
<h4 id="candidate-name"></h4>
|
152
|
+
<div class="d-flex align-items-center mb-3">
|
153
|
+
<div id="candidate-score-badge" class="me-3"></div>
|
154
|
+
<div id="candidate-contact" class="small text-muted"></div>
|
155
|
+
</div>
|
156
|
+
|
157
|
+
<div class="mb-3">
|
158
|
+
<h5>Skills Analysis</h5>
|
159
|
+
<div id="skills-match-container"></div>
|
160
|
+
</div>
|
161
|
+
|
162
|
+
<div class="mb-3">
|
163
|
+
<h5>Experience</h5>
|
164
|
+
<div id="experience-container"></div>
|
165
|
+
</div>
|
166
|
+
|
167
|
+
<div>
|
168
|
+
<h5>Education</h5>
|
169
|
+
<div id="education-container"></div>
|
170
|
+
</div>
|
171
|
+
</div>
|
172
|
+
<div class="col-md-4">
|
173
|
+
<div class="card border-info mb-3">
|
174
|
+
<div class="card-header bg-light">AI Analysis</div>
|
175
|
+
<div class="card-body">
|
176
|
+
<div id="ai-analysis-container"></div>
|
177
|
+
</div>
|
178
|
+
</div>
|
179
|
+
|
180
|
+
<div class="card border-success">
|
181
|
+
<div class="card-header bg-light">Actions</div>
|
182
|
+
<div class="card-body">
|
183
|
+
<div class="d-grid gap-2">
|
184
|
+
<button class="btn btn-outline-primary" id="view-resume-btn">
|
185
|
+
<i class="bi bi-file-earmark-text"></i> View Full Resume
|
186
|
+
</button>
|
187
|
+
<button class="btn btn-outline-success" id="schedule-interview-btn">
|
188
|
+
<i class="bi bi-calendar2-check"></i> Schedule Interview
|
189
|
+
</button>
|
190
|
+
<button class="btn btn-outline-warning" id="generate-questions-btn">
|
191
|
+
<i class="bi bi-question-circle"></i> Generate Questions
|
192
|
+
</button>
|
193
|
+
<div class="input-group">
|
194
|
+
<select class="form-select" id="status-update">
|
195
|
+
<option selected>Change Status...</option>
|
196
|
+
<option value="new">New</option>
|
197
|
+
<option value="shortlisted">Shortlist</option>
|
198
|
+
<option value="interviewed">Interviewed</option>
|
199
|
+
<option value="rejected">Reject</option>
|
200
|
+
<option value="hired">Hire</option>
|
201
|
+
</select>
|
202
|
+
<button class="btn btn-outline-secondary" id="update-status-btn">Update</button>
|
203
|
+
</div>
|
204
|
+
</div>
|
205
|
+
</div>
|
206
|
+
</div>
|
207
|
+
</div>
|
208
|
+
</div>
|
209
|
+
</div>
|
210
|
+
</div>
|
211
|
+
</div>
|
212
|
+
</div>
|
213
|
+
</div>
|
214
|
+
|
215
|
+
<!-- Modal for Interview Questions -->
|
216
|
+
<div class="modal fade" id="questions-modal" tabindex="-1" aria-hidden="true">
|
217
|
+
<div class="modal-dialog modal-lg">
|
218
|
+
<div class="modal-content">
|
219
|
+
<div class="modal-header bg-primary text-white">
|
220
|
+
<h5 class="modal-title">Interview Questions</h5>
|
221
|
+
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
222
|
+
</div>
|
223
|
+
<div class="modal-body" id="questions-container">
|
224
|
+
<p class="text-center">Generating personalized questions...</p>
|
225
|
+
</div>
|
226
|
+
<div class="modal-footer">
|
227
|
+
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
228
|
+
<button type="button" class="btn btn-primary" id="copy-questions-btn">Copy All</button>
|
229
|
+
</div>
|
230
|
+
</div>
|
231
|
+
</div>
|
232
|
+
</div>
|
233
|
+
|
234
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
235
|
+
<script>
|
236
|
+
document.addEventListener('DOMContentLoaded', function() {
|
237
|
+
// Chart initialization
|
238
|
+
const scoreChart = new Chart(document.getElementById('score-distribution-chart'), {
|
239
|
+
type: 'doughnut',
|
240
|
+
data: {
|
241
|
+
labels: ['Below 50', '50-70', '70-85', '85-100'],
|
242
|
+
datasets: [{
|
243
|
+
data: [0, 0, 0, 0],
|
244
|
+
backgroundColor: ['#dc3545', '#ffc107', '#0dcaf0', '#198754']
|
245
|
+
}]
|
246
|
+
},
|
247
|
+
options: {
|
248
|
+
responsive: true,
|
249
|
+
plugins: {
|
250
|
+
legend: {
|
251
|
+
position: 'bottom',
|
252
|
+
labels: {
|
253
|
+
font: {
|
254
|
+
size: 10
|
255
|
+
}
|
256
|
+
}
|
257
|
+
}
|
258
|
+
}
|
259
|
+
}
|
260
|
+
});
|
261
|
+
|
262
|
+
// Score filter range functionality
|
263
|
+
const scoreFilter = document.getElementById('score-filter');
|
264
|
+
const scoreValue = document.getElementById('score-value');
|
265
|
+
|
266
|
+
scoreFilter.addEventListener('input', function() {
|
267
|
+
scoreValue.textContent = this.value;
|
268
|
+
});
|
269
|
+
|
270
|
+
// Load job listings
|
271
|
+
fetch('/api/jobs')
|
272
|
+
.then(response => response.json())
|
273
|
+
.then(data => {
|
274
|
+
const jobSelect = document.getElementById('job-select');
|
275
|
+
jobSelect.innerHTML = '';
|
276
|
+
|
277
|
+
if (data.jobs.length === 0) {
|
278
|
+
const option = document.createElement('option');
|
279
|
+
option.value = '';
|
280
|
+
option.textContent = 'No jobs available';
|
281
|
+
jobSelect.appendChild(option);
|
282
|
+
} else {
|
283
|
+
const defaultOption = document.createElement('option');
|
284
|
+
defaultOption.value = '';
|
285
|
+
defaultOption.textContent = 'Select a job';
|
286
|
+
jobSelect.appendChild(defaultOption);
|
287
|
+
|
288
|
+
data.jobs.forEach(job => {
|
289
|
+
const option = document.createElement('option');
|
290
|
+
option.value = job.id;
|
291
|
+
option.textContent = job.title;
|
292
|
+
jobSelect.appendChild(option);
|
293
|
+
});
|
294
|
+
}
|
295
|
+
})
|
296
|
+
.catch(error => console.error('Error loading jobs:', error));
|
297
|
+
|
298
|
+
// Job selection change handler
|
299
|
+
document.getElementById('job-select').addEventListener('change', function() {
|
300
|
+
const jobId = this.value;
|
301
|
+
|
302
|
+
if (!jobId) {
|
303
|
+
document.getElementById('job-details-container').classList.add('d-none');
|
304
|
+
document.getElementById('resumes-table-body').innerHTML =
|
305
|
+
'<tr><td colspan="6" class="text-center">Select a job to view candidates</td></tr>';
|
306
|
+
return;
|
307
|
+
}
|
308
|
+
|
309
|
+
// Load job details
|
310
|
+
fetch(`/api/jobs/${jobId}`)
|
311
|
+
.then(response => response.json())
|
312
|
+
.then(job => {
|
313
|
+
document.getElementById('job-title').textContent = job.title;
|
314
|
+
document.getElementById('job-location').textContent = job.location;
|
315
|
+
document.getElementById('job-type').textContent = job.employment_type;
|
316
|
+
document.getElementById('job-level').textContent = job.experience_level;
|
317
|
+
|
318
|
+
// Display required skills
|
319
|
+
const skillsContainer = document.getElementById('required-skills-container');
|
320
|
+
skillsContainer.innerHTML = '';
|
321
|
+
job.required_skills.forEach(skill => {
|
322
|
+
const badge = document.createElement('span');
|
323
|
+
badge.className = 'badge bg-light text-dark';
|
324
|
+
badge.textContent = skill;
|
325
|
+
skillsContainer.appendChild(badge);
|
326
|
+
});
|
327
|
+
|
328
|
+
document.getElementById('job-details-container').classList.remove('d-none');
|
329
|
+
|
330
|
+
// Load candidates for this job
|
331
|
+
loadCandidates(jobId);
|
332
|
+
})
|
333
|
+
.catch(error => console.error('Error loading job details:', error));
|
334
|
+
});
|
335
|
+
|
336
|
+
// Load candidates for a specific job
|
337
|
+
function loadCandidates(jobId, filters = {}) {
|
338
|
+
fetch(`/api/candidates?job_id=${jobId}${getFilterQueryString(filters)}`)
|
339
|
+
.then(response => response.json())
|
340
|
+
.then(data => {
|
341
|
+
renderCandidatesTable(data.candidates);
|
342
|
+
updateStatistics(data.statistics);
|
343
|
+
})
|
344
|
+
.catch(error => console.error('Error loading candidates:', error));
|
345
|
+
}
|
346
|
+
|
347
|
+
// Helper to build query string from filters
|
348
|
+
function getFilterQueryString(filters) {
|
349
|
+
const params = [];
|
350
|
+
if (filters.minScore) params.push(`min_score=${filters.minScore}`);
|
351
|
+
if (filters.status && filters.status.length) {
|
352
|
+
params.push(`status=${filters.status.join(',')}`);
|
353
|
+
}
|
354
|
+
return params.length ? '&' + params.join('&') : '';
|
355
|
+
}
|
356
|
+
|
357
|
+
// Format score for display
|
358
|
+
function formatScore(score) {
|
359
|
+
if (score === undefined || score === null) {
|
360
|
+
return "N/A";
|
361
|
+
}
|
362
|
+
return (parseFloat(score) * 100).toFixed(1) + "%";
|
363
|
+
}
|
364
|
+
|
365
|
+
// Render candidates table
|
366
|
+
function renderCandidatesTable(candidates) {
|
367
|
+
const tableBody = document.getElementById('resumes-table-body');
|
368
|
+
tableBody.innerHTML = '';
|
369
|
+
|
370
|
+
if (candidates.length === 0) {
|
371
|
+
tableBody.innerHTML = '<tr><td colspan="6" class="text-center">No candidates found</td></tr>';
|
372
|
+
return;
|
373
|
+
}
|
374
|
+
|
375
|
+
candidates.forEach(candidate => {
|
376
|
+
const row = document.createElement('tr');
|
377
|
+
row.className = 'candidate-row';
|
378
|
+
row.setAttribute('data-id', candidate.id);
|
379
|
+
|
380
|
+
// Score color class
|
381
|
+
const scoreClass = getScoreClass(candidate.score);
|
382
|
+
|
383
|
+
// Format date
|
384
|
+
const date = new Date(candidate.date_processed);
|
385
|
+
const formattedDate = date.toLocaleDateString();
|
386
|
+
|
387
|
+
row.innerHTML = `
|
388
|
+
<td>
|
389
|
+
<div class="fw-bold">${candidate.name}</div>
|
390
|
+
<div class="small text-muted">${candidate.email || 'No email provided'}</div>
|
391
|
+
</td>
|
392
|
+
<td>${formattedDate}</td>
|
393
|
+
<td>
|
394
|
+
<div class="d-flex align-items-center">
|
395
|
+
<div class="progress flex-grow-1 me-2" style="height: 8px;">
|
396
|
+
<div class="progress-bar ${scoreClass}" style="width: ${candidate.score}%;"></div>
|
397
|
+
</div>
|
398
|
+
<span class="small ${scoreClass}-text">${candidate.score}%</span>
|
399
|
+
</div>
|
400
|
+
</td>
|
401
|
+
<td>
|
402
|
+
<div class="d-flex align-items-center">
|
403
|
+
<span class="me-2">${candidate.skills_match.matched}/${candidate.skills_match.total}</span>
|
404
|
+
<div class="progress flex-grow-1" style="height: 8px;">
|
405
|
+
<div class="progress-bar bg-info"
|
406
|
+
style="width: ${candidate.skills_match.matched / candidate.skills_match.total * 100}%;"></div>
|
407
|
+
</div>
|
408
|
+
</div>
|
409
|
+
</td>
|
410
|
+
<td><span class="badge bg-secondary">${formatStatus(candidate.status)}</span></td>
|
411
|
+
<td>
|
412
|
+
<button class="btn btn-sm btn-outline-primary view-candidate-btn" data-id="${candidate.id}">
|
413
|
+
View
|
414
|
+
</button>
|
415
|
+
</td>
|
416
|
+
`;
|
417
|
+
|
418
|
+
tableBody.appendChild(row);
|
419
|
+
});
|
420
|
+
|
421
|
+
// Add event listeners to view buttons
|
422
|
+
document.querySelectorAll('.view-candidate-btn').forEach(btn => {
|
423
|
+
btn.addEventListener('click', function() {
|
424
|
+
const candidateId = this.getAttribute('data-id');
|
425
|
+
loadCandidateDetails(candidateId);
|
426
|
+
});
|
427
|
+
});
|
428
|
+
}
|
429
|
+
|
430
|
+
// Load candidate details
|
431
|
+
function loadCandidateDetails(candidateId) {
|
432
|
+
console.log('Loading candidate details for:', candidateId);
|
433
|
+
// Show loading indicator
|
434
|
+
document.getElementById('resume-details-card').style.display = 'block';
|
435
|
+
document.getElementById('candidate-name').textContent = 'Loading...';
|
436
|
+
document.getElementById('candidate-score-badge').innerHTML =
|
437
|
+
`<span class="badge bg-secondary">Loading...</span>`;
|
438
|
+
document.getElementById('candidate-contact').innerHTML =
|
439
|
+
`<div class="spinner-border spinner-border-sm text-secondary" role="status">
|
440
|
+
<span class="visually-hidden">Loading...</span>
|
441
|
+
</div>`;
|
442
|
+
|
443
|
+
// Clear content areas
|
444
|
+
document.getElementById('skills-match-container').innerHTML = '<p>Loading skills data...</p>';
|
445
|
+
document.getElementById('experience-container').innerHTML = '<p>Loading experience data...</p>';
|
446
|
+
document.getElementById('education-container').innerHTML = '<p>Loading education data...</p>';
|
447
|
+
document.getElementById('ai-analysis-container').innerHTML = '<p>Loading analysis data...</p>';
|
448
|
+
|
449
|
+
fetch(`/api/candidates/${candidateId}`)
|
450
|
+
.then(response => {
|
451
|
+
if (!response.ok) {
|
452
|
+
throw new Error(`Failed to load candidate details: ${response.status}`);
|
453
|
+
}
|
454
|
+
return response.json();
|
455
|
+
})
|
456
|
+
.then(candidate => {
|
457
|
+
console.log('Candidate data received:', candidate);
|
458
|
+
|
459
|
+
try {
|
460
|
+
// Display candidate details
|
461
|
+
document.getElementById('candidate-name').textContent = candidate.name || 'Unknown Candidate';
|
462
|
+
|
463
|
+
// Score badge
|
464
|
+
const score = typeof candidate.score === 'number' ? candidate.score : 0;
|
465
|
+
const scoreClass = getScoreClass(score);
|
466
|
+
document.getElementById('candidate-score-badge').innerHTML =
|
467
|
+
`<span class="badge ${scoreClass} fs-6">${score}% Match</span>`;
|
468
|
+
|
469
|
+
// Contact info
|
470
|
+
document.getElementById('candidate-contact').innerHTML =
|
471
|
+
`<i class="bi bi-envelope"></i> ${candidate.email || 'No email'}
|
472
|
+
<i class="bi bi-telephone"></i> ${candidate.phone || 'No phone'}`;
|
473
|
+
|
474
|
+
// Skills matching - make sure we don't pass undefined
|
475
|
+
renderSkillsMatch(candidate.skills_analysis || {});
|
476
|
+
|
477
|
+
// Experience - make sure we don't pass undefined
|
478
|
+
renderExperience(candidate.experience || []);
|
479
|
+
|
480
|
+
// Education - make sure we don't pass undefined
|
481
|
+
renderEducation(candidate.education || []);
|
482
|
+
|
483
|
+
// AI Analysis - make sure we don't pass undefined
|
484
|
+
renderAIAnalysis(candidate.ai_analysis || {});
|
485
|
+
} catch (error) {
|
486
|
+
console.error('Error processing candidate data:', error);
|
487
|
+
document.getElementById('resume-details-card').style.display = 'block';
|
488
|
+
document.getElementById('skills-match-container').innerHTML =
|
489
|
+
'<div class="alert alert-warning">Error processing candidate data. Some information may be incomplete.</div>';
|
490
|
+
}
|
491
|
+
|
492
|
+
// Scroll to the details
|
493
|
+
document.getElementById('resume-details-card').scrollIntoView({ behavior: 'smooth' });
|
494
|
+
|
495
|
+
// Set up the view resume button
|
496
|
+
document.getElementById('view-resume-btn').onclick = function() {
|
497
|
+
if (candidate.resume_path) {
|
498
|
+
window.open(`/resumes/${candidateId}`, '_blank');
|
499
|
+
} else {
|
500
|
+
alert('Resume file not available for viewing');
|
501
|
+
}
|
502
|
+
};
|
503
|
+
|
504
|
+
// Set up generate questions button with modal
|
505
|
+
const questionsModal = new bootstrap.Modal(document.getElementById('questions-modal'));
|
506
|
+
document.getElementById('generate-questions-btn').onclick = function() {
|
507
|
+
questionsModal.show();
|
508
|
+
document.getElementById('questions-container').innerHTML =
|
509
|
+
'<div class="d-flex justify-content-center"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Loading...</span></div></div>';
|
510
|
+
|
511
|
+
// Make API call to generate questions
|
512
|
+
fetch(`/api/generate-questions/${candidateId}`)
|
513
|
+
.then(response => {
|
514
|
+
if (!response.ok) {
|
515
|
+
throw new Error(`Failed to generate questions: ${response.status}`);
|
516
|
+
}
|
517
|
+
return response.json();
|
518
|
+
})
|
519
|
+
.then(data => {
|
520
|
+
renderInterviewQuestions(data.questions);
|
521
|
+
})
|
522
|
+
.catch(error => {
|
523
|
+
console.error('Error generating questions:', error);
|
524
|
+
document.getElementById('questions-container').innerHTML =
|
525
|
+
'<div class="alert alert-danger">Failed to generate questions. Please try again later.</div>';
|
526
|
+
});
|
527
|
+
};
|
528
|
+
})
|
529
|
+
.catch(error => {
|
530
|
+
console.error('Error loading candidate details:', error);
|
531
|
+
document.getElementById('resume-details-card').style.display = 'block';
|
532
|
+
document.getElementById('candidate-name').textContent = 'Error Loading Data';
|
533
|
+
document.getElementById('candidate-score-badge').innerHTML = '';
|
534
|
+
document.getElementById('candidate-contact').innerHTML = '';
|
535
|
+
document.getElementById('skills-match-container').innerHTML =
|
536
|
+
`<div class="alert alert-danger">
|
537
|
+
<h5>Error loading candidate details</h5>
|
538
|
+
<p>We couldn't load the candidate details. Please try refreshing the page.</p>
|
539
|
+
<p><small>Error: ${error.message}</small></p>
|
540
|
+
</div>`;
|
541
|
+
document.getElementById('experience-container').innerHTML = '';
|
542
|
+
document.getElementById('education-container').innerHTML = '';
|
543
|
+
document.getElementById('ai-analysis-container').innerHTML = '';
|
544
|
+
});
|
545
|
+
}
|
546
|
+
|
547
|
+
// Render skills matching
|
548
|
+
function renderSkillsMatch(skillsAnalysis) {
|
549
|
+
const container = document.getElementById('skills-match-container');
|
550
|
+
container.innerHTML = '';
|
551
|
+
|
552
|
+
try {
|
553
|
+
// Handle missing skills analysis data
|
554
|
+
if (!skillsAnalysis || Object.keys(skillsAnalysis).length === 0) {
|
555
|
+
container.innerHTML = '<p class="text-muted">No skills analysis available</p>';
|
556
|
+
return;
|
557
|
+
}
|
558
|
+
|
559
|
+
// Create skills structure for required skills
|
560
|
+
const requiredDiv = document.createElement('div');
|
561
|
+
requiredDiv.className = 'mb-3';
|
562
|
+
requiredDiv.innerHTML = '<h6>Required Skills</h6>';
|
563
|
+
|
564
|
+
const requiredSkillsContainer = document.createElement('div');
|
565
|
+
requiredSkillsContainer.className = 'd-flex flex-wrap gap-2 mb-3';
|
566
|
+
|
567
|
+
// Handle both array format and object format for required skills
|
568
|
+
if (Array.isArray(skillsAnalysis.required)) {
|
569
|
+
const requiredSkills = skillsAnalysis.required;
|
570
|
+
|
571
|
+
if (requiredSkills.length === 0) {
|
572
|
+
requiredSkillsContainer.innerHTML = '<p class="text-muted">No required skills specified</p>';
|
573
|
+
} else {
|
574
|
+
requiredSkills.forEach(skill => {
|
575
|
+
if (skill) {
|
576
|
+
const badge = document.createElement('span');
|
577
|
+
badge.className = skill.found ? 'badge bg-success' : 'badge bg-danger';
|
578
|
+
badge.innerHTML = skill.name +
|
579
|
+
(skill.found ? ' <i class="bi bi-check"></i>' : ' <i class="bi bi-x"></i>');
|
580
|
+
requiredSkillsContainer.appendChild(badge);
|
581
|
+
}
|
582
|
+
});
|
583
|
+
}
|
584
|
+
} else if (skillsAnalysis.matched !== undefined && skillsAnalysis.total !== undefined) {
|
585
|
+
// Handle cases where we only have the counts but not the full skill details
|
586
|
+
requiredSkillsContainer.innerHTML = `<p>Matched ${skillsAnalysis.matched} of ${skillsAnalysis.total} required skills</p>`;
|
587
|
+
} else {
|
588
|
+
requiredSkillsContainer.innerHTML = '<p class="text-muted">No required skills data available</p>';
|
589
|
+
}
|
590
|
+
|
591
|
+
requiredDiv.appendChild(requiredSkillsContainer);
|
592
|
+
container.appendChild(requiredDiv);
|
593
|
+
|
594
|
+
// Additional skills
|
595
|
+
if (skillsAnalysis.additional && Array.isArray(skillsAnalysis.additional) && skillsAnalysis.additional.length > 0) {
|
596
|
+
const additionalDiv = document.createElement('div');
|
597
|
+
additionalDiv.innerHTML = '<h6>Additional Skills</h6>';
|
598
|
+
|
599
|
+
const additionalSkillsContainer = document.createElement('div');
|
600
|
+
additionalSkillsContainer.className = 'd-flex flex-wrap gap-2';
|
601
|
+
|
602
|
+
skillsAnalysis.additional.forEach(skill => {
|
603
|
+
if (typeof skill === 'string') {
|
604
|
+
const badge = document.createElement('span');
|
605
|
+
badge.className = 'badge bg-info';
|
606
|
+
badge.textContent = skill;
|
607
|
+
additionalSkillsContainer.appendChild(badge);
|
608
|
+
}
|
609
|
+
});
|
610
|
+
|
611
|
+
additionalDiv.appendChild(additionalSkillsContainer);
|
612
|
+
container.appendChild(additionalDiv);
|
613
|
+
}
|
614
|
+
|
615
|
+
} catch (error) {
|
616
|
+
console.error('Error rendering skills match:', error);
|
617
|
+
container.innerHTML = '<div class="alert alert-warning">Error displaying skills information</div>';
|
618
|
+
}
|
619
|
+
}
|
620
|
+
|
621
|
+
// Render experience
|
622
|
+
function renderExperience(experience) {
|
623
|
+
const container = document.getElementById('experience-container');
|
624
|
+
container.innerHTML = '';
|
625
|
+
|
626
|
+
try {
|
627
|
+
if (!experience || !Array.isArray(experience) || experience.length === 0) {
|
628
|
+
container.innerHTML = '<p class="text-muted">No experience information available</p>';
|
629
|
+
return;
|
630
|
+
}
|
631
|
+
|
632
|
+
experience.forEach(exp => {
|
633
|
+
try {
|
634
|
+
if (!exp) return; // Skip if experience entry is null or undefined
|
635
|
+
|
636
|
+
const expDiv = document.createElement('div');
|
637
|
+
expDiv.className = 'mb-3 border-bottom pb-3';
|
638
|
+
|
639
|
+
// Handle responsibilities - could be string, array of strings, or missing
|
640
|
+
let responsibilitiesHtml = 'No description available';
|
641
|
+
if (exp.responsibilities) {
|
642
|
+
if (Array.isArray(exp.responsibilities)) {
|
643
|
+
responsibilitiesHtml = exp.responsibilities.join('<br>');
|
644
|
+
} else if (typeof exp.responsibilities === 'string') {
|
645
|
+
responsibilitiesHtml = exp.responsibilities;
|
646
|
+
}
|
647
|
+
}
|
648
|
+
|
649
|
+
// Use proper property names from the snapshot data
|
650
|
+
expDiv.innerHTML = `
|
651
|
+
<div class="d-flex justify-content-between">
|
652
|
+
<div>
|
653
|
+
<h6 class="mb-0">${exp.position || 'Unknown Position'}</h6>
|
654
|
+
<div class="text-muted">${exp.company || 'Unknown Company'}</div>
|
655
|
+
</div>
|
656
|
+
<div class="text-muted small">${exp.duration || 'No dates'}</div>
|
657
|
+
</div>
|
658
|
+
<p class="mt-2 mb-0 small">${responsibilitiesHtml}</p>
|
659
|
+
`;
|
660
|
+
|
661
|
+
container.appendChild(expDiv);
|
662
|
+
} catch (expError) {
|
663
|
+
console.error('Error rendering experience entry:', expError);
|
664
|
+
}
|
665
|
+
});
|
666
|
+
|
667
|
+
// If we didn't add any entries due to errors
|
668
|
+
if (container.children.length === 0) {
|
669
|
+
container.innerHTML = '<p class="text-muted">No valid experience information available</p>';
|
670
|
+
}
|
671
|
+
} catch (error) {
|
672
|
+
console.error('Error rendering experience section:', error);
|
673
|
+
container.innerHTML = '<div class="alert alert-warning">Error displaying experience information</div>';
|
674
|
+
}
|
675
|
+
}
|
676
|
+
|
677
|
+
// Render education
|
678
|
+
function renderEducation(education) {
|
679
|
+
const container = document.getElementById('education-container');
|
680
|
+
container.innerHTML = '';
|
681
|
+
|
682
|
+
try {
|
683
|
+
if (!education || !Array.isArray(education) || education.length === 0) {
|
684
|
+
container.innerHTML = '<p class="text-muted">No education information available</p>';
|
685
|
+
return;
|
686
|
+
}
|
687
|
+
|
688
|
+
education.forEach(edu => {
|
689
|
+
try {
|
690
|
+
if (!edu) return; // Skip if education entry is null
|
691
|
+
|
692
|
+
const eduDiv = document.createElement('div');
|
693
|
+
eduDiv.className = 'mb-2';
|
694
|
+
|
695
|
+
const degree = edu.degree || 'Degree not specified';
|
696
|
+
const year = edu.year || 'N/A';
|
697
|
+
const institution = edu.institution || 'Institution not specified';
|
698
|
+
|
699
|
+
eduDiv.innerHTML = `
|
700
|
+
<div class="d-flex justify-content-between">
|
701
|
+
<h6 class="mb-0">${degree}</h6>
|
702
|
+
<div class="text-muted small">${year}</div>
|
703
|
+
</div>
|
704
|
+
<div class="text-muted">${institution}</div>
|
705
|
+
`;
|
706
|
+
|
707
|
+
container.appendChild(eduDiv);
|
708
|
+
} catch (eduError) {
|
709
|
+
console.error('Error rendering education entry:', eduError);
|
710
|
+
}
|
711
|
+
});
|
712
|
+
|
713
|
+
// If no entries were added due to errors
|
714
|
+
if (container.children.length === 0) {
|
715
|
+
container.innerHTML = '<p class="text-muted">No valid education information available</p>';
|
716
|
+
}
|
717
|
+
} catch (error) {
|
718
|
+
console.error('Error rendering education section:', error);
|
719
|
+
container.innerHTML = '<div class="alert alert-warning">Error displaying education information</div>';
|
720
|
+
}
|
721
|
+
}
|
722
|
+
|
723
|
+
// Render AI analysis
|
724
|
+
function renderAIAnalysis(analysis) {
|
725
|
+
const container = document.getElementById('ai-analysis-container');
|
726
|
+
container.innerHTML = '';
|
727
|
+
|
728
|
+
try {
|
729
|
+
if (!analysis || typeof analysis !== 'object') {
|
730
|
+
container.innerHTML = '<p class="text-muted">No AI analysis available</p>';
|
731
|
+
return;
|
732
|
+
}
|
733
|
+
|
734
|
+
// Check if we have any meaningful data
|
735
|
+
const hasData = analysis.strengths || analysis.weaknesses || analysis.overall;
|
736
|
+
if (!hasData) {
|
737
|
+
container.innerHTML = '<p class="text-muted">No AI analysis available</p>';
|
738
|
+
return;
|
739
|
+
}
|
740
|
+
|
741
|
+
// Strengths
|
742
|
+
const strengthsDiv = document.createElement('div');
|
743
|
+
strengthsDiv.className = 'mb-3';
|
744
|
+
strengthsDiv.innerHTML = '<h6 class="text-success">Strengths</h6>';
|
745
|
+
|
746
|
+
if (Array.isArray(analysis.strengths) && analysis.strengths.length > 0) {
|
747
|
+
const strengthsList = document.createElement('ul');
|
748
|
+
strengthsList.className = 'small';
|
749
|
+
|
750
|
+
analysis.strengths.forEach(strength => {
|
751
|
+
if (strength) {
|
752
|
+
const li = document.createElement('li');
|
753
|
+
li.textContent = strength;
|
754
|
+
strengthsList.appendChild(li);
|
755
|
+
}
|
756
|
+
});
|
757
|
+
|
758
|
+
// Only add if we have actual items
|
759
|
+
if (strengthsList.children.length > 0) {
|
760
|
+
strengthsDiv.appendChild(strengthsList);
|
761
|
+
} else {
|
762
|
+
strengthsDiv.innerHTML += '<p class="small text-muted">No strengths data available</p>';
|
763
|
+
}
|
764
|
+
} else {
|
765
|
+
strengthsDiv.innerHTML += '<p class="small text-muted">No strengths data available</p>';
|
766
|
+
}
|
767
|
+
|
768
|
+
container.appendChild(strengthsDiv);
|
769
|
+
|
770
|
+
// Weaknesses
|
771
|
+
const weaknessesDiv = document.createElement('div');
|
772
|
+
weaknessesDiv.className = 'mb-3';
|
773
|
+
weaknessesDiv.innerHTML = '<h6 class="text-danger">Areas for Improvement</h6>';
|
774
|
+
|
775
|
+
if (Array.isArray(analysis.weaknesses) && analysis.weaknesses.length > 0) {
|
776
|
+
const weaknessesList = document.createElement('ul');
|
777
|
+
weaknessesList.className = 'small';
|
778
|
+
|
779
|
+
analysis.weaknesses.forEach(weakness => {
|
780
|
+
if (weakness) {
|
781
|
+
const li = document.createElement('li');
|
782
|
+
li.textContent = weakness;
|
783
|
+
weaknessesList.appendChild(li);
|
784
|
+
}
|
785
|
+
});
|
786
|
+
|
787
|
+
// Only add if we have actual items
|
788
|
+
if (weaknessesList.children.length > 0) {
|
789
|
+
weaknessesDiv.appendChild(weaknessesList);
|
790
|
+
} else {
|
791
|
+
weaknessesDiv.innerHTML += '<p class="small text-muted">No areas for improvement identified</p>';
|
792
|
+
}
|
793
|
+
} else {
|
794
|
+
weaknessesDiv.innerHTML += '<p class="small text-muted">No areas for improvement identified</p>';
|
795
|
+
}
|
796
|
+
|
797
|
+
container.appendChild(weaknessesDiv);
|
798
|
+
|
799
|
+
// Overall assessment
|
800
|
+
container.innerHTML += `
|
801
|
+
<div>
|
802
|
+
<h6>Overall Assessment</h6>
|
803
|
+
<p class="small">${analysis.overall || 'No detailed analysis available.'}</p>
|
804
|
+
</div>
|
805
|
+
`;
|
806
|
+
} catch (error) {
|
807
|
+
console.error('Error rendering AI analysis:', error);
|
808
|
+
container.innerHTML = '<div class="alert alert-warning">Error displaying AI analysis</div>';
|
809
|
+
}
|
810
|
+
}
|
811
|
+
|
812
|
+
// Render interview questions
|
813
|
+
function renderInterviewQuestions(questions) {
|
814
|
+
const container = document.getElementById('questions-container');
|
815
|
+
container.innerHTML = '';
|
816
|
+
|
817
|
+
if (!questions) {
|
818
|
+
container.innerHTML = '<p class="text-muted">No questions could be generated</p>';
|
819
|
+
return;
|
820
|
+
}
|
821
|
+
|
822
|
+
// Technical questions - handle both array of strings and array of objects
|
823
|
+
if (questions.technical_questions || questions.technical) {
|
824
|
+
const technicalQuestions = questions.technical_questions || questions.technical;
|
825
|
+
|
826
|
+
if (technicalQuestions && technicalQuestions.length > 0) {
|
827
|
+
const techDiv = document.createElement('div');
|
828
|
+
techDiv.className = 'mb-4';
|
829
|
+
techDiv.innerHTML = '<h5 class="mb-3">Technical Questions</h5>';
|
830
|
+
|
831
|
+
const techList = document.createElement('ol');
|
832
|
+
techList.className = 'question-list';
|
833
|
+
|
834
|
+
technicalQuestions.forEach(question => {
|
835
|
+
const li = document.createElement('li');
|
836
|
+
li.className = 'mb-3';
|
837
|
+
|
838
|
+
if (typeof question === 'string') {
|
839
|
+
li.textContent = question;
|
840
|
+
} else if (typeof question === 'object') {
|
841
|
+
li.innerHTML = `
|
842
|
+
<div>${question.question}</div>
|
843
|
+
${question.purpose ? `<div class="small text-muted mt-1">Purpose: ${question.purpose}</div>` : ''}
|
844
|
+
`;
|
845
|
+
}
|
846
|
+
|
847
|
+
techList.appendChild(li);
|
848
|
+
});
|
849
|
+
|
850
|
+
techDiv.appendChild(techList);
|
851
|
+
container.appendChild(techDiv);
|
852
|
+
}
|
853
|
+
}
|
854
|
+
|
855
|
+
// Behavioral questions - handle both array of strings and array of objects
|
856
|
+
if (questions.behavioral_questions || questions.behavioral) {
|
857
|
+
const behavioralQuestions = questions.behavioral_questions || questions.behavioral;
|
858
|
+
|
859
|
+
if (behavioralQuestions && behavioralQuestions.length > 0) {
|
860
|
+
const behavDiv = document.createElement('div');
|
861
|
+
behavDiv.className = 'mb-4';
|
862
|
+
behavDiv.innerHTML = '<h5 class="mb-3">Behavioral Questions</h5>';
|
863
|
+
|
864
|
+
const behavList = document.createElement('ol');
|
865
|
+
behavList.className = 'question-list';
|
866
|
+
|
867
|
+
behavioralQuestions.forEach(question => {
|
868
|
+
const li = document.createElement('li');
|
869
|
+
li.className = 'mb-3';
|
870
|
+
|
871
|
+
if (typeof question === 'string') {
|
872
|
+
li.textContent = question;
|
873
|
+
} else if (typeof question === 'object') {
|
874
|
+
li.innerHTML = `
|
875
|
+
<div>${question.question}</div>
|
876
|
+
${question.purpose ? `<div class="small text-muted mt-1">Purpose: ${question.purpose}</div>` : ''}
|
877
|
+
`;
|
878
|
+
}
|
879
|
+
|
880
|
+
behavList.appendChild(li);
|
881
|
+
});
|
882
|
+
|
883
|
+
behavDiv.appendChild(behavList);
|
884
|
+
container.appendChild(behavDiv);
|
885
|
+
}
|
886
|
+
}
|
887
|
+
|
888
|
+
// Follow-up areas
|
889
|
+
if (questions.follow_up_areas && questions.follow_up_areas.length > 0) {
|
890
|
+
const followUpDiv = document.createElement('div');
|
891
|
+
followUpDiv.className = 'mb-4';
|
892
|
+
followUpDiv.innerHTML = '<h5 class="mb-3">Areas to Explore Further</h5>';
|
893
|
+
|
894
|
+
const followUpList = document.createElement('ul');
|
895
|
+
followUpList.className = 'question-list';
|
896
|
+
|
897
|
+
questions.follow_up_areas.forEach(area => {
|
898
|
+
const li = document.createElement('li');
|
899
|
+
li.className = 'mb-2';
|
900
|
+
li.textContent = area;
|
901
|
+
followUpList.appendChild(li);
|
902
|
+
});
|
903
|
+
|
904
|
+
followUpDiv.appendChild(followUpList);
|
905
|
+
container.appendChild(followUpDiv);
|
906
|
+
}
|
907
|
+
}
|
908
|
+
|
909
|
+
// Update statistics
|
910
|
+
function updateStatistics(stats) {
|
911
|
+
document.getElementById('total-resumes').textContent = stats.total;
|
912
|
+
document.getElementById('qualified-resumes').textContent = stats.qualified;
|
913
|
+
document.getElementById('unqualified-resumes').textContent = stats.unqualified;
|
914
|
+
|
915
|
+
// Update chart data
|
916
|
+
scoreChart.data.datasets[0].data = [
|
917
|
+
stats.score_ranges['0-50'],
|
918
|
+
stats.score_ranges['50-70'],
|
919
|
+
stats.score_ranges['70-85'],
|
920
|
+
stats.score_ranges['85-100']
|
921
|
+
];
|
922
|
+
scoreChart.update();
|
923
|
+
}
|
924
|
+
|
925
|
+
// Apply filters button
|
926
|
+
document.getElementById('apply-filters').addEventListener('click', function() {
|
927
|
+
const jobId = document.getElementById('job-select').value;
|
928
|
+
|
929
|
+
if (!jobId) {
|
930
|
+
alert('Please select a job first');
|
931
|
+
return;
|
932
|
+
}
|
933
|
+
|
934
|
+
const minScore = document.getElementById('score-filter').value;
|
935
|
+
|
936
|
+
const statusFilters = [];
|
937
|
+
if (document.getElementById('status-new').checked) statusFilters.push('new');
|
938
|
+
if (document.getElementById('status-reviewed').checked) statusFilters.push('reviewed');
|
939
|
+
|
940
|
+
loadCandidates(jobId, {
|
941
|
+
minScore: minScore,
|
942
|
+
status: statusFilters
|
943
|
+
});
|
944
|
+
});
|
945
|
+
|
946
|
+
// Close resume details
|
947
|
+
document.getElementById('close-resume-details').addEventListener('click', function() {
|
948
|
+
document.getElementById('resume-details-card').style.display = 'none';
|
949
|
+
});
|
950
|
+
|
951
|
+
// Refresh data
|
952
|
+
document.getElementById('refresh-data').addEventListener('click', function() {
|
953
|
+
const jobId = document.getElementById('job-select').value;
|
954
|
+
|
955
|
+
if (!jobId) {
|
956
|
+
alert('Please select a job first');
|
957
|
+
return;
|
958
|
+
}
|
959
|
+
|
960
|
+
loadCandidates(jobId);
|
961
|
+
});
|
962
|
+
|
963
|
+
// Export to CSV
|
964
|
+
document.getElementById('export-csv').addEventListener('click', function() {
|
965
|
+
const jobId = document.getElementById('job-select').value;
|
966
|
+
|
967
|
+
if (!jobId) {
|
968
|
+
alert('Please select a job first');
|
969
|
+
return;
|
970
|
+
}
|
971
|
+
|
972
|
+
window.open(`/api/export-candidates?job_id=${jobId}`, '_blank');
|
973
|
+
});
|
974
|
+
|
975
|
+
// Status update
|
976
|
+
document.getElementById('update-status-btn').addEventListener('click', function() {
|
977
|
+
const status = document.getElementById('status-update').value;
|
978
|
+
if (status === 'Change Status...') {
|
979
|
+
alert('Please select a status');
|
980
|
+
return;
|
981
|
+
}
|
982
|
+
|
983
|
+
const candidateRow = document.querySelector('.candidate-row.active');
|
984
|
+
if (!candidateRow) return;
|
985
|
+
|
986
|
+
const candidateId = candidateRow.getAttribute('data-id');
|
987
|
+
|
988
|
+
fetch(`/api/candidates/${candidateId}/status`, {
|
989
|
+
method: 'PUT',
|
990
|
+
headers: {
|
991
|
+
'Content-Type': 'application/json'
|
992
|
+
},
|
993
|
+
body: JSON.stringify({ status: status })
|
994
|
+
})
|
995
|
+
.then(response => response.json())
|
996
|
+
.then(data => {
|
997
|
+
if (data.success) {
|
998
|
+
alert('Status updated successfully');
|
999
|
+
// Refresh the candidate list
|
1000
|
+
const jobId = document.getElementById('job-select').value;
|
1001
|
+
loadCandidates(jobId);
|
1002
|
+
} else {
|
1003
|
+
alert('Error updating status: ' + data.error);
|
1004
|
+
}
|
1005
|
+
})
|
1006
|
+
.catch(error => {
|
1007
|
+
console.error('Error:', error);
|
1008
|
+
alert('Error updating status');
|
1009
|
+
});
|
1010
|
+
});
|
1011
|
+
|
1012
|
+
// Helper function to get score class based on score value
|
1013
|
+
function getScoreClass(score) {
|
1014
|
+
if (score >= 85) return 'bg-success';
|
1015
|
+
if (score >= 70) return 'bg-info';
|
1016
|
+
if (score >= 50) return 'bg-warning';
|
1017
|
+
return 'bg-danger';
|
1018
|
+
}
|
1019
|
+
|
1020
|
+
// Format status for display
|
1021
|
+
function formatStatus(status) {
|
1022
|
+
if (!status) return 'New';
|
1023
|
+
|
1024
|
+
// Capitalize first letter of each word
|
1025
|
+
return status.split('_')
|
1026
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
1027
|
+
.join(' ');
|
1028
|
+
}
|
1029
|
+
});
|
1030
|
+
</script>
|
1031
|
+
</body>
|
1032
|
+
</html>
|