participants models

This commit is contained in:
Faheed 2025-10-29 02:16:19 +03:00
parent f9aaaeb788
commit c4d401469f
24 changed files with 1287 additions and 11 deletions

View File

@ -10,7 +10,7 @@ import re
from .models import (
ZoomMeeting, Candidate,TrainingMaterial,JobPosting,
FormTemplate,InterviewSchedule,BreakTime,JobPostingImage,
Profile,MeetingComment,ScheduledInterview,Source
Profile,MeetingComment,ScheduledInterview,Source,Participants
)
# from django_summernote.widgets import SummernoteWidget
from django_ckeditor_5.widgets import CKEditor5Widget
@ -649,3 +649,52 @@ class CandidateExamDateForm(forms.ModelForm):
#participants form
class ParticipantsForm(forms.ModelForm):
"""Form for creating and editing Participants"""
class Meta:
model = Participants
fields = ['name', 'email', 'phone', 'designation']
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter participant name',
'required': True
}),
'email': forms.EmailInput(attrs={
'class': 'form-control',
'placeholder': 'Enter email address',
'required': True
}),
'phone': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter phone number'
}),
'designation': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter designation'
}),
# 'jobs': forms.CheckboxSelectMultiple(),
}
class ParticipantsSelectForm(forms.ModelForm):
"""Form for selecting Participants"""
participants=forms.ModelMultipleChoiceField(
queryset=Participants.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False,
label=_("Select Participants"))
users=forms.ModelMultipleChoiceField(
queryset=User.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False,
label=_("Select Users"))
class Meta:
model = JobPosting
fields = ['participants','users'] # No direct fields from Participants model

View File

@ -0,0 +1,24 @@
# Generated by Django 5.2.7 on 2025-10-28 12:12
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0005_alter_jobposting_linkedin_post_formated_data'),
]
operations = [
migrations.CreateModel(
name='Participants',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=255, verbose_name='Participant Name')),
('email', models.EmailField(max_length=254, verbose_name='Email')),
('phone', models.CharField(blank=True, max_length=20, verbose_name='Phone')),
('designation', models.CharField(blank=True, max_length=100, verbose_name='Designation')),
('job', models.ManyToManyField(blank=True, related_name='participants', to='recruitment.jobposting')),
],
),
]

View File

@ -0,0 +1,30 @@
# Generated by Django 5.2.7 on 2025-10-28 12:14
import django_extensions.db.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0006_participants'),
]
operations = [
migrations.AddField(
model_name='participants',
name='created_at',
field=models.DateTimeField(auto_now_add=True, default=None, verbose_name='Created at'),
preserve_default=False,
),
migrations.AddField(
model_name='participants',
name='slug',
field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'),
),
migrations.AddField(
model_name='participants',
name='updated_at',
field=models.DateTimeField(auto_now=True, verbose_name='Updated at'),
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-10-28 13:06
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0007_participants_created_at_participants_slug_and_more'),
]
operations = [
migrations.RenameField(
model_name='participants',
old_name='job',
new_name='jobs',
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.2.7 on 2025-10-28 16:41
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0008_rename_job_participants_jobs'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='jobposting',
name='assigned_users',
field=models.ManyToManyField(blank=True, related_name='assigned_jobs', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 5.2.7 on 2025-10-28 17:12
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0009_jobposting_assigned_users'),
]
operations = [
migrations.RemoveField(
model_name='jobposting',
name='assigned_users',
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 5.2.7 on 2025-10-28 20:42
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0010_remove_jobposting_assigned_users'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='jobposting',
name='internal_participant',
field=models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='internal_participant_jobs', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant'),
),
]

View File

@ -0,0 +1,29 @@
# Generated by Django 5.2.7 on 2025-10-28 21:30
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0011_jobposting_internal_participant'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='participants',
name='jobs',
),
migrations.AddField(
model_name='jobposting',
name='external_participant',
field=models.ManyToManyField(blank=True, help_text='External participants involved in the recruitment process for this job', related_name='jobs', to='recruitment.participants', verbose_name='External Participant'),
),
migrations.AlterField(
model_name='jobposting',
name='internal_participant',
field=models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='jobs', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant'),
),
]

View File

@ -0,0 +1,33 @@
# Generated by Django 5.2.7 on 2025-10-28 22:20
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0012_remove_participants_jobs_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='jobposting',
name='external_participant',
),
migrations.RemoveField(
model_name='jobposting',
name='internal_participant',
),
migrations.AddField(
model_name='jobposting',
name='participants',
field=models.ManyToManyField(blank=True, help_text='External participants involved in the recruitment process for this job', related_name='jobs_participating', to='recruitment.participants', verbose_name='External Participant'),
),
migrations.AddField(
model_name='jobposting',
name='users',
field=models.ManyToManyField(blank=True, help_text='Internal staff involved in the recruitment process for this job', related_name='jobs_assigned', to=settings.AUTH_USER_MODEL, verbose_name='Internal Participant'),
),
]

