participants models
This commit is contained in:
parent
f9aaaeb788
commit
c4d401469f
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
|
||||
24
recruitment/migrations/0006_participants.py
Normal file
24
recruitment/migrations/0006_participants.py
Normal 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')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
18
recruitment/migrations/0008_rename_job_participants_jobs.py
Normal file
18
recruitment/migrations/0008_rename_job_participants_jobs.py
Normal 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',
|
||||
),
|
||||
]
|
||||
20
recruitment/migrations/0009_jobposting_assigned_users.py
Normal file
20
recruitment/migrations/0009_jobposting_assigned_users.py
Normal 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),
|
||||
),
|
||||
]
|
||||
@ -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',
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
@ -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}"
|
||||
|
||||
|
||||
@ -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'),
|
||||
]
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
@ -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">
|
||||
|
||||
@ -357,6 +357,9 @@
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
<a href="{% url 'participants_create' %}">Create Participant</a>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
137
templates/participants/participants_create.html
Normal file
137
templates/participants/participants_create.html
Normal 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 %}
|
||||
295
templates/participants/participants_detail.html
Normal file
295
templates/participants/participants_detail.html
Normal 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 %}
|
||||
395
templates/participants/participants_list.html
Normal file
395
templates/participants/participants_list.html
Normal 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 %}
|
||||
@ -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 %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user