498 lines
27 KiB
HTML
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 %}
|