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

498 lines
27 KiB
HTML

{% extends "base.html" %}
{% load static i18n humanize %}
{% block title %}{{ application.name }} - {{ block.super }}{% endblock %}
{% block content %}
<div class="px-4 py-6" id="application-detail-content">
<nav aria-label="breadcrumb" class="mb-6">
<ol class="flex items-center space-x-2 text-sm">
<li><a href="{% url 'dashboard' %}" class="text-gray-500 hover:text-temple-red transition">Home</a></li>
<li class="text-gray-400">/</li>
<li><a href="{% url 'job_detail' application.job.slug %}" class="text-gray-500 hover:text-temple-red transition">Job: {{application.job.title}}</a></li>
<li class="text-gray-400">/</li>
<li class="font-semibold text-temple-red" aria-current="page">{% trans "Applicant Detail" %}</li>
</ol>
</nav>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
{# LEFT COLUMN: MAIN application DETAILS AND TABS #}
<div class="lg:col-span-2">
<div class="bg-white rounded-xl shadow-md overflow-hidden border border-gray-200">
{# HEADER SECTION #}
<div class="bg-gradient-to-br from-temple-red to-red-800 text-white p-6">
<div class="flex flex-col md:flex-row md:justify-between md:items-start gap-4">
<div>
<h1 class="text-3xl font-extrabold mb-2">{{ application.name }}</h1>
<div class="flex items-center gap-2 mb-2">
<span id="stageDisplay" class="bg-white/20 backdrop-blur-sm px-3 py-1 rounded-full text-sm font-medium">
{% trans "Stage:" %}
<span class="font-bold">{{ application.stage }}</span>
</span>
</div>
<p class="text-red-100 text-sm">
{% trans "Applied for:" %} <strong>{{ application.job.title }}</strong>
</p>
</div>
{# Change Stage button #}
{% if user.is_staff and user == application.job.assigned_to or user.is_superuser %}
<button type="button" class="stage-modal-trigger bg-white/20 hover:bg-white/30 backdrop-blur-sm text-white px-4 py-2 rounded-lg text-sm font-medium transition flex items-center gap-2" data-modal="stageUpdateModal">
<i data-lucide="repeat" class="w-4 h-4"></i> {% trans "Change Stage" %}
</button>
{% endif %}
</div>
</div>
{# LEFT TABS NAVIGATION #}
<div class="bg-gray-50 border-b border-gray-200 px-6">
<ul class="flex space-x-1" id="candidateTabs" role="tablist">
<li class="role-presentation">
<button class="tab-btn px-4 py-3 text-sm font-medium border-b-2 border-transparent hover:text-temple-red transition flex items-center gap-2 active text-temple-dark border-b-temple-red bg-white" id="contact-tab" data-tab="contact-pane" type="button" role="tab" aria-controls="contact-pane" aria-selected="true">
<i data-lucide="id-card" class="w-4 h-4"></i> {% trans "Contact & Job" %}
</button>
</li>
<li class="role-presentation">
<button class="tab-btn px-4 py-3 text-sm font-medium text-gray-600 border-b-2 border-transparent hover:text-temple-red transition flex items-center gap-2" id="timeline-tab" data-tab="timeline-pane" type="button" role="tab" aria-controls="timeline-pane" aria-selected="false">
<i data-lucide="route" class="w-4 h-4"></i> {% trans "Journey Timeline" %}
</button>
</li>
<li class="role-presentation">
<button class="tab-btn px-4 py-3 text-sm font-medium text-gray-600 border-b-2 border-transparent hover:text-temple-red transition flex items-center gap-2" id="documents-tab" data-tab="documents-pane" type="button" role="tab" aria-controls="documents-pane" aria-selected="false">
<i data-lucide="file-text" class="w-4 h-4"></i> {% trans "Documents" %}
</button>
</li>
</ul>
</div>
<div class="p-6">
<div class="tab-content" id="candidateTabsContent">
{# TAB 1 CONTENT: CONTACT & DATES #}
<div class="tab-pane block" id="contact-pane" role="tabpanel" aria-labelledby="contact-tab">
<h5 class="text-lg font-bold text-temple-red mb-4">{% trans "Core Details" %}</h5>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="flex items-center gap-3 p-4 bg-gray-50 rounded-lg">
<i data-lucide="mail" class="w-8 h-8 text-gray-400 shrink-0"></i>
<div>
<p class="text-xs text-gray-500">{% trans "Email" %}</p>
<p class="font-semibold text-gray-800">{{ application.email }}</p>
</div>
</div>
<div class="flex items-center gap-3 p-4 bg-gray-50 rounded-lg">
<i data-lucide="briefcase" class="w-8 h-8 text-gray-400 shrink-0"></i>
<div>
<p class="text-xs text-gray-500">{% trans "Position Applied" %}</p>
<p class="font-semibold text-gray-800">{{ application.job.title }}</p>
</div>
</div>
<div class="md:col-span-2 flex items-center gap-3 p-4 bg-gray-50 rounded-lg">
<i data-lucide="calendar-check" class="w-8 h-8 text-gray-400 shrink-0"></i>
<div class="flex-1">
<p class="text-xs text-gray-500">{% trans "Applied Date" %}</p>
<div class="flex items-center gap-3">
<p class="font-semibold text-gray-800">{{ application.created_at|date:"M d, Y H:i" }}</p>
<span class="bg-gray-200 text-gray-700 px-2 py-1 rounded text-xs font-medium">
<i data-lucide="clock" class="inline w-3 h-3 mr-1"></i>
{{ application.created_at|naturaltime }}
</span>
</div>
</div>
</div>
</div>
</div>
{# TAB 2 CONTENT: TIMELINE #}
<div class="tab-pane hidden" id="timeline-pane" role="tabpanel" aria-labelledby="timeline-tab">
<div class="bg-white border border-gray-200 rounded-xl">
<div class="p-4 border-b border-gray-200">
<h5 class="text-sm font-semibold text-gray-600 flex items-center gap-2">
<i data-lucide="route" class="w-4 h-4"></i>{% trans "Application Journey" %}
</h5>
</div>
<div class="p-6">
<h6 class="text-xs uppercase tracking-wider text-gray-500 font-bold mb-3">{% trans "Current Stage" %}</h6>
<div class="p-4 mb-4 rounded-lg bg-red-50 border border-red-200">
<p class="font-bold text-lg text-temple-dark mb-1">{{ application.stage }}</p>
<p class="text-xs text-gray-500">
{% trans "Latest status update:" %} {{ application.updated_at|date:"M d, Y" }}
</p>
</div>
<h6 class="text-xs uppercase tracking-wider text-gray-500 font-bold mb-3 pt-4 border-t border-gray-200">{% trans "Historical Timeline" %}</h6>
<div class="relative pl-8">
<div class="absolute left-3.5 top-0 bottom-0 w-0.5 bg-gray-200"></div>
<div class="relative mb-6">
<div class="absolute left-0 top-0 w-8 h-8 bg-temple-red rounded-full flex items-center justify-center text-white z-10 border-4 border-white">
<i data-lucide="file-signature" class="w-4 h-4"></i>
</div>
<div class="ml-12">
<p class="font-semibold text-gray-800">{% trans "Application Submitted" %}</p>
<p class="text-xs text-gray-500">
<i data-lucide="calendar" class="inline w-3 h-3 mr-1"></i> {{ application.created_at|date:"M d, Y" }}
<span class="mx-2">|</span>
<i data-lucide="clock" class="inline w-3 h-3 mr-1"></i> {{ application.created_at|date:"h:i A" }}
</p>
</div>
</div>
{% if application.exam_date %}
<div class="relative mb-6">
<div class="absolute left-0 top-0 w-8 h-8 bg-temple-red rounded-full flex items-center justify-center text-white z-10 border-4 border-white">
<i data-lucide="clipboard-check" class="w-4 h-4"></i>
</div>
<div class="ml-12">
<p class="font-semibold text-gray-800">{% trans "Exam" %}</p>
<p class="text-xs text-gray-500">
<i data-lucide="calendar" class="inline w-3 h-3 mr-1"></i> {{ application.exam_date|date:"M d, Y" }}
<span class="mx-2">|</span>
<i data-lucide="clock" class="inline w-3 h-3 mr-1"></i> {{ application.exam_date|date:"h:i A" }}
</p>
</div>
</div>
{% endif %}
{% if application.get_interview_date %}
<div class="relative mb-6">
<div class="absolute left-0 top-0 w-8 h-8 bg-temple-red rounded-full flex items-center justify-center text-white z-10 border-4 border-white">
<i data-lucide="message-circle" class="w-4 h-4"></i>
</div>
<div class="ml-12">
<p class="font-semibold text-gray-800">{% trans "Interview" %}</p>
<p class="text-xs text-gray-500">
<i data-lucide="calendar" class="inline w-3 h-3 mr-1"></i> {{ application.get_interview_date}}
<span class="mx-2">|</span>
<i data-lucide="clock" class="inline w-3 h-3 mr-1"></i> {{ application.get_interview_time}}
</p>
</div>
</div>
{% endif %}
{% if application.offer_date %}
<div class="relative mb-6">
<div class="absolute left-0 top-0 w-8 h-8 bg-temple-red rounded-full flex items-center justify-center text-white z-10 border-4 border-white">
<i data-lucide="handshake" class="w-4 h-4"></i>
</div>
<div class="ml-12">
<p class="font-semibold text-gray-800">{% trans "Offer" %}</p>
<p class="text-xs text-gray-500">
<i data-lucide="calendar" class="inline w-3 h-3 mr-1"></i> {{ application.offer_date|date:"M d, Y" }}
</p>
</div>
</div>
{% endif %}
{% if application.hired_date %}
<div class="relative mb-6">
<div class="absolute left-0 top-0 w-8 h-8 bg-temple-red rounded-full flex items-center justify-center text-white z-10 border-4 border-white">
<i data-lucide="check-circle" class="w-4 h-4"></i>
</div>
<div class="ml-12">
<p class="font-semibold text-gray-800">{% trans "Hired" %}</p>
<p class="text-xs text-gray-500">
<i data-lucide="calendar" class="inline w-3 h-3 mr-1"></i> {{ application.hired_date|date:"M d, Y" }}
</p>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{# TAB 3 CONTENT: DOCUMENTS #}
<div class="tab-pane hidden" id="documents-pane" role="tabpanel" aria-labelledby="documents-tab">
{% with documents=application.documents %}
{% include 'includes/document_list.html' %}
{% endwith %}
</div>
</div>
</div>
</div>
</div>
{# RIGHT COLUMN: ACTIONS AND TIMING #}
<div class="space-y-6">
{# ACTIONS CARD #}
<div class="bg-white rounded-xl shadow-md p-6 border border-gray-200">
<h5 class="text-sm font-semibold text-gray-600 mb-4 flex items-center gap-2">
<i data-lucide="settings" class="w-4 h-4"></i>{% trans "Management Actions" %}
</h5>
<div class="space-y-3">
<a href="{% url 'application_list' %}" class="w-full flex items-center justify-center gap-2 bg-gray-100 hover:bg-gray-200 text-gray-700 py-3 px-4 rounded-lg text-sm font-medium transition">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
{% trans "Back to List" %}
</a>
{% if application.resume %}
<a href="{{ application.resume.url }}" download class="w-full flex items-center justify-center gap-2 bg-temple-red hover:bg-red-800 text-white py-3 px-4 rounded-lg text-sm font-medium transition shadow-sm">
<i data-lucide="download" class="w-4 h-4"></i>
{% trans "Download Resume" %}
</a>
{% endif %}
</div>
</div>
{# TIME TO HIRE CARD #}
<div class="bg-white rounded-xl shadow-md p-6 border border-gray-200">
<h5 class="text-sm font-semibold text-gray-600 mb-4 flex items-center gap-2">
<i data-lucide="clock" class="w-4 h-4"></i>{% trans "Time to Hire:" %}
</h5>
<div class="text-center">
{% with days=application.time_to_hire_days %}
{% if days > 0 %}
<p class="text-2xl font-bold text-temple-dark">{{ days }}</p>
<p class="text-sm text-gray-500">day{{ days|pluralize }}</p>
{% else %}
<p class="text-2xl font-bold text-temple-dark">0</p>
<p class="text-sm text-gray-500">days</p>
{% endif %}
{% endwith %}
</div>
</div>
</div>
</div>
</div>
<div class="resume-parsed-section">
{% if application.is_resume_parsed %}
{% include 'recruitment/application_resume_template.html' %}
{% else %}
{% if application.scoring_timeout %}
<div class="flex justify-center items-center py-12">
<div class="flex items-center gap-3 text-temple-red">
<i data-lucide="robot" class="w-8 h-8 animate-pulse"></i>
<span class="text-sm">{% trans "Resume Analysis In Progress..." %}</span>
</div>
</div>
{% else %}
<div class="flex justify-center items-center py-12">
<button type="submit" class="flex items-center gap-2 bg-temple-red hover:bg-red-800 text-white py-3 px-6 rounded-lg text-sm font-medium transition shadow-sm" hx-get="{% url 'application_retry_scoring' application.slug %}" hx-select=".resume-parsed-section" hx-target=".resume-parsed-section" hx-swap="outerHTML" hx-on:click="this.disabled=true;this.innerHTML=`Scoring Resume, Please Wait... <i data-lucide='loader-2' class='w-4 h-4 animate-spin'></i>`">
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
{% trans "Unable to Parse Resume, click to retry" %}
</button>
</div>
{% endif %}
{% endif %}
</div>
{# STAGE UPDATE MODAL INCLUDED FOR STAFF USERS #}
{% if user.is_staff %}
{% include "recruitment/partials/stage_update_modal.html" with application=application form=stage_form %}
{% endif %}
{% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Lucide icons
lucide.createIcons();
// ========================================
// Tab Navigation Functionality
// ========================================
const tabButtons = document.querySelectorAll('.tab-btn');
const tabPanes = document.querySelectorAll('.tab-pane');
tabButtons.forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
// Get the target tab pane ID
const targetTabId = this.getAttribute('data-tab');
// Remove active state from all buttons
tabButtons.forEach(btn => {
btn.classList.remove('active', 'text-temple-dark', 'border-b-temple-red', 'bg-white');
btn.classList.add('text-gray-600', 'border-transparent');
btn.setAttribute('aria-selected', 'false');
});
// Add active state to clicked button
this.classList.add('active', 'text-temple-dark', 'border-b-temple-red', 'bg-white');
this.classList.remove('text-gray-600', 'border-transparent');
this.setAttribute('aria-selected', 'true');
// Hide all tab panes
tabPanes.forEach(pane => {
pane.classList.add('hidden');
pane.classList.remove('block');
});
// Show the target tab pane
const targetPane = document.getElementById(targetTabId);
if (targetPane) {
targetPane.classList.remove('hidden');
targetPane.classList.add('block');
}
});
});
// ========================================
// Document Upload Modal Functionality
// ========================================
const uploadModal = document.getElementById('documentUploadModal');
let isModalOpen = false;
// Open modal buttons
const openModalBtns = document.querySelectorAll('.upload-modal-trigger');
openModalBtns.forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (uploadModal) {
uploadModal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
isModalOpen = true;
// Initialize Lucide icons inside modal
setTimeout(() => lucide.createIcons(), 100);
}
});
});
// Close modal buttons (both close X and Cancel button)
document.querySelectorAll('.modal-close-btn, .modal-cancel-btn').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
if (uploadModal && isModalOpen) {
uploadModal.classList.add('hidden');
document.body.style.overflow = '';
isModalOpen = false;
}
});
});
// Close modal when clicking outside
if (uploadModal) {
uploadModal.addEventListener('click', function(e) {
if (e.target === uploadModal && isModalOpen) {
uploadModal.classList.add('hidden');
document.body.style.overflow = '';
isModalOpen = false;
}
});
}
// Close modal on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && uploadModal && isModalOpen) {
uploadModal.classList.add('hidden');
document.body.style.overflow = '';
isModalOpen = false;
}
});
// ========================================
// Stage Update Modal Functionality
// ========================================
const stageModal = document.getElementById('stageUpdateModal');
let isStageModalOpen = false;
const currentStage = '{{ application.stage }}';
// Open stage modal buttons
const openStageModalBtns = document.querySelectorAll('.stage-modal-trigger');
openStageModalBtns.forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (stageModal) {
stageModal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
isStageModalOpen = true;
// Initialize Lucide icons inside modal
setTimeout(() => lucide.createIcons(), 100);
// Set initial stage selection
const stageSelect = document.getElementById('id_stage');
if (stageSelect) {
stageSelect.value = currentStage;
// Call updateStageInfo if function exists
if (typeof updateStageInfo === 'function') {
updateStageInfo(currentStage, currentStage);
}
}
}
});
});
// Stage selection change handler
const stageSelect = document.getElementById('id_stage');
if (stageSelect) {
stageSelect.addEventListener('change', function(e) {
const selectedValue = e.target.value;
if (typeof updateStageInfo === 'function') {
updateStageInfo(selectedValue, currentStage);
}
});
}
// Close stage modal buttons
document.querySelectorAll('#stageUpdateModal button').forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
if (stageModal && isStageModalOpen) {
stageModal.classList.add('hidden');
document.body.style.overflow = '';
isStageModalOpen = false;
}
});
});
// Close stage modal when clicking outside
if (stageModal) {
stageModal.addEventListener('click', function(e) {
if (e.target === stageModal || (e.target.classList.contains('fixed') && e.target.classList.contains('inset-0'))) {
stageModal.classList.add('hidden');
document.body.style.overflow = '';
isStageModalOpen = false;
}
});
}
// Close stage modal on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && stageModal && isStageModalOpen) {
stageModal.classList.add('hidden');
document.body.style.overflow = '';
isStageModalOpen = false;
}
});
// Close modal after successful HTMX form submission
document.body.addEventListener('htmx:afterRequest', function(evt) {
// Close document upload modal
if (evt.detail.successful && uploadModal && isModalOpen) {
const form = evt.detail.elt;
if (form && form.tagName === 'FORM' && form.action.includes('document_upload')) {
uploadModal.classList.add('hidden');
document.body.style.overflow = '';
isModalOpen = false;
}
}
// Close stage update modal
if (evt.detail.successful && stageModal && isStageModalOpen) {
const form = evt.detail.elt;
if (form && form.tagName === 'FORM' && form.action.includes('application_update_stage')) {
stageModal.classList.add('hidden');
document.body.style.overflow = '';
isStageModalOpen = false;
// Reload page to update stage display
window.location.reload();
}
}
});
// ========================================
// Reinitialize Lucide icons after HTMX updates
// ========================================
document.body.addEventListener('htmx:afterSwap', function(evt) {
lucide.createIcons();
});
});
</script>
{% endblock %}