View File

@ -36,6 +36,7 @@ class Profile(models.Model):
class JobPosting(Base):
# Basic Job Information
JOB_TYPES = [
("FULL_TIME", "Full-time"),
("PART_TIME", "Part-time"),
@ -51,6 +52,19 @@ class JobPosting(Base):
("HYBRID", "Hybrid"),
]
users=models.ManyToManyField(
User,
blank=True,related_name="jobs_assigned",
verbose_name=_("Internal Participant"),
help_text=_("Internal staff involved in the recruitment process for this job"),
)
participants=models.ManyToManyField('Participants',
blank=True,related_name="jobs_participating",
verbose_name=_("External Participant"),
help_text=_("External participants involved in the recruitment process for this job"),
)
# Core Fields
title = models.CharField(max_length=200)
department = models.CharField(max_length=100, blank=True)
@ -1264,6 +1278,8 @@ class ScheduledInterview(Base):
related_name="scheduled_interviews",
db_index=True
)
job = models.ForeignKey(
"JobPosting", on_delete=models.CASCADE, related_name="scheduled_interviews", db_index=True
)
@ -1298,3 +1314,19 @@ class ScheduledInterview(Base):
models.Index(fields=['interview_date', 'interview_time']),
models.Index(fields=['candidate', 'job']),
]
class Participants(Base):
"""Model to store Participants details"""
name = models.CharField(max_length=255, verbose_name=_("Participant Name"))
email= models.EmailField(verbose_name=_("Email"))
phone = models.CharField(max_length=20, blank=True, verbose_name=_("Phone"))
designation = models.CharField(
max_length=100, blank=True, verbose_name=_("Designation")
)
def __str__(self):
return f"{self.name} - {self.email}"

View File

@ -144,4 +144,12 @@ urlpatterns = [
path('meetings/<slug:slug>/comments/<int:comment_id>/delete/', views.delete_meeting_comment, name='delete_meeting_comment'),
path('meetings/<slug:slug>/set_meeting_candidate/', views.set_meeting_candidate, name='set_meeting_candidate'),
#participants urls
path('participants/', views_frontend.ParticipantsListView.as_view(), name='participants_list'),
path('participants/create/', views_frontend.ParticipantsCreateView.as_view(), name='participants_create'),
path('participants/<slug:slug>/', views_frontend.ParticipantsDetailView.as_view(), name='participants_detail'),
path('participants/<slug:slug>/update/', views_frontend.ParticipantsUpdateView.as_view(), name='participants_update'),
path('participants/<slug:slug>/delete/', views_frontend.ParticipantsDeleteView.as_view(), name='participants_delete'),
]

View File

@ -33,7 +33,8 @@ from .forms import (
StaffUserCreationForm,
MeetingCommentForm,
ToggleAccountForm,
LinkedPostContentForm
LinkedPostContentForm,
ParticipantsSelectForm
)
from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent
@ -1421,7 +1422,42 @@ def candidate_update_status(request, slug):
@login_required
def candidate_interview_view(request,slug):
job = get_object_or_404(JobPosting,slug=slug)
context = {"job":job,"candidates":job.interview_candidates,'current_stage':'Interview'}
if request.method == "POST":
form = ParticipantsSelectForm(request.POST, instance=job)
print(form.errors)
if form.is_valid():
# Save the main instance (JobPosting)
job_instance = form.save(commit=False)
job_instance.save()
# MANUALLY set the M2M relationships based on submitted data
job_instance.participants.set(form.cleaned_data['participants'])
job_instance.users.set(form.cleaned_data['users'])
messages.success(request, "Interview participants updated successfully.")
return redirect("candidate_interview_view", slug=job.slug)
else:
# 🛑 FIX: Explicitly pass the initial data for M2M fields
initial_data = {
'participants': job.participants.all(),
'users': job.users.all(),
}
form = ParticipantsSelectForm(instance=job, initial=initial_data)
else:
form = ParticipantsSelectForm(instance=job)
context = {
"job":job,
"candidates":job.interview_candidates,
'current_stage':'Interview',
'form':form
}
return render(request,"recruitment/candidate_interview_view.html",context)
@login_required

