293 lines
18 KiB
HTML
293 lines
18 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static i18n widget_tweaks %}
|
|
|
|
{% block title %}{% trans "Bulk Interview Scheduling" %} - {{ job.title }} - ATS{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-7xl mx-auto space-y-6">
|
|
<!-- Header -->
|
|
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
|
<div>
|
|
<h1 class="text-2xl sm:text-3xl font-bold text-gray-900 flex items-center gap-3">
|
|
<div class="bg-temple-red/10 p-3 rounded-xl">
|
|
<i data-lucide="calendar-days" class="w-6 h-6 text-temple-red"></i>
|
|
</div>
|
|
{% trans "Bulk Interview Scheduling" %}
|
|
</h1>
|
|
<p class="text-gray-600 mt-1">
|
|
{% trans "Configure time slots for:" %} <strong class="text-temple-red">{{ job.title }}</strong>
|
|
</p>
|
|
</div>
|
|
<a href="{% url 'job_detail' job.slug %}"
|
|
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium border-2 border-gray-300 text-gray-700 hover:bg-gray-50 transition-all duration-200">
|
|
<i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Back to Job" %}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Form Card -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 p-6">
|
|
<form method="post" id="schedule-form" class="space-y-8">
|
|
{% csrf_token %}
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
<!-- Left Column: Candidates -->
|
|
<div class="lg:col-span-1">
|
|
<h2 class="text-lg font-bold text-gray-900 flex items-center gap-2 mb-4 pb-3 border-b-2 border-temple-red">
|
|
<i data-lucide="users" class="w-5 h-5 text-temple-red"></i> {% trans "Select Candidates" %}
|
|
</h2>
|
|
<div>
|
|
<label for="{{ form.applications.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
{% trans "Candidates to Schedule (Hold Ctrl/Cmd to select multiple)" %}
|
|
</label>
|
|
{{ form.applications|attr:"class: w-full px-3 py-2 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition max-h-[450px] overflow-y-auto min-h-[250px]"|attr:"multiple" }}
|
|
{% if form.applications.errors %}
|
|
<div class="mt-1 text-sm text-red-600">{{ form.applications.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Schedule Details -->
|
|
<div class="lg:col-span-3 space-y-6">
|
|
<h2 class="text-lg font-bold text-gray-900 flex items-center gap-2 mb-4 pb-3 border-b-2 border-temple-red">
|
|
<i data-lucide="settings" class="w-5 h-5 text-temple-red"></i> {% trans "Schedule Details" %}
|
|
</h2>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="md:col-span-2">
|
|
<label for="{{ form.topic.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
<i data-lucide="tag" class="w-4 h-4 inline mr-1 text-temple-red"></i> {% trans "Topic" %}
|
|
</label>
|
|
{{ form.topic|attr:"class: w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
|
|
{% if form.topic.errors %}
|
|
<div class="mt-1 text-sm text-red-600">{{ form.topic.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div>
|
|
<label for="{{ form.schedule_interview_type.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
<i data-lucide="video" class="w-4 h-4 inline mr-1 text-temple-red"></i> {% trans "Interview Type" %}
|
|
</label>
|
|
{{ form.schedule_interview_type|attr:"class: w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
|
|
{% if form.schedule_interview_type.errors %}
|
|
<div class="mt-1 text-sm text-red-600">{{ form.schedule_interview_type.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div>
|
|
<label for="{{ form.physical_address.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
<i data-lucide="map-pin" class="w-4 h-4 inline mr-1 text-temple-red"></i> {% trans "Physical Address" %}
|
|
</label>
|
|
{{ form.physical_address|attr:"class: w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
|
|
{% if form.physical_address.errors %}
|
|
<div class="mt-1 text-sm text-red-600">{{ form.physical_address.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div>
|
|
<label for="{{ form.start_date.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
<i data-lucide="calendar" class="w-4 h-4 inline mr-1 text-temple-red"></i> {% trans "Start Date" %}
|
|
</label>
|
|
{{ form.start_date|attr:"class: w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
|
|
{% if form.start_date.errors %}
|
|
<div class="mt-1 text-sm text-red-600">{{ form.start_date.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div>
|
|
<label for="{{ form.end_date.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
<i data-lucide="calendar-check" class="w-4 h-4 inline mr-1 text-temple-red"></i> {% trans "End Date" %}
|
|
</label>
|
|
{{ form.end_date|attr:"class: w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
|
|
{% if form.end_date.errors %}
|
|
<div class="mt-1 text-sm text-red-600">{{ form.end_date.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="block text-sm font-semibold text-gray-700 mb-3">
|
|
<i data-lucide="calendar-days" class="w-4 h-4 inline mr-1 text-temple-red"></i> {% trans "Working Days" %}
|
|
</label>
|
|
<div class="flex flex-wrap gap-3 p-3 border border-gray-200 rounded-xl bg-gray-50">
|
|
{{ form.working_days }}
|
|
{% if form.working_days.errors %}
|
|
<div class="mt-1 text-sm text-red-600">{{ form.working_days.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
<div>
|
|
<label for="{{ form.start_time.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
<i data-lucide="clock" class="w-4 h-4 inline mr-1 text-temple-red"></i> {% trans "Start Time" %}
|
|
</label>
|
|
{{ form.start_time|attr:"class: w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
|
|
{% if form.start_time.errors %}
|
|
<div class="mt-1 text-sm text-red-600">{{ form.start_time.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div>
|
|
<label for="{{ form.end_time.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
<i data-lucide="clock" class="w-4 h-4 inline mr-1 text-temple-red"></i> {% trans "End Time" %}
|
|
</label>
|
|
{{ form.end_time|attr:"class: w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
|
|
{% if form.end_time.errors %}
|
|
<div class="mt-1 text-sm text-red-600">{{ form.end_time.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div>
|
|
<label for="{{ form.interview_duration.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
<i data-lucide="timer" class="w-4 h-4 inline mr-1 text-temple-red"></i> {% trans "Duration (min)" %}
|
|
</label>
|
|
{{ form.interview_duration|attr:"class: w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
|
|
{% if form.interview_duration.errors %}
|
|
<div class="mt-1 text-sm text-red-600">{{ form.interview_duration.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div>
|
|
<label for="{{ form.buffer_time.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
<i data-lucide="shield" class="w-4 h-4 inline mr-1 text-temple-red"></i> {% trans "Buffer (min)" %}
|
|
</label>
|
|
{{ form.buffer_time|attr:"class: w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
|
|
{% if form.buffer_time.errors %}
|
|
<div class="mt-1 text-sm text-red-600">{{ form.buffer_time.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="pt-6 border-t border-gray-200">
|
|
<h2 class="text-lg font-bold text-gray-900 flex items-center gap-2 mb-4 pb-3 border-b-2 border-temple-red">
|
|
<i data-lucide="coffee" class="w-5 h-5 text-temple-red"></i> {% trans "Daily Break Times" %}
|
|
</h2>
|
|
<div id="break-times-container">
|
|
<div class="break-time-form bg-gray-50 border border-gray-200 rounded-xl p-4 mb-2">
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="{{ form.break_start_time.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
{% trans "Start Time" %}
|
|
</label>
|
|
{{ form.break_start_time|attr:"class: w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
|
|
{% if form.break_start_time.errors %}
|
|
<div class="mt-1 text-sm text-red-600">{{ form.break_start_time.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
<div>
|
|
<label for="{{ form.break_end_time.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
{% trans "End Time" %}
|
|
</label>
|
|
{{ form.break_end_time|attr:"class: w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
|
|
{% if form.break_end_time.errors %}
|
|
<div class="mt-1 text-sm text-red-600">{{ form.break_end_time.errors }}</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="pt-6 border-t border-gray-200 flex justify-end gap-3">
|
|
<button type="submit"
|
|
class="inline-flex items-center gap-2 px-6 py-3 rounded-xl font-medium text-white transition-all duration-200 bg-temple-red hover:bg-[#7a1a29] shadow-sm hover:shadow-md">
|
|
<i data-lucide="calendar-check" class="w-5 h-5"></i> {% trans "Preview Schedule" %}
|
|
</button>
|
|
<a href="{% url 'job_detail' job.slug %}"
|
|
class="inline-flex items-center gap-2 px-6 py-3 rounded-xl text-sm font-medium border-2 border-gray-300 text-gray-700 hover:bg-gray-50 transition-all duration-200">
|
|
{% trans "Cancel" %}
|
|
</a>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
if (typeof lucide !== 'undefined') {
|
|
lucide.createIcons();
|
|
}
|
|
|
|
const addBreakBtn = document.getElementById('add-break');
|
|
const breakTimesContainer = document.getElementById('break-times-container');
|
|
|
|
// Formset prefix for reliability
|
|
const FORMSET_PREFIX = 'breaktime';
|
|
const totalFormsInput = document.querySelector(`input[name="${FORMSET_PREFIX}-TOTAL_FORMS"]`);
|
|
|
|
if (!totalFormsInput) {
|
|
console.error(`TOTAL_FORMS input with name ${FORMSET_PREFIX}-TOTAL_FORMS not found. Cannot add break dynamically. Please ensure formset prefix is set to '${FORMSET_PREFIX}' in Python view and management_form is rendered.`);
|
|
return;
|
|
}
|
|
|
|
addBreakBtn.addEventListener('click', function() {
|
|
const formCount = parseInt(totalFormsInput.value);
|
|
|
|
// Template for a new form with Tailwind classes
|
|
const newFormHtml = `
|
|
<div class="break-time-form bg-gray-50 border border-gray-200 rounded-xl p-4 mb-2">
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
<div>
|
|
<label for="id_${FORMSET_PREFIX}-${formCount}-start_time" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
{% trans "Start Time" %}
|
|
</label>
|
|
<input type="time" name="${FORMSET_PREFIX}-${formCount}-start_time" class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" id="id_${FORMSET_PREFIX}-${formCount}-start_time">
|
|
</div>
|
|
<div>
|
|
<label for="id_${FORMSET_PREFIX}-${formCount}-end_time" class="block text-sm font-semibold text-gray-700 mb-2">
|
|
{% trans "End Time" %}
|
|
</label>
|
|
<input type="time" name="${FORMSET_PREFIX}-${formCount}-end_time" class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" id="id_${FORMSET_PREFIX}-${formCount}-end_time">
|
|
</div>
|
|
</div>
|
|
<div class="mt-4 flex justify-end">
|
|
<!-- Hidden management fields for new forms (ID and DELETE) -->
|
|
<input type="hidden" name="${FORMSET_PREFIX}-${formCount}-id" id="id_${FORMSET_PREFIX}-${formCount}-id" value="">
|
|
<input type="checkbox" name="${FORMSET_PREFIX}-${formCount}-DELETE" id="id_${FORMSET_PREFIX}-${formCount}-DELETE" style="display:none;">
|
|
|
|
<button type="button" class="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium bg-red-600 hover:bg-red-700 text-white transition-all duration-200">
|
|
<i data-lucide="trash-2" class="w-4 h-4"></i> {% trans "Remove" %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
const tempDiv = document.createElement('div');
|
|
tempDiv.innerHTML = newFormHtml.trim();
|
|
const newForm = tempDiv.firstChild;
|
|
|
|
breakTimesContainer.appendChild(newForm);
|
|
totalFormsInput.value = formCount + 1;
|
|
|
|
// Initialize Lucide icons for new form
|
|
if (typeof lucide !== 'undefined') {
|
|
lucide.createIcons();
|
|
}
|
|
});
|
|
|
|
// Handle remove button clicks (both existing and dynamically added)
|
|
breakTimesContainer.addEventListener('click', function(e) {
|
|
if (e.target.closest('button')) {
|
|
const btn = e.target.closest('button');
|
|
if (btn.textContent.includes('Remove') || btn.querySelector('.lucide-trash-2')) {
|
|
const form = btn.closest('.break-time-form');
|
|
if (form) {
|
|
const deleteCheckbox = form.querySelector('input[name$="-DELETE"]');
|
|
if (deleteCheckbox) {
|
|
// Check DELETE box and hide form
|
|
deleteCheckbox.checked = true;
|
|
form.style.display = 'none';
|
|
} else {
|
|
// If it's a new form, remove it entirely
|
|
form.remove();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %} |