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

@ -196,7 +196,6 @@ SOCIALACCOUNT_PROVIDERS = {
} }
} }
ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A' ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A'
ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA' ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA'
ZOOM_CLIENT_SECRET = 'rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L' ZOOM_CLIENT_SECRET = 'rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L'
@ -215,7 +214,6 @@ CELERY_TASK_SERIALIZER = 'json'
CELERY_RESULT_SERIALIZER = 'json' CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'UTC' CELERY_TIMEZONE = 'UTC'
LINKEDIN_CLIENT_ID = '867jwsiyem1504' LINKEDIN_CLIENT_ID = '867jwsiyem1504'
LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw==' LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='
LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/' LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/'

Binary file not shown.

View File

@ -470,3 +470,17 @@ class InterviewScheduleForm(forms.ModelForm):
working_days = self.cleaned_data.get('working_days') working_days = self.cleaned_data.get('working_days')
# Convert string values to integers # 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" LINKEDIN_API_BASE = "https://api.linkedin.com/v2"
class LinkedInService: class LinkedInService:
def __init__(self, access_token): def __init__(self, access_token):
self.headers = { self.headers = {
'Authorization': f'Bearer {access_token}', "Authorization": f"Bearer {access_token}",
'X-Restli-Protocol-Version': '2.0.0', "X-Restli-Protocol-Version": "2.0.0",
'Content-Type': 'application/json' "Content-Type": "application/json",
} }
def post_job(self, organization_id, job_data): def post_job(self, organization_id, job_data):
@ -17,10 +18,10 @@ class LinkedInService:
"lifecycleState": "PUBLISHED", "lifecycleState": "PUBLISHED",
"specificContent": { "specificContent": {
"com.linkedin.ugc.ShareContent": { "com.linkedin.ugc.ShareContent": {
"shareCommentary": {"text": job_data['text']}, "shareCommentary": {"text": job_data["text"]},
"shareMediaCategory": "NONE" "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.core.validators
import django.db.models.deletion import django.db.models.deletion
@ -213,6 +213,7 @@ class Migration(migrations.Migration):
('last_name', models.CharField(max_length=255, verbose_name='Last Name')), ('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
('email', models.EmailField(max_length=254, verbose_name='Email')), ('email', models.EmailField(max_length=254, verbose_name='Email')),
('phone', models.CharField(max_length=20, verbose_name='Phone')), ('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')), ('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')),
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')), ('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
('applied', models.BooleanField(default=False, verbose_name='Applied')), ('applied', models.BooleanField(default=False, verbose_name='Applied')),
@ -311,6 +312,14 @@ class Migration(migrations.Migration):
name='job', name='job',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_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( migrations.CreateModel(
name='SharedFormTemplate', name='SharedFormTemplate',
fields=[ fields=[
@ -374,6 +383,7 @@ class Migration(migrations.Migration):
name='ScheduledInterview', name='ScheduledInterview',
fields=[ fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('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_date', models.DateField(verbose_name='Interview Date')),
('interview_time', models.TimeField(verbose_name='Interview Time')), ('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)), ('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')), ('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')), ('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): class Profile(models.Model):
profile_image=models.ImageField(null=True,blank=True,upload_to='profile_pic/') profile_image = models.ImageField(null=True, blank=True, upload_to="profile_pic/")
user=models.OneToOneField(User,on_delete=models.CASCADE,related_name='profile') user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
class Base(models.Model): class Base(models.Model):
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at")) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at")) updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at"))
@ -105,8 +107,9 @@ class JobPosting(Base):
# Status Fields # Status Fields
STATUS_CHOICES = [ STATUS_CHOICES = [
("DRAFT", "Draft"), ("DRAFT", "Draft"),
("PUBLISHED", "Published"), ("ACTIVE", "Active"),
("CLOSED", "Closed"), ("CLOSED", "Closed"),
("CANCELLED", "Cancelled"),
("ARCHIVED", "Archived"), ("ARCHIVED", "Archived"),
] ]
status = models.CharField( status = models.CharField(
@ -165,6 +168,18 @@ class JobPosting(Base):
"External agency responsible for sourcing candidates for this role" "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: class Meta:
ordering = ["-created_at"] ordering = ["-created_at"]
@ -197,7 +212,7 @@ class JobPosting(Base):
else: else:
next_num = 1 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) super().save(*args, **kwargs)
@ -262,6 +277,9 @@ class Candidate(Base):
phone = models.CharField(max_length=20, verbose_name=_("Phone")) 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")) 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")) parsed_summary = models.TextField(blank=True, verbose_name=_("Parsed Summary"))
applied = models.BooleanField(default=False, verbose_name=_("Applied")) applied = models.BooleanField(default=False, verbose_name=_("Applied"))
stage = models.CharField( stage = models.CharField(
@ -331,6 +349,7 @@ class Candidate(Base):
if self.resume: if self.resume:
return self.resume.size return self.resume.size
return 0 return 0
def clean(self): def clean(self):
"""Validate stage transitions""" """Validate stage transitions"""
# Only validate if this is an existing record (not being created) # 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 old_stage = self.__class__.objects.get(pk=self.pk).stage
return self.STAGE_SEQUENCE.get(old_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): def __str__(self):
return self.full_name return self.full_name
@ -449,7 +476,7 @@ class FormTemplate(Base):
User, on_delete=models.CASCADE, related_name="form_templates" User, on_delete=models.CASCADE, related_name="form_templates"
) )
is_active = models.BooleanField( is_active = models.BooleanField(
default=True, help_text="Whether this template is active" default=False, help_text="Whether this template is active"
) )
class Meta: class Meta:
@ -595,6 +622,9 @@ class FormField(Base):
if self.order < 0: if self.order < 0:
raise ValidationError("Order must be a positive integer") 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): class FormSubmission(Base):
""" """
@ -658,16 +688,19 @@ class FieldResponse(Base):
if self.uploaded_file: if self.uploaded_file:
return True return True
return False return False
@property @property
def get_file(self): def get_file(self):
if self.is_file: if self.is_file:
return self.uploaded_file return self.uploaded_file
return None return None
@property @property
def get_file_size(self): def get_file_size(self):
if self.is_file: if self.is_file:
return self.uploaded_file.size return self.uploaded_file.size
return 0 return 0
@property @property
def display_value(self): def display_value(self):
"""Return a human-readable representation of the response value""" """Return a human-readable representation of the response value"""
@ -885,9 +918,7 @@ class InterviewSchedule(Base):
job = models.ForeignKey( job = models.ForeignKey(
JobPosting, on_delete=models.CASCADE, related_name="interview_schedules" JobPosting, on_delete=models.CASCADE, related_name="interview_schedules"
) )
candidates = models.ManyToManyField( candidates = models.ManyToManyField(Candidate, related_name="interview_schedules")
Candidate, related_name="interview_schedules"
)
start_date = models.DateField(verbose_name=_("Start Date")) start_date = models.DateField(verbose_name=_("Start Date"))
end_date = models.DateField(verbose_name=_("End Date")) end_date = models.DateField(verbose_name=_("End Date"))
working_days = models.JSONField( working_days = models.JSONField(
@ -895,9 +926,7 @@ class InterviewSchedule(Base):
) # Store days of week as [0,1,2,3,4] for Mon-Fri ) # Store days of week as [0,1,2,3,4] for Mon-Fri
start_time = models.TimeField(verbose_name=_("Start Time")) start_time = models.TimeField(verbose_name=_("Start Time"))
end_time = models.TimeField(verbose_name=_("End Time")) end_time = models.TimeField(verbose_name=_("End Time"))
breaks = models.ManyToManyField( breaks = models.ManyToManyField(BreakTime, blank=True, related_name="schedules")
BreakTime, blank=True, related_name="schedules"
)
interview_duration = models.PositiveIntegerField( interview_duration = models.PositiveIntegerField(
verbose_name=_("Interview Duration (minutes)") verbose_name=_("Interview Duration (minutes)")
) )

View File

@ -24,6 +24,8 @@ import asyncio
@receiver(post_save, sender=models.Candidate) @receiver(post_save, sender=models.Candidate)
def score_candidate_resume(sender, instance, created, **kwargs): def score_candidate_resume(sender, instance, created, **kwargs):
if instance.is_resume_parsed:
return
try: try:
# Get absolute file path # Get absolute file path
file_path = instance.resume.path file_path = instance.resume.path
@ -108,12 +110,12 @@ def score_candidate_resume(sender, instance, created, **kwargs):
instance.weaknesses = result1.get('weaknesses', '') instance.weaknesses = result1.get('weaknesses', '')
instance.criteria_checklist = result1.get('criteria_checklist', {}) instance.criteria_checklist = result1.get('criteria_checklist', {})
instance.is_resume_parsed = True
# Save only scoring-related fields to avoid recursion # Save only scoring-related fields to avoid recursion
instance.save(update_fields=[ instance.save(update_fields=[
'match_score', 'strengths', 'weaknesses', '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}") logger.info(f"Successfully scored resume for candidate {instance.id}")

View File

@ -536,3 +536,18 @@ def get_available_time_slots(schedule, breaks=None):
print(f"Total slots generated: {len(slots)}") 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.shortcuts import render, get_object_or_404
from django.http import JsonResponse from django.http import JsonResponse
from recruitment.utils import json_to_markdown_table
from . import models from . import models
from django.utils.translation import get_language from django.utils.translation import get_language
from . import forms from . import forms
@ -19,6 +22,8 @@ from datastar_py.django import (
ServerSentEventGenerator as SSE, ServerSentEventGenerator as SSE,
read_signals, read_signals,
) )
# from rich import print
from rich.markdown import CodeBlock
class JobListView(LoginRequiredMixin, ListView): class JobListView(LoginRequiredMixin, ListView):
model = models.JobPosting model = models.JobPosting
@ -201,6 +206,7 @@ def training_list(request):
def candidate_detail(request, slug): def candidate_detail(request, slug):
from rich.json import JSON
candidate = get_object_or_404(models.Candidate, slug=slug) candidate = get_object_or_404(models.Candidate, slug=slug)
try: try:
parsed = ast.literal_eval(candidate.parsed_summary) parsed = ast.literal_eval(candidate.parsed_summary)
@ -212,6 +218,8 @@ def candidate_detail(request, slug):
if request.user.is_staff: if request.user.is_staff:
stage_form = forms.CandidateStageForm(candidate=candidate) 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', { return render(request, 'recruitment/candidate_detail.html', {
'candidate': candidate, 'candidate': candidate,
'parsed': parsed, 'parsed': parsed,

View File

@ -1,50 +1,238 @@
{% 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 %} {% block content %}
<div class="container mx-auto px-4 py-8"> <div class="container py-4">
<nav class="mb-6"> <nav aria-label="breadcrumb">
<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"> <ol class="breadcrumb">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-1" viewBox="0 0 20 20" fill="currentColor"> <li class="breadcrumb-item"><a href="{% url 'dashboard' %}">{% trans "Dashboard" %}</a></li>
<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" /> <li class="breadcrumb-item"><a href="{% url 'form_templates_list' %}">{% trans "Form Templates" %}</a></li>
</svg> <li class="breadcrumb-item active">{% trans "Submissions" %}</li>
Back to Form Templates </ol>
</a>
</nav> </nav>
<div class="bg-white dark:bg-gray-800 shadow-md rounded-lg overflow-hidden"> <div class="card shadow-sm">
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700"> <div class="card-header d-flex justify-content-between align-items-center">
<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> <div>
<p class="text-gray-600 dark:text-gray-400 mt-1">Template ID: {{ template.id }}</p> <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> </div>
<a href="{% url 'form_templates_list' %}" class="btn btn-outline-light btn-sm">
<div class="p-6"> <i class="fas fa-arrow-left me-1"></i> {% trans "Back to Templates" %}
</a>
</div>
<div class="card-body">
{% if page_obj.object_list %} {% if page_obj.object_list %}
<div class="overflow-x-auto"> <div id="form-template-submissions-list">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700"> {# View Switcher #}
<thead class="bg-gray-50 dark:bg-gray-700"> {% 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> <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">{% trans "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">{% trans "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">{% trans "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">{% trans "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> <th scope="col" class="text-end">{% trans "Actions" %}</th>
</tr> </tr>
</thead> </thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700"> <tbody>
{% for submission in page_obj %} {% for submission in page_obj %}
<tr class="hover:bg-gray-50 dark:hover:bg-gray-750"> <tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">{{ submission.id }}</td> <td class="fw-medium">{{ 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>{{ 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>{{ 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>{{ submission.submitted_at|date:"M d, Y H:i" }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium"> <td class="text-end">
<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"> <a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="btn btn-sm btn-outline-primary">
View Details <i class="fas fa-eye me-1"></i> {% trans "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> </a>
</td> </td>
</tr> </tr>
@ -52,50 +240,93 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
{# Card View #}
<div class="card-view">
<div class="row g-4">
{% for submission in page_obj %}
<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 %}
</div>
</div>
</div>
<!-- Pagination --> <!-- Pagination -->
{% if page_obj.has_other_pages %} {% if page_obj.has_other_pages %}
<nav class="mt-6 flex items-center justify-between" aria-label="Pagination"> <div class="d-flex flex-column flex-md-row justify-content-between align-items-center">
<div class="hidden sm:block"> <div class="pagination-info mb-3 mb-md-0">
<p class="text-sm text-gray-700 dark:text-gray-300"> {% blocktrans with start=page_obj.start_index end=page_obj.end_index total=page_obj.paginator.count %}
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. Showing {{ start }} to {{ end }} of {{ total }} results.
</p> {% endblocktrans %}
</div> </div>
<div class="flex-1 flex justify-between sm:justify-end mt-4 sm:mt-0"> <nav aria-label="Page navigation">
<ul class="pagination pagination-sm mb-0">
{% if page_obj.has_previous %} {% 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"> <li class="page-item">
Previous <a class="page-link" href="?page=1" aria-label="First">
<span aria-hidden="true">&laquo;</span>
</a> </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 %} {% 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"> <li class="page-item active">
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }} <span class="page-link">
{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}
</span> </span>
</li>
{% if page_obj.has_next %} {% 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"> <li class="page-item">
Next <a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
<span aria-hidden="true">&rsaquo;</span>
</a> </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 %} {% endif %}
</div> </ul>
</nav> </nav>
</div>
{% endif %} {% endif %}
{% else %} {% else %}
<div class="text-center py-12"> <div class="empty-state">
<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"> <i class="fas fa-inbox"></i>
<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" /> <h3 class="h5 mb-3">{% trans "No Submissions Found" %}</h3>
</svg> <p class="text-muted mb-4">
<h3 class="mt-2 text-lg font-medium text-gray-900 dark:text-white">No submissions found</h3> {% trans "There are no submissions for this form template yet." %}
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">There are no submissions for this form template yet.</p> </p>
<div class="mt-6"> <a href="{% url 'form_templates_list' %}" class="btn btn-main-action">
<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"> <i class="fas fa-arrow-left me-1"></i> {% trans "Back to Templates" %}
<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> </a>
</div> </div>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -231,7 +231,12 @@
</div> </div>
{% if templates %} {% if templates %}
<div class="row g-4"> <div id="form-templates-list">
{# View Switcher #}
{% include "includes/_list_view_switcher.html" with list_id="form-templates-list" %}
{# Card View (Default) #}
<div class="card-view active row g-4">
{% for template in templates %} {% for template in templates %}
<div class="col-lg-4 col-md-6"> <div class="col-lg-4 col-md-6">
<div class="card template-card h-100"> <div class="card template-card h-100">
@ -292,6 +297,54 @@
{% endfor %} {% endfor %}
</div> </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>
</div>
{% if templates.has_other_pages %} {% if templates.has_other_pages %}
<nav aria-label="Page navigation" class="mt-4"> <nav aria-label="Page navigation" class="mt-4">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">

View File

@ -1,12 +1,18 @@
{% load static i18n %} {% load static i18n %}
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{% translate "Application Form" %}</title> <title>{% translate "Application Form" %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
/>
<style> <style>
/* KAAT-S Theme Variables */ /* KAAT-S Theme Variables */
:root { :root {
@ -57,14 +63,20 @@
padding-top: 56px; /* Space for the sticky navbar */ padding-top: 56px; /* Space for the sticky navbar */
/* Dark gradient background to match the theme */ /* Dark gradient background to match the theme */
background: linear-gradient(135deg, var(--kaauh-teal-dark) 0%, #1e3a47 100%); background: linear-gradient(
135deg,
var(--kaauh-teal-dark) 0%,
#1e3a47 100%
);
background-image: url("{% static 'image/vision.svg' %}"); background-image: url("{% static 'image/vision.svg' %}");
background-repeat: no-repeat; background-repeat: no-repeat;
background-position: 60px; background-position: 60px;
background-size: 320px auto; background-size: 320px auto;
min-height: 100vh; min-height: 100vh;
padding: 0; /* Remove padding from body */ padding: 0; /* Remove padding from body */
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
} }
/* Wrapper to center the wizard content below the navbar */ /* Wrapper to center the wizard content below the navbar */
@ -73,7 +85,9 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 20px; /* Re-apply padding here for the content area */ padding: 20px; /* Re-apply padding here for the content area */
min-height: calc(100vh - 56px); /* Adjust height to account for navbar */ min-height: calc(
100vh - 56px
); /* Adjust height to account for navbar */
} }
.wizard-container { .wizard-container {
@ -489,33 +503,60 @@
</style> </style>
</head> </head>
<body> <body>
<nav
<nav id="topNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: white; z-index: 1030;"> id="topNavbar"
class="navbar navbar-expand-lg sticky-top"
style="background-color: white; z-index: 1030"
>
<div class="container-fluid"> <div class="container-fluid">
<a class="navbar-brand text-white fw-bold" href="/"> <a class="navbar-brand text-white fw-bold" href="/">
<img src="{% static 'image/kaauh.jpeg' %}" alt="{% translate 'KAAUH IMAGE' %}" style="height: 50px; margin-right: 10px;"> <img
src="{% static 'image/kaauh.jpeg' %}"
alt="{% translate 'KAAUH IMAGE' %}"
style="height: 50px; margin-right: 10px"
/>
</a> </a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" <button
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#navbarNav"
aria-controls="navbarNav"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span> <span class="navbar-toggler-icon"></span>
</button> </button>
<div class="collapse navbar-collapse" id="navbarNav"> <div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto"> <ul class="navbar-nav ms-auto">
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-secondary" href="/applications/">{% translate "Applications" %}</a> <a
class="nav-link text-secondary"
href="/applications/"
>{% translate "Applications" %}</a
>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-secondary" href="/profile/">{% translate "Profile" %}</a> <a class="nav-link text-secondary" href="/profile/"
>{% translate "Profile" %}</a
>
</li> </li>
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-secondary" href="https://kaauh.edu.sa/career">{% translate "Careers" %}</a> <a
class="nav-link text-secondary"
href="https://kaauh.edu.sa/career"
>{% translate "Careers" %}</a
>
</li> </li>
</ul> </ul>
</div> </div>
</div> </div>
</nav> </nav>
<nav id="bottomNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: var(--kaauh-teal); z-index: 1030;"> <nav
id="bottomNavbar"
class="navbar navbar-expand-lg sticky-top"
style="background-color: var(--kaauh-teal); z-index: 1030"
>
<span class="ms-2 text-white">JOB ID:&nbsp;&nbsp;{{job_id}}</span> <span class="ms-2 text-white">JOB ID:&nbsp;&nbsp;{{job_id}}</span>
</nav> </nav>
@ -528,55 +569,58 @@
<div class="wizard-header"> <div class="wizard-header">
<div class="logo"> <div class="logo">
<i class="fas fa-file-alt"></i> <i class="fas fa-file-alt"></i>
<span id="formTitle">{% translate "Application Form" %}</span> <span id="formTitle"
>{% translate "Application Form" %}</span
>
</div> </div>
<div class="progress-text" id="progressText">1 of 1</div> <div class="progress-text" id="progressText">1 of 1</div>
</div> </div>
<div class="wizard-content"> <div class="wizard-content">
<div class="stage-container" id="stageContainer"> <div class="stage-container" id="stageContainer"></div>
</div>
<div class="preview-container" id="previewContainer" style="display: none;"> <div
<h3 class="mb-4">{% translate "Review Your Application" %}</h3> class="preview-container"
id="previewContainer"
style="display: none"
>
<h3 class="mb-4">
{% translate "Review Your Application" %}
</h3>
<div id="previewContent"></div> <div id="previewContent"></div>
</div> </div>
</div> </div>
<div class="wizard-footer"> <div class="wizard-footer">
<button id="backBtn" class="nav-btn btn-back" style="display: none;"> <button
id="backBtn"
class="nav-btn btn-back"
style="display: none"
>
<i class="fas fa-arrow-left"></i> {% translate "Back" %} <i class="fas fa-arrow-left"></i> {% translate "Back" %}
</button> </button>
<button id="nextBtn" class="nav-btn btn-next"> <button id="nextBtn" class="nav-btn btn-next">
{% translate "Next" %} <i class="fas fa-arrow-right"></i> {% translate "Next" %}
<i class="fas fa-arrow-right"></i>
</button> </button>
<button id="submitBtn" class="nav-btn btn-submit" style="display: none;"> <button
{% translate "Submit Application" %} <i class="fas fa-paper-plane"></i> id="submitBtn"
class="nav-btn btn-submit"
style="display: none"
>
{% translate "Submit Application" %}
<i class="fas fa-paper-plane"></i>
</button> </button>
</div> </div>
</div> </div>
</div> <script> </div>
// Placeholder for the complete JavaScript logic (omitted for brevity, but required for functionality)
// This script block should contain the Application State, DOM Elements, Validation, API, Rendering, and Event Handlers
console.log("JavaScript logic for form wizard needs to be included here.");
// --- COMPLETE JAVASCRIPT LOGIC GOES HERE ---
// (The logic provided in the previous turn, including the completed createFieldElement and renderPreview functions)
// Example structure for reference:
// function validateEmail(email) { ... }
// function loadFormTemplate() { ... }
// function renderCurrentStage() { ... }
// function createFieldElement(field) { ... }
// function renderPreview() { ... }
// document.addEventListener('DOMContentLoaded', loadFormTemplate);
</script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
// Application State // Application State
const csrfToken = '{{ csrf_token }}'; const csrfToken = '{{ csrf_token }}';
const state = { const state = {
templateId: {{ template_id }}, templateId: {{ template_id }},
stages: [], stages: [],
@ -803,6 +847,7 @@
if (result.success) { if (result.success) {
const templateData = result.template; const templateData = result.template;
console.log(templateData);
state.stages = templateData.stages; state.stages = templateData.stages;
elements.formTitle.textContent = templateData.name; elements.formTitle.textContent = templateData.name;
updateProgress(); updateProgress();
@ -831,11 +876,11 @@
} }
const formData = new FormData(); const formData = new FormData();
formData.append('csrfmiddlewaretoken', csrfToken);
// Add applicant info // Add applicant info
//formData.append('applicant_name', state.formData.applicant_name || ''); //formData.append('applicant_name', state.formData.applicant_name || '');
//formData.append('applicant_email', state.formData.applicant_email || ''); //formData.append('applicant_email', state.formData.applicant_email || '');
console.log(state.formData)
// Add field responses // Add field responses
state.stages.forEach(stage => { state.stages.forEach(stage => {
stage.fields.forEach(field => { stage.fields.forEach(field => {
@ -855,9 +900,6 @@
try { try {
const response = await fetch(`/forms/form/${state.templateId}/submit/`, { const response = await fetch(`/forms/form/${state.templateId}/submit/`, {
method: 'POST', method: 'POST',
headers: {
'X-CSRFToken': csrfToken,
},
body: formData body: formData
}); });

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,8 +97,14 @@
</div> </div>
</div> </div>
<!-- Candidates Table --> <!-- Candidates -->
{% if candidates %} {% if candidates %}
<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">
<div class="card-header"> <div class="card-header">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
@ -219,6 +225,51 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
</div>
{# 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 %}
<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>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% else %} {% else %}
<div class="text-center py-5"> <div class="text-center py-5">
<i class="fas fa-user-slash fa-3x text-muted mb-3"></i> <i class="fas fa-user-slash fa-3x text-muted mb-3"></i>

View File

@ -250,9 +250,10 @@
{# HEADER SECTION #} {# HEADER SECTION #}
<div class="job-header-card d-flex justify-content-between align-items-center flex-wrap"> <div class="job-header-card d-flex justify-content-between align-items-center flex-wrap">
<h2>{{job}}</h2> <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 }} {{ job.get_status_display }}
</span> </button>
</div> </div>
{# LEFT TABS NAVIGATION #} {# LEFT TABS NAVIGATION #}

View File

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static %} {% load static i18n %}
{% block title %}Job Postings - University ATS{% endblock %} {% block title %}Job Postings - University ATS{% endblock %}
@ -164,7 +164,12 @@
</div> </div>
{% if page_obj %} {% if page_obj %}
<div class="row"> <div id="job-list">
{# View Switcher #}
{% include "includes/_list_view_switcher.html" with list_id="job-list" %}
{# Card View (Default) #}
<div class="card-view active row">
{% for job in page_obj %} {% for job in page_obj %}
<div class="col-md-6 col-lg-4 mb-4"> <div class="col-md-6 col-lg-4 mb-4">
<div class="card job-card h-100"> <div class="card job-card h-100">
@ -205,6 +210,48 @@
{% endfor %} {% endfor %}
</div> </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>
</div>
</div>
{% if page_obj.has_other_pages %} {% if page_obj.has_other_pages %}
<nav aria-label="Job pagination" class="mt-4"> <nav aria-label="Job pagination" class="mt-4">
<ul class="pagination justify-content-center"> <ul class="pagination justify-content-center">

View File

@ -246,6 +246,12 @@
</div> </div>
{% if meetings %} {% if meetings %}
<div id="meetings-list">
{# View Switcher #}
{% include "includes/_list_view_switcher.html" with list_id="meetings-list" %}
{# Card View (Default) #}
<div class="card-view active">
<div class="meetings-grid"> <div class="meetings-grid">
{% for meeting in meetings %} {% for meeting in meetings %}
<div class="meeting-card"> <div class="meeting-card">
@ -316,6 +322,63 @@
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</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 %} {% if is_paginated %}
<nav aria-label="Page navigation" class="mt-4"> <nav aria-label="Page navigation" class="mt-4">

View File

@ -143,6 +143,12 @@
</div> </div>
{% if candidates %} {% if candidates %}
<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"> <div class="table-responsive">
<table class="table table-hover mb-0"> <table class="table table-hover mb-0">
<thead> <thead>
@ -192,6 +198,52 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
{# 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.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 "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 %}
</div>
</div>
</div>
{% if is_paginated %} {% if is_paginated %}
<div class="card-footer bg-white border-top"> <div class="card-footer bg-white border-top">

View File

@ -143,6 +143,12 @@
{% if materials %} {% if materials %}
<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"> <div class="table-responsive">
<table class="table table-hover align-middle mb-0"> <table class="table table-hover align-middle mb-0">
<thead> <thead>
@ -182,6 +188,49 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
{# Card View #}
<div class="card-view">
<div class="row g-4">
{% for material in materials %}
<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>
<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 %}
</div>
</div>
</div>
{% if is_paginated %} {% if is_paginated %}
<div class="card-footer bg-light border-top"> <div class="card-footer bg-light border-top">