View File

@ -522,3 +522,71 @@ def update_candidate_status(request, job_slug, candidate_slug, stage_type, statu
# Removed incorrect JobDetailView class.
# The job_detail view is handled by function-based view in recruitment.views
#participants views
class ParticipantsListView(LoginRequiredMixin, ListView):
model = models.Participants
template_name = 'participants/participants_list.html'
context_object_name = 'participants'
paginate_by = 10
def get_queryset(self):
queryset = super().get_queryset()
# Handle search
search_query = self.request.GET.get('search', '')
if search_query:
queryset = queryset.filter(
Q(name__icontains=search_query) |
Q(email__icontains=search_query) |
Q(phone__icontains=search_query) |
Q(designation__icontains=search_query)
)
# Filter for non-staff users
if not self.request.user.is_staff:
return models.Participants.objects.none() # Restrict for non-staff
return queryset.order_by('-created_at')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['search_query'] = self.request.GET.get('search', '')
return context
class ParticipantsDetailView(LoginRequiredMixin, DetailView):
model = models.Participants
template_name = 'participants/participants_detail.html'
context_object_name = 'participant'
slug_url_kwarg = 'slug'
class ParticipantsCreateView(LoginRequiredMixin, SuccessMessageMixin, CreateView):
model = models.Participants
form_class = forms.ParticipantsForm
template_name = 'participants/participants_create.html'
success_url = reverse_lazy('job_list')
success_message = 'Participant created successfully.'
# def get_initial(self):
# initial = super().get_initial()
# if 'slug' in self.kwargs:
# job = get_object_or_404(models.JobPosting, slug=self.kwargs['slug'])
# initial['jobs'] = [job]
# return initial
class ParticipantsUpdateView(LoginRequiredMixin, SuccessMessageMixin, UpdateView):
model = models.Participants
form_class = forms.ParticipantsForm
template_name = 'participants/participants_create.html'
success_url = reverse_lazy('job_list')
success_message = 'Participant updated successfully.'
slug_url_kwarg = 'slug'
class ParticipantsDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
model = models.Participants
success_url = reverse_lazy('participants_list') # Redirect to the participants list after success
success_message = 'Participant deleted successfully.'
slug_url_kwarg = 'slug'

View File

@ -223,16 +223,18 @@
</span>
</a>
</li>
{% comment %} <li class="nav-item me-lg-4">
<a class="nav-link {% if request.resolver_match.url_name == 'training_list' %}active{% endif %}" href="{% url 'training_list' %}">
<li class="nav-item me-lg-4">
<a class="nav-link {% if request.resolver_match.url_name == 'participants_list' %}active{% endif %}" href="{% url 'participants_list' %}">
<span class="d-flex align-items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.26 10.147a60.438 60.438 0 0 0-.491 6.347A48.62 48.62 0 0 1 12 20.904a48.62 48.62 0 0 1 8.232-4.41 60.46 60.46 0 0 0-.491-6.347m-15.482 0a50.636 50.636 0 0 0-2.658-.813A59.906 59.906 0 0 1 12 3.493a59.903 59.903 0 0 1 10.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.717 50.717 0 0 1 12 13.489a50.702 50.702 0 0 1 7.74-3.342M6.75 15a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm0 0v-3.675A55.378 55.378 0 0 1 12 8.443m-7.007 11.55A5.981 5.981 0 0 0 6.75 15.75v-1.5" />
</svg>
{% trans "Training" %}
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 0 1 6 3.75h2.25A2.25 2.25 0 0 1 10.5 6v2.25a2.25 2.25 0 0 1-2.25 2.25H6a2.25 2.25 0 0 1-2.25-2.25V6ZM3.75 15.75A2.25 2.25 0 0 1 6 13.5h2.25a2.25 2.25 0 0 1 2.25 2.25V18a2.25 2.25 0 0 1-2.25 2.25H6A2.25 2.25 0 0 1 3.75 18v-2.25ZM13.5 6a2.25 2.25 0 0 1 2.25-2.25H18A2.25 2.25 0 0 1 20.25 6v2.25A2.25 2.25 0 0 1 18 10.5h-2.25a2.25 2.25 0 0 1-2.25-2.25V6ZM13.5 15.75a2.25 2.25 0 0 1 2.25-2.25H18a2.25 2.25 0 0 1 2.25 2.25V18A2.25 2.25 0 0 1 18 20.25h-2.25A2.25 2.25 0 0 1 13.5 18v-2.25Z" />
</svg>
{% trans "Participants" %}
</span>
</a>
</li> {% endcomment %}
</li>
{% comment %} <li class="nav-item dropdown ms-lg-2">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
data-bs-offset="0, 8" data-bs-auto-close="outside">

View File

@ -357,6 +357,9 @@
{% endif %}
<a href="{% url 'participants_create' %}">Create Participant</a>
</div>
</div>

View File

@ -0,0 +1,137 @@
{% extends "base.html" %}
{% load static i18n crispy_forms_tags %}
{% block title %}Create Participant - {{ block.super }}{% endblock %}
{% block customCSS %}
<style>
/* ================================================= */
/* THEME VARIABLES AND GLOBAL STYLES (KAASUH ATS - Teal Theme) */
/* ================================================= */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
}
/* Primary Color Overrides */
.text-primary { color: var(--kaauh-teal) !important; }
/* Main Action Button Style */
.btn-main-action, .btn-primary {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
padding: 0.6rem 1.2rem;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-main-action:hover, .btn-primary:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Outlined Button Styles */
.btn-secondary, .btn-outline-secondary {
background-color: #f8f9fa;
color: var(--kaauh-teal-dark);
border: 1px solid var(--kaauh-teal);
font-weight: 500;
}
.btn-secondary:hover, .btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
/* Card enhancements */
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
/* Colored Header Card */
.participant-header-card {
background: linear-gradient(135deg, var(--kaauh-teal), #004d57);
color: white;
border-radius: 0.75rem 0.75rem 0 0;
padding: 1.5rem;
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
}
.participant-header-card h1 {
font-weight: 700;
margin: 0;
font-size: 1.8rem;
}
.heroicon {
width: 1.25rem;
height: 1.25rem;
vertical-align: text-bottom;
stroke: currentColor;
margin-right: 0.5rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="card mb-4">
<div class="participant-header-card">
<div class="d-flex justify-content-between align-items-start flex-wrap">
<div class="flex-grow-1">
<h1 class="h3 mb-1">
<i class="fas fa-user-plus"></i>
{% trans "Create New Participant" %}
</h1>
<p class="text-white opacity-75 mb-0">{% trans "Enter details to create a new participant record." %}</p>
</div>
<div class="d-flex gap-2 mt-1">
<a href="{% url 'participants_list'%}" class="btn btn-outline-light btn-sm" title="{% trans 'Back to List' %}">
<i class="fas fa-arrow-left"></i>
<span class="d-none d-sm-inline">{% trans "Back to List" %}</span>
</a>
</div>
</div>
</div>
</div>
<div class="card shadow-sm">
<div class="card-header bg-white border-bottom">
<h2 class="h5 mb-0 text-primary">
<i class="fas fa-file-alt me-1"></i>
{% trans "Participant Information" %}
</h2>
</div>
<div class="card-body">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{# Split form into two columns for better horizontal use #}
<div class="row g-4">
{% for field in form %}
<div class="col-md-6">
{{ field|as_crispy_field }}
</div>
{% endfor %}
</div>
<hr class="mt-4 mb-4">
<button class="btn btn-main-action" type="submit">
<i class="fas fa-save me-1"></i>
{% trans "Save Participant" %}
</button>
</form>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,295 @@
{% extends "base.html" %}
{% load static i18n %}
{% block title %}{{ participant.name }} - Participant Details{% endblock %}
{% block customCSS %}
<style>
/* ================================================= */
/* THEME VARIABLES AND GLOBAL STYLES (KAAT-S Teal Theme) */
/* ================================================= */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-bg-light: #f8f9fa;
}
/* Primary Color Overrides */
.text-primary-theme { color: var(--kaauh-teal) !important; }
/* Main Action Button Style */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1.2rem;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Secondary/Outline Button Styles */
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border: 1px solid var(--kaauh-teal);
font-weight: 500;
padding: 0.6rem 1.2rem;
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
/* Card enhancements */
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
/* Primary Header Card (For Details Page Banner) */
.detail-header-card {
background: linear-gradient(135deg, var(--kaauh-teal), #004d57);
color: white;
border-radius: 0.75rem 0.75rem 0 0;
padding: 1.5rem 2rem;
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
}
.detail-header-card h1 {
font-weight: 700;
font-size: 2rem;
margin: 0;
}
/* Detail Labels */
.detail-label {
font-size: 0.8rem;
font-weight: 600;
color: #6c757d;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.25rem;
display: block;
}
.detail-value {
font-size: 1.1rem;
color: var(--kaauh-primary-text);
font-weight: 500;
}
/* Badge Styling for Jobs */
.job-badge {
font-weight: 600;
padding: 0.4em 0.7em;
border-radius: 0.3rem;
background-color: var(--kaauh-teal);
color: white;
display: inline-block;
margin-right: 0.5rem;
margin-bottom: 0.5rem;
transition: background-color 0.2s;
}
.job-badge:hover {
background-color: var(--kaauh-teal-dark);
text-decoration: none;
}
/* Section Separator */
.section-title {
color: var(--kaauh-teal-dark);
font-weight: 600;
font-size: 1.4rem;
margin-bottom: 1.5rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
{# --- HEADER CARD WITH ACTIONS --- #}
<div class="card mb-4 border-0" style="border-radius: 0.75rem;">
<div class="detail-header-card">
<div class="d-flex justify-content-between align-items-center flex-wrap">
<div class="flex-grow-1">
<h1 class="mb-1">
<i class="fas fa-user-tag me-2"></i>
{{ participant.name }}
</h1>
<p class="text-white opacity-75 mb-0">{% trans "Participant Details" %}</p>
</div>
<div class="d-flex gap-3 mt-3 mt-lg-0">
<a href="{% url 'participants_list' %}" class="btn btn-outline-light" title="{% trans 'Back to List' %}">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to List" %}
</a>
{% if user.is_staff %}
<a href="{% url 'participants_update' participant.slug %}" class="btn btn-main-action" title="{% trans 'Edit Participant' %}">
<i class="fas fa-edit me-1"></i> {% trans "Edit Profile" %}
</a>
<button type="button" class="btn btn-danger" title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-delete-url="{% url 'participants_delete' participant.slug %}"
data-item-name="{{ participant.name }}">
<i class="fas fa-trash-alt"></i>
</button>
{% endif %}
</div>
</div>
</div>
</div>
{# --- END HEADER CARD --- #}
<div class="row g-4">
{# --- LEFT COLUMN: CORE CONTACT AND PROFESSIONAL INFO --- #}
<div class="col-lg-8">
<div class="card p-4 h-100">
<div class="card-body">
<h2 class="section-title mb-4">{% trans "Contact & Role Information" %}</h2>
<div class="row g-4">
{# Name (Redundant here but included for clarity) #}
<div class="col-md-6">
<span class="detail-label">{% trans "Full Name" %}</span>
<span class="detail-value">{{ participant.name }}</span>
</div>
{# Email #}
<div class="col-md-6">
<span class="detail-label">{% trans "Email Address" %}</span>
<span class="detail-value text-primary-theme">{{ participant.email }}</span>
</div>
{# Phone #}
<div class="col-md-6">
<span class="detail-label">{% trans "Phone Number" %}</span>
<span class="detail-value">{{ participant.phone|default:"N/A" }}</span>
</div>
{# Designation #}
<div class="col-md-6">
<span class="detail-label">{% trans "Designation" %}</span>
<span class="detail-value">{{ participant.designation|default:"N/A" }}</span>
</div>
</div>
<hr class="my-5">
{# Assigned Jobs Section #}
<h2 class="section-title">{% trans "Assigned Jobs" %}</h2>
<div class="d-flex flex-wrap">
{% for job in participant.jobs_participating.all %}
<a href="{% url 'job_detail' job.slug %}" class="job-badge text-decoration-none">
<i class="fas fa-briefcase me-1"></i> {{ job.title }}
</a>
{% empty %}
<p class="text-muted">{% trans "This participant is not currently assigned to any job." %}</p>
{% endfor %}
</div>
</div>
</div>
</div>
{# --- RIGHT COLUMN: TIMESTAMPS AND METADATA --- #}
<div class="col-lg-4">
<div class="card p-4 h-100 bg-light">
<div class="card-body">
<h2 class="section-title mb-4">{% trans "Metadata" %}</h2>
<div class="mb-4">
<span class="detail-label">{% trans "Record Created" %}</span>
<span class="detail-value">{{ participant.created_at|date:"F d, Y" }} ({% trans "at" %} {{ participant.created_at|time:"H:i" }})</span>
</div>
<div class="mb-4">
<span class="detail-label">{% trans "Last Updated" %}</span>
<span class="detail-value">{{ participant.updated_at|date:"F d, Y" }} ({% trans "at" %} {{ participant.updated_at|time:"H:i" }})</span>
</div>
<hr class="my-4">
<div class="mb-2">
<span class="detail-label">{% trans "Total Assigned Jobs" %}</span>
<h3 class="fw-bold text-primary-theme">{{ participant.jobs_participating.count }}</h3>
</div>
</div>
</div>
</div>
</div>
</div>
{# Delete Confirmation Modal #}
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">{% trans "Confirm Deletion" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>{% trans "Are you sure you want to delete" %} <strong id="itemName"></strong>?</p>
<p class="text-danger">
<i class="fas fa-exclamation-triangle me-1"></i>
{% trans "This action cannot be undone." %}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<form method="post" id="deleteForm">
{% csrf_token %}
<button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
// Populate Delete Modal with dynamic data
var deleteModal = document.getElementById('deleteModal');
// We must check if the modal element exists before adding the listener
if (deleteModal) {
deleteModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
// Get data from the button that triggered the modal
var deleteUrl = button.getAttribute('data-delete-url');
var itemName = button.getAttribute('data-item-name');
// Get modal elements
var modalItemName = deleteModal.querySelector('#itemName');
var deleteForm = deleteModal.querySelector('#deleteForm');
// Set the dynamic content
if (modalItemName) {
modalItemName.textContent = itemName;
}
// Set the form action URL
if (deleteForm) {
deleteForm.action = deleteUrl;
}
});
}
</script>
{% endblock %}

View File

@ -0,0 +1,395 @@
{% extends "base.html" %}
{% load static i18n %}
{% block title %}Participants - {{ block.super }}{% endblock %}
{% block customCSS %}
<style>
/* UI Variables for the KAAT-S Theme (Consistent with Reference) */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-gray-light: #f8f9fa; /* Added for hover/background consistency */
}
/* Primary Color Overrides */
.text-primary-theme { color: var(--kaauh-teal) !important; }
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
.text-success { color: var(--kaauh-success) !important; }
.text-danger { color: var(--kaauh-danger) !important; }
.text-info { color: #17a2b8 !important; }
/* Enhanced Card Styling (Consistent) */
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
transition: transform 0.2s, box-shadow 0.2s;
background-color: white;
}
.card:not(.no-hover):hover { /* Use no-hover class for main structure cards */
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
}
.card.no-hover:hover {
transform: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
/* Main Action Button Style (Teal Theme) */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.4rem;
padding: 0.5rem 1rem;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Secondary Button Style (For Edit/Outline - Consistent) */
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
/* Card Specifics */
.participant-card .card-title {
color: var(--kaauh-teal-dark);
font-weight: 600;
font-size: 1.15rem;
}
.participant-card .card-text i {
color: var(--kaauh-teal);
width: 1.25rem;
}
/* Table & Card Badge Styling (Unified) */
.badge {
font-weight: 600;
padding: 0.4em 0.7em;
border-radius: 0.3rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Status Badge Mapping (Unified to Primary Theme Color) */
.bg-primary { background-color: var(--kaauh-teal) !important; color: white !important;} /* Main job/stage badge */
.bg-success { background-color: #28a745 !important; color: white !important;}
.bg-warning { background-color: #ffc107 !important; color: #343a40 !important;}
/* Table Styling (Consistent with Reference) */
.table-view .table thead th {
background-color: var(--kaauh-teal-dark); /* Dark header background */
color: white;
font-weight: 600;
border-color: var(--kaauh-border);
text-transform: uppercase;
font-size: 0.8rem;
letter-spacing: 0.5px;
padding: 1rem;
}
.table-view .table tbody td {
vertical-align: middle;
padding: 1rem;
border-color: var(--kaauh-border);
}
.table-view .table tbody tr:hover {
background-color: var(--kaauh-gray-light);
}
/* Pagination Link Styling (Consistent) */
.pagination .page-item .page-link {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-border);
}
.pagination .page-item.active .page-link {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
}
.pagination .page-item:hover .page-link:not(.active) {
background-color: #e9ecef;
}
/* Filter & Search Layout Adjustments */
.filter-buttons {
display: flex;
gap: 0.5rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-users me-2"></i> {% trans "Participants List" %}
</h1>
{% comment %} {% if user.is_staff %}
<a href="{% url 'participants_create' %}" class="btn btn-main-action">
<i class="fas fa-plus me-1"></i> {% trans "Add New Participant" %}
</a>
{% endif %} {% endcomment %}
</div>
<div class="card mb-4 shadow-sm no-hover">
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label>
<div class="input-group input-group-lg">
<form method="get" action="" class="w-100">
{# Assuming this includes your search input and submit button #}
{% include 'includes/search_form.html' %}
</form>
</div>
</div>
<div class="col-md-6">
{% url 'participant_list' as participant_list_url %}
<form method="GET" class="row g-3 align-items-end h-100">
{% if search_query %}<input type="hidden" name="q" value="{{ search_query }}">{% endif %}
<div class="col-md-8">
<label for="job_filter" class="form-label small text-muted">{% trans "Filter by Assigned Job" %}</label>
<select name="job" id="job_filter" class="form-select form-select-sm">
<option value="">{% trans "All Jobs" %}</option>
{# available_jobs should be passed from the view #}
{% for job in available_jobs %}
<option value="{{ job.slug }}" {% if job_filter == job.slug %}selected{% endif %}>{{ job.title }}</option>
{% endfor %}
</select>
</div>
{# Buttons Group (pushed to the right/bottom) #}
<div class="col-md-4 d-flex justify-content-end align-self-end">
<div class="filter-buttons">
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-filter me-1"></i> {% trans "Apply" %}
</button>
{% if job_filter or search_query %}
<a href="{% url 'participant_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-times me-1"></i> {% trans "Clear" %}
</a>
{% endif %}
</div>
</div>
</form>
</div>
</div>
</div>
</div>
{% if participants %}
<div id="participant-list">
{# View Switcher - list_id must match the container ID #}
{% include "includes/_list_view_switcher.html" with list_id="participant-list" %}
{# Table View (Default) #}
<div class="table-view active">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead>
<tr>
<th scope="col" style="width: 15%;">{% trans "Name" %}</th>
<th scope="col" style="width: 15%;">{% trans "Email" %}</th>
<th scope="col" style="width: 10%;">{% trans "Phone" %}</th>
<th scope="col" style="width: 25%;">{% trans "Assigned Jobs" %}</th>
<th scope="col" style="width: 15%;">{% trans "Designation" %}</th>
<th scope="col" style="width: 15%;">{% trans "Created At" %}</th>
<th scope="col" style="width: 5%;" class="text-end">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for participant in participants %}
<tr>
<td class="fw-medium"><a href="{% url 'participants_detail' participant.slug %}" class="text-decoration-none link-secondary">{{ participant.name }}<a></td>
<td>{{ participant.email }}</td>
<td>{{ participant.phone|default:"N/A" }}</td>
<td>
{# Iterate over the many-to-many relationship (jobs) #}
{% for job in participant.jobs_participating.all %}
<span class="badge bg-primary me-1 mb-1">
<a href="{% url 'job_detail' job.slug %}" class="text-decoration-none text-white">{{ job.title }}</a>
</span>
{% empty %}
<span class="text-muted small">{% trans "None Assigned" %}</span>
{% endfor %}
</td>
<td>{{ participant.designation|default:"N/A" }}</td>
<td>{{ participant.created_at|date:"d-m-Y" }}</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<a href="{% url 'participants_detail' participant.slug%}" class="btn btn-outline-primary" title="{% trans 'View' %}">
<i class="fas fa-eye"></i>
</a>
{% if user.is_staff %}
<a href="{% url 'participants_update' participant.slug%}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
<i class="fas fa-edit"></i>
</a>
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-delete-url="{% url 'participants_delete' participant.slug %}"
data-item-name="{{ participant.name }}">
<i class="fas fa-trash-alt"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{# Card View #}
<div class="card-view row">
{% for participant in participants %}
<div class="col-md-6 col-lg-4 mb-4">
<div class="card participant-card h-100 shadow-sm">
<div class="card-body d-flex flex-column">
<div class="d-flex justify-content-between align-items-start mb-2">
<h5 class="card-title flex-grow-1 me-3"><a href="{% url 'participants_detail' participant.slug%}" class="text-decoration-none text-primary-theme ">{{ participant.name }}</a></h5>
</div>
<p class="card-text text-muted small">
<i class="fas fa-envelope"></i> {{ participant.email }}<br>
<i class="fas fa-phone-alt"></i> {{ participant.phone|default:"N/A" }}<br>
<i class="fas fa-briefcase"></i> {{ participant.designation|default:"N/A" }}
</p>
<div class="mb-2">
<strong class="small text-muted">{% trans "Assigned Jobs:" %}</strong><br>
{% for job in participant.jobs.all %}
<span class="badge bg-primary me-1 mb-1">
<a href="{% url 'job_detail' job.slug %}" class="text-decoration-none text-white small">{{ job.title }}</a>
</span>
{% empty %}
<span class="text-muted small">{% trans "None" %}</span>
{% endfor %}
</div>
<div class="mt-auto pt-2 border-top">
<div class="d-flex gap-2">
<a href="{% url 'participants_detail' participant.slug %}" class="btn btn-sm btn-main-action">
<i class="fas fa-eye"></i> {% trans "View" %}
</a>
{% if user.is_staff %}
<a href="#" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-edit"></i> {% trans "Edit" %}
</a>
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-delete-url="{% url 'participants_delete' participant.slug %}"
data-item-name="{{ participant.name }}">
<i class="fas fa-trash-alt"></i>
</button>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
{# Pagination #}
{% include "includes/paginator.html" %}
{% else %}
<div class="text-center py-5 card shadow-sm">
<div class="card-body">
<i class="fas fa-users fa-3x mb-3" style="color: var(--kaauh-teal-dark);"></i>
<h3>{% trans "No participants found" %}</h3>
<p class="text-muted">{% trans "Create your first participant record or adjust your filters." %}</p>
{% if user.is_staff %}
<a href="{% url 'participants_create' %}" class="btn btn-main-action mt-3">
<i class="fas fa-plus me-1"></i> {% trans "Add Participant" %}
</a>
{% endif %}
</div>
</div>
{% endif %}
</div>
{# Delete Confirmation Modal #}
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="deleteModalLabel">{% trans "Confirm Deletion" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>{% trans "Are you sure you want to delete" %} <strong id="itemName"></strong>?</p>
<p class="text-danger">
<i class="fas fa-exclamation-triangle me-1"></i>
{% trans "This action cannot be undone." %}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<form method="post" id="deleteForm">
{% csrf_token %}
<button type="submit" class="btn btn-danger">{% trans "Delete" %}</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
// Populate Delete Modal with dynamic data
var deleteModal = document.getElementById('deleteModal');
// We must check if the modal element exists before adding the listener
if (deleteModal) {
deleteModal.addEventListener('show.bs.modal', function (event) {
var button = event.relatedTarget;
// Get data from the button that triggered the modal
var deleteUrl = button.getAttribute('data-delete-url');
var itemName = button.getAttribute('data-item-name');
// Get modal elements
var modalItemName = deleteModal.querySelector('#itemName');
var deleteForm = deleteModal.querySelector('#deleteForm');
// Set the dynamic content
if (modalItemName) {
modalItemName.textContent = itemName;
}
// Set the form action URL
if (deleteForm) {
deleteForm.action = deleteUrl;
}
});
}
</script>
{% endblock %}

View File

@ -167,6 +167,8 @@
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
@ -222,8 +224,13 @@
<i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %}
</button>
</form>
</div>
<div class="vr" style="height: 28px;"></div>
<!--manage participants for interview-->
<button type="button" class="btn btn-main-action btn-sm"
data-bs-toggle="modal"
data-bs-target="#jobAssignmentModal">
<i class="fas fa-users-cog me-1"></i> {% trans "Manage Participants" %}
</button>
</div>
{% endif %}
<div class="table-responsive">
@ -413,8 +420,47 @@
</div>
</div>
</div>
<div class="modal fade" id="jobAssignmentModal" tabindex="-1" aria-labelledby="jobAssignmentLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="jobAssignmentLabel">{% trans "Manage all participants" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{% url 'candidate_interview_view' job.slug %}">
{% csrf_token %}
<div class="modal-body">
{{ job.internal_job_id }} {{ job.title}}
<hr>
<h3>👥 {% trans "Participants" %}</h3>
{{ form.participants.errors }}
{{ form.participants }}
<hr>
<h3>🧑‍💼 {% trans "Users" %}</h3>
{{ form.users.errors }}
{{ form.users }}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-main-action">{% trans "Save" %}</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
@ -516,5 +562,19 @@
}
});
});
$(document).ready(function() {
// Check the flag passed from the Django view
var shouldOpenModal = {{ show_modal_on_load|yesno:"true,false" }};
// If the view detected an invalid form submission (POST request), open the modal
if (shouldOpenModal) {
// Use the native Bootstrap 5 JS function to show the modal
var myModal = new bootstrap.Modal(document.getElementById('jobAssignmentModal'));
myModal.show();
}
});
</script>
{% endblock %}