update on the models and forms

This commit is contained in:
ismail 2025-10-09 16:57:53 +03:00
parent 579cc085e2
commit a23c96cc17
87 changed files with 3045 additions and 2136 deletions

View File

@ -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/'

Binary file not shown.

View File

@ -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']

View File

@ -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)

View File

@ -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,
},
),
]

View File

@ -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,
),
]

View File

@ -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),
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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)),
],
),
]

View File

@ -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)")
)

View File

@ -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}")

View File

@ -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

View File

@ -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()

View File

@ -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">&laquo;</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">&lsaquo;</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">&rsaquo;</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">&raquo;</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>

View File

@ -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

View 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>

View File

@ -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">&laquo;</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">&raquo;</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">&laquo;</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">&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
{% else %}

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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 %}