update on the models and forms
This commit is contained in:
parent
579cc085e2
commit
a23c96cc17
Binary file not shown.
Binary file not shown.
@ -67,7 +67,7 @@ MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
@ -196,7 +196,6 @@ SOCIALACCOUNT_PROVIDERS = {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A'
|
||||
ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA'
|
||||
ZOOM_CLIENT_SECRET = 'rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L'
|
||||
@ -215,7 +214,6 @@ CELERY_TASK_SERIALIZER = 'json'
|
||||
CELERY_RESULT_SERIALIZER = 'json'
|
||||
CELERY_TIMEZONE = 'UTC'
|
||||
|
||||
|
||||
LINKEDIN_CLIENT_ID = '867jwsiyem1504'
|
||||
LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='
|
||||
LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/'
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -469,4 +469,18 @@ class InterviewScheduleForm(forms.ModelForm):
|
||||
def clean_working_days(self):
|
||||
working_days = self.cleaned_data.get('working_days')
|
||||
# Convert string values to integers
|
||||
return [int(day) for day in working_days]
|
||||
return [int(day) for day in working_days]
|
||||
|
||||
|
||||
class JobPostingCancelReasonForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = JobPosting
|
||||
fields = ['cancel_reason']
|
||||
class JobPostingStatusForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = JobPosting
|
||||
fields = ['status']
|
||||
class FormTemplateIsActiveForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = FormTemplate
|
||||
fields = ['is_active']
|
||||
@ -2,12 +2,13 @@ import requests
|
||||
|
||||
LINKEDIN_API_BASE = "https://api.linkedin.com/v2"
|
||||
|
||||
|
||||
class LinkedInService:
|
||||
def __init__(self, access_token):
|
||||
self.headers = {
|
||||
'Authorization': f'Bearer {access_token}',
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json'
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"X-Restli-Protocol-Version": "2.0.0",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def post_job(self, organization_id, job_data):
|
||||
@ -17,10 +18,10 @@ class LinkedInService:
|
||||
"lifecycleState": "PUBLISHED",
|
||||
"specificContent": {
|
||||
"com.linkedin.ugc.ShareContent": {
|
||||
"shareCommentary": {"text": job_data['text']},
|
||||
"shareMediaCategory": "NONE"
|
||||
"shareCommentary": {"text": job_data["text"]},
|
||||
"shareMediaCategory": "NONE",
|
||||
}
|
||||
},
|
||||
"visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"}
|
||||
"visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"},
|
||||
}
|
||||
return requests.post(url, json=data, headers=self.headers)
|
||||
return requests.post(url, json=data, headers=self.headers)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-08 15:48
|
||||
# Generated by Django 5.2.6 on 2025-10-09 10:10
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
@ -213,6 +213,7 @@ class Migration(migrations.Migration):
|
||||
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
|
||||
('email', models.EmailField(max_length=254, verbose_name='Email')),
|
||||
('phone', models.CharField(max_length=20, verbose_name='Phone')),
|
||||
('address', models.TextField(max_length=200, verbose_name='Address')),
|
||||
('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')),
|
||||
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
|
||||
('applied', models.BooleanField(default=False, verbose_name='Applied')),
|
||||
@ -311,6 +312,14 @@ class Migration(migrations.Migration):
|
||||
name='job',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SharedFormTemplate',
|
||||
fields=[
|
||||
@ -374,6 +383,7 @@ class Migration(migrations.Migration):
|
||||
name='ScheduledInterview',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('interview_date', models.DateField(verbose_name='Interview Date')),
|
||||
('interview_time', models.TimeField(verbose_name='Interview Time')),
|
||||
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], default='scheduled', max_length=20)),
|
||||
@ -384,5 +394,8 @@ class Migration(migrations.Migration):
|
||||
('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')),
|
||||
('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-08 17:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='candidate',
|
||||
name='address',
|
||||
field=models.TextField(default='', max_length=200, verbose_name='Address'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-09 10:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='jobposting',
|
||||
name='cancel_reason',
|
||||
field=models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobposting',
|
||||
name='cancelled_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobposting',
|
||||
name='cancelled_by',
|
||||
field=models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobposting',
|
||||
name='status',
|
||||
field=models.CharField(blank=True, choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20, null=True),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-09 12:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='candidate',
|
||||
name='is_resume_parsed',
|
||||
field=models.BooleanField(default=False, verbose_name='Resume Parsed'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='formtemplate',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=False, help_text='Whether this template is active'),
|
||||
),
|
||||
]
|
||||
@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-08 17:47
|
||||
|
||||
import django_extensions.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0002_candidate_address'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='scheduledinterview',
|
||||
name='slug',
|
||||
field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'),
|
||||
),
|
||||
]
|
||||
@ -1,24 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-08 13:01
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0026_interviewschedule_scheduledinterview'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -11,8 +11,10 @@ from django.urls import reverse
|
||||
|
||||
|
||||
class Profile(models.Model):
|
||||
profile_image=models.ImageField(null=True,blank=True,upload_to='profile_pic/')
|
||||
user=models.OneToOneField(User,on_delete=models.CASCADE,related_name='profile')
|
||||
profile_image = models.ImageField(null=True, blank=True, upload_to="profile_pic/")
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
|
||||
|
||||
|
||||
class Base(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at"))
|
||||
@ -105,8 +107,9 @@ class JobPosting(Base):
|
||||
# Status Fields
|
||||
STATUS_CHOICES = [
|
||||
("DRAFT", "Draft"),
|
||||
("PUBLISHED", "Published"),
|
||||
("ACTIVE", "Active"),
|
||||
("CLOSED", "Closed"),
|
||||
("CANCELLED", "Cancelled"),
|
||||
("ARCHIVED", "Archived"),
|
||||
]
|
||||
status = models.CharField(
|
||||
@ -165,6 +168,18 @@ class JobPosting(Base):
|
||||
"External agency responsible for sourcing candidates for this role"
|
||||
),
|
||||
)
|
||||
cancel_reason = models.TextField(
|
||||
blank=True,
|
||||
help_text=_("Reason for canceling the job posting"),
|
||||
verbose_name=_("Cancel Reason"),
|
||||
)
|
||||
cancelled_by = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text=_("Name of person who cancelled this job"),
|
||||
verbose_name=_("Cancelled By"),
|
||||
)
|
||||
cancelled_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
@ -197,7 +212,7 @@ class JobPosting(Base):
|
||||
else:
|
||||
next_num = 1
|
||||
|
||||
self.internal_job_id = f"{prefix}-{year}-{next_num:04d}"
|
||||
self.internal_job_id = f"{prefix}-{year}-{next_num:06d}"
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@ -260,8 +275,11 @@ class Candidate(Base):
|
||||
last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
|
||||
email = models.EmailField(verbose_name=_("Email"))
|
||||
phone = models.CharField(max_length=20, verbose_name=_("Phone"))
|
||||
address = models.TextField(max_length=200,verbose_name=_("Address"))
|
||||
address = models.TextField(max_length=200, verbose_name=_("Address"))
|
||||
resume = models.FileField(upload_to="resumes/", verbose_name=_("Resume"))
|
||||
is_resume_parsed = models.BooleanField(
|
||||
default=False, verbose_name=_("Resume Parsed")
|
||||
)
|
||||
parsed_summary = models.TextField(blank=True, verbose_name=_("Parsed Summary"))
|
||||
applied = models.BooleanField(default=False, verbose_name=_("Applied"))
|
||||
stage = models.CharField(
|
||||
@ -331,6 +349,7 @@ class Candidate(Base):
|
||||
if self.resume:
|
||||
return self.resume.size
|
||||
return 0
|
||||
|
||||
def clean(self):
|
||||
"""Validate stage transitions"""
|
||||
# Only validate if this is an existing record (not being created)
|
||||
@ -376,6 +395,14 @@ class Candidate(Base):
|
||||
old_stage = self.__class__.objects.get(pk=self.pk).stage
|
||||
return self.STAGE_SEQUENCE.get(old_stage, [])
|
||||
|
||||
@property
|
||||
def submission(self):
|
||||
return FormSubmission.objects.filter(template__job=self.job).first()
|
||||
@property
|
||||
def responses(self):
|
||||
if self.submission:
|
||||
return self.submission.responses.all()
|
||||
return []
|
||||
def __str__(self):
|
||||
return self.full_name
|
||||
|
||||
@ -449,7 +476,7 @@ class FormTemplate(Base):
|
||||
User, on_delete=models.CASCADE, related_name="form_templates"
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
default=True, help_text="Whether this template is active"
|
||||
default=False, help_text="Whether this template is active"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -595,6 +622,9 @@ class FormField(Base):
|
||||
if self.order < 0:
|
||||
raise ValidationError("Order must be a positive integer")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.stage.template.name} - {self.stage.name} - {self.label}"
|
||||
|
||||
|
||||
class FormSubmission(Base):
|
||||
"""
|
||||
@ -658,16 +688,19 @@ class FieldResponse(Base):
|
||||
if self.uploaded_file:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def get_file(self):
|
||||
if self.is_file:
|
||||
return self.uploaded_file
|
||||
return None
|
||||
|
||||
@property
|
||||
def get_file_size(self):
|
||||
if self.is_file:
|
||||
return self.uploaded_file.size
|
||||
return 0
|
||||
|
||||
@property
|
||||
def display_value(self):
|
||||
"""Return a human-readable representation of the response value"""
|
||||
@ -885,9 +918,7 @@ class InterviewSchedule(Base):
|
||||
job = models.ForeignKey(
|
||||
JobPosting, on_delete=models.CASCADE, related_name="interview_schedules"
|
||||
)
|
||||
candidates = models.ManyToManyField(
|
||||
Candidate, related_name="interview_schedules"
|
||||
)
|
||||
candidates = models.ManyToManyField(Candidate, related_name="interview_schedules")
|
||||
start_date = models.DateField(verbose_name=_("Start Date"))
|
||||
end_date = models.DateField(verbose_name=_("End Date"))
|
||||
working_days = models.JSONField(
|
||||
@ -895,9 +926,7 @@ class InterviewSchedule(Base):
|
||||
) # Store days of week as [0,1,2,3,4] for Mon-Fri
|
||||
start_time = models.TimeField(verbose_name=_("Start Time"))
|
||||
end_time = models.TimeField(verbose_name=_("End Time"))
|
||||
breaks = models.ManyToManyField(
|
||||
BreakTime, blank=True, related_name="schedules"
|
||||
)
|
||||
breaks = models.ManyToManyField(BreakTime, blank=True, related_name="schedules")
|
||||
interview_duration = models.PositiveIntegerField(
|
||||
verbose_name=_("Interview Duration (minutes)")
|
||||
)
|
||||
|
||||
@ -24,6 +24,8 @@ import asyncio
|
||||
|
||||
@receiver(post_save, sender=models.Candidate)
|
||||
def score_candidate_resume(sender, instance, created, **kwargs):
|
||||
if instance.is_resume_parsed:
|
||||
return
|
||||
try:
|
||||
# Get absolute file path
|
||||
file_path = instance.resume.path
|
||||
@ -108,12 +110,12 @@ def score_candidate_resume(sender, instance, created, **kwargs):
|
||||
instance.weaknesses = result1.get('weaknesses', '')
|
||||
instance.criteria_checklist = result1.get('criteria_checklist', {})
|
||||
|
||||
|
||||
instance.is_resume_parsed = True
|
||||
|
||||
# Save only scoring-related fields to avoid recursion
|
||||
instance.save(update_fields=[
|
||||
'match_score', 'strengths', 'weaknesses',
|
||||
'criteria_checklist','parsed_summary'
|
||||
'criteria_checklist','parsed_summary', 'is_resume_parsed'
|
||||
])
|
||||
|
||||
logger.info(f"Successfully scored resume for candidate {instance.id}")
|
||||
|
||||
@ -535,4 +535,19 @@ def get_available_time_slots(schedule, breaks=None):
|
||||
current_date += timedelta(days=1)
|
||||
|
||||
print(f"Total slots generated: {len(slots)}")
|
||||
return slots
|
||||
return slots
|
||||
|
||||
|
||||
|
||||
def json_to_markdown_table(data_list):
|
||||
if not data_list:
|
||||
return ""
|
||||
|
||||
headers = data_list[0].keys()
|
||||
markdown = "| " + " | ".join(headers) + " |\n"
|
||||
markdown += "| " + " | ".join(["---"] * len(headers)) + " |\n"
|
||||
|
||||
for row in data_list:
|
||||
values = [str(row.get(header, "")) for header in headers]
|
||||
markdown += "| " + " | ".join(values) + " |\n"
|
||||
return markdown
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,8 @@
|
||||
import json
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import JsonResponse
|
||||
|
||||
from recruitment.utils import json_to_markdown_table
|
||||
from . import models
|
||||
from django.utils.translation import get_language
|
||||
from . import forms
|
||||
@ -19,6 +22,8 @@ from datastar_py.django import (
|
||||
ServerSentEventGenerator as SSE,
|
||||
read_signals,
|
||||
)
|
||||
# from rich import print
|
||||
from rich.markdown import CodeBlock
|
||||
|
||||
class JobListView(LoginRequiredMixin, ListView):
|
||||
model = models.JobPosting
|
||||
@ -41,7 +46,7 @@ class JobListView(LoginRequiredMixin, ListView):
|
||||
# Filter for non-staff users
|
||||
if not self.request.user.is_staff:
|
||||
queryset = queryset.filter(status='Published')
|
||||
|
||||
|
||||
status=self.request.GET.get('status')
|
||||
if status:
|
||||
queryset=queryset.filter(status=status)
|
||||
@ -49,7 +54,7 @@ class JobListView(LoginRequiredMixin, ListView):
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
|
||||
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['search_query'] = self.request.GET.get('search', '')
|
||||
context['lang'] = get_language()
|
||||
@ -201,6 +206,7 @@ def training_list(request):
|
||||
|
||||
|
||||
def candidate_detail(request, slug):
|
||||
from rich.json import JSON
|
||||
candidate = get_object_or_404(models.Candidate, slug=slug)
|
||||
try:
|
||||
parsed = ast.literal_eval(candidate.parsed_summary)
|
||||
@ -212,6 +218,8 @@ def candidate_detail(request, slug):
|
||||
if request.user.is_staff:
|
||||
stage_form = forms.CandidateStageForm(candidate=candidate)
|
||||
|
||||
# parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False)
|
||||
parsed = json_to_markdown_table([parsed])
|
||||
return render(request, 'recruitment/candidate_detail.html', {
|
||||
'candidate': candidate,
|
||||
'parsed': parsed,
|
||||
@ -219,7 +227,7 @@ def candidate_detail(request, slug):
|
||||
})
|
||||
|
||||
def candidate_update_stage(request, slug):
|
||||
"""Handle HTMX stage update requests"""
|
||||
"""Handle HTMX stage update requests"""
|
||||
try:
|
||||
if not request.user.is_staff:
|
||||
return render(request, 'recruitment/partials/error.html', {'error': 'Permission denied'}, status=403)
|
||||
@ -293,7 +301,7 @@ class TrainingListView(LoginRequiredMixin, ListView):
|
||||
template_name = 'recruitment/training_list.html'
|
||||
context_object_name = 'materials'
|
||||
paginate_by = 10
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
|
||||
|
||||
@ -1,100 +1,331 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n crispy_forms_tags %}
|
||||
{% load partials %}
|
||||
|
||||
{% block title %}Submissions for {{ template.name }}{% endblock %}
|
||||
{% block title %}Submissions for {{ template.name }} - ATS{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* ================================================= */
|
||||
/* UI Variables (Matching Form Templates List) */
|
||||
/* ================================================= */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-primary-text: #343a40;
|
||||
--kaauh-gray-light: #f8f9fa;
|
||||
}
|
||||
|
||||
/* --- Typography and Color Overrides --- */
|
||||
.text-primary { color: var(--kaauh-teal) !important; }
|
||||
|
||||
/* --- Button Base Styles (Matching Form Templates List) --- */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.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);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Secondary Button Style (for Edit/Preview) */
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Size Utilities (matching Bootstrap convention) */
|
||||
.btn-lg {
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
.btn-sm {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.3rem 0.6rem;
|
||||
}
|
||||
|
||||
/* --- Card and Layout Styles (Matching Form Templates List) --- */
|
||||
.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;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: var(--kaauh-teal-dark) !important;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
color: white !important;
|
||||
font-weight: 600;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 0.75rem 0.75rem 0 0;
|
||||
}
|
||||
|
||||
.card-header h1 {
|
||||
color: white !important;
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.card-header .fas {
|
||||
color: white !important;
|
||||
}
|
||||
.card-header .small {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
/* --- Table Styles --- */
|
||||
.table-responsive {
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.table thead th {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
border-color: var(--kaauh-border);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 1rem;
|
||||
}
|
||||
.table tbody td {
|
||||
padding: 1rem;
|
||||
vertical-align: middle;
|
||||
border-color: var(--kaauh-border);
|
||||
}
|
||||
.table tbody tr {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.table tbody tr:hover {
|
||||
background-color: var(--kaauh-gray-light);
|
||||
}
|
||||
|
||||
/* --- Pagination Styling (Matching Form Templates List) --- */
|
||||
.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;
|
||||
}
|
||||
.pagination-info {
|
||||
color: var(--kaauh-primary-text);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* --- Empty State Theming --- */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
color: var(--kaauh-primary-text);
|
||||
border: 2px dashed var(--kaauh-border);
|
||||
border-radius: 0.75rem;
|
||||
background-color: var(--kaauh-gray-light);
|
||||
}
|
||||
.empty-state i {
|
||||
font-size: 3.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--kaauh-teal-dark);
|
||||
}
|
||||
.empty-state .btn-main-action .fas {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
/* --- Breadcrumb --- */
|
||||
.breadcrumb {
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.breadcrumb-item a {
|
||||
color: var(--kaauh-teal-dark);
|
||||
text-decoration: none;
|
||||
}
|
||||
.breadcrumb-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.breadcrumb-item.active {
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<nav class="mb-6">
|
||||
<a href="{% url 'form_templates_list' %}" class="text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M9.707 16.707a1 1 0 01-1.414 0l-6-6a1 1 0 010-1.414l6-6a1 1 0 011.414 1.414L5.414 9H17a1 1 0 110 2H5.414l4.293 4.293a1 1 0 010 1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
Back to Form Templates
|
||||
</a>
|
||||
<div class="container py-4">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}">{% trans "Dashboard" %}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'form_templates_list' %}">{% trans "Form Templates" %}</a></li>
|
||||
<li class="breadcrumb-item active">{% trans "Submissions" %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h1 class="text-2xl font-bold text-gray-800 dark:text-white">Submissions for: <span class="text-blue-600 dark:text-blue-400">{{ template.name }}</span></h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Template ID: {{ template.id }}</p>
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h1 class="h3 mb-1 d-flex align-items-center">
|
||||
<i class="fas fa-file-alt me-2"></i>
|
||||
{% trans "Submissions for" %}: <span class="text-white ms-2">{{ template.name }}</span>
|
||||
</h1>
|
||||
<small class="text-white-50">Template ID: #{{ template.id }}</small>
|
||||
</div>
|
||||
<a href="{% url 'form_templates_list' %}" class="btn btn-outline-light btn-sm">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Templates" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="card-body">
|
||||
{% if page_obj.object_list %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Submission ID</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Applicant Name</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Applicant Email</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Submitted At</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-300 uppercase tracking-wider">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<div id="form-template-submissions-list">
|
||||
{# View Switcher #}
|
||||
{% include "includes/_list_view_switcher.html" with list_id="form-template-submissions-list" %}
|
||||
|
||||
{# Table View (Default) #}
|
||||
<div class="table-view active">
|
||||
<div class="table-responsive mb-4">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Submission ID" %}</th>
|
||||
<th scope="col">{% trans "Applicant Name" %}</th>
|
||||
<th scope="col">{% trans "Applicant Email" %}</th>
|
||||
<th scope="col">{% trans "Submitted At" %}</th>
|
||||
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for submission in page_obj %}
|
||||
<tr>
|
||||
<td class="fw-medium">{{ submission.id }}</td>
|
||||
<td>{{ submission.applicant_name|default:"N/A" }}</td>
|
||||
<td>{{ submission.applicant_email|default:"N/A" }}</td>
|
||||
<td>{{ submission.submitted_at|date:"M d, Y H:i" }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Card View #}
|
||||
<div class="card-view">
|
||||
<div class="row g-4">
|
||||
{% for submission in page_obj %}
|
||||
<tr class="hover:bg-gray-50 dark:hover:bg-gray-750">
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ submission.id }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">{{ submission.applicant_name|default:"N/A" }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">{{ submission.applicant_email|default:"N/A" }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-300">{{ submission.submitted_at|date:"Y-m-d H:i:s" }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 inline-flex items-center">
|
||||
View Details
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="h5 mb-2">{% trans "Submission" %} #{{ submission.id }}</h3>
|
||||
<small class="text-white-50">{{ template.name }}</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
<strong>{% trans "Applicant Name" %}:</strong> {{ submission.applicant_name|default:"N/A" }}<br>
|
||||
<strong>{% trans "Applicant Email" %}:</strong> {{ submission.applicant_email|default:"N/A" }}<br>
|
||||
<strong>{% trans "Submitted At" %}:</strong> {{ submission.submitted_at|date:"M d, Y H:i" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="btn btn-sm btn-outline-primary w-100">
|
||||
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if page_obj.has_other_pages %}
|
||||
<nav class="mt-6 flex items-center justify-between" aria-label="Pagination">
|
||||
<div class="hidden sm:block">
|
||||
<p class="text-sm text-gray-700 dark:text-gray-300">
|
||||
Showing <span class="font-medium">{{ page_obj.start_index }}</span> to <span class="font-medium">{{ page_obj.end_index }}</span> of <span class="font-medium">{{ page_obj.paginator.count }}</span> results.
|
||||
</p>
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center">
|
||||
<div class="pagination-info mb-3 mb-md-0">
|
||||
{% blocktrans with start=page_obj.start_index end=page_obj.end_index total=page_obj.paginator.count %}
|
||||
Showing {{ start }} to {{ end }} of {{ total }} results.
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
<div class="flex-1 flex justify-between sm:justify-end mt-4 sm:mt-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<a href="?page={{ page_obj.previous_page_number }}" class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1" aria-label="First">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="Previous">
|
||||
<span aria-hidden="true">‹</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
<li class="page-item active">
|
||||
<span class="page-link">
|
||||
{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<a href="?page={{ page_obj.next_page_number }}" class="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 text-sm font-medium rounded-md text-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
Next
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
|
||||
<span aria-hidden="true">›</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}" aria-label="Last">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="text-center py-12">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<h3 class="mt-2 text-lg font-medium text-gray-900 dark:text-white">No submissions found</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">There are no submissions for this form template yet.</p>
|
||||
<div class="mt-6">
|
||||
<a href="{% url 'form_templates_list' %}" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mr-2 -ml-1 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" />
|
||||
</svg>
|
||||
Back to Templates
|
||||
</a>
|
||||
</div>
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-inbox"></i>
|
||||
<h3 class="h5 mb-3">{% trans "No Submissions Found" %}</h3>
|
||||
<p class="text-muted mb-4">
|
||||
{% trans "There are no submissions for this form template yet." %}
|
||||
</p>
|
||||
<a href="{% url 'form_templates_list' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Templates" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -231,65 +231,118 @@
|
||||
</div>
|
||||
|
||||
{% if templates %}
|
||||
<div class="row g-4">
|
||||
{% for template in templates %}
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card template-card h-100">
|
||||
<div class="card-header ">
|
||||
<h3 class="h5 mb-2">{{ template.name }}</h3>
|
||||
<span><i class="fas fa-sync-alt me-1"></i> {{ template.job }}</span>
|
||||
<div class="d-flex justify-content-between text-muted small">
|
||||
<span><i class="fas fa-calendar me-1"></i> {{ template.created_at|date:"M d, Y" }}</span>
|
||||
<span><i class="fas fa-sync-alt me-1"></i> {{ template.updated_at|timesince }} {% trans "ago" %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div id="form-templates-list">
|
||||
{# View Switcher #}
|
||||
{% include "includes/_list_view_switcher.html" with list_id="form-templates-list" %}
|
||||
|
||||
{# Content area - includes stats and description #}
|
||||
<div class="flex-grow-1">
|
||||
<div class="row text-center mb-3">
|
||||
<div class="col-6">
|
||||
<div class="stat-value">{{ template.get_stage_count }}</div>
|
||||
<div class="stat-label">{% trans "Stages" %}</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-value">{{ template.get_field_count }}</div>
|
||||
<div class="stat-label">{% trans "Fields" %}</div>
|
||||
</div>
|
||||
{# Card View (Default) #}
|
||||
<div class="card-view active row g-4">
|
||||
{% for template in templates %}
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card template-card h-100">
|
||||
<div class="card-header ">
|
||||
<h3 class="h5 mb-2">{{ template.name }}</h3>
|
||||
<span><i class="fas fa-sync-alt me-1"></i> {{ template.job }}</span>
|
||||
<div class="d-flex justify-content-between text-muted small">
|
||||
<span><i class="fas fa-calendar me-1"></i> {{ template.created_at|date:"M d, Y" }}</span>
|
||||
<span><i class="fas fa-sync-alt me-1"></i> {{ template.updated_at|timesince }} {% trans "ago" %}</span>
|
||||
</div>
|
||||
<p class="card-text card-description small">
|
||||
{% if template.description %}
|
||||
{{ template.description|truncatewords:20 }}
|
||||
{% else %}
|
||||
<em class="text-muted">{% trans "No description provided" %}</em>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
|
||||
{# Action area - visually separated with pt-2 border-top #}
|
||||
<div class="mt-auto pt-2 border-top">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
{# Content area - includes stats and description #}
|
||||
<div class="flex-grow-1">
|
||||
<div class="row text-center mb-3">
|
||||
<div class="col-6">
|
||||
<div class="stat-value">{{ template.get_stage_count }}</div>
|
||||
<div class="stat-label">{% trans "Stages" %}</div>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<div class="stat-value">{{ template.get_field_count }}</div>
|
||||
<div class="stat-label">{% trans "Fields" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="card-text card-description small">
|
||||
{% if template.description %}
|
||||
{{ template.description|truncatewords:20 }}
|
||||
{% else %}
|
||||
<em class="text-muted">{% trans "No description provided" %}</em>
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<a href="{% url 'form_wizard' template.id %}" class="btn btn-outline-secondary btn-sm action-btn">
|
||||
<i class="fas fa-eye me-1"></i> {% trans "Preview" %}
|
||||
</a>
|
||||
<a href="{% url 'form_builder' template.id %}" class="btn btn-outline-secondary btn-sm action-btn">
|
||||
<i class="fas fa-edit me-1"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-secondary btn-sm action-btn">
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans "Submissions" %}
|
||||
</a>
|
||||
<button class="btn btn-outline-danger btn-sm action-btn delete"
|
||||
data-template-id="{{ template.id }}"
|
||||
data-template-name="{{ template.name }}">
|
||||
<i class="fas fa-trash me-1"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
{# Action area - visually separated with pt-2 border-top #}
|
||||
<div class="mt-auto pt-2 border-top">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
|
||||
<a href="{% url 'form_wizard' template.id %}" class="btn btn-outline-secondary btn-sm action-btn">
|
||||
<i class="fas fa-eye me-1"></i> {% trans "Preview" %}
|
||||
</a>
|
||||
<a href="{% url 'form_builder' template.id %}" class="btn btn-outline-secondary btn-sm action-btn">
|
||||
<i class="fas fa-edit me-1"></i> {% trans "Edit" %}
|
||||
</a>
|
||||
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-secondary btn-sm action-btn">
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans "Submissions" %}
|
||||
</a>
|
||||
<button class="btn btn-outline-danger btn-sm action-btn delete"
|
||||
data-template-id="{{ template.id }}"
|
||||
data-template-name="{{ template.name }}">
|
||||
<i class="fas fa-trash me-1"></i> {% trans "Delete" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Table View #}
|
||||
<div class="table-view">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Template Name" %}</th>
|
||||
<th scope="col">{% trans "Job" %}</th>
|
||||
<th scope="col">{% trans "Stages" %}</th>
|
||||
<th scope="col">{% trans "Fields" %}</th>
|
||||
<th scope="col">{% trans "Created" %}</th>
|
||||
<th scope="col">{% trans "Last Updated" %}</th>
|
||||
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for template in templates %}
|
||||
<tr>
|
||||
<td class="fw-medium">{{ template.name }}</td>
|
||||
<td>{{ template.job }}</td>
|
||||
<td>{{ template.get_stage_count }}</td>
|
||||
<td>{{ template.get_field_count }}</td>
|
||||
<td>{{ template.created_at|date:"M d, Y" }}</td>
|
||||
<td>{{ template.updated_at|date:"M d, Y" }}</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'form_wizard' template.id %}" class="btn btn-outline-primary" title="{% trans 'Preview' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'form_builder' template.id %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-info" title="{% trans 'Submissions' %}">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
</a>
|
||||
<button class="btn btn-outline-danger delete-btn" data-bs-toggle="modal" data-bs-target="#deleteModal" data-template-id="{{ template.id }}" data-template-name="{{ template.name }}" title="{% trans 'Delete' %}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% if templates.has_other_pages %}
|
||||
@ -371,4 +424,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
159
templates/includes/_list_view_switcher.html
Normal file
159
templates/includes/_list_view_switcher.html
Normal file
@ -0,0 +1,159 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm view-toggle active" data-view="table" data-list-id="{{ list_id }}">
|
||||
<i class="fas fa-table me-1"></i> Table
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm view-toggle" data-view="card" data-list-id="{{ list_id }}">
|
||||
<i class="fas fa-th me-1"></i> Card
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* View Toggle Styles */
|
||||
.view-toggle {
|
||||
border-radius: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
.view-toggle.active {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
}
|
||||
.view-toggle.active:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
/* Hide elements by default */
|
||||
.table-view,
|
||||
.card-view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show active view */
|
||||
.table-view.active,
|
||||
.card-view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Card View Styles */
|
||||
.card-view .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;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.card-view .card-header {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 0.75rem 0.75rem 0 0;
|
||||
}
|
||||
.card-view .card-body {
|
||||
padding: 1.25rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.card-view .card-title {
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.card-view .card-text {
|
||||
color: var(--kaauh-primary-text);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.card-view .card-footer {
|
||||
padding: 0.75rem 1.25rem;
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid var(--kaauh-border);
|
||||
border-radius: 0 0 0.75rem 0.75rem;
|
||||
}
|
||||
.card-view .btn-sm {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.3rem 0.6rem;
|
||||
}
|
||||
|
||||
/* Table View Styles */
|
||||
.table-view .table-responsive {
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.table-view .table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.table-view .table thead th {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
border-color: var(--kaauh-border);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 1rem;
|
||||
}
|
||||
.table-view .table tbody td {
|
||||
padding: 1rem;
|
||||
vertical-align: middle;
|
||||
border-color: var(--kaauh-border);
|
||||
}
|
||||
.table-view .table tbody tr {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.table-view .table tbody tr:hover {
|
||||
background-color: var(--kaauh-gray-light);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Get the list ID from the data attribute
|
||||
const listId = document.querySelector('.view-toggle').getAttribute('data-list-id');
|
||||
const listContainer = document.getElementById(listId);
|
||||
|
||||
// Get saved view preference from localStorage
|
||||
const savedView = localStorage.getItem(`list_view_${listId}`) || 'table';
|
||||
|
||||
// Set initial view
|
||||
setView(savedView);
|
||||
|
||||
// Add click event listeners to view toggle buttons
|
||||
document.querySelectorAll('.view-toggle').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const view = this.getAttribute('data-view');
|
||||
setView(view);
|
||||
});
|
||||
});
|
||||
|
||||
function setView(view) {
|
||||
// Update button states
|
||||
document.querySelectorAll('.view-toggle').forEach(button => {
|
||||
if (button.getAttribute('data-view') === view) {
|
||||
button.classList.add('active');
|
||||
} else {
|
||||
button.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Update view visibility
|
||||
const tableView = listContainer.querySelector('.table-view');
|
||||
const cardView = listContainer.querySelector('.card-view');
|
||||
|
||||
if (view === 'table') {
|
||||
tableView.classList.add('active');
|
||||
cardView.classList.remove('active');
|
||||
} else {
|
||||
tableView.classList.remove('active');
|
||||
cardView.classList.add('active');
|
||||
}
|
||||
|
||||
// Save preference to localStorage
|
||||
localStorage.setItem(`list_view_${listId}`, view);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -97,126 +97,177 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Candidates Table -->
|
||||
<!-- Candidates -->
|
||||
{% if candidates %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Applicants ({{ candidates.count }})</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select form-select-sm" style="width: auto;" onchange="window.location.href='?stage='+this.value+'&search={{ search_query }}'">
|
||||
<option value="">All Stages</option>
|
||||
<option value="Applied" {% if request.GET.stage == 'Applied' %}selected{% endif %}>Applied</option>
|
||||
<option value="Interview" {% if request.GET.stage == 'Interview' %}selected{% endif %}>Interview</option>
|
||||
<option value="Offer" {% if request.GET.stage == 'Offer' %}selected{% endif %}>Offer</option>
|
||||
</select>
|
||||
<div id="job-candidates-list">
|
||||
{# View Switcher #}
|
||||
{% include "includes/_list_view_switcher.html" with list_id="job-candidates-list" %}
|
||||
|
||||
{# Table View (Default) #}
|
||||
<div class="table-view active">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">Applicants ({{ candidates.count }})</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select form-select-sm" style="width: auto;" onchange="window.location.href='?stage='+this.value+'&search={{ search_query }}'">
|
||||
<option value="">All Stages</option>
|
||||
<option value="Applied" {% if request.GET.stage == 'Applied' %}selected{% endif %}>Applied</option>
|
||||
<option value="Interview" {% if request.GET.stage == 'Interview' %}selected{% endif %}>Interview</option>
|
||||
<option value="Offer" {% if request.GET.stage == 'Offer' %}selected{% endif %}>Offer</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<input type="checkbox" class="form-check-input" id="selectAll">
|
||||
</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Email</th>
|
||||
<th scope="col">Phone</th>
|
||||
<th scope="col">Stage</th>
|
||||
<th scope="col">Applied Date</th>
|
||||
<th scope="col" class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" class="form-check-input candidate-checkbox" value="{{ candidate.slug }}">
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<strong>{{ candidate.first_name }} {{ candidate.last_name }}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ candidate.email }}</td>
|
||||
<td>{{ candidate.phone|default:"-" }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if candidate.stage == 'Applied' %}primary{% elif candidate.stage == 'Interview' %}info{% elif candidate.stage == 'Offer' %}success{% else %}secondary{% endif %}">
|
||||
{{ candidate.stage }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ candidate.created_at|date:"M d, Y" }}</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-primary btn-sm" title="View">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-secondary btn-sm" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" title="Delete"
|
||||
data-bs-toggle="deleteModal"
|
||||
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Bulk Actions -->
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="me-3">Selected: <span id="selectedCount">0</span></span>
|
||||
<button class="btn btn-sm btn-outline-primary me-2" onclick="bulkAction('interview')"
|
||||
{% if not user.is_staff %}disabled{% endif %}>
|
||||
<i class="fas fa-comments"></i> Mark as Interview
|
||||
</button>
|
||||
<button class="btn btn-sm btn-success" onclick="bulkAction('offer')"
|
||||
{% if not user.is_staff %}disabled{% endif %}>
|
||||
<i class="fas fa-handshake"></i> Mark as Offer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active"><span class="page-link">{{ num }}</span></li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}{% if search_query %}&search={{ search_query }}{% endif %}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<input type="checkbox" class="form-check-input" id="selectAll">
|
||||
</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Email</th>
|
||||
<th scope="col">Phone</th>
|
||||
<th scope="col">Stage</th>
|
||||
<th scope="col">Applied Date</th>
|
||||
<th scope="col" class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" class="form-check-input candidate-checkbox" value="{{ candidate.slug }}">
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<strong>{{ candidate.first_name }} {{ candidate.last_name }}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ candidate.email }}</td>
|
||||
<td>{{ candidate.phone|default:"-" }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if candidate.stage == 'Applied' %}primary{% elif candidate.stage == 'Interview' %}info{% elif candidate.stage == 'Offer' %}success{% else %}secondary{% endif %}">
|
||||
{{ candidate.stage }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ candidate.created_at|date:"M d, Y" }}</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-primary btn-sm" title="View">
|
||||
<i class="fas fa-eye"></i>
|
||||
|
||||
{# Card View #}
|
||||
<div class="card-view">
|
||||
<div class="row g-4">
|
||||
{% for candidate in candidates %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="h5 mb-1">{{ candidate.first_name }} {{ candidate.last_name }}</h5>
|
||||
<small class="text-white-50">{{ candidate.email }}</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
<strong>{% trans "Phone" %}:</strong> {{ candidate.phone|default:"N/A" }}<br>
|
||||
<strong>{% trans "Stage" %}:</strong> <span class="badge bg-{% if candidate.stage == 'Applied' %}primary{% elif candidate.stage == 'Interview' %}info{% elif candidate.stage == 'Offer' %}success{% else %}secondary{% endif %}">{{ candidate.stage }}</span><br>
|
||||
<strong>{% trans "Applied Date" %}:</strong> {{ candidate.created_at|date:"M d, Y" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-sm btn-outline-primary w-100">
|
||||
<i class="fas fa-eye"></i> {% trans "View" %}
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-secondary btn-sm" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" title="Delete"
|
||||
data-bs-toggle="deleteModal"
|
||||
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<div class="btn-group w-100" role="group">
|
||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-sm btn-outline-secondary" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" title="Delete"
|
||||
data-bs-toggle="deleteModal"
|
||||
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Bulk Actions -->
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="me-3">Selected: <span id="selectedCount">0</span></span>
|
||||
<button class="btn btn-sm btn-outline-primary me-2" onclick="bulkAction('interview')"
|
||||
{% if not user.is_staff %}disabled{% endif %}>
|
||||
<i class="fas fa-comments"></i> Mark as Interview
|
||||
</button>
|
||||
<button class="btn btn-sm btn-success" onclick="bulkAction('offer')"
|
||||
{% if not user.is_staff %}disabled{% endif %}>
|
||||
<i class="fas fa-handshake"></i> Mark as Offer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active"><span class="page-link">{{ num }}</span></li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}{% if search_query %}&search={{ search_query }}{% endif %}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.7px;
|
||||
}
|
||||
|
||||
|
||||
/* Mapped color classes for status badges */
|
||||
.bg-success { background-color: var(--kaauh-teal) !important; }
|
||||
.bg-warning { background-color: #ffc107 !important; }
|
||||
@ -58,7 +58,7 @@
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
|
||||
/* Standard Card Header (used for single cards or fallback) */
|
||||
.card-header {
|
||||
font-weight: 600;
|
||||
@ -70,7 +70,7 @@
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
|
||||
|
||||
.card-footer {
|
||||
padding: 1rem 1.25rem;
|
||||
background-color: #f8f9fa;
|
||||
@ -103,7 +103,7 @@
|
||||
border-bottom: 3px solid var(--kaauh-teal);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
/* ==================================== */
|
||||
/* RIGHT COLUMN TABS STYLING (IMPROVED) */
|
||||
/* ==================================== */
|
||||
@ -122,13 +122,13 @@
|
||||
display: flex; /* Ensure the nav-items take up equal space */
|
||||
}
|
||||
.right-column-tabs .nav-item {
|
||||
flex-grow: 1;
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.right-column-tabs .nav-link {
|
||||
/* Base style for all right column tabs */
|
||||
padding: 0.9rem 1rem; /* Slightly larger padding for better spacing */
|
||||
font-size: 0.95rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
border-radius: 0;
|
||||
@ -182,7 +182,7 @@
|
||||
.applicant-stats .stat-item small {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
|
||||
/* Primary Color Overrides */
|
||||
.text-primary { color: var(--kaauh-teal) !important; }
|
||||
.text-info { color: #17a2b8 !important; }
|
||||
@ -219,14 +219,14 @@
|
||||
color: white;
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
|
||||
/* Specific styling for the deadline box */
|
||||
.deadline-box {
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
|
||||
/* Table styling for the Applicant preview */
|
||||
.table-applicants tbody tr:hover {
|
||||
background-color: #f3f9f9; /* Light teal hover for rows */
|
||||
@ -242,19 +242,20 @@
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row g-4">
|
||||
|
||||
|
||||
{# LEFT COLUMN: JOB DETAILS WITH TABS #}
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm no-hover">
|
||||
|
||||
|
||||
{# HEADER SECTION #}
|
||||
<div class="job-header-card d-flex justify-content-between align-items-center flex-wrap">
|
||||
<h2>{{job}}</h2>
|
||||
<span class="badge bg-{{ job.status|lower|striptags|yesno:'success,warning,secondary,danger' }} status-badge">
|
||||
<button class="badge bg-success status-badge">
|
||||
{% include "icons/edit.html" %}
|
||||
{{ job.get_status_display }}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
{# LEFT TABS NAVIGATION #}
|
||||
<ul class="nav nav-tabs" id="jobTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
@ -278,7 +279,7 @@
|
||||
|
||||
<div class="card-body">
|
||||
<div class="tab-content" id="jobTabsContent">
|
||||
|
||||
|
||||
{# TAB 1 CONTENT: CORE DETAILS #}
|
||||
<div class="tab-pane fade show active" id="details" role="tabpanel" aria-labelledby="details-tab">
|
||||
<h5 class="text-muted mb-3">{% trans "Administrative & Location" %}</h5>
|
||||
@ -365,16 +366,16 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{# FOOTER ACTIONS #}
|
||||
<div class="card-footer d-flex flex-wrap gap-2">
|
||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-edit"></i> {% trans "Edit Job" %}
|
||||
</a>
|
||||
|
||||
|
||||
{% if job.application_url %}
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#myModalForm">
|
||||
<i class="fas fa-image me-1"></i> {% trans "Upload Image for Post" %}
|
||||
@ -387,7 +388,7 @@
|
||||
{# RIGHT COLUMN: TABBED CARDS #}
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm no-hover">
|
||||
|
||||
|
||||
{# RIGHT TABS NAVIGATION #}
|
||||
<ul class="nav nav-tabs right-column-tabs" id="rightJobTabs" role="tablist">
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
@ -408,7 +409,7 @@
|
||||
</ul>
|
||||
|
||||
<div class="tab-content mx-2 my-3" id="rightJobTabsContent">
|
||||
|
||||
|
||||
{# TAB 1: APPLICANTS CONTENT #}
|
||||
<div class="tab-pane fade show active" id="applicants-pane" role="tabpanel" aria-labelledby="applicants-tab">
|
||||
<h5 class="mb-3">{% trans "Candidates" %} (<span id="total_candidates">{{ total_candidates }}</span>)</h5>
|
||||
@ -476,7 +477,7 @@
|
||||
|
||||
{# TAB 2: MANAGEMENT (LinkedIn & Forms) CONTENT #}
|
||||
<div class="tab-pane fade" id="manage-pane" role="tabpanel" aria-labelledby="manage-tab">
|
||||
|
||||
|
||||
{# LinkedIn Integration (Content from old card) #}
|
||||
<h5 class="mb-3"><i class="fab fa-linkedin me-2 text-info"></i>{% trans "LinkedIn Integration" %}</h5>
|
||||
<div class="mb-4">
|
||||
@ -533,13 +534,13 @@
|
||||
<a href="" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-list-alt me-1"></i> {% trans "View All Existing Forms" %}
|
||||
</a>
|
||||
|
||||
|
||||
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus"></i> {% trans "Create Candidate" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{# TAB 3: INTERNAL INFO CONTENT #}
|
||||
<div class="tab-pane fade" id="internal-pane" role="tabpanel" aria-labelledby="internal-tab">
|
||||
<h5 class="mb-3"><i class="fas fa-info-circle me-2 text-secondary"></i>{% trans "Internal Information" %}</h5>
|
||||
@ -551,7 +552,7 @@
|
||||
<p class="mb-0"><strong>{% trans "Reports To:" %}</strong> {{ job.reporting_to }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{% url 'job_list' %}" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to Jobs" %}
|
||||
@ -560,7 +561,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Job Postings - University ATS{% endblock %}
|
||||
|
||||
@ -134,7 +134,7 @@
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
{% url 'job_list' as job_list_url %}
|
||||
|
||||
|
||||
<form method="GET" class="row g-3 align-items-end" >
|
||||
|
||||
<div class="col-md-3">
|
||||
@ -159,50 +159,97 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if page_obj %}
|
||||
<div class="row">
|
||||
{% for job in page_obj %}
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card job-card h-100">
|
||||
<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">{{ job.title }}</h5>
|
||||
<span class="badge bg-{{ job.status|lower|striptags|yesno:'active,draft,closed,archived' }} status-badge">
|
||||
{{ job.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
<div id="job-list">
|
||||
{# View Switcher #}
|
||||
{% include "includes/_list_view_switcher.html" with list_id="job-list" %}
|
||||
|
||||
<p class="card-text text-muted small">
|
||||
<i class="fas fa-building"></i> {{ job.department|default:"No Department" }}<br>
|
||||
<i class="fas fa-map-marker-alt"></i> {{ job.get_location_display }}<br>
|
||||
<i class="fas fa-clock"></i> {{ job.get_job_type_display }}<br>
|
||||
<i class="fas fa-briefcase"></i> {{ job.get_source }}
|
||||
</p>
|
||||
|
||||
<div class="mt-auto pt-2 border-top">
|
||||
{% if job.posted_to_linkedin %}
|
||||
<span class="badge bg-info mb-2">
|
||||
<i class="fab fa-linkedin me-1"></i> Posted to LinkedIn
|
||||
{# Card View (Default) #}
|
||||
<div class="card-view active row">
|
||||
{% for job in page_obj %}
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card job-card h-100">
|
||||
<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">{{ job.title }}</h5>
|
||||
<span class="badge bg-{{ job.status|lower|striptags|yesno:'active,draft,closed,archived' }} status-badge">
|
||||
{{ job.get_status_display }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-sm btn-main-action">
|
||||
<i class="fas fa-eye"></i> View
|
||||
</a>
|
||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
<p class="card-text text-muted small">
|
||||
<i class="fas fa-building"></i> {{ job.department|default:"No Department" }}<br>
|
||||
<i class="fas fa-map-marker-alt"></i> {{ job.get_location_display }}<br>
|
||||
<i class="fas fa-clock"></i> {{ job.get_job_type_display }}<br>
|
||||
<i class="fas fa-briefcase"></i> {{ job.get_source }}
|
||||
</p>
|
||||
|
||||
<div class="mt-auto pt-2 border-top">
|
||||
{% if job.posted_to_linkedin %}
|
||||
<span class="badge bg-info mb-2">
|
||||
<i class="fab fa-linkedin me-1"></i> Posted to LinkedIn
|
||||
</span>
|
||||
{% endif %}
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-sm btn-main-action">
|
||||
<i class="fas fa-eye"></i> View
|
||||
</a>
|
||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-sm btn-outline-secondary">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Table View #}
|
||||
<div class="table-view">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Job Title" %}</th>
|
||||
<th scope="col">{% trans "Department" %}</th>
|
||||
<th scope="col">{% trans "Location" %}</th>
|
||||
<th scope="col">{% trans "Job Type" %}</th>
|
||||
<th scope="col">{% trans "Status" %}</th>
|
||||
<th scope="col">{% trans "Source" %}</th>
|
||||
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for job in page_obj %}
|
||||
<tr>
|
||||
<td class="fw-medium">{{ job.title }}</td>
|
||||
<td>{{ job.department|default:"N/A" }}</td>
|
||||
<td>{{ job.get_location_display }}</td>
|
||||
<td>{{ job.get_job_type_display }}</td>
|
||||
<td><span class="badge bg-{{ job.status|lower|striptags|yesno:'active,draft,closed,archived' }} status-badge">{{ job.get_status_display }}</span></td>
|
||||
<td>{{ job.get_source }}</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-primary" title="View">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-outline-secondary" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if page_obj.has_other_pages %}
|
||||
@ -245,4 +292,4 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -33,19 +33,19 @@
|
||||
border: 1px solid transparent;
|
||||
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||||
/* FIX: Remove link underline for anchor tags used as buttons */
|
||||
text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
/* Small Button Size */
|
||||
.btn-sm {
|
||||
padding: 0.3rem 0.6rem;
|
||||
padding: 0.3rem 0.6rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
|
||||
/* Main Action Button (Create Meeting) */
|
||||
.btn-main-action {
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
@ -57,7 +57,7 @@
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
text-decoration: none; /* Ensure no hover underline if base fails */
|
||||
}
|
||||
|
||||
|
||||
/* Outline Primary (View/Join buttons) */
|
||||
.btn-kaats-outline-primary {
|
||||
color: var(--kaauh-teal);
|
||||
@ -118,7 +118,7 @@
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
|
||||
/* TOPIC AND DETAILS STYLES */
|
||||
.meeting-topic {
|
||||
font-size: 1.15rem;
|
||||
@ -169,7 +169,7 @@
|
||||
background: var(--kaauh-danger) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
|
||||
/* ACTION AREA STYLES */
|
||||
.actions {
|
||||
margin-top: 1rem;
|
||||
@ -177,7 +177,7 @@
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
/* Header Styling */
|
||||
.card-header h1 {
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -193,7 +193,7 @@
|
||||
.text-muted.mb-3 {
|
||||
color: var(--kaauh-teal-dark) !important;
|
||||
}
|
||||
|
||||
|
||||
/* Pagination Link Styling */
|
||||
.pagination .page-item .page-link {
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -246,75 +246,138 @@
|
||||
</div>
|
||||
|
||||
{% if meetings %}
|
||||
<div class="meetings-grid">
|
||||
{% for meeting in meetings %}
|
||||
<div class="meeting-card">
|
||||
<div class="meeting-topic">{{ meeting.topic }}</div>
|
||||
<div id="meetings-list">
|
||||
{# View Switcher #}
|
||||
{% include "includes/_list_view_switcher.html" with list_id="meetings-list" %}
|
||||
|
||||
<div class="meeting-detail">
|
||||
<div class="detail-label">{% trans "ID" %}:</div>
|
||||
<div class="detail-value">{{ meeting.meeting_id|default:meeting.id }}</div>
|
||||
</div>
|
||||
{# Card View (Default) #}
|
||||
<div class="card-view active">
|
||||
<div class="meetings-grid">
|
||||
{% for meeting in meetings %}
|
||||
<div class="meeting-card">
|
||||
<div class="meeting-topic">{{ meeting.topic }}</div>
|
||||
|
||||
<div class="meeting-detail">
|
||||
<div class="detail-label">{% trans "Start Time" %}:</div>
|
||||
<div class="detail-value">{{ meeting.start_time|date:"M d, Y H:i" }}</div>
|
||||
</div>
|
||||
<div class="meeting-detail">
|
||||
<div class="detail-label">{% trans "ID" %}:</div>
|
||||
<div class="detail-value">{{ meeting.meeting_id|default:meeting.id }}</div>
|
||||
</div>
|
||||
|
||||
<div class="meeting-detail">
|
||||
<div class="detail-label">{% trans "Duration" %}:</div>
|
||||
<div class="detail-value">{{ meeting.duration }} minutes</div>
|
||||
</div>
|
||||
<div class="meeting-detail">
|
||||
<div class="detail-label">{% trans "Start Time" %}:</div>
|
||||
<div class="detail-value">{{ meeting.start_time|date:"M d, Y H:i" }}</div>
|
||||
</div>
|
||||
|
||||
<div class="meeting-detail">
|
||||
<div class="detail-label">{% trans "Status" %}:</div>
|
||||
<div class="detail-value">
|
||||
<span class="status-badge {% if meeting.status == 'waiting' %}bg-warning{% elif meeting.status == 'started' %}bg-success{% elif meeting.status == 'ended' %}bg-danger{% endif %}">
|
||||
{% if meeting.status == 'waiting' %}
|
||||
{% trans "Waiting" %}
|
||||
{% elif meeting.status == 'started' %}
|
||||
{% trans "Started" %}
|
||||
{% elif meeting.status == 'ended' %}
|
||||
{% trans "Ended" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meeting-detail">
|
||||
<div class="detail-label">{% trans "Duration" %}:</div>
|
||||
<div class="detail-value">{{ meeting.duration }} minutes</div>
|
||||
</div>
|
||||
|
||||
{% if meeting.join_url %}
|
||||
<div class="meeting-detail">
|
||||
<div class="detail-label">{% trans "Join URL" %}:</div>
|
||||
<div class="detail-value">
|
||||
<a href="{{ meeting.join_url }}" target="_blank" class="btn-base btn-kaats-outline-primary btn-sm">
|
||||
{% trans "Join Meeting" %}
|
||||
<div class="meeting-detail">
|
||||
<div class="detail-label">{% trans "Status" %}:</div>
|
||||
<div class="detail-value">
|
||||
<span class="status-badge {% if meeting.status == 'waiting' %}bg-warning{% elif meeting.status == 'started' %}bg-success{% elif meeting.status == 'ended' %}bg-danger{% endif %}">
|
||||
{% if meeting.status == 'waiting' %}
|
||||
{% trans "Waiting" %}
|
||||
{% elif meeting.status == 'started' %}
|
||||
{% trans "Started" %}
|
||||
{% elif meeting.status == 'ended' %}
|
||||
{% trans "Ended" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if meeting.join_url %}
|
||||
<div class="meeting-detail">
|
||||
<div class="detail-label">{% trans "Join URL" %}:</div>
|
||||
<div class="detail-value">
|
||||
<a href="{{ meeting.join_url }}" target="_blank" class="btn-base btn-kaats-outline-primary btn-sm">
|
||||
{% trans "Join Meeting" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="actions">
|
||||
<a href="{% url 'meeting_details' meeting.pk %}" class="btn-base btn-kaats-outline-primary btn-sm" title="{% trans 'View' %}">
|
||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0 8.268-2.943-9.542-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{% url 'update_meeting' meeting.pk %}" class="btn-base btn-kaats-outline-secondary btn-sm" title="{% trans 'Update' %}">
|
||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<button type="button" class="btn-base btn-kaats-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="deleteModal"
|
||||
data-delete-url="{% url 'delete_meeting' meeting.pk %}"
|
||||
data-item-name="{{ meeting.topic }}">
|
||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="actions">
|
||||
<a href="{% url 'meeting_details' meeting.pk %}" class="btn-base btn-kaats-outline-primary btn-sm" title="{% trans 'View' %}">
|
||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0 8.268-2.943-9.542-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="{% url 'update_meeting' meeting.pk %}" class="btn-base btn-kaats-outline-secondary btn-sm" title="{% trans 'Update' %}">
|
||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</a>
|
||||
<button type="button" class="btn-base btn-kaats-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="deleteModal"
|
||||
data-delete-url="{% url 'delete_meeting' meeting.pk %}"
|
||||
data-item-name="{{ meeting.topic }}">
|
||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{# Table View #}
|
||||
<div class="table-view">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Topic" %}</th>
|
||||
<th scope="col">{% trans "ID" %}</th>
|
||||
<th scope="col">{% trans "Start Time" %}</th>
|
||||
<th scope="col">{% trans "Duration" %}</th>
|
||||
<th scope="col">{% trans "Status" %}</th>
|
||||
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for meeting in meetings %}
|
||||
<tr>
|
||||
<td><strong>{{ meeting.topic }}</strong></td>
|
||||
<td>{{ meeting.meeting_id|default:meeting.id }}</td>
|
||||
<td>{{ meeting.start_time|date:"M d, Y H:i" }}</td>
|
||||
<td>{{ meeting.duration }} minutes</td>
|
||||
<td>
|
||||
<span class="status-badge {% if meeting.status == 'waiting' %}bg-warning{% elif meeting.status == 'started' %}bg-success{% elif meeting.status == 'ended' %}bg-danger{% endif %}">
|
||||
{% if meeting.status == 'waiting' %}
|
||||
{% trans "Waiting" %}
|
||||
{% elif meeting.status == 'started' %}
|
||||
{% trans "Started" %}
|
||||
{% elif meeting.status == 'ended' %}
|
||||
{% trans "Ended" %}
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'meeting_details' meeting.pk %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'update_meeting' meeting.pk %}" class="btn btn-outline-secondary" title="{% trans 'Update' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="deleteModal"
|
||||
data-delete-url="{% url 'delete_meeting' meeting.pk %}"
|
||||
data-item-name="{{ meeting.topic }}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_paginated %}
|
||||
@ -365,4 +428,4 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -143,54 +143,106 @@
|
||||
</div>
|
||||
|
||||
{% if candidates %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 20%;">{% trans "Name" %}</th>
|
||||
<th scope="col" style="width: 20%;">{% trans "Email" %}</th>
|
||||
<th scope="col" style="width: 15%;">{% trans "Phone" %}</th>
|
||||
<th scope="col" style="width: 15%;">{% trans "Job" %}</th>
|
||||
<th scope="col" style="width: 10%;">{% trans "Stage" %}</th>
|
||||
<th scope="col" style="width: 10%;">{% trans "Created" %}</th>
|
||||
<th scope="col" style="width: 10%;" class="text-center">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div id="candidate-list">
|
||||
{# View Switcher #}
|
||||
{% include "includes/_list_view_switcher.html" with list_id="candidate-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: 20%;">{% trans "Name" %}</th>
|
||||
<th scope="col" style="width: 20%;">{% trans "Email" %}</th>
|
||||
<th scope="col" style="width: 15%;">{% trans "Phone" %}</th>
|
||||
<th scope="col" style="width: 15%;">{% trans "Job" %}</th>
|
||||
<th scope="col" style="width: 10%;">{% trans "Stage" %}</th>
|
||||
<th scope="col" style="width: 10%;">{% trans "Created" %}</th>
|
||||
<th scope="col" style="width: 10%;" class="text-center">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
<tr>
|
||||
<td><strong>{{ candidate.name }}</strong></td>
|
||||
<td>{{ candidate.email }}</td>
|
||||
<td>{{ candidate.phone }}</td>
|
||||
<td> <span class="badge bg-primary">{{ candidate.job.title }}</span></td>
|
||||
<td>
|
||||
<span class="badge bg-primary">
|
||||
{{ candidate.stage }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ candidate.created_at|date:"M d, Y" }}</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-primary" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="deleteModal"
|
||||
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.name }}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Card View #}
|
||||
<div class="card-view">
|
||||
<div class="row g-4">
|
||||
{% for candidate in candidates %}
|
||||
<tr>
|
||||
<td><strong>{{ candidate.name }}</strong></td>
|
||||
<td>{{ candidate.email }}</td>
|
||||
<td>{{ candidate.phone }}</td>
|
||||
<td> <span class="badge bg-primary">{{ candidate.job.title }}</span></td>
|
||||
<td>
|
||||
<span class="badge bg-primary">
|
||||
{{ candidate.stage }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ candidate.created_at|date:"M d, Y" }}</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-primary" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="deleteModal"
|
||||
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.name }}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="h5 mb-1">{{ candidate.name }}</h5>
|
||||
<small class="text-white-50">{{ candidate.email }}</small>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
<strong>{% trans "Phone" %}:</strong> {{ candidate.phone|default:"N/A" }}<br>
|
||||
<strong>{% trans "Job" %}:</strong> <span class="badge bg-primary">{{ candidate.job.title }}</span><br>
|
||||
<strong>{% trans "Stage" %}:</strong> <span class="badge bg-primary">{{ candidate.stage }}</span><br>
|
||||
<strong>{% trans "Created" %}:</strong> {{ candidate.created_at|date:"M d, Y" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-sm btn-outline-primary w-100">
|
||||
<i class="fas fa-eye"></i> {% trans "View" %}
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<div class="btn-group w-100" role="group">
|
||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-sm btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="deleteModal"
|
||||
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.name }}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_paginated %}
|
||||
@ -242,4 +294,4 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -37,7 +37,7 @@
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
|
||||
/* Outlined Button Styles for Table Actions */
|
||||
.btn-outline-primary {
|
||||
color: var(--kaauh-teal);
|
||||
@ -66,7 +66,7 @@
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
|
||||
/* Colored Header Card */
|
||||
.list-header-card {
|
||||
background: linear-gradient(135deg, var(--kaauh-teal), #004d57);
|
||||
@ -101,7 +101,7 @@
|
||||
.btn-group .btn-sm {
|
||||
padding: 0.35rem 0.6rem;
|
||||
}
|
||||
|
||||
|
||||
/* Pagination Styling */
|
||||
.pagination .page-link {
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -118,7 +118,7 @@
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="card shadow-sm">
|
||||
|
||||
|
||||
<div class="list-header-card">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap">
|
||||
<h1 class="h3 mb-0">
|
||||
@ -126,7 +126,7 @@
|
||||
{% trans "Training Materials" %}
|
||||
</h1>
|
||||
<div class="d-flex gap-3 align-items-center mt-2 mt-md-0">
|
||||
|
||||
|
||||
<div class="order-3 order-md-1">
|
||||
{% include "includes/search_form.html" with search_query=search_query %}
|
||||
</div>
|
||||
@ -140,47 +140,96 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% if materials %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Title" %}</th>
|
||||
<th scope="col">{% trans "Created By" %}</th>
|
||||
<th scope="col">{% trans "Created" %}</th>
|
||||
<th scope="col" class="text-center">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<div id="training-materials-list">
|
||||
{# View Switcher #}
|
||||
{% include "includes/_list_view_switcher.html" with list_id="training-materials-list" %}
|
||||
|
||||
{# Table View (Default) #}
|
||||
<div class="table-view active">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Title" %}</th>
|
||||
<th scope="col">{% trans "Created By" %}</th>
|
||||
<th scope="col">{% trans "Created" %}</th>
|
||||
<th scope="col" class="text-center">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for material in materials %}
|
||||
<tr>
|
||||
<td><strong class="text-primary">{{ material.title }}</strong></td>
|
||||
<td>{{ material.created_by.username|default:"Anonymous" }}</td>
|
||||
<td>{{ material.created_at|date:"M d, Y" }}</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'training_detail' material.pk %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if user.is_authenticated and material.created_by == user %}
|
||||
<a href="{% url 'training_update' material.pk %}" 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="deleteModal"
|
||||
data-delete-url="{% url 'training_delete' material.pk %}"
|
||||
data-item-name="{{ material.title }}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Card View #}
|
||||
<div class="card-view">
|
||||
<div class="row g-4">
|
||||
{% for material in materials %}
|
||||
<tr>
|
||||
<td><strong class="text-primary">{{ material.title }}</strong></td>
|
||||
<td>{{ material.created_by.username|default:"Anonymous" }}</td>
|
||||
<td>{{ material.created_at|date:"M d, Y" }}</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'training_detail' material.pk %}" class="btn btn-outline-primary" title="{% trans 'View' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if user.is_authenticated and material.created_by == user %}
|
||||
<a href="{% url 'training_update' material.pk %}" 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="deleteModal"
|
||||
data-delete-url="{% url 'training_delete' material.pk %}"
|
||||
data-item-name="{{ material.title }}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h5 class="h5 mb-1">{{ material.title }}</h5>
|
||||
<small class="text-white-50">{{ material.created_by.username|default:"Anonymous" }}</small>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<div class="card-body">
|
||||
<p class="card-text">
|
||||
<strong>{% trans "Created" %}:</strong> {{ material.created_at|date:"M d, Y" }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'training_detail' material.pk %}" class="btn btn-sm btn-outline-primary w-100">
|
||||
<i class="fas fa-eye"></i> {% trans "View" %}
|
||||
</a>
|
||||
{% if user.is_authenticated and material.created_by == user %}
|
||||
<div class="btn-group w-100" role="group">
|
||||
<a href="{% url 'training_update' material.pk %}" class="btn btn-sm btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="deleteModal"
|
||||
data-delete-url="{% url 'training_delete' material.pk %}"
|
||||
data-item-name="{{ material.title }}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if is_paginated %}
|
||||
@ -194,7 +243,7 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active"><span class="page-link">{{ num }}</span></li>
|
||||
@ -204,7 +253,7 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
|
||||
@ -216,7 +265,7 @@
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-book-open fa-3x text-muted mb-3"></i>
|
||||
@ -232,4 +281,4 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user