ATS/templates/recruitment/applications_exam_view.html
2026-02-01 13:38:06 +03:00

587 lines
30 KiB
HTML

{% extends 'base.html' %}
{% load static i18n %}
{% block title %}{% blocktrans %}Exam Stage - {{ job.title }} - University ATS{% endblocktrans %}{% endblock %}
{% block content %}
<div class="p-3 sm:p-4 lg:p-8">
<!-- Page Header -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-6 gap-4">
<div>
<h1 class="text-2xl sm:text-3xl font-bold text-temple-dark mb-2 flex items-center gap-2">
<i data-lucide="edit-3" class="w-6 h-6 sm:w-7 sm:h-7"></i>
{% trans "Exam Management" %}
</h1>
<p class="text-gray-500 text-sm sm:text-base">
{% trans "Job:" %} {{ job.title }}
<span class="inline-block ml-2 px-2 py-0.5 bg-gray-200 text-gray-700 rounded-full text-xs font-normal">
{{ job.internal_job_id }}
</span>
</p>
</div>
<div class="flex gap-2 w-full sm:w-auto">
<a href="{% url 'export_applications_csv' job.slug 'exam' %}"
class="flex items-center gap-2 px-4 py-2 border-2 border-temple-red text-temple-red rounded-lg hover:bg-temple-red hover:text-white transition text-sm font-medium"
title="{% trans 'Export exam applications to CSV' %}">
<i data-lucide="download" class="w-4 h-4"></i>
<span class="hidden sm:inline">{% trans "Export CSV" %}</span>
</a>
<a href="{% url 'job_detail' job.slug %}"
class="flex items-center gap-2 px-4 py-2 border-2 border-temple-red text-temple-red rounded-lg hover:bg-temple-red hover:text-white transition text-sm font-medium">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
<span class="hidden sm:inline">{% trans "Back to Job" %}</span>
</a>
</div>
</div>
<!-- Applicant Tracking Timeline -->
<div class="mb-6">
{% include 'jobs/partials/applicant_tracking.html' %}
</div>
<!-- Application List Header -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center mb-4 gap-3">
<h2 class="text-xl font-bold text-temple-dark flex items-center gap-2">
<i data-lucide="users" class="w-5 h-5"></i>
{% trans "Application List" %}
<span class="ml-2 px-2 py-1 bg-temple-red text-white text-xs rounded-full">
{{ applications|length }} / {{ total_candidates }} {% trans "Total" %}
</span>
</h2>
<div class="text-xs text-gray-500">
{% trans "Sorted by AI Score" %}
</div>
</div>
<!-- Main Card -->
<div class="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
<!-- Bulk Action Bar -->
{% if applications %}
<div class="p-3 sm:p-4 bg-gray-50 border-b border-gray-200">
<form hx-boost="true" hx-include="#application-form"
action="{% url 'application_update_status' job.slug %}"
method="post"
class="flex flex-col sm:flex-row gap-3 sm:gap-4 items-end">
{% csrf_token %}
<div class="flex-1 w-full sm:w-auto">
<select name="mark_as" id="update_status"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-temple-red focus:border-transparent text-sm">
<option selected>----------</option>
<option value="Interview">{% trans "Interview Stage" %}</option>
<option value="Applied">{% trans "Screening Stage" %}</option>
</select>
</div>
<button id="changeStage" type="submit"
class="w-full sm:w-auto px-4 py-2 bg-temple-red text-white rounded-lg hover:bg-temple-red/90 transition text-sm font-medium flex items-center justify-center gap-2">
<i data-lucide="arrow-right" class="w-4 h-4"></i>
{% trans "Change Stage" %}
</button>
<button id="emailBotton" type="button"
class="w-full sm:w-auto px-4 py-2 border-2 border-temple-red text-temple-red rounded-lg hover:bg-temple-red hover:text-white transition text-sm font-medium flex items-center justify-center gap-2"
onclick="openEmailModal()"
title="{% trans 'Email Participants' %}">
<i data-lucide="mail" class="w-4 h-4"></i>
</button>
</form>
</div>
{% endif %}
<!-- Table -->
<div class="overflow-x-auto">
<form id="application-form" action="{% url 'application_update_status' job.slug %}" method="post">
{% csrf_token %}
<table class="w-full border-collapse">
<thead class="bg-gray-50">
<tr>
<th class="px-4 py-3 text-left text-xs font-semibold text-temple-dark border-b-2 border-temple-red w-[2%]">
{% if applications %}
<div class="flex items-center">
<input type="checkbox" class="h-4 w-4 text-temple-red rounded border-gray-300 focus:ring-temple-red" id="selectAllCheckbox">
</div>
{% endif %}
</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-temple-dark border-b-2 border-temple-red w-[15%]">
<i data-lucide="user" class="w-3 h-3 inline mr-1"></i> {% trans "Name" %}
</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-temple-dark border-b-2 border-temple-red w-[15%]">
<i data-lucide="phone" class="w-3 h-3 inline mr-1"></i> {% trans "Contact Info" %}
</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-temple-dark border-b-2 border-temple-red w-[10%]">
<i data-lucide="bot" class="w-3 h-3 inline mr-1"></i> {% trans "AI Score" %}
</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-temple-dark border-b-2 border-temple-red w-[10%]">
{% trans "Exam Date" %}
</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-temple-dark border-b-2 border-temple-red w-[10%]">
{% trans "Exam Score" %}
</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-temple-dark border-b-2 border-temple-red w-[10%]">
{% trans "Exam Results" %}
</th>
<th class="px-4 py-3 text-left text-xs font-semibold text-temple-dark border-b-2 border-temple-red w-[10%]">
{% trans "Notes" %}
</th>
<th class="px-4 py-3 text-center text-xs font-semibold text-temple-dark border-b-2 border-temple-red w-[18%]">
<i data-lucide="settings" class="w-3 h-3 inline mr-1"></i> {% trans "Actions" %}
</th>
</tr>
</thead>
<tbody>
{% for application in applications %}
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-4 py-3 border-b border-gray-200">
<div class="flex items-center">
<input name="candidate_ids" value="{{ application.id }}"
type="checkbox"
class="h-4 w-4 text-temple-red rounded border-gray-300 focus:ring-temple-red rowCheckbox"
id="application-{{ application.id }}">
</div>
</td>
<td class="px-4 py-3 border-b border-gray-200">
<div class="font-semibold text-temple-red">
{{ application.name }}
</div>
</td>
<td class="px-4 py-3 border-b border-gray-200">
<div class="text-xs text-gray-500">
<i data-lucide="mail" class="w-3 h-3 inline mr-1"></i> {{ application.email }}<br>
<i data-lucide="phone" class="w-3 h-3 inline mr-1"></i> {{ application.phone }}
</div>
</td>
<td class="px-4 py-3 text-center border-b border-gray-200">
<span class="inline-block px-2 py-1 bg-temple-red text-white text-xs font-bold rounded-md">
{{ application.match_score|default:"0" }}%
</span>
</td>
<td class="px-4 py-3 border-b border-gray-200 text-sm">
{{application.exam_date|date:"d-m-Y h:i A"|default:"--"}}
</td>
<td class="px-4 py-3 border-b border-gray-200 text-sm" id="exam-score-{{ application.pk }}">
{{application.exam_score|default:"--"}}
</td>
<td class="px-4 py-3 text-center border-b border-gray-200" id="status-result-{{ application.pk }}">
{% if not application.exam_status %}
<button type="button"
class="px-3 py-1.5 bg-yellow-400 hover:bg-yellow-500 text-yellow-900 rounded-lg transition text-xs font-medium flex items-center gap-1 mx-auto"
onclick="openCandidateModal('{% url 'update_application_status' job.slug application.slug 'exam' 'passed' %}')"
title="{% trans 'Pass Exam' %}">
<i data-lucide="plus" class="w-3 h-3"></i>
</button>
{% else %}
{% if application.exam_status %}
<button type="button"
class="px-3 py-1.5 {% if application.exam_status == 'Passed' %}bg-green-500 hover:bg-green-600 text-white{% else %}bg-red-500 hover:bg-red-600 text-white{% endif %} rounded-lg transition text-xs font-medium"
onclick="openCandidateModal('{% url 'update_application_status' job.slug application.slug 'exam' 'passed' %}')"
title="{% trans 'Update Exam Status' %}">
{{ application.exam_status }}
</button>
{% else %}
--
{% endif %}
{% endif %}
</td>
<td class="px-4 py-3 border-b border-gray-200">
<button type="button"
class="px-3 py-1.5 border-2 border-temple-red text-temple-red rounded-lg hover:bg-temple-red hover:text-white transition text-xs font-medium flex items-center gap-1 mx-auto"
onclick="openNoteModal('{% url 'application_add_note' application.slug %}')">
<i data-lucide="plus-circle" class="w-3 h-3"></i>
{% trans "Add note" %}
</button>
</td>
<td class="px-4 py-3 border-b border-gray-200 text-center">
<button type="button"
class="px-3 py-1.5 border-2 border-gray-300 text-gray-600 rounded-lg hover:border-temple-red hover:text-temple-red transition text-xs font-medium"
onclick="openCandidateModal('{% url 'application_criteria_view_htmx' application.pk %}')"
title="{% trans 'View Application Profile' %}">
<i data-lucide="eye" class="w-3 h-3"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if not applications %}
<div class="p-8 text-center bg-blue-50 border border-blue-200 rounded-lg m-4">
<i data-lucide="info" class="w-8 h-8 text-blue-500 mx-auto mb-2"></i>
<p class="text-blue-700 text-sm">{% trans "No applications are currently in the Exam stage for this job." %}</p>
</div>
{% endif %}
</form>
</div>
</div>
</div>
<!-- Candidate View Modal -->
<div id="candidateviewModal" class="fixed inset-0 z-50 hidden" aria-labelledby="candidateviewModalLabel" role="dialog" aria-modal="true">
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" onclick="closeCandidateModal()"></div>
<!-- Modal Content -->
<div class="relative z-10 flex min-h-screen items-center justify-center p-4">
<div class="bg-white border border-gray-200 rounded-xl shadow-lg w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
<div class="flex justify-between items-center px-6 py-4 border-b border-gray-200">
<h5 class="text-lg font-bold text-temple-dark" id="candidateviewModalLabel">
{% trans "Application Details & Exam Update" %}
</h5>
<button type="button" class="text-gray-400 hover:text-gray-600 transition p-1" onclick="closeCandidateModal()" aria-label="Close">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div id="candidateviewModalBody" class="flex-1 overflow-y-auto p-6">
<div class="text-center py-10 text-gray-500">
<i data-lucide="loader-2" class="w-10 h-10 animate-spin mx-auto mb-3 text-temple-red"></i>
<p>{% trans "Loading application data..." %}</p>
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 bg-gray-50">
<button type="button" class="w-full px-4 py-2 border-2 border-gray-300 text-gray-600 rounded-lg hover:border-temple-red hover:text-temple-red transition text-sm font-medium" onclick="closeCandidateModal()">
{% trans "Close" %}
</button>
</div>
</div>
</div>
</div>
<!-- Email Modal -->
<div id="emailModal" class="fixed inset-0 z-50 hidden" aria-labelledby="emailModalLabel" role="dialog" aria-modal="true">
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" onclick="closeEmailModal()"></div>
<!-- Modal Content -->
<div class="relative z-10 flex min-h-screen items-center justify-center p-4">
<div class="bg-white border border-gray-200 rounded-xl shadow-lg w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
<div class="flex justify-between items-center px-6 py-4 border-b border-gray-200">
<h5 class="text-lg font-bold text-temple-dark" id="emailModalLabel">
<i data-lucide="mail" class="w-5 h-5 inline mr-2"></i>{% trans "Compose Email" %}
</h5>
<button type="button" class="text-gray-400 hover:text-gray-600 transition p-1" onclick="closeEmailModal()" aria-label="Close">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div id="emailModalBody" class="flex-1 overflow-y-auto p-6">
<div class="text-center py-10 text-gray-500">
<i data-lucide="loader-2" class="w-10 h-10 animate-spin mx-auto mb-3 text-temple-red"></i>
<p>{% trans "Loading email form..." %}</p>
</div>
</div>
</div>
</div>
</div>
<!-- Note Modal -->
<div id="noteModal" class="fixed inset-0 z-50 hidden" aria-labelledby="noteModalLabel" role="dialog" aria-modal="true">
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" onclick="closeNoteModal()"></div>
<!-- Modal Content -->
<div class="relative z-10 flex min-h-screen items-center justify-center p-4">
<div class="bg-white border border-gray-200 rounded-xl shadow-lg w-full max-w-4xl max-h-[90vh] overflow-hidden flex flex-col">
<div class="flex justify-between items-center px-6 py-4 border-b border-gray-200">
<h5 class="text-lg font-bold text-temple-dark" id="noteModalLabel">
<i data-lucide="sticky-note" class="w-5 h-5 inline mr-2"></i>{% trans "Add Note" %}
</h5>
<button type="button" class="text-gray-400 hover:text-gray-600 transition p-1" onclick="closeNoteModal()" aria-label="Close">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div id="noteModalBody" class="flex-1 overflow-y-auto p-6">
<div class="text-center py-10 text-gray-500">
<i data-lucide="loader-2" class="w-10 h-10 animate-spin mx-auto mb-3 text-temple-red"></i>
<p>{% trans "Loading note form..." %}</p>
</div>
</div>
</div>
</div>
</div>
<!-- Stage Confirmation Modal -->
<div id="stageConfirmationModal" class="fixed inset-0 z-50 hidden" aria-labelledby="stageConfirmationModalLabel" role="dialog" aria-modal="true">
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" onclick="closeStageConfirmationModal()"></div>
<!-- Modal Content -->
<div class="relative z-10 flex min-h-screen items-center justify-center p-4">
<div class="bg-white border border-gray-200 rounded-xl shadow-lg w-full max-w-lg">
<div class="flex justify-between items-center px-6 py-4 border-b border-gray-200">
<h5 class="text-lg font-bold text-temple-dark" id="stageConfirmationModalLabel">
<i data-lucide="info" class="w-5 h-5 inline mr-2"></i>{% trans "Confirm Stage Change" %}
</h5>
<button type="button" class="text-gray-400 hover:text-gray-600 transition p-1" onclick="closeStageConfirmationModal()" aria-label="Close">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="p-6">
<div class="flex items-center justify-center py-3 mb-3">
<i data-lucide="arrow-right-left" class="w-16 h-16 text-temple-red"></i>
</div>
<p class="text-center mb-2 text-base text-gray-800">
<span id="stageConfirmationMessage">{% trans "Are you sure you want to change to this stage?" %}</span>
</p>
<div class="bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded-lg text-center" role="alert">
<i data-lucide="user-check" class="w-4 h-4 inline mr-2"></i>
<span class="font-semibold">{% trans "Selected Stage:" %}</span>
<span id="targetStageName" class="font-bold">{% trans "--" %}</span>
</div>
</div>
<div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-2">
<button type="button" class="px-4 py-2 border-2 border-gray-300 text-gray-600 rounded-lg hover:border-temple-red hover:text-temple-red transition text-sm font-medium" onclick="closeStageConfirmationModal()">
<i data-lucide="x" class="w-4 h-4 inline mr-1"></i>{% trans "Cancel" %}
</button>
<button type="button" class="px-4 py-2 bg-temple-red text-white rounded-lg hover:bg-temple-red/90 transition text-sm font-medium" id="confirmStageChangeButton">
<i data-lucide="check" class="w-4 h-4 inline mr-1"></i>{% trans "Confirm" %}
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
// Reinitialize Lucide icons after content loads
document.addEventListener('DOMContentLoaded', function () {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
// ========================================
// Modal Functions
// ========================================
function openCandidateModal(url) {
const modal = document.getElementById('candidateviewModal');
const modalBody = document.getElementById('candidateviewModalBody');
// Reset content
modalBody.innerHTML = `
<div class="text-center py-10 text-gray-500">
<i data-lucide="loader-2" class="w-10 h-10 animate-spin mx-auto mb-3 text-temple-red"></i>
<p>{% trans "Loading application data..." %}</p>
</div>
`;
// Show modal
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
// Load content via HTMX
if (url && typeof htmx !== 'undefined') {
htmx.ajax('GET', url, {target: '#candidateviewModalBody', swap: 'innerHTML'});
}
// Reinitialize icons
setTimeout(() => {
if (typeof lucide !== 'undefined') lucide.createIcons();
}, 100);
}
function closeCandidateModal() {
const modal = document.getElementById('candidateviewModal');
modal.classList.add('hidden');
document.body.style.overflow = '';
}
function openEmailModal() {
const modal = document.getElementById('emailModal');
const modalBody = document.getElementById('emailModalBody');
const applicationForm = document.getElementById('application-form');
const url = '{% url "compose_application_email" job.slug %}';
// Reset content
modalBody.innerHTML = `
<div class="text-center py-10 text-gray-500">
<i data-lucide="loader-2" class="w-10 h-10 animate-spin mx-auto mb-3 text-temple-red"></i>
<p>{% trans "Loading email form..." %}</p>
</div>
`;
// Show modal
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
// Load content via HTMX with form data
if (url && typeof htmx !== 'undefined' && applicationForm) {
htmx.ajax('GET', url, {
target: '#emailModalBody',
swap: 'innerHTML',
values: htmx.values(applicationForm)
});
}
// Reinitialize icons
setTimeout(() => {
if (typeof lucide !== 'undefined') lucide.createIcons();
}, 100);
}
function closeEmailModal() {
const modal = document.getElementById('emailModal');
modal.classList.add('hidden');
document.body.style.overflow = '';
}
function openNoteModal(url) {
const modal = document.getElementById('noteModal');
const modalBody = document.getElementById('noteModalBody');
// Reset content
modalBody.innerHTML = `
<div class="text-center py-10 text-gray-500">
<i data-lucide="loader-2" class="w-10 h-10 animate-spin mx-auto mb-3 text-temple-red"></i>
<p>{% trans "Loading note form..." %}</p>
</div>
`;
// Show modal
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
// Load content via HTMX
if (url && typeof htmx !== 'undefined') {
htmx.ajax('GET', url, {target: '#noteModalBody', swap: 'innerHTML'});
}
// Reinitialize icons
setTimeout(() => {
if (typeof lucide !== 'undefined') lucide.createIcons();
}, 100);
}
function closeNoteModal() {
const modal = document.getElementById('noteModal');
modal.classList.add('hidden');
document.body.style.overflow = '';
}
function openStageConfirmationModal(selectedStage) {
const modal = document.getElementById('stageConfirmationModal');
const messageElement = document.getElementById('stageConfirmationMessage');
const targetStageElement = document.getElementById('targetStageName');
// Update confirmation message
if (messageElement && targetStageElement) {
const checkedCount = Array.from(document.querySelectorAll('.rowCheckbox:checked')).length;
if (checkedCount > 0) {
messageElement.textContent = `{% trans "Are you sure you want to move" %} ${checkedCount} {% trans "candidate(s) to this stage?" %}`;
targetStageElement.textContent = selectedStage;
} else {
messageElement.textContent = '{% trans "Please select at least one candidate." %}';
targetStageElement.textContent = '--';
}
}
// Show modal
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
function closeStageConfirmationModal() {
const modal = document.getElementById('stageConfirmationModal');
modal.classList.add('hidden');
document.body.style.overflow = '';
}
// ========================================
// Checkbox and Form Logic
// ========================================
document.addEventListener('DOMContentLoaded', function () {
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
const changeStageButton = document.getElementById('changeStage');
const emailButton = document.getElementById('emailBotton');
const updateStatus = document.getElementById('update_status');
const confirmStageChangeButton = document.getElementById('confirmStageChangeButton');
let isConfirmed = false;
if (selectAllCheckbox) {
// Function to safely update the header checkbox state
function updateSelectAllState() {
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
const totalCount = rowCheckboxes.length;
if (checkedCount === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
if (changeStageButton) changeStageButton.disabled = true;
if (emailButton) emailButton.disabled = true;
if (updateStatus) updateStatus.disabled = true;
} else if (checkedCount === totalCount) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
if (changeStageButton) changeStageButton.disabled = false;
if (emailButton) emailButton.disabled = false;
if (updateStatus) updateStatus.disabled = false;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
if (changeStageButton) changeStageButton.disabled = false;
if (emailButton) emailButton.disabled = false;
if (updateStatus) updateStatus.disabled = false;
}
}
// Logic for 'Select All' checkbox
selectAllCheckbox.addEventListener('change', function () {
const isChecked = selectAllCheckbox.checked;
rowCheckboxes.forEach(checkbox => checkbox.removeEventListener('change', updateSelectAllState));
rowCheckboxes.forEach(function (checkbox) {
checkbox.checked = isChecked;
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
});
rowCheckboxes.forEach(checkbox => checkbox.addEventListener('change', updateSelectAllState));
updateSelectAllState();
});
rowCheckboxes.forEach(function (checkbox) {
checkbox.addEventListener('change', updateSelectAllState);
});
updateSelectAllState();
}
// Stage Confirmation Logic
if (changeStageButton) {
changeStageButton.addEventListener('click', function(event) {
const selectedStage = updateStatus.value;
if (selectedStage && selectedStage.trim() !== '') {
if (!isConfirmed) {
event.preventDefault();
openStageConfirmationModal(selectedStage);
return false;
}
isConfirmed = false;
}
});
if (confirmStageChangeButton) {
confirmStageChangeButton.addEventListener('click', function() {
closeStageConfirmationModal();
isConfirmed = true;
changeStageButton.click();
});
}
}
// Close modals on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closeCandidateModal();
closeEmailModal();
closeNoteModal();
closeStageConfirmationModal();
}
});
});
</script>
{% endblock %}