update on the models and forms
This commit is contained in:
parent
579cc085e2
commit
a23c96cc17
Binary file not shown.
Binary file not shown.
@ -196,7 +196,6 @@ SOCIALACCOUNT_PROVIDERS = {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A'
|
||||
ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA'
|
||||
ZOOM_CLIENT_SECRET = 'rvfhjlbID4ychXPOvZ2lYsoAC0B0Ny2L'
|
||||
@ -215,7 +214,6 @@ CELERY_TASK_SERIALIZER = 'json'
|
||||
CELERY_RESULT_SERIALIZER = 'json'
|
||||
CELERY_TIMEZONE = 'UTC'
|
||||
|
||||
|
||||
LINKEDIN_CLIENT_ID = '867jwsiyem1504'
|
||||
LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='
|
||||
LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/'
|
||||
BIN
db.sqlite3
BIN
db.sqlite3
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -470,3 +470,17 @@ class InterviewScheduleForm(forms.ModelForm):
|
||||
working_days = self.cleaned_data.get('working_days')
|
||||
# Convert string values to integers
|
||||
return [int(day) for day in working_days]
|
||||
|
||||
|
||||
class JobPostingCancelReasonForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = JobPosting
|
||||
fields = ['cancel_reason']
|
||||
class JobPostingStatusForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = JobPosting
|
||||
fields = ['status']
|
||||
class FormTemplateIsActiveForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = FormTemplate
|
||||
fields = ['is_active']
|
||||
@ -2,12 +2,13 @@ import requests
|
||||
|
||||
LINKEDIN_API_BASE = "https://api.linkedin.com/v2"
|
||||
|
||||
|
||||
class LinkedInService:
|
||||
def __init__(self, access_token):
|
||||
self.headers = {
|
||||
'Authorization': f'Bearer {access_token}',
|
||||
'X-Restli-Protocol-Version': '2.0.0',
|
||||
'Content-Type': 'application/json'
|
||||
"Authorization": f"Bearer {access_token}",
|
||||
"X-Restli-Protocol-Version": "2.0.0",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
def post_job(self, organization_id, job_data):
|
||||
@ -17,10 +18,10 @@ class LinkedInService:
|
||||
"lifecycleState": "PUBLISHED",
|
||||
"specificContent": {
|
||||
"com.linkedin.ugc.ShareContent": {
|
||||
"shareCommentary": {"text": job_data['text']},
|
||||
"shareMediaCategory": "NONE"
|
||||
"shareCommentary": {"text": job_data["text"]},
|
||||
"shareMediaCategory": "NONE",
|
||||
}
|
||||
},
|
||||
"visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"}
|
||||
"visibility": {"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"},
|
||||
}
|
||||
return requests.post(url, json=data, headers=self.headers)
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-08 15:48
|
||||
# Generated by Django 5.2.6 on 2025-10-09 10:10
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
@ -213,6 +213,7 @@ class Migration(migrations.Migration):
|
||||
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
|
||||
('email', models.EmailField(max_length=254, verbose_name='Email')),
|
||||
('phone', models.CharField(max_length=20, verbose_name='Phone')),
|
||||
('address', models.TextField(max_length=200, verbose_name='Address')),
|
||||
('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')),
|
||||
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
|
||||
('applied', models.BooleanField(default=False, verbose_name='Applied')),
|
||||
@ -311,6 +312,14 @@ class Migration(migrations.Migration):
|
||||
name='job',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SharedFormTemplate',
|
||||
fields=[
|
||||
@ -374,6 +383,7 @@ class Migration(migrations.Migration):
|
||||
name='ScheduledInterview',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('interview_date', models.DateField(verbose_name='Interview Date')),
|
||||
('interview_time', models.TimeField(verbose_name='Interview Time')),
|
||||
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], default='scheduled', max_length=20)),
|
||||
@ -384,5 +394,8 @@ class Migration(migrations.Migration):
|
||||
('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')),
|
||||
('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-08 17:46
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='candidate',
|
||||
name='address',
|
||||
field=models.TextField(default='', max_length=200, verbose_name='Address'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,33 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-09 10:33
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='jobposting',
|
||||
name='cancel_reason',
|
||||
field=models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobposting',
|
||||
name='cancelled_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobposting',
|
||||
name='cancelled_by',
|
||||
field=models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobposting',
|
||||
name='status',
|
||||
field=models.CharField(blank=True, choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20, null=True),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-09 12:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0002_jobposting_cancel_reason_jobposting_cancelled_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='candidate',
|
||||
name='is_resume_parsed',
|
||||
field=models.BooleanField(default=False, verbose_name='Resume Parsed'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='formtemplate',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=False, help_text='Whether this template is active'),
|
||||
),
|
||||
]
|
||||
@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.2.6 on 2025-10-08 17:47
|
||||
|
||||
import django_extensions.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0002_candidate_address'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='scheduledinterview',
|
||||
name='slug',
|
||||
field=django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug'),
|
||||
),
|
||||
]
|
||||
@ -1,24 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-08 13:01
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0026_interviewschedule_scheduledinterview'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -11,8 +11,10 @@ from django.urls import reverse
|
||||
|
||||
|
||||
class Profile(models.Model):
|
||||
profile_image=models.ImageField(null=True,blank=True,upload_to='profile_pic/')
|
||||
user=models.OneToOneField(User,on_delete=models.CASCADE,related_name='profile')
|
||||
profile_image = models.ImageField(null=True, blank=True, upload_to="profile_pic/")
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
|
||||
|
||||
|
||||
class Base(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Created at"))
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("Updated at"))
|
||||
@ -105,8 +107,9 @@ class JobPosting(Base):
|
||||
# Status Fields
|
||||
STATUS_CHOICES = [
|
||||
("DRAFT", "Draft"),
|
||||
("PUBLISHED", "Published"),
|
||||
("ACTIVE", "Active"),
|
||||
("CLOSED", "Closed"),
|
||||
("CANCELLED", "Cancelled"),
|
||||
("ARCHIVED", "Archived"),
|
||||
]
|
||||
status = models.CharField(
|
||||
@ -165,6 +168,18 @@ class JobPosting(Base):
|
||||
"External agency responsible for sourcing candidates for this role"
|
||||
),
|
||||
)
|
||||
cancel_reason = models.TextField(
|
||||
blank=True,
|
||||
help_text=_("Reason for canceling the job posting"),
|
||||
verbose_name=_("Cancel Reason"),
|
||||
)
|
||||
cancelled_by = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
help_text=_("Name of person who cancelled this job"),
|
||||
verbose_name=_("Cancelled By"),
|
||||
)
|
||||
cancelled_at = models.DateTimeField(null=True, blank=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ["-created_at"]
|
||||
@ -197,7 +212,7 @@ class JobPosting(Base):
|
||||
else:
|
||||
next_num = 1
|
||||
|
||||
self.internal_job_id = f"{prefix}-{year}-{next_num:04d}"
|
||||
self.internal_job_id = f"{prefix}-{year}-{next_num:06d}"
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@ -260,8 +275,11 @@ class Candidate(Base):
|
||||
last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
|
||||
email = models.EmailField(verbose_name=_("Email"))
|
||||
phone = models.CharField(max_length=20, verbose_name=_("Phone"))
|
||||
address = models.TextField(max_length=200,verbose_name=_("Address"))
|
||||
address = models.TextField(max_length=200, verbose_name=_("Address"))
|
||||
resume = models.FileField(upload_to="resumes/", verbose_name=_("Resume"))
|
||||
is_resume_parsed = models.BooleanField(
|
||||
default=False, verbose_name=_("Resume Parsed")
|
||||
)
|
||||
parsed_summary = models.TextField(blank=True, verbose_name=_("Parsed Summary"))
|
||||
applied = models.BooleanField(default=False, verbose_name=_("Applied"))
|
||||
stage = models.CharField(
|
||||
@ -331,6 +349,7 @@ class Candidate(Base):
|
||||
if self.resume:
|
||||
return self.resume.size
|
||||
return 0
|
||||
|
||||
def clean(self):
|
||||
"""Validate stage transitions"""
|
||||
# Only validate if this is an existing record (not being created)
|
||||
@ -376,6 +395,14 @@ class Candidate(Base):
|
||||
old_stage = self.__class__.objects.get(pk=self.pk).stage
|
||||
return self.STAGE_SEQUENCE.get(old_stage, [])
|
||||
|
||||
@property
|
||||
def submission(self):
|
||||
return FormSubmission.objects.filter(template__job=self.job).first()
|
||||
@property
|
||||
def responses(self):
|
||||
if self.submission:
|
||||
return self.submission.responses.all()
|
||||
return []
|
||||
def __str__(self):
|
||||
return self.full_name
|
||||
|
||||
@ -449,7 +476,7 @@ class FormTemplate(Base):
|
||||
User, on_delete=models.CASCADE, related_name="form_templates"
|
||||
)
|
||||
is_active = models.BooleanField(
|
||||
default=True, help_text="Whether this template is active"
|
||||
default=False, help_text="Whether this template is active"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -595,6 +622,9 @@ class FormField(Base):
|
||||
if self.order < 0:
|
||||
raise ValidationError("Order must be a positive integer")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.stage.template.name} - {self.stage.name} - {self.label}"
|
||||
|
||||
|
||||
class FormSubmission(Base):
|
||||
"""
|
||||
@ -658,16 +688,19 @@ class FieldResponse(Base):
|
||||
if self.uploaded_file:
|
||||
return True
|
||||
return False
|
||||
|
||||
@property
|
||||
def get_file(self):
|
||||
if self.is_file:
|
||||
return self.uploaded_file
|
||||
return None
|
||||
|
||||
@property
|
||||
def get_file_size(self):
|
||||
if self.is_file:
|
||||
return self.uploaded_file.size
|
||||
return 0
|
||||
|
||||
@property
|
||||
def display_value(self):
|
||||
"""Return a human-readable representation of the response value"""
|
||||
@ -885,9 +918,7 @@ class InterviewSchedule(Base):
|
||||
job = models.ForeignKey(
|
||||
JobPosting, on_delete=models.CASCADE, related_name="interview_schedules"
|
||||
)
|
||||
candidates = models.ManyToManyField(
|
||||
Candidate, related_name="interview_schedules"
|
||||
)
|
||||
candidates = models.ManyToManyField(Candidate, related_name="interview_schedules")
|
||||
start_date = models.DateField(verbose_name=_("Start Date"))
|
||||
end_date = models.DateField(verbose_name=_("End Date"))
|
||||
working_days = models.JSONField(
|
||||
@ -895,9 +926,7 @@ class InterviewSchedule(Base):
|
||||
) # Store days of week as [0,1,2,3,4] for Mon-Fri
|
||||
start_time = models.TimeField(verbose_name=_("Start Time"))
|
||||
end_time = models.TimeField(verbose_name=_("End Time"))
|
||||
breaks = models.ManyToManyField(
|
||||
BreakTime, blank=True, related_name="schedules"
|
||||
)
|
||||
breaks = models.ManyToManyField(BreakTime, blank=True, related_name="schedules")
|
||||
interview_duration = models.PositiveIntegerField(
|
||||
verbose_name=_("Interview Duration (minutes)")
|
||||
)
|
||||
|
||||
@ -24,6 +24,8 @@ import asyncio
|
||||
|
||||
@receiver(post_save, sender=models.Candidate)
|
||||
def score_candidate_resume(sender, instance, created, **kwargs):
|
||||
if instance.is_resume_parsed:
|
||||
return
|
||||
try:
|
||||
# Get absolute file path
|
||||
file_path = instance.resume.path
|
||||
@ -108,12 +110,12 @@ def score_candidate_resume(sender, instance, created, **kwargs):
|
||||
instance.weaknesses = result1.get('weaknesses', '')
|
||||
instance.criteria_checklist = result1.get('criteria_checklist', {})
|
||||
|
||||
|
||||
instance.is_resume_parsed = True
|
||||
|
||||
# Save only scoring-related fields to avoid recursion
|
||||
instance.save(update_fields=[
|
||||
'match_score', 'strengths', 'weaknesses',
|
||||
'criteria_checklist','parsed_summary'
|
||||
'criteria_checklist','parsed_summary', 'is_resume_parsed'
|
||||
])
|
||||
|
||||
logger.info(f"Successfully scored resume for candidate {instance.id}")
|
||||
|
||||
@ -536,3 +536,18 @@ def get_available_time_slots(schedule, breaks=None):
|
||||
|
||||
print(f"Total slots generated: {len(slots)}")
|
||||
return slots
|
||||
|
||||
|
||||
|
||||
def json_to_markdown_table(data_list):
|
||||
if not data_list:
|
||||
return ""
|
||||
|
||||
headers = data_list[0].keys()
|
||||
markdown = "| " + " | ".join(headers) + " |\n"
|
||||
markdown += "| " + " | ".join(["---"] * len(headers)) + " |\n"
|
||||
|
||||
for row in data_list:
|
||||
values = [str(row.get(header, "")) for header in headers]
|
||||
markdown += "| " + " | ".join(values) + " |\n"
|
||||
return markdown
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,8 @@
|
||||
import json
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.http import JsonResponse
|
||||
|
||||
from recruitment.utils import json_to_markdown_table
|
||||
from . import models
|
||||
from django.utils.translation import get_language
|
||||
from . import forms
|
||||
@ -19,6 +22,8 @@ from datastar_py.django import (
|
||||
ServerSentEventGenerator as SSE,
|
||||
read_signals,
|
||||
)
|
||||
# from rich import print
|
||||
from rich.markdown import CodeBlock
|
||||
|
||||
class JobListView(LoginRequiredMixin, ListView):
|
||||
model = models.JobPosting
|
||||
@ -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,
|
||||
|
||||
@ -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 %}
|
||||
<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>
|
||||
|
||||
<div class="p-6">
|
||||
<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="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">
|
||||
<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" 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>
|
||||
<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 class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tbody>
|
||||
{% 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>
|
||||
<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>
|
||||
@ -52,50 +240,93 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</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 -->
|
||||
{% 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">
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm mb-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
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1" aria-label="First">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="Previous">
|
||||
<span aria-hidden="true">‹</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<span class="relative inline-flex items-center px-4 py-2 border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}
|
||||
<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
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
|
||||
<span aria-hidden="true">›</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}" aria-label="Last">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</div>
|
||||
</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
|
||||
<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>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -231,7 +231,12 @@
|
||||
</div>
|
||||
|
||||
{% 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 %}
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="card template-card h-100">
|
||||
@ -292,6 +297,54 @@
|
||||
{% 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>
|
||||
</div>
|
||||
|
||||
{% if templates.has_other_pages %}
|
||||
<nav aria-label="Page navigation" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
{% load static i18n %}
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>{% translate "Application Form" %}</title>
|
||||
<link 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">
|
||||
<link
|
||||
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>
|
||||
/* KAAT-S Theme Variables */
|
||||
:root {
|
||||
@ -57,14 +63,20 @@
|
||||
padding-top: 56px; /* Space for the sticky navbar */
|
||||
|
||||
/* 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-repeat: no-repeat;
|
||||
background-position: 60px;
|
||||
background-size: 320px auto;
|
||||
min-height: 100vh;
|
||||
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 */
|
||||
@ -73,7 +85,9 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
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 {
|
||||
@ -486,36 +500,63 @@
|
||||
top: 72px;
|
||||
/* The z-index is already 1030 in the inline style, which is correct */
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav id="topNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: white; z-index: 1030;">
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav
|
||||
id="topNavbar"
|
||||
class="navbar navbar-expand-lg sticky-top"
|
||||
style="background-color: white; z-index: 1030"
|
||||
>
|
||||
<div class="container-fluid">
|
||||
<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>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
||||
<button
|
||||
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>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
|
||||
<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 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 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>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<nav id="bottomNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: var(--kaauh-teal); z-index: 1030;">
|
||||
</nav>
|
||||
<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: {{job_id}}</span>
|
||||
</nav>
|
||||
|
||||
@ -528,55 +569,58 @@
|
||||
<div class="wizard-header">
|
||||
<div class="logo">
|
||||
<i class="fas fa-file-alt"></i>
|
||||
<span id="formTitle">{% translate "Application Form" %}</span>
|
||||
<span id="formTitle"
|
||||
>{% translate "Application Form" %}</span
|
||||
>
|
||||
</div>
|
||||
<div class="progress-text" id="progressText">1 of 1</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-content">
|
||||
<div class="stage-container" id="stageContainer">
|
||||
</div>
|
||||
<div class="stage-container" id="stageContainer"></div>
|
||||
|
||||
<div class="preview-container" id="previewContainer" style="display: none;">
|
||||
<h3 class="mb-4">{% translate "Review Your Application" %}</h3>
|
||||
<div
|
||||
class="preview-container"
|
||||
id="previewContainer"
|
||||
style="display: none"
|
||||
>
|
||||
<h3 class="mb-4">
|
||||
{% translate "Review Your Application" %}
|
||||
</h3>
|
||||
<div id="previewContent"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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" %}
|
||||
</button>
|
||||
|
||||
<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 id="submitBtn" class="nav-btn btn-submit" style="display: none;">
|
||||
{% translate "Submit Application" %} <i class="fas fa-paper-plane"></i>
|
||||
<button
|
||||
id="submitBtn"
|
||||
class="nav-btn btn-submit"
|
||||
style="display: none"
|
||||
>
|
||||
{% translate "Submit Application" %}
|
||||
<i class="fas fa-paper-plane"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div> <script>
|
||||
// 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>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script>
|
||||
// Application State
|
||||
const csrfToken = '{{ csrf_token }}';
|
||||
|
||||
const state = {
|
||||
templateId: {{ template_id }},
|
||||
stages: [],
|
||||
@ -803,6 +847,7 @@
|
||||
|
||||
if (result.success) {
|
||||
const templateData = result.template;
|
||||
console.log(templateData);
|
||||
state.stages = templateData.stages;
|
||||
elements.formTitle.textContent = templateData.name;
|
||||
updateProgress();
|
||||
@ -831,11 +876,11 @@
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('csrfmiddlewaretoken', csrfToken);
|
||||
// Add applicant info
|
||||
//formData.append('applicant_name', state.formData.applicant_name || '');
|
||||
//formData.append('applicant_email', state.formData.applicant_email || '');
|
||||
|
||||
console.log(state.formData)
|
||||
// Add field responses
|
||||
state.stages.forEach(stage => {
|
||||
stage.fields.forEach(field => {
|
||||
@ -855,9 +900,6 @@
|
||||
try {
|
||||
const response = await fetch(`/forms/form/${state.templateId}/submit/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
@ -1279,5 +1321,5 @@
|
||||
// Start the application
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
</script>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
159
templates/includes/_list_view_switcher.html
Normal file
159
templates/includes/_list_view_switcher.html
Normal file
@ -0,0 +1,159 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="btn-group" role="group">
|
||||
<button type="button" class="btn btn-outline-primary btn-sm view-toggle active" data-view="table" data-list-id="{{ list_id }}">
|
||||
<i class="fas fa-table me-1"></i> Table
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm view-toggle" data-view="card" data-list-id="{{ list_id }}">
|
||||
<i class="fas fa-th me-1"></i> Card
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* View Toggle Styles */
|
||||
.view-toggle {
|
||||
border-radius: 0.25rem;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
.view-toggle.active {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
}
|
||||
.view-toggle.active:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
/* Hide elements by default */
|
||||
.table-view,
|
||||
.card-view {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Show active view */
|
||||
.table-view.active,
|
||||
.card-view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Card View Styles */
|
||||
.card-view .card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
background-color: white;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.card-view .card-header {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 0.75rem 0.75rem 0 0;
|
||||
}
|
||||
.card-view .card-body {
|
||||
padding: 1.25rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.card-view .card-title {
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.card-view .card-text {
|
||||
color: var(--kaauh-primary-text);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.card-view .card-footer {
|
||||
padding: 0.75rem 1.25rem;
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid var(--kaauh-border);
|
||||
border-radius: 0 0 0.75rem 0.75rem;
|
||||
}
|
||||
.card-view .btn-sm {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.3rem 0.6rem;
|
||||
}
|
||||
|
||||
/* Table View Styles */
|
||||
.table-view .table-responsive {
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.table-view .table {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.table-view .table thead th {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
border-color: var(--kaauh-border);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
font-size: 0.8rem;
|
||||
letter-spacing: 0.5px;
|
||||
padding: 1rem;
|
||||
}
|
||||
.table-view .table tbody td {
|
||||
padding: 1rem;
|
||||
vertical-align: middle;
|
||||
border-color: var(--kaauh-border);
|
||||
}
|
||||
.table-view .table tbody tr {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.table-view .table tbody tr:hover {
|
||||
background-color: var(--kaauh-gray-light);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Get the list ID from the data attribute
|
||||
const listId = document.querySelector('.view-toggle').getAttribute('data-list-id');
|
||||
const listContainer = document.getElementById(listId);
|
||||
|
||||
// Get saved view preference from localStorage
|
||||
const savedView = localStorage.getItem(`list_view_${listId}`) || 'table';
|
||||
|
||||
// Set initial view
|
||||
setView(savedView);
|
||||
|
||||
// Add click event listeners to view toggle buttons
|
||||
document.querySelectorAll('.view-toggle').forEach(button => {
|
||||
button.addEventListener('click', function() {
|
||||
const view = this.getAttribute('data-view');
|
||||
setView(view);
|
||||
});
|
||||
});
|
||||
|
||||
function setView(view) {
|
||||
// Update button states
|
||||
document.querySelectorAll('.view-toggle').forEach(button => {
|
||||
if (button.getAttribute('data-view') === view) {
|
||||
button.classList.add('active');
|
||||
} else {
|
||||
button.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Update view visibility
|
||||
const tableView = listContainer.querySelector('.table-view');
|
||||
const cardView = listContainer.querySelector('.card-view');
|
||||
|
||||
if (view === 'table') {
|
||||
tableView.classList.add('active');
|
||||
cardView.classList.remove('active');
|
||||
} else {
|
||||
tableView.classList.remove('active');
|
||||
cardView.classList.add('active');
|
||||
}
|
||||
|
||||
// Save preference to localStorage
|
||||
localStorage.setItem(`list_view_${listId}`, view);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -97,8 +97,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Candidates Table -->
|
||||
<!-- 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-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
@ -219,6 +225,51 @@
|
||||
{% endif %}
|
||||
</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 %}
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-user-slash fa-3x text-muted mb-3"></i>
|
||||
|
||||
@ -250,9 +250,10 @@
|
||||
{# 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 #}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Job Postings - University ATS{% endblock %}
|
||||
|
||||
@ -164,7 +164,12 @@
|
||||
</div>
|
||||
|
||||
{% 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 %}
|
||||
<div class="col-md-6 col-lg-4 mb-4">
|
||||
<div class="card job-card h-100">
|
||||
@ -205,6 +210,48 @@
|
||||
{% 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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if page_obj.has_other_pages %}
|
||||
<nav aria-label="Job pagination" class="mt-4">
|
||||
<ul class="pagination justify-content-center">
|
||||
|
||||
@ -246,6 +246,12 @@
|
||||
</div>
|
||||
|
||||
{% 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">
|
||||
{% for meeting in meetings %}
|
||||
<div class="meeting-card">
|
||||
@ -316,6 +322,63 @@
|
||||
</div>
|
||||
{% endfor %}
|
||||
</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 %}
|
||||
<nav aria-label="Page navigation" class="mt-4">
|
||||
|
||||
@ -143,6 +143,12 @@
|
||||
</div>
|
||||
|
||||
{% 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">
|
||||
<table class="table table-hover mb-0">
|
||||
<thead>
|
||||
@ -192,6 +198,52 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</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 %}
|
||||
<div class="card-footer bg-white border-top">
|
||||
|
||||
@ -143,6 +143,12 @@
|
||||
|
||||
|
||||
{% 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">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
@ -182,6 +188,49 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</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 %}
|
||||
<div class="card-footer bg-light border-top">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user