Compare commits
6 Commits
7b02120508
...
4caf57a3e0
| Author | SHA1 | Date | |
|---|---|---|---|
| 4caf57a3e0 | |||
| 30acc14775 | |||
| f4f84db0c1 | |||
| 9a768726e5 | |||
| 916cbc4fcf | |||
| 322c98222d |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
<<<<<<< HEAD
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.pyc
|
||||
@ -110,4 +110,3 @@ local_settings.py
|
||||
# If a rule in .gitignore ends with a directory separator (i.e. `/`
|
||||
# character), then remove the file in the remaining pattern string and all
|
||||
# files with the same name in subdirectories.
|
||||
>>>>>>> 29790ab (add external integration)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -55,7 +55,8 @@ INSTALLED_APPS = [
|
||||
'django_extensions',
|
||||
'template_partials',
|
||||
'django_countries',
|
||||
'django_celery_results'
|
||||
'django_celery_results',
|
||||
'django_q',
|
||||
]
|
||||
|
||||
SITE_ID = 1
|
||||
@ -223,129 +224,65 @@ LINKEDIN_CLIENT_ID = '867jwsiyem1504'
|
||||
LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='
|
||||
LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/'
|
||||
|
||||
# customColorPalette = [
|
||||
# {
|
||||
# 'color': 'hsl(4, 90%, 58%)',
|
||||
# 'label': 'Red'
|
||||
# },
|
||||
# {
|
||||
# 'color': 'hsl(340, 82%, 52%)',
|
||||
# 'label': 'Pink'
|
||||
# },
|
||||
# {
|
||||
# 'color': 'hsl(291, 64%, 42%)',
|
||||
# 'label': 'Purple'
|
||||
# },
|
||||
# {
|
||||
# 'color': 'hsl(262, 52%, 47%)',
|
||||
# 'label': 'Deep Purple'
|
||||
# },
|
||||
# {
|
||||
# 'color': 'hsl(231, 48%, 48%)',
|
||||
# 'label': 'Indigo'
|
||||
# },
|
||||
# {
|
||||
# 'color': 'hsl(207, 90%, 54%)',
|
||||
# 'label': 'Blue'
|
||||
# },
|
||||
# ]
|
||||
|
||||
# #ckeditor_5 config setthings:
|
||||
# CKEDITOR_5_CONFIGS = {
|
||||
# 'default': {
|
||||
# 'toolbar': {
|
||||
# 'items': ['heading', '|', 'bold', 'italic', 'link',
|
||||
# 'bulletedList', 'numberedList', 'blockQuote', 'imageUpload', ],
|
||||
# }
|
||||
|
||||
# },
|
||||
# 'extends': {
|
||||
# 'blockToolbar': [
|
||||
# 'paragraph', 'heading1', 'heading2', 'heading3',
|
||||
# '|',
|
||||
# 'bulletedList', 'numberedList',
|
||||
# '|',
|
||||
# 'blockQuote',
|
||||
# ],
|
||||
# 'toolbar': {
|
||||
# 'items': ['heading', '|', 'outdent', 'indent', '|', 'bold', 'italic', 'link', 'underline', 'strikethrough',
|
||||
# 'code','subscript', 'superscript', 'highlight', '|', 'codeBlock', 'sourceEditing', 'insertImage',
|
||||
# 'bulletedList', 'numberedList', 'todoList', '|', 'blockQuote', 'imageUpload', '|',
|
||||
# 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', 'mediaEmbed', 'removeFormat',
|
||||
# 'insertTable',
|
||||
# ],
|
||||
# 'shouldNotGroupWhenFull': 'true'
|
||||
# },
|
||||
# 'image': {
|
||||
# 'toolbar': ['imageTextAlternative', '|', 'imageStyle:alignLeft',
|
||||
# 'imageStyle:alignRight', 'imageStyle:alignCenter', 'imageStyle:side', '|'],
|
||||
# 'styles': [
|
||||
# 'full',
|
||||
# 'side',
|
||||
# 'alignLeft',
|
||||
# 'alignRight',
|
||||
# 'alignCenter',
|
||||
# ]
|
||||
Q_CLUSTER = {
|
||||
'name': 'KAAUH_CLUSTER',
|
||||
'workers': 4,
|
||||
'recycle': 500,
|
||||
'timeout': 60,
|
||||
'compress': True,
|
||||
'save_limit': 250,
|
||||
'queue_limit': 500,
|
||||
'cpu_affinity': 1,
|
||||
'label': 'Django Q2',
|
||||
'redis': {
|
||||
'host': '127.0.0.1',
|
||||
'port': 6379,
|
||||
'db': 0, },
|
||||
'ALT_CLUSTERS': {
|
||||
'long': {
|
||||
'timeout': 3000,
|
||||
'retry': 3600,
|
||||
'max_attempts': 2,
|
||||
},
|
||||
'short': {
|
||||
'timeout': 10,
|
||||
'max_attempts': 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
# },
|
||||
# 'table': {
|
||||
# 'contentToolbar': [ 'tableColumn', 'tableRow', 'mergeTableCells',
|
||||
# 'tableProperties', 'tableCellProperties' ],
|
||||
# 'tableProperties': {
|
||||
# 'borderColors': customColorPalette,
|
||||
# 'backgroundColors': customColorPalette
|
||||
# },
|
||||
# 'tableCellProperties': {
|
||||
# 'borderColors': customColorPalette,
|
||||
# 'backgroundColors': customColorPalette
|
||||
# }
|
||||
# },
|
||||
# 'heading' : {
|
||||
# 'options': [
|
||||
# { 'model': 'paragraph', 'title': 'Paragraph', 'class': 'ck-heading_paragraph' },
|
||||
# { 'model': 'heading1', 'view': 'h1', 'title': 'Heading 1', 'class': 'ck-heading_heading1' },
|
||||
# { 'model': 'heading2', 'view': 'h2', 'title': 'Heading 2', 'class': 'ck-heading_heading2' },
|
||||
# { 'model': 'heading3', 'view': 'h3', 'title': 'Heading 3', 'class': 'ck-heading_heading3' }
|
||||
# ]
|
||||
# }
|
||||
# },
|
||||
# 'list': {
|
||||
# 'properties': {
|
||||
# 'styles': 'true',
|
||||
# 'startIndex': 'true',
|
||||
# 'reversed': 'true',
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
# The customColorPalette constant must be defined before CKEDITOR_5_CONFIGS
|
||||
customColorPalette = [
|
||||
{
|
||||
'color': 'hsl(4, 90%, 58%)',
|
||||
'label': 'Red'
|
||||
},
|
||||
{
|
||||
'color': 'hsl(340, 82%, 52%)',
|
||||
'label': 'Pink'
|
||||
},
|
||||
{
|
||||
'color': 'hsl(291, 64%, 42%)',
|
||||
'label': 'Purple'
|
||||
},
|
||||
{
|
||||
'color': 'hsl(262, 52%, 47%)',
|
||||
'label': 'Deep Purple'
|
||||
},
|
||||
{
|
||||
'color': 'hsl(231, 48%, 48%)',
|
||||
'label': 'Indigo'
|
||||
},
|
||||
{
|
||||
'color': 'hsl(207, 90%, 54%)',
|
||||
'label': 'Blue'
|
||||
},
|
||||
]
|
||||
{
|
||||
'color': 'hsl(4, 90%, 58%)',
|
||||
'label': 'Red'
|
||||
},
|
||||
{
|
||||
'color': 'hsl(340, 82%, 52%)',
|
||||
'label': 'Pink'
|
||||
},
|
||||
{
|
||||
'color': 'hsl(291, 64%, 42%)',
|
||||
'label': 'Purple'
|
||||
},
|
||||
{
|
||||
'color': 'hsl(262, 52%, 47%)',
|
||||
'label': 'Deep Purple'
|
||||
},
|
||||
{
|
||||
'color': 'hsl(231, 48%, 48%)',
|
||||
'label': 'Indigo'
|
||||
},
|
||||
{
|
||||
'color': 'hsl(207, 90%, 54%)',
|
||||
'label': 'Blue'
|
||||
},
|
||||
]
|
||||
|
||||
CKEDITOR_5_CUSTOM_CSS = 'path_to.css' # optional
|
||||
CKEDITOR_5_FILE_STORAGE = "path_to_storage.CustomStorage" # optional
|
||||
CKEDITOR_5_CONFIGS = {
|
||||
'default': {
|
||||
'toolbar': {
|
||||
@ -354,7 +291,6 @@ CKEDITOR_5_CONFIGS = {
|
||||
}
|
||||
|
||||
},
|
||||
# Your existing 'extends' configuration remains unchanged
|
||||
'extends': {
|
||||
'blockToolbar': [
|
||||
'paragraph', 'heading1', 'heading2', 'heading3',
|
||||
@ -405,25 +341,14 @@ CKEDITOR_5_CONFIGS = {
|
||||
]
|
||||
}
|
||||
},
|
||||
# Your existing 'list' configuration remains unchanged
|
||||
'list': {
|
||||
'properties': {
|
||||
'styles': 'true',
|
||||
'startIndex': 'true',
|
||||
'reversed': 'true',
|
||||
}
|
||||
},
|
||||
|
||||
# *** NEW 'comment' CONFIGURATION ***
|
||||
'comment': {
|
||||
'toolbar': {
|
||||
'items': [
|
||||
'bold', 'italic', 'underline', 'link',
|
||||
'bulletedList', 'numberedList', 'blockQuote',
|
||||
'|', 'undo', 'redo'
|
||||
],
|
||||
},
|
||||
# You can add other specific configurations for a comment field here,
|
||||
# such as disabling image upload or advanced features to keep it lightweight.
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Define a constant in settings.py to specify file upload permissions
|
||||
CKEDITOR_5_FILE_UPLOAD_PERMISSION = "staff" # Possible values: "staff", "authenticated", "any"
|
||||
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.
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.
@ -143,7 +143,7 @@ class JobPostingAdmin(admin.ModelAdmin):
|
||||
|
||||
@admin.register(Candidate)
|
||||
class CandidateAdmin(admin.ModelAdmin):
|
||||
list_display = ['full_name', 'job', 'email', 'phone', 'stage', 'applied', 'created_at']
|
||||
list_display = ['full_name', 'job', 'email', 'phone', 'stage', 'applied','is_resume_parsed', 'created_at']
|
||||
list_filter = ['stage', 'applied', 'created_at', 'job__department']
|
||||
search_fields = ['first_name', 'last_name', 'email', 'phone']
|
||||
readonly_fields = ['slug', 'created_at', 'updated_at']
|
||||
|
||||
@ -195,9 +195,9 @@ class JobPostingForm(forms.ModelForm):
|
||||
fields = [
|
||||
'title', 'department', 'job_type', 'workplace_type',
|
||||
'location_city', 'location_state', 'location_country',
|
||||
'description', 'qualifications', 'salary_range', 'benefits'
|
||||
'description', 'qualifications', 'salary_range', 'benefits','application_start_date'
|
||||
,'application_deadline', 'application_instructions',
|
||||
'position_number', 'reporting_to', 'start_date', 'status',
|
||||
'position_number', 'reporting_to', 'joining_date', 'status',
|
||||
'created_by','open_positions','hash_tags'
|
||||
]
|
||||
widgets = {
|
||||
@ -247,6 +247,10 @@ class JobPostingForm(forms.ModelForm):
|
||||
# 'placeholder': 'https://university.edu/careers/job123',
|
||||
# 'required': True
|
||||
# }),
|
||||
'application_start_date': forms.DateInput(attrs={
|
||||
'class': 'form-control',
|
||||
'type': 'date'
|
||||
}),
|
||||
'application_deadline': forms.DateInput(attrs={
|
||||
'class': 'form-control',
|
||||
'type': 'date'
|
||||
@ -272,7 +276,7 @@ class JobPostingForm(forms.ModelForm):
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Department Chair, Director, etc.'
|
||||
}),
|
||||
'start_date': forms.DateInput(attrs={
|
||||
'joining_date': forms.DateInput(attrs={
|
||||
'class': 'form-control',
|
||||
'type': 'date'
|
||||
}),
|
||||
@ -333,31 +337,31 @@ class JobPostingForm(forms.ModelForm):
|
||||
raise forms.ValidationError('Please enter a valid URL (e.g., https://example.com)')
|
||||
return url
|
||||
|
||||
def clean(self):
|
||||
"""Cross-field validation"""
|
||||
cleaned_data = super().clean()
|
||||
# def clean(self):
|
||||
# """Cross-field validation"""
|
||||
# cleaned_data = super().clean()
|
||||
|
||||
# Validate dates
|
||||
start_date = cleaned_data.get('start_date')
|
||||
application_deadline = cleaned_data.get('application_deadline')
|
||||
# # Validate dates
|
||||
# start_date = cleaned_data.get('start_date')
|
||||
# application_deadline = cleaned_data.get('application_deadline')
|
||||
|
||||
# Perform cross-field validation only if both fields have values
|
||||
if start_date and application_deadline:
|
||||
if application_deadline > start_date:
|
||||
self.add_error('application_deadline',
|
||||
'The application deadline must be set BEFORE the job start date.')
|
||||
# # Perform cross-field validation only if both fields have values
|
||||
# if start_date and application_deadline:
|
||||
# if application_deadline > start_date:
|
||||
# self.add_error('application_deadline',
|
||||
# 'The application deadline must be set BEFORE the job start date.')
|
||||
|
||||
# # Validate that if status is ACTIVE, we have required fields
|
||||
# status = cleaned_data.get('status')
|
||||
# if status == 'ACTIVE':
|
||||
# if not cleaned_data.get('application_url'):
|
||||
# self.add_error('application_url',
|
||||
# 'Application URL is required for active jobs.')
|
||||
# if not cleaned_data.get('description'):
|
||||
# self.add_error('description',
|
||||
# 'Job description is required for active jobs.')
|
||||
# # # Validate that if status is ACTIVE, we have required fields
|
||||
# # status = cleaned_data.get('status')
|
||||
# # if status == 'ACTIVE':
|
||||
# # if not cleaned_data.get('application_url'):
|
||||
# # self.add_error('application_url',
|
||||
# # 'Application URL is required for active jobs.')
|
||||
# # if not cleaned_data.get('description'):
|
||||
# # self.add_error('description',
|
||||
# # 'Job description is required for active jobs.')
|
||||
|
||||
return cleaned_data
|
||||
# return cleaned_data
|
||||
|
||||
class JobPostingImageForm(forms.ModelForm):
|
||||
class Meta:
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-11 11:04
|
||||
# Generated by Django 5.2.6 on 2025-10-12 10:34
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
@ -220,6 +220,7 @@ class Migration(migrations.Migration):
|
||||
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
|
||||
('applied', models.BooleanField(default=False, verbose_name='Applied')),
|
||||
('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], default='Applied', max_length=100, verbose_name='Stage')),
|
||||
('applicant_status', models.CharField(blank=True, choices=[('Applicant', 'Applicant'), ('Candidate', 'Candidate')], default='Applicant', max_length=100, null=True, verbose_name='Applicant Status')),
|
||||
('exam_date', models.DateField(blank=True, null=True, verbose_name='Exam Date')),
|
||||
('exam_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Exam Status')),
|
||||
('interview_date', models.DateField(blank=True, null=True, verbose_name='Interview Date')),
|
||||
@ -321,9 +322,7 @@ class Migration(migrations.Migration):
|
||||
name='JobPostingImage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('post_image', models.ImageField(height_field='photo_height', upload_to='post/', width_field='photo_width')),
|
||||
('post_image_height', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('post_image_width', models.PositiveIntegerField(blank=True, null=True)),
|
||||
('post_image', models.ImageField(upload_to='post/')),
|
||||
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='post_images', to='recruitment.jobposting')),
|
||||
],
|
||||
),
|
||||
|
||||
18
recruitment/migrations/0002_alter_jobposting_status.py
Normal file
18
recruitment/migrations/0002_alter_jobposting_status.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-12 10:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='jobposting',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], default='DRAFT', max_length=20),
|
||||
),
|
||||
]
|
||||
@ -1,26 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-11 12:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='jobpostingimage',
|
||||
name='post_image_height',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='jobpostingimage',
|
||||
name='post_image_width',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='jobpostingimage',
|
||||
name='post_image',
|
||||
field=models.ImageField(upload_to='post/'),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-12 13:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0002_alter_jobposting_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RenameField(
|
||||
model_name='jobposting',
|
||||
old_name='start_date',
|
||||
new_name='joining_date',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='jobposting',
|
||||
name='application_start_date',
|
||||
field=models.DateField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
@ -79,7 +79,7 @@ class JobPosting(Base):
|
||||
|
||||
# Job Details
|
||||
description = CKEditor5Field(
|
||||
'Description',
|
||||
'Description',
|
||||
config_name='extends' # Matches the config name you defined in settings.py
|
||||
)
|
||||
|
||||
@ -98,6 +98,7 @@ class JobPosting(Base):
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
application_start_date=models.DateField(null=True, blank=True)
|
||||
application_deadline = models.DateField(null=True, blank=True)
|
||||
application_instructions =CKEditor5Field(
|
||||
blank=True, null=True,config_name='extends'
|
||||
@ -118,7 +119,7 @@ class JobPosting(Base):
|
||||
("ARCHIVED", "Archived"),
|
||||
]
|
||||
status = models.CharField(
|
||||
max_length=20, choices=STATUS_CHOICES, default="DRAFT", null=True, blank=True
|
||||
max_length=20, choices=STATUS_CHOICES, default="DRAFT"
|
||||
)
|
||||
|
||||
# hashtags for social media
|
||||
@ -150,7 +151,7 @@ class JobPosting(Base):
|
||||
reporting_to = models.CharField(
|
||||
max_length=100, blank=True, help_text="Who this position reports to"
|
||||
)
|
||||
start_date = models.DateField(null=True, blank=True, help_text="Desired start date")
|
||||
joining_date = models.DateField(null=True, blank=True, help_text="Desired start date")
|
||||
open_positions = models.PositiveIntegerField(
|
||||
default=1, help_text="Number of open positions for this job"
|
||||
)
|
||||
@ -250,7 +251,7 @@ class JobPosting(Base):
|
||||
class JobPostingImage(models.Model):
|
||||
job=models.ForeignKey('JobPosting',on_delete=models.CASCADE,related_name='post_images')
|
||||
post_image = models.ImageField(upload_to='post/')
|
||||
|
||||
|
||||
|
||||
class Candidate(Base):
|
||||
class Stage(models.TextChoices):
|
||||
@ -267,6 +268,10 @@ class Candidate(Base):
|
||||
ACCEPTED = "Accepted", _("Accepted")
|
||||
REJECTED = "Rejected", _("Rejected")
|
||||
|
||||
class ApplicantType(models.TextChoices):
|
||||
APPLICANT = "Applicant", _("Applicant")
|
||||
CANDIDATE = "Candidate", _("Candidate")
|
||||
|
||||
# Stage transition validation constants
|
||||
STAGE_SEQUENCE = {
|
||||
"Applied": ["Exam", "Interview", "Offer"],
|
||||
@ -298,7 +303,14 @@ class Candidate(Base):
|
||||
choices=Stage.choices,
|
||||
verbose_name=_("Stage"),
|
||||
)
|
||||
|
||||
applicant_status = models.CharField(
|
||||
choices=ApplicantType.choices,
|
||||
default="Applicant",
|
||||
max_length=100,
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("Applicant Status"),
|
||||
)
|
||||
exam_date = models.DateField(null=True, blank=True, verbose_name=_("Exam Date"))
|
||||
exam_status = models.CharField(
|
||||
choices=ExamStatus.choices,
|
||||
|
||||
@ -1,136 +1,21 @@
|
||||
from . import models
|
||||
from django.urls import reverse
|
||||
import logging
|
||||
from django.db import transaction
|
||||
from django.dispatch import receiver
|
||||
from django_q.tasks import async_task
|
||||
from django.db.models.signals import post_save
|
||||
from .models import FormField,FormStage,FormTemplate
|
||||
from .models import FormField,FormStage,FormTemplate,Candidate
|
||||
|
||||
# @receiver(post_save, sender=models.Candidate)
|
||||
# def parse_resume(sender, instance, created, **kwargs):
|
||||
# if instance.resume and not instance.summary:
|
||||
# from .utils import extract_summary_from_pdf,match_resume_with_job_description
|
||||
# summary = extract_summary_from_pdf(instance.resume.path)
|
||||
# if 'error' not in summary:
|
||||
# instance.summary = summary
|
||||
# instance.save()
|
||||
|
||||
# match_resume_with_job_description
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
import os
|
||||
from .utils import extract_text_from_pdf,score_resume_with_openrouter
|
||||
import asyncio
|
||||
|
||||
@receiver(post_save, sender=models.Candidate)
|
||||
@receiver(post_save, sender=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
|
||||
if not os.path.exists(file_path):
|
||||
logger.warning(f"Resume file not found: {file_path}")
|
||||
return
|
||||
|
||||
resume_text = extract_text_from_pdf(file_path)
|
||||
# if not resume_text:
|
||||
# instance.scoring_error = "Could not extract text from resume."
|
||||
# instance.save(update_fields=['scoring_error'])
|
||||
# return
|
||||
job_detail=str(instance.job.description)+str(instance.job.qualifications)
|
||||
prompt1 = f"""
|
||||
You are an expert resume parser and summarizer. Given a resume in plain text format, extract and organize the following key-value information into a clean, valid JSON object:
|
||||
|
||||
full_name: Full name of the candidate
|
||||
current_title: Most recent or current job title
|
||||
location: City and state (or country if outside the U.S.)
|
||||
contact: Phone number and email (as a single string or separate fields)
|
||||
linkedin: LinkedIn profile URL (if present)
|
||||
github: GitHub or portfolio URL (if present)
|
||||
summary: Brief professional profile or summary (1–2 sentences)
|
||||
education: List of degrees, each with:
|
||||
institution
|
||||
degree
|
||||
year
|
||||
gpa (if provided)
|
||||
relevant_courses (as a list, if mentioned)
|
||||
skills: Grouped by category if possible (e.g., programming, big data, visualization), otherwise as a flat list of technologies/tools
|
||||
experience: List of roles, each with:
|
||||
company
|
||||
job_title
|
||||
location
|
||||
start_date and end_date (or "Present" if applicable)
|
||||
key_achievements (as a list of concise bullet points)
|
||||
projects: List of notable projects (if clearly labeled), each with:
|
||||
name
|
||||
year
|
||||
technologies_used
|
||||
brief_description
|
||||
Instructions:
|
||||
|
||||
Be concise but preserve key details.
|
||||
Normalize formatting (e.g., “Jun. 2014” → “2014-06”).
|
||||
Omit redundant or promotional language.
|
||||
If a section is missing, omit the key or set it to null/empty list as appropriate.
|
||||
Output only valid JSON—no markdown, no extra text.
|
||||
Now, process the following resume text:
|
||||
{resume_text}
|
||||
"""
|
||||
result = score_resume_with_openrouter(prompt1)
|
||||
prompt = f"""
|
||||
You are an expert technical recruiter. Your task is to score the following candidate for the role of a Senior Data Analyst based on the provided job criteria.
|
||||
|
||||
**Job Criteria:**
|
||||
{job_detail}
|
||||
|
||||
**Candidate's Extracted Resume Json:**
|
||||
\"\"\"
|
||||
{result}
|
||||
\"\"\"
|
||||
|
||||
**Your Task:**
|
||||
Provide a response in strict JSON format with the following keys:
|
||||
1. 'match_score': A score from 0 to 100 representing how well the candidate fits the role.
|
||||
2. 'strengths': A brief summary of why the candidate is a strong fit, referencing specific criteria.
|
||||
3. 'weaknesses': A brief summary of where the candidate falls short or what criteria are missing.
|
||||
4. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}).
|
||||
|
||||
|
||||
Only output valid JSON. Do not include any other text.
|
||||
"""
|
||||
|
||||
result1 = score_resume_with_openrouter(prompt)
|
||||
|
||||
instance.parsed_summary = str(result)
|
||||
|
||||
# Update candidate with scoring results
|
||||
instance.match_score = result1.get('match_score')
|
||||
instance.strengths = result1.get('strengths', '')
|
||||
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', 'is_resume_parsed'
|
||||
])
|
||||
|
||||
logger.info(f"Successfully scored resume for candidate {instance.id}")
|
||||
|
||||
except Exception as e:
|
||||
# error_msg = str(e)[:500] # Truncate to fit TextField
|
||||
# instance.scoring_error = error_msg
|
||||
# instance.save(update_fields=['scoring_error'])
|
||||
logger.error(f"Failed to score resume for candidate {instance.id}: {e}")
|
||||
|
||||
|
||||
# @receiver(post_save,sender=models.Candidate)
|
||||
# def trigger_scoring(sender,intance,created,**kwargs):
|
||||
|
||||
|
||||
if not instance.is_resume_parsed:
|
||||
logger.info(f"Scoring resume for candidate {instance.pk}")
|
||||
async_task(
|
||||
'recruitment.tasks.handle_reume_parsing_and_scoring',
|
||||
instance.pk,
|
||||
# hook='myapp.tasks.email_sent_callback' # Optional callback
|
||||
)
|
||||
|
||||
@receiver(post_save, sender=FormTemplate)
|
||||
def create_default_stages(sender, instance, created, **kwargs):
|
||||
|
||||
155
recruitment/tasks.py
Normal file
155
recruitment/tasks.py
Normal file
@ -0,0 +1,155 @@
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
import requests
|
||||
from PyPDF2 import PdfReader
|
||||
from recruitment.models import Candidate
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OPENROUTER_API_KEY ='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'
|
||||
OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free'
|
||||
|
||||
if not OPENROUTER_API_KEY:
|
||||
logger.warning("OPENROUTER_API_KEY not set. Resume scoring will be skipped.")
|
||||
|
||||
def extract_text_from_pdf(file_path):
|
||||
print("text extraction")
|
||||
text = ""
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
reader = PdfReader(f)
|
||||
for page in reader.pages:
|
||||
text += (page.extract_text() or "")
|
||||
except Exception as e:
|
||||
logger.error(f"PDF extraction failed: {e}")
|
||||
raise
|
||||
return text.strip()
|
||||
|
||||
def ai_handler(prompt):
|
||||
print("model call")
|
||||
response = requests.post(
|
||||
url="https://openrouter.ai/api/v1/chat/completions",
|
||||
headers={
|
||||
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
data=json.dumps({
|
||||
"model": OPENROUTER_MODEL,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
},
|
||||
)
|
||||
)
|
||||
res = {}
|
||||
print(response.status_code)
|
||||
if response.status_code == 200:
|
||||
res = response.json()
|
||||
content = res["choices"][0]['message']['content']
|
||||
try:
|
||||
|
||||
content = content.replace("```json","").replace("```","")
|
||||
|
||||
res = json.loads(content)
|
||||
|
||||
except Exception as e:
|
||||
print(e)
|
||||
|
||||
# res = raw_output["choices"][0]["message"]["content"]
|
||||
else:
|
||||
print("error response")
|
||||
return res
|
||||
|
||||
def handle_reume_parsing_and_scoring(pk):
|
||||
logger.info(f"Scoring resume for candidate {pk}")
|
||||
try:
|
||||
instance = Candidate.objects.get(pk=pk)
|
||||
file_path = instance.resume.path
|
||||
if not os.path.exists(file_path):
|
||||
logger.warning(f"Resume file not found: {file_path}")
|
||||
return
|
||||
|
||||
resume_text = extract_text_from_pdf(file_path)
|
||||
job_detail= f"{instance.job.description} {instance.job.qualifications}"
|
||||
resume_parser_prompt = f"""
|
||||
You are an expert resume parser and summarizer. Given a resume in plain text format, extract and organize the following key-value information into a clean, valid JSON object:
|
||||
|
||||
full_name: Full name of the candidate
|
||||
current_title: Most recent or current job title
|
||||
location: City and state (or country if outside the U.S.)
|
||||
contact: Phone number and email (as a single string or separate fields)
|
||||
linkedin: LinkedIn profile URL (if present)
|
||||
github: GitHub or portfolio URL (if present)
|
||||
summary: Brief professional profile or summary (1–2 sentences)
|
||||
education: List of degrees, each with:
|
||||
institution
|
||||
degree
|
||||
year
|
||||
gpa (if provided)
|
||||
relevant_courses (as a list, if mentioned)
|
||||
skills: Grouped by category if possible (e.g., programming, big data, visualization), otherwise as a flat list of technologies/tools
|
||||
experience: List of roles, each with:
|
||||
company
|
||||
job_title
|
||||
location
|
||||
start_date and end_date (or "Present" if applicable)
|
||||
key_achievements (as a list of concise bullet points)
|
||||
projects: List of notable projects (if clearly labeled), each with:
|
||||
name
|
||||
year
|
||||
technologies_used
|
||||
brief_description
|
||||
Instructions:
|
||||
|
||||
Be concise but preserve key details.
|
||||
Normalize formatting (e.g., “Jun. 2014” → “2014-06”).
|
||||
Omit redundant or promotional language.
|
||||
If a section is missing, omit the key or set it to null/empty list as appropriate.
|
||||
Output only valid JSON—no markdown, no extra text.
|
||||
Now, process the following resume text:
|
||||
{resume_text}
|
||||
"""
|
||||
resume_parser_result = ai_handler(resume_parser_prompt)
|
||||
resume_scoring_prompt = f"""
|
||||
You are an expert technical recruiter. Your task is to score the following candidate for the role of a Senior Data Analyst based on the provided job criteria.
|
||||
|
||||
**Job Criteria:**
|
||||
{job_detail}
|
||||
|
||||
**Candidate's Extracted Resume Json:**
|
||||
\"\"\"
|
||||
{resume_parser_result}
|
||||
\"\"\"
|
||||
|
||||
**Your Task:**
|
||||
Provide a response in strict JSON format with the following keys:
|
||||
1. 'match_score': A score from 0 to 100 representing how well the candidate fits the role.
|
||||
2. 'strengths': A brief summary of why the candidate is a strong fit, referencing specific criteria.
|
||||
3. 'weaknesses': A brief summary of where the candidate falls short or what criteria are missing.
|
||||
4. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}).
|
||||
|
||||
|
||||
Only output valid JSON. Do not include any other text.
|
||||
"""
|
||||
|
||||
resume_scoring_result = ai_handler(resume_scoring_prompt)
|
||||
|
||||
instance.parsed_summary = str(resume_parser_result)
|
||||
|
||||
# Update candidate with scoring results
|
||||
instance.match_score = resume_scoring_result.get('match_score')
|
||||
instance.strengths = resume_scoring_result.get('strengths', '')
|
||||
instance.weaknesses = resume_scoring_result.get('weaknesses', '')
|
||||
instance.criteria_checklist = resume_scoring_result.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', 'is_resume_parsed'
|
||||
])
|
||||
|
||||
logger.info(f"Successfully scored resume for candidate {instance.id}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to score resume for candidate {instance.id}: {e}")
|
||||
Binary file not shown.
Binary file not shown.
@ -15,38 +15,60 @@ def get_stage_responses(stage_responses, stage_id):
|
||||
return []
|
||||
|
||||
@register.simple_tag
|
||||
def get_all_responses_flat(stage_responses):
|
||||
def get_all_responses_flat(submission):
|
||||
"""
|
||||
Template tag to get all responses flattened for table display.
|
||||
Usage: {% get_all_responses_flat stage_responses as all_responses %}
|
||||
Template tag to get all responses from a FormSubmission flattened for table display.
|
||||
Usage: {% get_all_responses_flat submission as flat_responses %}
|
||||
"""
|
||||
all_responses = []
|
||||
if stage_responses:
|
||||
for stage_id, responses in stage_responses.items():
|
||||
if responses: # Check if responses list exists and is not empty
|
||||
for response in responses:
|
||||
# Check if response is an object or string
|
||||
if hasattr(response, 'stage') and hasattr(response, 'field'):
|
||||
stage_name = response.stage.name if hasattr(response.stage, 'name') else f"Stage {stage_id}"
|
||||
field_label = response.field.label if hasattr(response.field, 'label') else "Unknown Field"
|
||||
field_type = response.field.get_field_type_display() if hasattr(response.field, 'get_field_type_display') else "Unknown Type"
|
||||
required = response.field.required if hasattr(response.field, 'required') else False
|
||||
value = response.value if hasattr(response, 'value') else response
|
||||
uploaded_file = response.uploaded_file if hasattr(response, 'uploaded_file') else None
|
||||
else:
|
||||
stage_name = f"Stage {stage_id}"
|
||||
field_label = "Unknown Field"
|
||||
field_type = "Text"
|
||||
required = False
|
||||
value = response
|
||||
uploaded_file = None
|
||||
if submission:
|
||||
# Fetch all responses related to this submission, selecting related field and stage objects for efficiency
|
||||
field_responses = submission.responses.all().select_related('field', 'field__stage').order_by('field__stage__order', 'field__order')
|
||||
|
||||
all_responses.append({
|
||||
'stage_name': stage_name,
|
||||
'field_label': field_label,
|
||||
'field_type': field_type,
|
||||
'required': required,
|
||||
'value': value,
|
||||
'uploaded_file': uploaded_file
|
||||
})
|
||||
for response in field_responses:
|
||||
stage_name = "N/A"
|
||||
field_label = "Unknown Field"
|
||||
field_type = "Text"
|
||||
required = False
|
||||
value = None
|
||||
uploaded_file = None
|
||||
|
||||
if response.field:
|
||||
field_label = response.field.label
|
||||
field_type = response.field.get_field_type_display()
|
||||
required = response.field.required
|
||||
if response.field.stage:
|
||||
stage_name = response.field.stage.name
|
||||
|
||||
value = response.value
|
||||
uploaded_file = response.uploaded_file
|
||||
|
||||
all_responses.append({
|
||||
'stage_name': stage_name,
|
||||
'field_label': field_label,
|
||||
'field_type': field_type,
|
||||
'required': required,
|
||||
'value': value,
|
||||
'uploaded_file': uploaded_file
|
||||
})
|
||||
return all_responses
|
||||
|
||||
|
||||
@register.simple_tag
|
||||
def get_field_response_for_submission(submission, field):
|
||||
"""
|
||||
Template tag to get the FieldResponse for a specific submission and field.
|
||||
Usage: {% get_field_response_for_submission submission field as response %}
|
||||
"""
|
||||
try:
|
||||
return submission.responses.filter(field=field).first()
|
||||
except:
|
||||
return None
|
||||
|
||||
@register.filter
|
||||
def to_list(data):
|
||||
"""
|
||||
Template tag to convert a string to a list.
|
||||
Usage: {% to_list "item1,item2,item3" as list %}
|
||||
"""
|
||||
return data.split(",") if data else []
|
||||
|
||||
@ -9,12 +9,13 @@ urlpatterns = [
|
||||
# Job URLs (using JobPosting model)
|
||||
path('jobs/', views_frontend.JobListView.as_view(), name='job_list'),
|
||||
path('jobs/create/', views.create_job, name='job_create'),
|
||||
path('job/<slug:slug>/upload_image_simple/', views.job_image_upload, name='job_image_upload'),
|
||||
path('job/<slug:slug>/upload_image_simple/', views.job_image_upload, name='job_image_upload'),
|
||||
path('jobs/<slug:slug>/update/', views.edit_job, name='job_update'),
|
||||
# path('jobs/<slug:slug>/delete/', views., name='job_delete'),
|
||||
path('jobs/<slug:slug>/', views.job_detail, name='job_detail'),
|
||||
path('jobs/<slug:slug>/candidate/', views.job_detail_candidate, name='job_detail_candidate'),
|
||||
path('jobs/<slug:slug>/candidate/application/success', views.application_success, name='application_success'),
|
||||
path('careers/',views.kaauh_career,name='kaauh_career'),
|
||||
|
||||
|
||||
# LinkedIn Integration URLs
|
||||
@ -32,7 +33,7 @@ urlpatterns = [
|
||||
path('candidates/<slug:slug>/delete/', views_frontend.CandidateDeleteView.as_view(), name='candidate_delete'),
|
||||
path('candidate/<slug:slug>/view/', views_frontend.candidate_detail, name='candidate_detail'),
|
||||
path('candidate/<slug:slug>/update-stage/', views_frontend.candidate_update_stage, name='candidate_update_stage'),
|
||||
|
||||
|
||||
|
||||
# Training URLs
|
||||
path('training/', views_frontend.TrainingListView.as_view(), name='training_list'),
|
||||
@ -64,11 +65,14 @@ urlpatterns = [
|
||||
path('forms/builder/<int:template_id>/', views.form_builder, name='form_builder'),
|
||||
path('forms/', views.form_templates_list, name='form_templates_list'),
|
||||
path('forms/create-template/', views.create_form_template, name='create_form_template'),
|
||||
path('jobs/<slug:slug>/candidate-tiers/', views.candidate_tier_management_view, name='candidate_tier_management'),
|
||||
path('htmx/<int:pk>/candidate_criteria_view/', views.candidate_criteria_view_htmx, name='candidate_criteria_view_htmx'),
|
||||
|
||||
# path('forms/form/<int:template_id>/submit/', views.submit_form, name='submit_form'),
|
||||
# path('forms/form/<int:template_id>/', views.form_wizard_view, name='form_wizard'),
|
||||
path('forms/<int:form_id>/submissions/<int:submission_id>/', views.form_submission_details, name='form_submission_details'),
|
||||
path('forms/<int:template_id>/submissions/<slug:slug>/', views.form_submission_details, name='form_submission_details'),
|
||||
path('forms/template/<slug:slug>/submissions/', views.form_template_submissions_list, name='form_template_submissions_list'),
|
||||
path('forms/template/<int:template_id>/all-submissions/', views.form_template_all_submissions, name='form_template_all_submissions'),
|
||||
|
||||
# path('forms/<int:form_id>/', views.form_preview, name='form_preview'),
|
||||
# path('forms/<int:form_id>/submit/', views.form_submit, name='form_submit'),
|
||||
|
||||
@ -276,17 +276,19 @@ def job_detail(request, slug):
|
||||
|
||||
|
||||
# Get all candidates for this job, ordered by most recent
|
||||
candidates = job.candidates.all().order_by("-created_at")
|
||||
applicants = job.candidates.all().order_by("-created_at")
|
||||
|
||||
# Count candidates by stage for summary statistics
|
||||
total_candidates = candidates.count()
|
||||
applied_count = candidates.filter(stage="Applied").count()
|
||||
interview_count = candidates.filter(stage="Interview").count()
|
||||
offer_count = candidates.filter(stage="Offer").count()
|
||||
total_applicant = applicants.count()
|
||||
applied_count = applicants.filter(stage="Applied").count()
|
||||
interview_count = applicants.filter(stage="Interview").count()
|
||||
offer_count = applicants.filter(stage="Offer").count()
|
||||
|
||||
status_form = JobPostingStatusForm(instance=job)
|
||||
image_upload_form=JobPostingImageForm(instance=job)
|
||||
|
||||
|
||||
|
||||
# 2. Check for POST request (Status Update Submission)
|
||||
if request.method == 'POST':
|
||||
|
||||
@ -308,8 +310,8 @@ def job_detail(request, slug):
|
||||
|
||||
context = {
|
||||
"job": job,
|
||||
"candidates": candidates,
|
||||
"total_candidates": total_candidates,
|
||||
"applicants": applicants,
|
||||
"total_applicants": total_applicant,
|
||||
"applied_count": applied_count,
|
||||
"interview_count": interview_count,
|
||||
"offer_count": offer_count,
|
||||
@ -338,6 +340,16 @@ def job_image_upload(request, slug):
|
||||
return redirect('job_detail', slug=job.slug)
|
||||
|
||||
|
||||
def kaauh_career(request):
|
||||
active_jobs = JobPosting.objects.select_related(
|
||||
'form_template'
|
||||
).filter(
|
||||
status='ACTIVE',
|
||||
form_template__is_active=True
|
||||
)
|
||||
|
||||
return render(request,'jobs/career.html',{'active_jobs':active_jobs})
|
||||
|
||||
|
||||
|
||||
# job detail facing the candidate:
|
||||
@ -991,13 +1003,38 @@ def form_template_submissions_list(request, slug):
|
||||
)
|
||||
|
||||
|
||||
def form_submission_details(request, template_id, submission_id):
|
||||
def form_template_all_submissions(request, template_id):
|
||||
"""Display all submissions for a form template in table format"""
|
||||
template = get_object_or_404(FormTemplate, id=template_id)
|
||||
print(template)
|
||||
# Get all submissions for this template
|
||||
submissions = FormSubmission.objects.filter(template=template).order_by("-submitted_at")
|
||||
|
||||
# Get all fields for this template, ordered by stage and field order
|
||||
fields = FormField.objects.filter(stage__template=template).select_related('stage').order_by('stage__order', 'order')
|
||||
|
||||
# Pagination
|
||||
paginator = Paginator(submissions, 10) # Show 10 submissions per page
|
||||
page_number = request.GET.get("page")
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"forms/form_template_all_submissions.html",
|
||||
{
|
||||
"template": template,
|
||||
"page_obj": page_obj,
|
||||
"fields": fields,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def form_submission_details(request, template_id, slug):
|
||||
"""Display detailed view of a specific form submission"""
|
||||
# Get the form template and verify ownership
|
||||
template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user)
|
||||
|
||||
template = get_object_or_404(FormTemplate, id=template_id)
|
||||
# Get the specific submission
|
||||
submission = get_object_or_404(FormSubmission, id=submission_id, template=template)
|
||||
submission = get_object_or_404(FormSubmission, slug=slug, template=template)
|
||||
|
||||
# Get all stages with their fields
|
||||
stages = template.stages.prefetch_related("fields").order_by("order")
|
||||
@ -1192,3 +1229,133 @@ def schedule_interviews_view(request, job_id):
|
||||
"interviews/schedule_interviews.html",
|
||||
{"form": form, "break_formset": break_formset, "job": job},
|
||||
)
|
||||
|
||||
|
||||
def candidate_tier_management_view(request, slug):
|
||||
"""
|
||||
Manage candidate tiers and stage transitions
|
||||
"""
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
|
||||
# Get all candidates for this job, ordered by match score (descending)
|
||||
candidates = job.candidates.all().order_by("-match_score")
|
||||
|
||||
# Get tier categorization parameters
|
||||
tier1_count = int(request.GET.get("tier1_count", 100))
|
||||
|
||||
# Categorize candidates into tiers
|
||||
tier1_candidates = candidates[:tier1_count] if tier1_count > 0 else []
|
||||
remaining_candidates = candidates[tier1_count:] if tier1_count > 0 else []
|
||||
|
||||
if len(remaining_candidates) > 0:
|
||||
# Tier 2: Next 50% of remaining candidates
|
||||
tier2_count = max(1, len(remaining_candidates) // 2)
|
||||
tier2_candidates = remaining_candidates[:tier2_count]
|
||||
tier3_candidates = remaining_candidates[tier2_count:]
|
||||
else:
|
||||
tier2_candidates = []
|
||||
tier3_candidates = []
|
||||
|
||||
# Handle form submissions
|
||||
if request.method == "POST":
|
||||
# Update tier categorization
|
||||
if "update_tiers" in request.POST:
|
||||
tier1_count = int(request.POST.get("tier1_count", 100))
|
||||
messages.success(request, f"Tier categorization updated. Tier 1: {tier1_count} candidates")
|
||||
return redirect("candidate_tier_management", slug=slug)
|
||||
|
||||
# Update individual candidate stages
|
||||
elif "update_stage" in request.POST:
|
||||
candidate_id = request.POST.get("candidate_id")
|
||||
new_stage = request.POST.get("new_stage")
|
||||
candidate = get_object_or_404(Candidate, id=candidate_id, job=job)
|
||||
|
||||
if candidate.can_transition_to(new_stage):
|
||||
old_stage = candidate.stage
|
||||
candidate.stage = new_stage
|
||||
candidate.save()
|
||||
messages.success(request, f"Updated {candidate.name} from {old_stage} to {new_stage}")
|
||||
else:
|
||||
messages.error(request, f"Cannot transition {candidate.name} from {candidate.stage} to {new_stage}")
|
||||
|
||||
# Update exam status
|
||||
elif "update_exam_status" in request.POST:
|
||||
candidate_id = request.POST.get("candidate_id")
|
||||
exam_status = request.POST.get("exam_status")
|
||||
exam_date = request.POST.get("exam_date")
|
||||
candidate = get_object_or_404(Candidate, id=candidate_id, job=job)
|
||||
|
||||
if candidate.stage == "Exam":
|
||||
candidate.exam_status = exam_status
|
||||
if exam_date:
|
||||
candidate.exam_date = exam_date
|
||||
candidate.save()
|
||||
messages.success(request, f"Updated exam status for {candidate.name}")
|
||||
else:
|
||||
messages.error(request, f"Can only update exam status for candidates in Exam stage")
|
||||
|
||||
# Bulk stage update
|
||||
elif "bulk_update_stage" in request.POST:
|
||||
selected_candidates = request.POST.getlist("selected_candidates")
|
||||
new_stage = request.POST.get("bulk_new_stage")
|
||||
updated_count = 0
|
||||
|
||||
for candidate_id in selected_candidates:
|
||||
candidate = get_object_or_404(Candidate, id=candidate_id, job=job)
|
||||
if candidate.can_transition_to(new_stage):
|
||||
candidate.stage = new_stage
|
||||
candidate.save()
|
||||
updated_count += 1
|
||||
|
||||
messages.success(request, f"Updated {updated_count} candidates to {new_stage} stage")
|
||||
|
||||
# Mark individual candidate as Candidate
|
||||
elif "mark_as_candidate" in request.POST:
|
||||
candidate_id = request.POST.get("candidate_id")
|
||||
candidate = get_object_or_404(Candidate, id=candidate_id, job=job)
|
||||
|
||||
if candidate.applicant_status == "Applicant":
|
||||
candidate.applicant_status = "Candidate"
|
||||
candidate.save()
|
||||
messages.success(request, f"Marked {candidate.name} as Candidate")
|
||||
else:
|
||||
messages.info(request, f"{candidate.name} is already marked as Candidate")
|
||||
|
||||
# Mark all Tier 1 candidates as Candidates
|
||||
elif "mark_as_candidates" in request.POST:
|
||||
updated_count = 0
|
||||
for candidate in tier1_candidates:
|
||||
if candidate.applicant_status == "Applicant":
|
||||
candidate.applicant_status = "Candidate"
|
||||
candidate.save()
|
||||
updated_count += 1
|
||||
|
||||
if updated_count > 0:
|
||||
messages.success(request, f"Marked {updated_count} Tier 1 candidates as Candidates")
|
||||
else:
|
||||
messages.info(request, "All Tier 1 candidates are already marked as Candidates")
|
||||
|
||||
# Group candidates by current stage for display
|
||||
stage_groups = {
|
||||
"Applied": candidates.filter(stage="Applied"),
|
||||
"Exam": candidates.filter(stage="Exam"),
|
||||
"Interview": candidates.filter(stage="Interview"),
|
||||
"Offer": candidates.filter(stage="Offer"),
|
||||
}
|
||||
|
||||
context = {
|
||||
"job": job,
|
||||
"tier1_candidates": tier1_candidates,
|
||||
"tier2_candidates": tier2_candidates,
|
||||
"tier3_candidates": tier3_candidates,
|
||||
"stage_groups": stage_groups,
|
||||
"tier1_count": tier1_count,
|
||||
"total_candidates": candidates.count(),
|
||||
}
|
||||
|
||||
return render(request, "recruitment/candidate_tier_management.html", context)
|
||||
|
||||
def candidate_criteria_view_htmx(request, pk):
|
||||
candidate = get_object_or_404(Candidate, pk=pk)
|
||||
print(candidate)
|
||||
return render(request, "includes/candidate_modal_body.html", {"candidate": candidate})
|
||||
@ -92,6 +92,8 @@ class JobCandidatesListView(LoginRequiredMixin, ListView):
|
||||
context_object_name = 'candidates'
|
||||
paginate_by = 10
|
||||
|
||||
|
||||
|
||||
def get_queryset(self):
|
||||
# Get the job by slug
|
||||
self.job = get_object_or_404(models.JobPosting, slug=self.kwargs['slug'])
|
||||
@ -99,6 +101,11 @@ class JobCandidatesListView(LoginRequiredMixin, ListView):
|
||||
# Filter candidates for this specific job
|
||||
queryset = models.Candidate.objects.filter(job=self.job)
|
||||
|
||||
if self.request.GET.get('stage'):
|
||||
stage=self.request.GET.get('stage')
|
||||
queryset=queryset.filter(stage=stage)
|
||||
|
||||
|
||||
# Handle search
|
||||
search_query = self.request.GET.get('search', '')
|
||||
if search_query:
|
||||
@ -223,7 +230,7 @@ def candidate_detail(request, slug):
|
||||
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])
|
||||
# parsed = json_to_markdown_table([parsed])
|
||||
return render(request, 'recruitment/candidate_detail.html', {
|
||||
'candidate': candidate,
|
||||
'parsed': parsed,
|
||||
|
||||
@ -21,8 +21,16 @@
|
||||
--kaauh-light-bg: #f9fbfd;
|
||||
--kaauh-border: #eaeff3;
|
||||
}
|
||||
|
||||
|
||||
/* NEW CLASS FOR WIDER CONTENT */
|
||||
.max-width-1600 {
|
||||
max-width: 1600px;
|
||||
margin-right: auto;
|
||||
margin-left: auto;
|
||||
padding-right: var(--bs-gutter-x, 0.75rem); /* Add Bootstrap padding for responsiveness */
|
||||
padding-left: var(--bs-gutter-x, 0.75rem);
|
||||
}
|
||||
|
||||
/* === Top Bar === */
|
||||
.top-bar {
|
||||
background-color: white;
|
||||
@ -45,7 +53,7 @@
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
.top-bar .logo-container img {
|
||||
height: 40px;
|
||||
height: 60px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
@ -68,6 +76,10 @@
|
||||
background-color: var(--kaauh-teal) !important;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
|
||||
}
|
||||
/* Change the outer navbar container to fluid, rely on inner max-width */
|
||||
.navbar-dark > .container {
|
||||
max-width: 100%; /* Override default container width */
|
||||
}
|
||||
.nav-link {
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
@ -205,7 +217,8 @@
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
|
||||
}
|
||||
main.container {
|
||||
/* The main content width is already handled by the inline style, but making it explicit here */
|
||||
main.container-fluid {
|
||||
min-height: calc(100vh - 200px);
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
@ -235,244 +248,265 @@
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
|
||||
<div class="top-bar d-none d-md-block">
|
||||
<div class="container d-flex justify-content-between align-items-center gap-2">
|
||||
<div class="d-flex align-items-center gap-3 social-icons">
|
||||
{% comment %} <span class="text-muted">{% trans "Follow Us:" %}</span>
|
||||
<a href="#" aria-label="Facebook"><i class="fab fa-facebook-f"></i></a>
|
||||
<a href="#" aria-label="Twitter"><i class="fab fa-twitter"></i></a>
|
||||
<a href="#" aria-label="Instagram"><i class="fab fa-instagram"></i></a> {% endcomment %}
|
||||
</div>
|
||||
<div class="contact-info d-flex gap-3">
|
||||
{% comment %} <div class="contact-item">
|
||||
<i class="fas fa-envelope text-primary"></i>
|
||||
<span>info@kaauh.edu.sa</span>
|
||||
{# Changed container to container-fluid and added max-width-1600 to inner div #}
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center gap-2 max-width-1600">
|
||||
<div class="d-flex align-items-center gap-3 social-icons">
|
||||
{% comment %} <span class="text-muted">{% trans "Follow Us:" %}</span>
|
||||
<a href="#" aria-label="Facebook"><i class="fab fa-facebook-f"></i></a>
|
||||
<a href="#" aria-label="Twitter"><i class="fab fa-twitter"></i></a>
|
||||
<a href="#" aria-label="Instagram"><i class="fab fa-instagram"></i></a> {% endcomment %}
|
||||
</div>
|
||||
<div class="contact-info d-flex gap-3">
|
||||
{% comment %} <div class="contact-item">
|
||||
<i class="fas fa-envelope text-primary"></i>
|
||||
<span>info@kaauh.edu.sa</span>
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<i class="fas fa-phone text-primary"></i>
|
||||
<span>+966 11 820 0000</span>
|
||||
</div> {% endcomment %}
|
||||
</div>
|
||||
<div class="logo-container d-flex gap-2">
|
||||
<img src="{% static 'image/vision.svg' %}" alt="{% trans 'Saudi Vision 2030' %}" loading="lazy">
|
||||
<div class="kaauh-logo-container d-flex flex-column flex-md-row align-items-center gap-2 ms-4">
|
||||
<div class="hospital-text text-center text-md-start me-3">
|
||||
<div class="ar small">جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية</div>
|
||||
<div class="ar small">ومستشفى الملك عبدالله بن عبدالعزيز التخصصي</div>
|
||||
<div class="en small">Princess Nourah bint Abdulrahman University</div>
|
||||
<div class="en small">King Abdullah bin Abdulaziz University Hospital</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<img src="{% static 'image/kaauh.png' %}" alt="KAAUH Logo" style="max-height: 100px;max-width:100px;">
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<i class="fas fa-phone text-primary"></i>
|
||||
<span>+966 11 820 0000</span>
|
||||
</div> {% endcomment %}
|
||||
</div>
|
||||
<div class="logo-container d-flex gap-2">
|
||||
<img src="{% static 'image/vision.svg' %}" alt="{% trans 'Saudi Vision 2030' %}" loading="lazy">
|
||||
<img src="{% static 'image/hospital_logo_3.png' %}" alt="{% trans 'King Abdullah Academic University Hospital' %}" loading="lazy">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
|
||||
<div class="container">
|
||||
<a class="navbar-brand text-white" href="{% url 'dashboard' %}">
|
||||
<img src="{% static 'image/kaauh.jpeg' %}" alt="{% trans 'Saudi Vision 2030' %}" style="width: 60px; height: 60px;">
|
||||
</a>
|
||||
{# Changed container to container-fluid and added max-width-1600 to inner div #}
|
||||
<div class="container-fluid">
|
||||
<div class="navbar-content-wrapper max-width-1600 d-flex justify-content-between align-items-center" style="width: 100%;">
|
||||
<a class="navbar-brand text-white d-none d-md-block" href="{% url 'dashboard' %}">
|
||||
<img src="{% static 'image/kaauh_green1.png' %}" alt="{% trans 'kaauh logo green bg' %}" style="width: 60px; height: 60px;">
|
||||
</a>
|
||||
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav" aria-expanded="false" aria-label="{% trans 'Toggle navigation' %}">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
|
||||
aria-controls="navbarNav" aria-expanded="false" aria-label="{% trans 'Toggle navigation' %}">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
{% comment %} <li class="nav-item">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}" href="{% url 'dashboard' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
{% include "icons/dashboard.html" %}
|
||||
{% trans "Dashboard" %}
|
||||
</span>
|
||||
</a>
|
||||
</li> {% endcomment %}
|
||||
<li class="nav-item me-2">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'job_list' %}active{% endif %}" href="{% url 'job_list' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
{% include "icons/jobs.html" %}
|
||||
{% trans "Jobs" %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item me-2">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'candidate_list' %}active{% endif %}" href="{% url 'candidate_list' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
{% include "icons/users.html" %}
|
||||
{% trans "Candidates" %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item me-2">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'training_list' %}active{% endif %}" href="{% url 'training_list' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||
{% comment %} <li class="nav-item">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}" href="{% url 'dashboard' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
{% include "icons/dashboard.html" %}
|
||||
{% trans "Dashboard" %}
|
||||
</span>
|
||||
</a>
|
||||
</li> {% endcomment %}
|
||||
<li class="nav-item me-2">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'job_list' %}active{% endif %}" href="{% url 'job_list' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
{% include "icons/jobs.html" %}
|
||||
{% trans "Jobs" %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item me-2">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'form_templates_list' %}active{% endif %}" href="{% url 'form_templates_list' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
||||
</svg>
|
||||
|
||||
{% trans "Form Templates" %}
|
||||
</span>
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item me-2">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'candidate_list' %}active{% endif %}" href="{% url 'candidate_list' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
{% include "icons/users.html" %}
|
||||
{% trans "Candidates" %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item me-2">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'list_meetings' %}active{% endif %}" href="{% url 'list_meetings' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.26 10.147a60.438 60.438 0 0 0-.491 6.347A48.62 48.62 0 0 1 12 20.904a48.62 48.62 0 0 1 8.232-4.41 60.46 60.46 0 0 0-.491-6.347m-15.482 0a50.636 50.636 0 0 0-2.658-.813A59.906 59.906 0 0 1 12 3.493a59.903 59.903 0 0 1 10.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.717 50.717 0 0 1 12 13.489a50.702 50.702 0 0 1 7.74-3.342M6.75 15a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm0 0v-3.675A55.378 55.378 0 0 1 12 8.443m-7.007 11.55A5.981 5.981 0 0 0 6.75 15.75v-1.5" />
|
||||
</svg>
|
||||
|
||||
{% trans "Training" %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item me-2">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'list_meetings' %}active{% endif %}" href="{% url 'list_meetings' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" />
|
||||
</svg>
|
||||
|
||||
{% trans "Meetings" %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item me-2">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'form_templates_list' %}active{% endif %}" href="{% url 'form_templates_list' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m15.75 10.5 4.72-4.72a.75.75 0 0 1 1.28.53v11.38a.75.75 0 0 1-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 0 0 2.25-2.25v-9a2.25 2.25 0 0 0-2.25-2.25h-9A2.25 2.25 0 0 0 2.25 7.5v9a2.25 2.25 0 0 0 2.25 2.25Z" />
|
||||
</svg>
|
||||
|
||||
{% trans "Form Templates" %}
|
||||
</span>
|
||||
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown ms-2">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
|
||||
data-bs-offset="0, 8" data-bs-auto-close="outside">
|
||||
{% trans "More" %}
|
||||
</a>
|
||||
<ul class="dropdown-menu" data-bs-popper="static">
|
||||
<li><a class="dropdown-item" href="#"><i class="fas fa-calendar me-2"></i> {% trans "Meetings" %}</a></li>
|
||||
<li><a class="dropdown-item" href="#"><i class="fas fa-clock me-2"></i> {% trans "Schedule" %}</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#"><i class="fas fa-briefcase me-2"></i> {% trans "Active Jobs" %}</a></li>
|
||||
<li><a class="dropdown-item" href="#"><i class="fas fa-file-alt me-2"></i> {% trans "Draft Jobs" %}</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#"><i class="fas fa-users me-2"></i> {% trans "All Candidates" %}</a></li>
|
||||
<li><a class="dropdown-item" href="#"><i class="fas fa-user-plus me-2"></i> {% trans "New Candidates" %}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<ul class="navbar-nav me-2">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="language-toggle-btn dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
|
||||
data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}">
|
||||
<i class="fas fa-globe"></i>
|
||||
<span class="d-none d-lg-inline">{{ LANGUAGE_CODE|upper }}</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" data-bs-popper="static">
|
||||
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
<li>
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
<button name="language" value="en" class="dropdown-item {% if LANGUAGE_CODE == 'en' %}active bg-light-subtle{% endif %}" type="submit">
|
||||
<span class="me-2">🇺🇸</span> English
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
<button name="language" value="ar" class="dropdown-item {% if LANGUAGE_CODE == 'ar' %}active bg-light-subtle{% endif %}" type="submit">
|
||||
<span class="me-2">🇸🇦</span> العربية (Arabic)
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav ms-4">
|
||||
<li class="nav-item dropdown">
|
||||
<button
|
||||
class="nav-link p-0 border-0 bg-transparent dropdown-toggle"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
aria-label="{% trans 'Toggle user menu' %}"
|
||||
data-bs-auto-close="outside"
|
||||
data-bs-offset="0, 8"
|
||||
>
|
||||
{% if user.profile.profile_image %}
|
||||
<img src="{{ user.profile.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar"
|
||||
style="width: 36px; height: 36px; object-fit: cover; background-color: var(--kaauh-teal); display: inline-block; vertical-align: middle;"
|
||||
title="{% trans 'Your account' %}">
|
||||
{% else %}
|
||||
<div class="profile-avatar" title="{% trans 'Your account' %}">
|
||||
{{ user.username|first|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% comment %} <span class="ms-2 d-none d-lg-inline fw-semibold">{{ user.username }}</span> {% endcomment %}
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-menu dropdown-menu-end py-0 shadow border-0 rounded-3"
|
||||
data-bs-popper="static"
|
||||
style="min-width: 240px;"
|
||||
>
|
||||
<li class="px-4 py-3 ">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="me-3 d-flex align-items-center justify-content-center" style="min-width: 48px;">
|
||||
{% if user.profile.profile_image %}
|
||||
<img src="{{ user.profile.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar shadow-sm border"
|
||||
style="width: 44px; height: 44px; object-fit: cover; background-color: var(--kaauh-teal); display: block;"
|
||||
title="{% trans 'Your account' %}">
|
||||
{% else %}
|
||||
<div class="profile-avatar shadow-sm border d-flex align-items-center justify-content-center"
|
||||
style="width: 44px; height: 44px; background-color: var(--kaauh-teal); font-size: 1.2rem;">
|
||||
{{ user.username|first|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-semibold text-dark">{{ user.get_full_name|default:user.username }}</div>
|
||||
<div class="text-muted small">{{ user.email|truncatechars:24 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider my-1"></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-user-circle me-3 text-primary fs-5"></i> <span>{% trans "My Profile" %}</span></a></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-cog me-3 text-primary fs-5"></i> <span>{% trans "Settings" %}</span></a></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-history me-3 text-primary fs-5"></i> <span>{% trans "Activity Log" %}</span></a></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-question-circle me-3 text-primary fs-5"></i> <span>{% trans "Help & Support" %}</span></a></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#">
|
||||
{% if not request.session.linkedin_authenticated %}
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'linkedin_login' %}">
|
||||
<i class="fab fa-linkedin me-1"></i> {% trans "Connect LinkedIn" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<i class="fab fa-linkedin text-primary me-1"></i>
|
||||
<span class="text-primary d-none d-lg-inline ms-auto me-3">
|
||||
{% trans "LinkedIn Connected" %}
|
||||
{% trans "Meetings" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a></li>
|
||||
|
||||
<li><hr class="dropdown-divider my-1"></li>
|
||||
<li>
|
||||
<form method="post" action="" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<button
|
||||
type="submit"
|
||||
class="dropdown-item py-2 px-4 text-danger d-flex align-items-center border-0 bg-transparent text-start"
|
||||
aria-label="{% trans 'Sign out' %}"
|
||||
>
|
||||
<i class="fas fa-sign-out-alt me-3 fs-5"></i>
|
||||
<span>{% trans "Sign Out" %}</span>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="nav-item me-2">
|
||||
<a class="nav-link {% if request.resolver_match.url_name == 'training_list' %}active{% endif %}" href="{% url 'training_list' %}">
|
||||
<span class="d-flex align-items-center gap-2">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.26 10.147a60.438 60.438 0 0 0-.491 6.347A48.62 48.62 0 0 1 12 20.904a48.62 48.62 0 0 1 8.232-4.41 60.46 60.46 0 0 0-.491-6.347m-15.482 0a50.636 50.636 0 0 0-2.658-.813A59.906 59.906 0 0 1 12 3.493a59.903 59.903 0 0 1 10.399 5.84c-.896.248-1.783.52-2.658.814m-15.482 0A50.717 50.717 0 0 1 12 13.489a50.702 50.702 0 0 1 7.74-3.342M6.75 15a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Zm0 0v-3.675A55.378 55.378 0 0 1 12 8.443m-7.007 11.55A5.981 5.981 0 0 0 6.75 15.75v-1.5" />
|
||||
</svg>
|
||||
|
||||
{% trans "Training" %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<li class="nav-item dropdown ms-2">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
|
||||
data-bs-offset="0, 8" data-bs-auto-close="outside">
|
||||
{% trans "More" %}
|
||||
</a>
|
||||
<ul class="dropdown-menu" data-bs-popper="static">
|
||||
<li><a class="dropdown-item" href="#"><i class="fas fa-calendar me-2"></i> {% trans "Meetings" %}</a></li>
|
||||
<li><a class="dropdown-item" href="#"><i class="fas fa-clock me-2"></i> {% trans "Schedule" %}</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#"><i class="fas fa-briefcase me-2"></i> {% trans "Active Jobs" %}</a></li>
|
||||
<li><a class="dropdown-item" href="#"><i class="fas fa-file-alt me-2"></i> {% trans "Draft Jobs" %}</a></li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="#"><i class="fas fa-users me-2"></i> {% trans "All Candidates" %}</a></li>
|
||||
<li><a class="dropdown-item" href="#"><i class="fas fa-user-plus me-2"></i> {% trans "New Candidates" %}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
||||
<ul class="navbar-nav me-2">
|
||||
<li class="nav-item dropdown">
|
||||
<a class="language-toggle-btn dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
|
||||
data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}">
|
||||
<i class="fas fa-globe"></i>
|
||||
<span class="d-none d-lg-inline">{{ LANGUAGE_CODE|upper }}</span>
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" data-bs-popper="static">
|
||||
|
||||
{% get_current_language as LANGUAGE_CODE %}
|
||||
|
||||
<li>
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
<button name="language" value="en" class="dropdown-item {% if LANGUAGE_CODE == 'en' %}active bg-light-subtle{% endif %}" type="submit">
|
||||
<span class="me-2">🇺🇸</span> English
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
|
||||
<input name="next" type="hidden" value="{{ request.get_full_path }}">
|
||||
<button name="language" value="ar" class="dropdown-item {% if LANGUAGE_CODE == 'ar' %}active bg-light-subtle{% endif %}" type="submit">
|
||||
<span class="me-2">🇸🇦</span> العربية (Arabic)
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul class="navbar-nav ms-4">
|
||||
<li class="nav-item dropdown">
|
||||
<button
|
||||
class="nav-link p-0 border-0 bg-transparent dropdown-toggle"
|
||||
type="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
aria-label="{% trans 'Toggle user menu' %}"
|
||||
data-bs-auto-close="outside"
|
||||
data-bs-offset="0, 8"
|
||||
>
|
||||
{% if user.profile.profile_image %}
|
||||
<img src="{{ user.profile.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar"
|
||||
style="width: 36px; height: 36px; object-fit: cover; background-color: var(--kaauh-teal); display: inline-block; vertical-align: middle;"
|
||||
title="{% trans 'Your account' %}">
|
||||
{% else %}
|
||||
<div class="profile-avatar" title="{% trans 'Your account' %}">
|
||||
{{ user.username|first|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% comment %} <span class="ms-2 d-none d-lg-inline fw-semibold">{{ user.username }}</span> {% endcomment %}
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-menu dropdown-menu-end py-0 shadow border-0 rounded-3"
|
||||
data-bs-popper="static"
|
||||
style="min-width: 240px;"
|
||||
>
|
||||
<li class="px-4 py-3 ">
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="me-3 d-flex align-items-center justify-content-center" style="min-width: 48px;">
|
||||
{% if user.profile.profile_image %}
|
||||
<img src="{{ user.profile.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar shadow-sm border"
|
||||
style="width: 44px; height: 44px; object-fit: cover; background-color: var(--kaauh-teal); display: block;"
|
||||
title="{% trans 'Your account' %}">
|
||||
{% else %}
|
||||
<div class="profile-avatar shadow-sm border d-flex align-items-center justify-content-center"
|
||||
style="width: 44px; height: 44px; background-color: var(--kaauh-teal); font-size: 1.2rem;">
|
||||
{{ user.username|first|upper }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<div class="fw-semibold text-dark">{{ user.get_full_name|default:user.username }}</div>
|
||||
<div class="text-muted small">{{ user.email|truncatechars:24 }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider my-1"></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-user-circle me-3 text-primary fs-5"></i> <span>{% trans "My Profile" %}</span></a></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-cog me-3 text-primary fs-5"></i> <span>{% trans "Settings" %}</span></a></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-history me-3 text-primary fs-5"></i> <span>{% trans "Activity Log" %}</span></a></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-question-circle me-3 text-primary fs-5"></i> <span>{% trans "Help & Support" %}</span></a></li>
|
||||
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#">
|
||||
{% if not request.session.linkedin_authenticated %}
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="{% url 'linkedin_login' %}">
|
||||
<i class="fab fa-linkedin me-1"></i> {% trans "Connect LinkedIn" %}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
{% else %}
|
||||
<i class="fab fa-linkedin text-primary me-1"></i>
|
||||
<span class="text-primary d-none d-lg-inline ms-auto me-3">
|
||||
{% trans "LinkedIn Connected" %}
|
||||
</span>
|
||||
{% endif %}
|
||||
</a></li>
|
||||
|
||||
<li><hr class="dropdown-divider my-1"></li>
|
||||
<li>
|
||||
<form method="post" action="" class="d-inline">
|
||||
{% csrf_token %}
|
||||
<button
|
||||
type="submit"
|
||||
class="dropdown-item py-2 px-4 text-danger d-flex align-items-center border-0 bg-transparent text-start"
|
||||
aria-label="{% trans 'Sign out' %}"
|
||||
>
|
||||
<i class="fas fa-sign-out-alt me-3 fs-5"></i>
|
||||
<span>{% trans "Sign Out" %}</span>
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<main class="container flex-grow-1">
|
||||
<main class="container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;">
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
@ -484,14 +518,24 @@
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
<footer class="footer mt-auto">
|
||||
<div class="container text-center">
|
||||
<p class="mb-0">
|
||||
© {% now "Y" %} {% trans "King Abdullah Academic University Hospital (KAAUH)." %}<br>
|
||||
<small>{% trans "All rights reserved." %}</small>
|
||||
</p>
|
||||
|
||||
<footer class="mt-auto">
|
||||
<div class="footer-bottom py-3 small text-muted" style="background-color: #00363a;">
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center flex-wrap max-width-1600">
|
||||
<p class="mb-0 text-white-50">
|
||||
© {% now "Y" %} {% trans "King Abdullah Academic University Hospital (KAAUH)." %}
|
||||
{% trans "All rights reserved." %}
|
||||
</p>
|
||||
<a class="text-decoration-none" href="https://tenhal.sa/" target='_blank'>
|
||||
<p class="mb-0 text-white-50">
|
||||
{% trans "Powered by" %} <strong class="text-white">Tenhal</strong>
|
||||
</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
{% include 'includes/delete_modal.html' %}
|
||||
|
||||
|
||||
@ -1,45 +1,198 @@
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% load form_filters %}
|
||||
|
||||
{% block title %}{{ form.name }} - Submission Details{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* ================================================= */
|
||||
/* THEME VARIABLES AND GLOBAL STYLES */
|
||||
/* ================================================= */
|
||||
:root {
|
||||
--kaauh-teal: #00636e; /* Primary */
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-primary-text: #343a40;
|
||||
}
|
||||
|
||||
/* Primary Color Overrides */
|
||||
.text-primary { color: var(--kaauh-teal) !important; }
|
||||
.text-info { color: #17a2b8 !important; }
|
||||
.text-success { color: #28a745 !important; }
|
||||
.text-secondary { color: #6c757d !important; }
|
||||
.bg-info { background-color: #17a2b8 !important; }
|
||||
.bg-secondary { background-color: #6c757d !important; }
|
||||
|
||||
/* Card enhancements */
|
||||
.card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-weight: 600;
|
||||
padding: 1rem 1.25rem;
|
||||
background-color: #f8f9fa; /* Light background */
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
/* Main Action Button Style */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 1rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.btn-main-action:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Secondary outline button */
|
||||
.btn-outline-secondary {
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal);
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn-outline-secondary:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
/* ================================================= */
|
||||
/* RESPONSES TABLE SPECIFIC STYLES (Horizontal Layout) */
|
||||
/* ================================================= */
|
||||
|
||||
/* Main table container */
|
||||
.table-submission {
|
||||
margin-bottom: 0;
|
||||
border: none;
|
||||
table-layout: auto; /* Allow columns to resize based on content */
|
||||
}
|
||||
|
||||
/* Fixed first column (the row headers) */
|
||||
.table-submission th:first-child,
|
||||
.table-submission td:first-child {
|
||||
background-color: #f0f4f7; /* Slightly darker than header */
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
width: 150px;
|
||||
min-width: 150px;
|
||||
position: sticky; /* Keep it visible when scrolling right */
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
border-right: 1px solid var(--kaauh-border);
|
||||
}
|
||||
.table-submission th:first-child {
|
||||
top: 0; /* Important for sticky header/row-header intersection */
|
||||
}
|
||||
|
||||
/* Field Label Header Row (Top Row) */
|
||||
.table-submission thead th {
|
||||
font-weight: 600;
|
||||
background-color: #e9ecef; /* Light gray for headers */
|
||||
color: var(--kaauh-primary-text);
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-bottom: 2px solid var(--kaauh-teal); /* Highlight the bottom of the header */
|
||||
padding: 0.75rem 0.5rem;
|
||||
}
|
||||
.table-submission thead th:not(:first-child) {
|
||||
min-width: 200px; /* Give response columns space */
|
||||
max-width: 300px;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Data Cells */
|
||||
.table-submission tbody td {
|
||||
vertical-align: top;
|
||||
padding: 0.75rem 0.75rem;
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
/* Styling for multi-value responses */
|
||||
.table-submission .badge {
|
||||
font-weight: 500;
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* File display */
|
||||
.table-submission .fa-file {
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
.table-submission .btn-outline-primary {
|
||||
color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_detail' submission.template.job.slug %}">Job Detail</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'form_builder' submission.template.pk%}">Form Template</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Submission Details</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h2>Submission Details</h2>
|
||||
<a href="" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2 class="text-primary fw-bold">{% trans "Submission Details" %}</h2>
|
||||
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Submissions" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Basic Information -->
|
||||
<div class="card mb-3">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">{% trans "Submission Metadata" %}</h5>
|
||||
</div>
|
||||
<div class="card-body small">
|
||||
<div class="row g-3">
|
||||
<div class="col-md-4">
|
||||
<strong>Submission ID:</strong> {{ submission.id }}
|
||||
<i class="fas fa-fingerprint me-2 text-primary"></i>
|
||||
<strong>{% trans "Submission ID:" %}</strong> <span class="text-secondary">{{ submission.id }}</span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<strong>Submitted:</strong> {{ submission.submitted_at|date:"M d, Y H:i" }}
|
||||
<i class="fas fa-calendar-check me-2 text-primary"></i>
|
||||
<strong>{% trans "Submitted:" %}</strong> <span class="text-secondary">{{ submission.submitted_at|date:"M d, Y H:i" }}</span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<strong>Form:</strong> {{ form.name }}
|
||||
<i class="fas fa-file-alt me-2 text-primary"></i>
|
||||
<strong>{% trans "Form:" %}</strong> <span class="text-secondary">{{ submission.template.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% if submission.applicant_name or submission.applicant_email %}
|
||||
<div class="row mt-2">
|
||||
<div class="row g-3 mt-1">
|
||||
{% if submission.applicant_name %}
|
||||
<div class="col-md-6">
|
||||
<strong>Applicant Name:</strong> {{ submission.applicant_name }}
|
||||
<div class="col-md-4">
|
||||
<i class="fas fa-user me-2 text-primary"></i>
|
||||
<strong>{% trans "Applicant Name:" %}</strong> <span class="text-secondary">{{ submission.applicant_name }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if submission.applicant_email %}
|
||||
<div class="col-md-6">
|
||||
<strong>Email:</strong> {{ submission.applicant_email }}
|
||||
<div class="col-md-4">
|
||||
<i class="fas fa-envelope me-2 text-primary"></i>
|
||||
<strong>{% trans "Email:" %}</strong> <span class="text-secondary">{{ submission.applicant_email }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -47,84 +200,87 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Responses Table -->
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h5 class="mb-3">Responses</h5>
|
||||
{% get_all_responses_flat stage_responses as all_responses %}
|
||||
{% if all_responses %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field Label</th>
|
||||
<th>Response Value</th>
|
||||
<th>File</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for response in all_responses %}
|
||||
<tr>
|
||||
<td>
|
||||
<strong>{{ response.field_label }}</strong>
|
||||
{% if response.required %}
|
||||
<span class="text-danger">*</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if response.uploaded_file %}
|
||||
<span class="text-primary">File: {{ response.uploaded_file.name }}</span>
|
||||
{% elif response.value %}
|
||||
{% if response.field_type == 'checkbox' and response.value|length > 0 %}
|
||||
<div>
|
||||
{% for val in response.value %}
|
||||
<span class="badge bg-secondary me-1">{{ val }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif response.field_type == 'radio' or response.field_type == 'select' %}
|
||||
<span class="badge bg-info">{{ response.value }}</span>
|
||||
{% else %}
|
||||
<p class="mb-0">{{ response.value|linebreaksbr }}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">Not provided</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if response.uploaded_file %}
|
||||
<a href="{{ response.uploaded_file.url }}" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-download"></i> Download
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-4">
|
||||
<p>No responses found for this submission.</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">{% trans "Form Responses" %}</h5>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
{% with submission=submission %}
|
||||
{% get_all_responses_flat submission as flat_responses %}
|
||||
|
||||
{% if flat_responses %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-submission table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Field Property" %}</th>
|
||||
{% for response in flat_responses %}
|
||||
<th scope="col">{{ response.field_label }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>{% trans "Response Value" %}</strong></td>
|
||||
{% for response in flat_responses %}
|
||||
<td>
|
||||
{% if response.uploaded_file %}
|
||||
<div>
|
||||
<span class="d-block text-truncate" style="max-width: 180px;"><i class="fas fa-file me-1"></i> {{ response.uploaded_file.name }}</span>
|
||||
<a href="{{ response.uploaded_file.url }}" class="btn btn-sm btn-outline-secondary mt-1" target="_blank" title="{% trans 'Download File' %}">
|
||||
<i class="fas fa-download"></i> {% trans "Download" %}
|
||||
</a>
|
||||
</div>
|
||||
{% elif response.value %}
|
||||
{% if response.field_type == 'checkbox' and response.value|length > 0 %}
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for val in response.value %}
|
||||
<span class="badge bg-secondary">{{ val }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif response.field_type == 'radio' or response.field_type == 'select' %}
|
||||
<span class="badge bg-info">{{ response.value }}</span>
|
||||
{% else %}
|
||||
<p class="mb-0 small text-wrap">{{ response.value|linebreaksbr }}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted small">{% trans "Not provided" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{% trans "Associated Stage" %}</strong></td>
|
||||
{% for response in flat_responses %}
|
||||
<td>
|
||||
<span class="small text-secondary">{{ response.stage_name|default:"N/A" }}</span>
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{% trans "Field Required" %}</strong></td>
|
||||
{% for response in flat_responses %}
|
||||
<td>
|
||||
{% if response.required %}
|
||||
<span class="text-danger small"><i class="fas fa-asterisk"></i> {% trans "Yes" %}</span>
|
||||
{% else %}
|
||||
<span class="small text-success">{% trans "No" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center text-muted py-5 px-3">
|
||||
<i class="fas fa-exclamation-circle fa-2x mb-3"></i>
|
||||
<p class="lead">{% trans "No response fields were found for this submission." %}</p>
|
||||
<p class="small">{% trans "This may occur if the form template was modified or responses were cleared." %}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_css %}
|
||||
<style>
|
||||
/* Minimal styling */
|
||||
.table th {
|
||||
border-top: none;
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
}
|
||||
.table td {
|
||||
vertical-align: top;
|
||||
}
|
||||
.response-value {
|
||||
max-width: 300px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
371
templates/forms/form_template_all_submissions.html
Normal file
371
templates/forms/form_template_all_submissions.html
Normal file
@ -0,0 +1,371 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n form_filters %}
|
||||
{% load partials %}
|
||||
|
||||
{% block title %}All 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;
|
||||
}
|
||||
|
||||
/* --- Compact Table Styles --- */
|
||||
.table-responsive {
|
||||
border-radius: 0.5rem;
|
||||
overflow: auto;
|
||||
max-height: 70vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.table {
|
||||
margin-bottom: 0;
|
||||
min-width: max-content;
|
||||
}
|
||||
.table thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
.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.7rem;
|
||||
letter-spacing: 0.3px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.table tbody td {
|
||||
padding: 0.5rem 0.75rem;
|
||||
vertical-align: middle;
|
||||
border-color: var(--kaauh-border);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.table tbody tr {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.table tbody tr:hover {
|
||||
background-color: var(--kaauh-gray-light);
|
||||
}
|
||||
/* Compact form elements */
|
||||
.file-response {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
.badge-response {
|
||||
margin: 0.05rem;
|
||||
font-size: 0.75rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
}
|
||||
.response-value p {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.btn-sm {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.2rem 0.4rem;
|
||||
}
|
||||
|
||||
/* --- 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);
|
||||
}
|
||||
|
||||
/* --- Response Value Styling --- */
|
||||
.response-value {
|
||||
word-break: break-word;
|
||||
max-width: 200px;
|
||||
}
|
||||
.file-response {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.badge-response {
|
||||
margin: 0.1rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<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"><a href="{% url 'form_template_submissions_list' template.slug %}">{% trans "Submissions" %}</a></li>
|
||||
<li class="breadcrumb-item active">{% trans "All Submissions Table" %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<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-table me-2"></i>
|
||||
{% trans "All Submissions for" %}: <span class="text-white ms-2">{{ template.name }}</span>
|
||||
</h1>
|
||||
<small class="text-white-50">Template ID: #{{ template.id }}</small>
|
||||
</div>
|
||||
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-light btn-sm">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Submissions" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if page_obj.object_list %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Submission ID" %}</th>
|
||||
<th scope="col">{% trans "Applicant Name" %}</th>
|
||||
<th scope="col">{% trans "Applicant Email" %}</th>
|
||||
<th scope="col">{% trans "Submitted At" %}</th>
|
||||
{% for field in fields %}
|
||||
<th scope="col">{{ field.label }}</th>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for submission in page_obj %}
|
||||
<tr>
|
||||
<td class="fw-medium">{{ submission.id }}</td>
|
||||
<td>{{ submission.applicant_name|default:"N/A" }}</td>
|
||||
<td>{{ submission.applicant_email|default:"N/A" }}</td>
|
||||
<td>{{ submission.submitted_at|date:"M d, Y H:i" }}</td>
|
||||
{% for field in fields %}
|
||||
{% get_field_response_for_submission submission field as response %}
|
||||
<td class="response-value">
|
||||
{% if response %}
|
||||
{% if response.uploaded_file %}
|
||||
<div class="file-response">
|
||||
|
||||
<a href="{{ response.uploaded_file.url }}" class="btn btn-sm btn-outline-primary" target="_blank" title="Download File">
|
||||
<i class="fas fa-download"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% elif response.value %}
|
||||
{% if response.field.field_type == 'checkbox' and response.value|length > 0 %}
|
||||
<div>
|
||||
{% for val in response.value|to_list %}
|
||||
<span class="badge bg-secondary badge-response">{{ val }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% elif response.field.field_type == 'radio' or response.field.field_type == 'select' %}
|
||||
<span class="badge bg-info">{{ response.value }}</span>
|
||||
{% else %}
|
||||
<p class="mb-0">{{ response.value|linebreaksbr|truncatewords:10 }}</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">Not provided</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-muted">Not provided</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if page_obj.has_other_pages %}
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center mt-4">
|
||||
<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>
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page=1" aria-label="First">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="Previous">
|
||||
<span aria-hidden="true">‹</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<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 %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
|
||||
<span aria-hidden="true">›</span>
|
||||
</a>
|
||||
</li>
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}" aria-label="Last">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="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_template_submissions_list' template.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Submissions" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -25,7 +25,7 @@
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
font-weight: 900 ;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
@ -200,9 +200,14 @@
|
||||
</h1>
|
||||
<small class="text-white-50">Template ID: #{{ template.id }}</small>
|
||||
</div>
|
||||
<a href="{% url 'form_templates_list' %}" class="btn btn-outline-light btn-sm">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Templates" %}
|
||||
</a>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'form_template_all_submissions' template.id %}" class="btn btn-outline-light btn-sm">
|
||||
<i class="fas fa-table me-1"></i> {% trans "View All in Table" %}
|
||||
</a>
|
||||
<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>
|
||||
<div class="card-body">
|
||||
{% if page_obj.object_list %}
|
||||
@ -231,10 +236,10 @@
|
||||
<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">
|
||||
<a href="{% url 'form_submission_details' template_id=submission.template.id slug=submission.slug %}" class="btn btn-sm btn-outline-primary">
|
||||
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
|
||||
</a>
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -246,7 +251,7 @@
|
||||
<div class="card-view">
|
||||
<div class="row g-4">
|
||||
{% for submission in page_obj %}
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<div class="col-12">
|
||||
<div class="card h-100">
|
||||
<div class="card-header">
|
||||
<h3 class="h5 mb-2">{% trans "Submission" %} #{{ submission.id }}</h3>
|
||||
@ -260,7 +265,7 @@
|
||||
</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">
|
||||
<a href="{% url 'form_submission_details' template_id=template.id slug=submission.slug %}" class="btn btn-sm btn-outline-primary w-100">
|
||||
<i class="fas fa-eye me-1"></i> {% trans "View Details" %}
|
||||
</a>
|
||||
</div>
|
||||
@ -323,7 +328,7 @@
|
||||
<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">
|
||||
<a href="{% url 'form_templates_list' %}" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Templates" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -148,7 +148,7 @@
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-file-alt me-2"></i>{% trans "Form Templates" %}
|
||||
</h1>
|
||||
<button type="button" class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#createTemplateModal">
|
||||
|
||||
@ -591,7 +591,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-footer">
|
||||
<div class="wizard-footer mt-2">
|
||||
<button
|
||||
id="backBtn"
|
||||
class="nav-btn btn-back"
|
||||
@ -881,17 +881,28 @@
|
||||
formData.append('csrfmiddlewaretoken', csrfToken);
|
||||
|
||||
// Add field responses
|
||||
state.stages.forEach(stage => {
|
||||
state.stages.forEach(stage => {
|
||||
stage.fields.forEach(field => {
|
||||
const value = state.formData[field.id];
|
||||
if (value !== undefined && value !== null) {
|
||||
if (field.type === 'file' && value instanceof File) {
|
||||
|
||||
// Always include the field, even if it's empty
|
||||
if (field.type === 'file') {
|
||||
if (value instanceof File) {
|
||||
formData.append(`field_${field.id}`, value);
|
||||
} else if (field.type === 'checkbox') {
|
||||
} else {
|
||||
// Include empty file field
|
||||
formData.append(`field_${field.id}`, '');
|
||||
}
|
||||
} else if (field.type === 'checkbox') {
|
||||
// For checkboxes, send empty array if no selection
|
||||
if (Array.isArray(value) && value.length > 0) {
|
||||
formData.append(`field_${field.id}`, JSON.stringify(value));
|
||||
} else {
|
||||
formData.append(`field_${field.id}`, value);
|
||||
formData.append(`field_${field.id}`, JSON.stringify([]));
|
||||
}
|
||||
} else {
|
||||
// For other field types, send the value or empty string
|
||||
formData.append(`field_${field.id}`, value || '');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
21
templates/includes/candidate_modal_body.html
Normal file
21
templates/includes/candidate_modal_body.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% load i18n %}
|
||||
<h5 class="modal-title" id="candidateviewModalLabel">{{ candidate.name }} - {% trans "Score" %}: <span class="badge bg-success"> {{ candidate.match_score }} </span></h5>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{% trans "Strengths" %}</label>
|
||||
<textarea class="form-control" rows="3" readonly>{{ candidate.strengths }}</textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{% trans "Weaknesses" %}</label>
|
||||
<textarea class="form-control" rows="3" readonly>{{ candidate.weaknesses }}</textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{% trans "Criteria Checklist" %}</label>
|
||||
<ul class="list-group">
|
||||
{% for key, value in candidate.criteria_checklist.items %}
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<span>{{ key }}</span>
|
||||
<span class="badge bg-{{ value|yesno:"success,danger" }}">{{ value|yesno:"Yes,No" }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
9
templates/jobs/applicants_of_stage.html
Normal file
9
templates/jobs/applicants_of_stage.html
Normal file
@ -0,0 +1,9 @@
|
||||
{{stage}}
|
||||
{% for applicant in applicants_of_stage %}
|
||||
|
||||
|
||||
{{applicant}}
|
||||
{% endfor %}
|
||||
|
||||
|
||||
|
||||
628
templates/jobs/base_public.html
Normal file
628
templates/jobs/base_public.html
Normal file
@ -0,0 +1,628 @@
|
||||
{% load i18n static %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ LANGUAGE_CODE }}" dir="{% if LANGUAGE_CODE == 'ar' %}rtl{% else %}ltr{% endif %}">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}{% trans 'Careers' %} - KAAUH{% endblock %}</title>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css">
|
||||
|
||||
<style>
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-light-bg: #f9fbfd;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-green-dark: #00363a;
|
||||
--kaauh-nav-bg: #004a53;
|
||||
--kaauh-dark-nav-active: #00363a;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.max-width-container {
|
||||
max-width: 1600px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
/* === Header Top Info === */
|
||||
.header-top-info {
|
||||
background-color: white;
|
||||
padding: 15px 0;
|
||||
font-size: 0.95rem;
|
||||
color: #333;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.header-top-info .section-divider {
|
||||
border-inline-start: 1px solid #ddd;
|
||||
height: 60px;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
.header-top-info .follow-us-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.header-top-info .social-icon-circle {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.header-top-info .social-icon-circle:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
.header-top-info .contact-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.header-top-info .contact-icon-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 0 15px;
|
||||
min-height: 60px;
|
||||
}
|
||||
.header-top-info .contact-icon {
|
||||
font-size: 2rem;
|
||||
color: var(--kaauh-teal);
|
||||
margin-inline-end: 15px;
|
||||
}
|
||||
.header-top-info .contact-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: start;
|
||||
}
|
||||
.header-top-info .contact-details span:first-child {
|
||||
font-weight: bold;
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
.header-top-info .contact-details span:last-child {
|
||||
font-size: 0.85rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.header-top-info .logo-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
.header-top-info .vision-logo-container {
|
||||
border-inline-start: 1px solid #ddd;
|
||||
padding-inline-start: 20px;
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header-top-info .vision-logo {
|
||||
max-height: 50px;
|
||||
width: auto;
|
||||
}
|
||||
.header-top-info .kaauh-logo-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-inline-start: 1px solid #ddd;
|
||||
padding-inline-start: 20px;
|
||||
}
|
||||
.header-top-info .kaauh-logo-container img {
|
||||
max-height: 80px;
|
||||
width: auto;
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
.header-top-info .hospital-text {
|
||||
line-height: 1.2;
|
||||
text-align: initial;
|
||||
}
|
||||
.header-top-info .hospital-text .ar {
|
||||
font-size: 1.1rem;
|
||||
font-weight: bold;
|
||||
color: var(--kaauh-teal-dark);
|
||||
}
|
||||
.header-top-info .hospital-text .en {
|
||||
font-size: 0.8rem;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
/* === Main Navbar === */
|
||||
.navbar-main {
|
||||
background-color: var(--kaauh-nav-bg);
|
||||
padding: 0;
|
||||
min-height: 60px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||
}
|
||||
.navbar-main .nav-link {
|
||||
color: white !important;
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
padding: 1.1rem 1.2rem;
|
||||
transition: background-color 0.2s;
|
||||
border-inline-end: 1px solid rgba(255, 255, 255, 0.1);
|
||||
line-height: 1;
|
||||
}
|
||||
.navbar-main .nav-link:hover,
|
||||
.navbar-main .nav-link.active {
|
||||
background-color: var(--kaauh-dark-nav-active);
|
||||
border-inline-end: 1px solid var(--kaauh-dark-nav-active);
|
||||
}
|
||||
.navbar-main .nav-icons-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--kaauh-dark-nav-active);
|
||||
height: 100%;
|
||||
min-height: 60px;
|
||||
}
|
||||
.navbar-main .nav-icon {
|
||||
color: white;
|
||||
padding: 0 15px;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.navbar-main .nav-icon:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
.navbar-main .profile-icon {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
.navbar-main .lang-switch {
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
line-height: 1;
|
||||
}
|
||||
.navbar-main .dropdown-menu {
|
||||
border-radius: 0;
|
||||
margin-top: 0;
|
||||
border: none;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
|
||||
}
|
||||
.navbar-main .dropdown-item:hover {
|
||||
background-color: var(--kaauh-light-bg);
|
||||
color: var(--kaauh-teal-dark);
|
||||
}
|
||||
.navbar-toggler {
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
.navbar-toggler:focus {
|
||||
box-shadow: none;
|
||||
}
|
||||
.navbar-toggler-icon {
|
||||
filter: invert(1);
|
||||
}
|
||||
.navbar-main .collapse {
|
||||
justify-content: flex-start !important;
|
||||
}
|
||||
|
||||
/* === Footer === */
|
||||
.footer-main {
|
||||
background-color: var(--kaauh-green-dark);
|
||||
color: white;
|
||||
padding-top: 3rem;
|
||||
}
|
||||
.footer-main a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.footer-main a:hover {
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
.footer-main h5 {
|
||||
color: var(--kaauh-teal);
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.footer-main .social-icons a {
|
||||
font-size: 1.5rem;
|
||||
margin-inline-end: 15px;
|
||||
}
|
||||
.footer-main .contact-info,
|
||||
.footer-main .app-download {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.footer-bottom {
|
||||
background-color: #00282b;
|
||||
padding: 1rem 0;
|
||||
font-size: 0.85rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.footer-bottom strong {
|
||||
color: white;
|
||||
}
|
||||
.footer-main .col-lg-3, .footer-main .col-lg-5 {
|
||||
padding-bottom: 2rem;
|
||||
}
|
||||
.footer-main .vision-logo {
|
||||
max-width: 100px;
|
||||
}
|
||||
.footer-main .app-store-badge,
|
||||
.footer-main .play-store-badge {
|
||||
max-width: 150px;
|
||||
height: auto;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* === Hero Section === */
|
||||
.hero-section {
|
||||
height: 50vh;
|
||||
background: url('{% static "image/hospital-bg.jpg" %}') no-repeat center center;
|
||||
background-size: cover;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
text-align: center;
|
||||
}
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
.hero-content {
|
||||
z-index: 10;
|
||||
}
|
||||
.hero-content h1 {
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.breadcrumb-section {
|
||||
padding: 10px 0;
|
||||
background-color: #f1f1f1;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.breadcrumb-item a {
|
||||
color: #6c757d;
|
||||
}
|
||||
.breadcrumb-item.active {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
/* === RTL Adjustments === */
|
||||
html[dir="rtl"] {
|
||||
text-align: right;
|
||||
direction: rtl;
|
||||
}
|
||||
html[dir="rtl"] .max-width-container {
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
html[dir="rtl"] .header-top-info .section-divider {
|
||||
border-inline-start: none;
|
||||
border-inline-end: 1px solid #ddd;
|
||||
}
|
||||
html[dir="rtl"] .header-top-info .contact-icon {
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 15px;
|
||||
}
|
||||
html[dir="rtl"] .header-top-info .contact-details {
|
||||
text-align: right;
|
||||
}
|
||||
html[dir="rtl"] .header-top-info .vision-logo-container {
|
||||
border-inline-start: none;
|
||||
border-inline-end: 1px solid #ddd;
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: 20px;
|
||||
}
|
||||
html[dir="rtl"] .header-top-info .kaauh-logo-container {
|
||||
border-inline-start: none;
|
||||
border-inline-end: 1px solid #ddd;
|
||||
padding-inline-start: 0;
|
||||
padding-inline-end: 20px;
|
||||
}
|
||||
html[dir="rtl"] .header-top-info .kaauh-logo-container img {
|
||||
margin-inline-end: 0;
|
||||
margin-inline-start: 10px;
|
||||
}
|
||||
html[dir="rtl"] .header-top-info .hospital-text {
|
||||
text-align: right;
|
||||
}
|
||||
html[dir="rtl"] .navbar-main .nav-link {
|
||||
border-inline-end: none;
|
||||
border-inline-start: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
html[dir="rtl"] .navbar-main .nav-link:hover,
|
||||
html[dir="rtl"] .navbar-main .nav-link.active {
|
||||
border-inline-start: 1px solid var(--kaauh-dark-nav-active);
|
||||
}
|
||||
html[dir="rtl"] .nav-icons i {
|
||||
margin-inline-start: 10px;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
html[dir="rtl"] .top-bar .social-icons a {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: 10px;
|
||||
}
|
||||
html[dir="rtl"] .footer-main .social-icons a {
|
||||
margin-inline-start: 15px;
|
||||
margin-inline-end: 0;
|
||||
}
|
||||
|
||||
/* === Responsive Adjustments for Small Screens === */
|
||||
@media (max-width: 991.98px) {
|
||||
.header-top-info .max-width-container {
|
||||
flex-direction: column !important;
|
||||
align-items: flex-start !important;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.header-top-info .follow-us-section,
|
||||
.header-top-info .d-flex.align-items-center.flex-grow-1.justify-content-end {
|
||||
width: 100%;
|
||||
justify-content: space-between !important;
|
||||
}
|
||||
|
||||
.header-top-info .contact-block,
|
||||
.header-top-info .logo-section {
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.header-top-info .section-divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-top-info .kaauh-logo-container img {
|
||||
max-height: 60px;
|
||||
}
|
||||
|
||||
.header-top-info .hospital-text .ar,
|
||||
.header-top-info .hospital-text .en {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.header-top-info .contact-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.header-top-info .contact-icon-block {
|
||||
min-height: auto;
|
||||
padding: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 575.98px) {
|
||||
.header-top-info .kaauh-logo-container,
|
||||
.header-top-info .vision-logo-container {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-top-info .hospital-text {
|
||||
text-align: center !important;
|
||||
}
|
||||
|
||||
.header-top-info .kaauh-logo-container img {
|
||||
max-height: 50px;
|
||||
}
|
||||
|
||||
.header-top-info .follow-us-section span,
|
||||
.header-top-info .contact-details span {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.header-top-info .social-icon-circle {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.hero-content h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% block customCSS %}{% endblock %}
|
||||
</head>
|
||||
<body class="d-flex flex-column min-vh-100">
|
||||
|
||||
<!-- Responsive Header Top Info -->
|
||||
<div class="header-top-info">
|
||||
<div class="max-width-container d-flex flex-column flex-md-row justify-content-between align-items-start align-items-md-center gap-3">
|
||||
|
||||
<div class="follow-us-section d-flex flex-wrap align-items-center gap-2">
|
||||
<span>{% trans "Follow Us On:" %}</span>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="#" class="social-icon-circle"><i class="fab fa-facebook-f"></i></a>
|
||||
<a href="#" class="social-icon-circle"><i class="fab fa-twitter"></i></a>
|
||||
<a href="#" class="social-icon-circle"><i class="fab fa-instagram"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column flex-md-row align-items-start align-items-md-center gap-3 w-100 w-md-auto">
|
||||
<div class="d-flex flex-wrap justify-content-between gap-3">
|
||||
<div class="contact-icon-block d-flex align-items-center">
|
||||
<i class="fas fa-headset contact-icon"></i>
|
||||
<div class="contact-details ms-2">
|
||||
<span>24/7 {% trans "Online Support" %}</span>
|
||||
<span class="d-block d-md-inline">info@kaauh.edu.sa</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="contact-icon-block d-flex align-items-center">
|
||||
<i class="fas fa-phone-alt contact-icon"></i>
|
||||
<div class="contact-details ms-2">
|
||||
<span>{% trans "Contact Us Free" %}</span>
|
||||
<span class="d-block d-md-inline">+966118200000</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="logo-section d-flex flex-wrap align-items-center gap-3 mt-2 mt-md-0">
|
||||
|
||||
<div class="kaauh-logo-container d-flex flex-column flex-md-row align-items-center gap-2">
|
||||
<img src="{% static 'image/vision.svg' %}" alt="Vision 2030" class="vision-logo" style="min-height: 70px; min-width:300px;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="logo-section d-flex flex-wrap align-items-center gap-3 mt-2 mt-md-0 ms-6">
|
||||
|
||||
<div class="kaauh-logo-container d-flex flex-column flex-md-row align-items-center gap-2 ms-4">
|
||||
<div class="hospital-text text-center text-md-start me-3">
|
||||
<div class="ar">جامعة الأميرة نورة بنت عبدالرحمن الأكاديمية</div>
|
||||
<div class="ar">ومستشفى الملك عبدالله بن عبدالعزيز التخصصي</div>
|
||||
<div class="en">Princess Nourah bint Abdulrahman University</div>
|
||||
<div class="en">King Abdullah bin Abdulaziz University Hospital</div>
|
||||
</div>
|
||||
<img src="{% static 'image/kaauh.png' %}" alt="KAAUH Logo" style="max-height: 80px;min-width:80px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main Navigation -->
|
||||
<nav class="navbar navbar-expand-lg navbar-dark navbar-main">
|
||||
<div class="max-width-container d-flex justify-content-between align-items-stretch w-100">
|
||||
<button class="navbar-toggler ms-auto" type="button" data-bs-toggle="collapse" data-bs-target="#publicNavCollapse"
|
||||
aria-controls="publicNavCollapse" aria-expanded="false" aria-label="{% trans 'Toggle navigation' %}">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="publicNavCollapse">
|
||||
<ul class="navbar-nav mb-2 mb-lg-0">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">{% trans "About KAAUH" %}</a>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{% trans "Patients & Visitor" %}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#">{% trans "Find a Doctor" %}</a></li>
|
||||
<li><a class="dropdown-item" href="#">{% trans "Visiting Hours" %}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{% trans "Training & Education" %}
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a class="dropdown-item" href="#">{% trans "Courses" %}</a></li>
|
||||
<li><a class="dropdown-item" href="#">{% trans "Residency" %}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="{% url 'kaauh_career' %}">{% trans "Careers" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">{% trans "Gallery" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">{% trans "Connect" %}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="nav-icons-group d-none d-lg-flex">
|
||||
<a href="#" class="nav-icon profile-icon"><i class="fas fa-user-circle"></i></a>
|
||||
<a href="#" class="nav-icon lang-switch">ع</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="flex-grow-1">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="footer-main mt-auto">
|
||||
<div class="max-width-container">
|
||||
<div class="row">
|
||||
<div class="col-lg-3 col-md-6 app-download">
|
||||
<h5 class="text-white-50">{% trans "Download our mobile app" %}</h5>
|
||||
<p>{% trans "Get the latest updates and services on the go." %}</p>
|
||||
<div class="d-flex flex-column gap-2 mt-3">
|
||||
<a href="#"><img src="{% static 'image/google-play-badge.png' %}" alt="Google Play" class="play-store-badge"></a>
|
||||
<a href="#"><img src="{% static 'image/app-store-badge.png' %}" alt="App Store" class="app-store-badge"></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-2 col-md-6">
|
||||
<h5>{% trans "Information" %}</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="#">{% trans "About the Hospital" %}</a></li>
|
||||
<li><a href="{% url 'kaauh_career' %}">{% trans "Careers" %}</a></li>
|
||||
<li><a href="#">{% trans "Today's Clinic Hours" %}</a></li>
|
||||
<li><a href="#">{% trans "Support Us" %}</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-3 col-md-6">
|
||||
<h5>{% trans "Need Help" %}</h5>
|
||||
<ul class="list-unstyled">
|
||||
<li><a href="#">{% trans "Support and Services" %}</a></li>
|
||||
<li><a href="#">{% trans "Contact Us" %}</a></li>
|
||||
<li><a href="#">{% trans "FAQ" %}</a></li>
|
||||
<li><a href="#">{% trans "Sitemap" %}</a></li>
|
||||
</ul>
|
||||
<div class="contact-info mt-3">
|
||||
<i class="fas fa-phone-alt me-2 text-white-50"></i> <strong class="text-white">966118200000</strong><br>
|
||||
<i class="fas fa-envelope me-2 text-white-50"></i> <a href="mailto:info@kaauh.edu.sa">info@kaauh.edu.sa</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-lg-4 col-md-6">
|
||||
<h5>{% trans "Contact & Address" %}</h5>
|
||||
<p>{% trans "KAAUH Campus, Riyadh, Saudi Arabia" %}</p>
|
||||
<p class="small text-white-50">{% trans "Postal Code 11564, King Fahd Road, Al-Rabi District." %}</p>
|
||||
|
||||
<div class="d-flex align-items-center justify-content-start gap-3 mt-4">
|
||||
<img src="{% static 'image/vision.svg' %}" alt="Vision 2030" class="vision-logo">
|
||||
<div class="social-icons">
|
||||
<a href="#"><i class="fab fa-facebook-f"></i></a>
|
||||
<a href="#"><i class="fab fa-twitter"></i></a>
|
||||
<a href="#"><i class="fab fa-instagram"></i></a>
|
||||
<a href="#"><i class="fab fa-youtube"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-bottom mt-5">
|
||||
<div class="max-width-container d-flex justify-content-between align-items-center flex-wrap">
|
||||
<p class="mb-0">
|
||||
© {% now "Y" %} {% trans "King Abdullah Academic University Hospital (KAAUH)." %}
|
||||
{% trans "All rights reserved." %}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
{% trans "Powered by" %} <strong class="text-white">Tenhal</strong>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
{% block customJS %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
154
templates/jobs/career.html
Normal file
154
templates/jobs/career.html
Normal file
@ -0,0 +1,154 @@
|
||||
{% extends "jobs/base_public.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* Custom style for the job list table */
|
||||
.job-listing-section {
|
||||
padding: 3rem 0;
|
||||
background-color: white;
|
||||
}
|
||||
.job-table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.08);
|
||||
width: 100%;
|
||||
}
|
||||
.job-table thead th {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 1rem 1.5rem;
|
||||
text-align: start;
|
||||
border-bottom: 2px solid var(--kaauh-teal);
|
||||
}
|
||||
.job-table tbody td {
|
||||
padding: 1rem 1.5rem;
|
||||
vertical-align: middle;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
font-size: 0.95rem;
|
||||
font-weight: 500;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.job-table tbody tr:hover {
|
||||
background-color: var(--kaauh-light-bg);
|
||||
}
|
||||
.job-link-cell {
|
||||
font-size: 0.85rem;
|
||||
text-align: center; /* Center the button in its cell */
|
||||
}
|
||||
.job-link-cell i {
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
/* Apply Button */
|
||||
.btn-apply {
|
||||
background-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 0.4rem 1.2rem;
|
||||
border-radius: 5px;
|
||||
transition: background-color 0.2s;
|
||||
min-width: 80px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
border: 1px solid var(--kaauh-teal);
|
||||
}
|
||||
.btn-apply:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
}
|
||||
.hero-section{
|
||||
background-image: url("{% static 'image/kaauh_banner.png' %}");
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/* Hero Text Positioning */
|
||||
.hero-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.arabic-title {
|
||||
font-size: 4rem;
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* RTL specific table alignment */
|
||||
html[dir="rtl"] .job-table thead th {
|
||||
text-align: right;
|
||||
}
|
||||
html[dir="rtl"] .job-link-cell {
|
||||
text-align: center; /* Ensure button remains centered in RTL */
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
<div class="hero-section">
|
||||
<div class="hero-overlay"></div>
|
||||
<div class="hero-content">
|
||||
<p class="text-uppercase small fw-bold mb-1">
|
||||
{% trans "Home Page" %}
|
||||
</p>
|
||||
<h1 class="arabic-title">
|
||||
{% if LANGUAGE_CODE == 'ar' %}التوظيف{% else %}Careers{% endif %}
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="job-listing-section">
|
||||
<div class="max-width-container">
|
||||
<h2 class="h3 mb-4 text-center" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
{% trans "Open Positions" %}
|
||||
</h2>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="job-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="width: 10%;">{% trans "Job ID" %}</th>
|
||||
<th scope="col">{% trans "Job Title" %}</th>
|
||||
<th scope="col" style="width: 10%;">{% trans "Hiring" %}</th>
|
||||
<th scope="col" style="width: 15%;">{% trans "Posting Date" %}</th>
|
||||
<th scope="col" style="width: 15%;">{% trans "Apply Before" %}</th>
|
||||
<th scope="col" style="width: 10%;">{% trans "Apply" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% comment %} Django loop would typically go here: {% for job in jobs %} {% endcomment %}
|
||||
{% for job in active_jobs %}
|
||||
<tr>
|
||||
<td class="text-nowrap">{{job.internal_job_id}}</td>
|
||||
<td class="text-nowrap">{{job.title}}</td>
|
||||
<td>{{job.open_positions}}</td>
|
||||
<td>{{job.application_start_date}}</td>
|
||||
<td>{{job.application_deadline}}</td>
|
||||
<td class="job-link-cell">
|
||||
<a href="{% url 'job_detail_candidate' job.slug %}" class="btn-apply">{% trans "Apply" %}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="text-center mt-5">
|
||||
<a href="#" class="btn btn-lg btn-secondary">
|
||||
<i class="fas fa-list-alt me-2"></i> {% trans "View All Openings" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@ -3,286 +3,387 @@
|
||||
|
||||
{% block title %}{{ job.title }} - Applicants{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1">
|
||||
<a href="{% url 'job_detail' job.slug %}" class="text-decoration-none">
|
||||
<i class="fas fa-arrow-left me-2"></i>
|
||||
</a>
|
||||
Applicants for "{{ job.title }}"
|
||||
</h1>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}">Dashboard</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_list' %}">Jobs</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_detail' job.slug %}">{{ job.title }}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Applicants</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-success">
|
||||
<i class="fas fa-user-plus"></i> Add New Applicant
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* ================================================= */
|
||||
/* THEME VARIABLES AND GLOBAL STYLES */
|
||||
/* ================================================= */
|
||||
:root {
|
||||
--kaauh-teal: #00636e; /* Primary */
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-primary-text: #343a40;
|
||||
}
|
||||
|
||||
<!-- Job Summary Card -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<h5 class="card-title mb-3">{{ job.title }}</h5>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">Department:</small>
|
||||
<div>{{ job.department|default:"Not specified" }}</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">Location:</small>
|
||||
<div>{{ job.get_location_display }}</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">Job Type:</small>
|
||||
<div>{{ job.get_job_type_display }}</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<small class="text-muted">Workplace:</small>
|
||||
<div>{{ job.get_workplace_type_display }}</div>
|
||||
/* Primary Color Overrides */
|
||||
.text-primary { color: var(--kaauh-teal) !important; }
|
||||
.text-info { color: #17a2b8 !important; }
|
||||
.text-success { color: #28a745 !important; }
|
||||
.text-secondary { color: #6c757d !important; }
|
||||
.bg-primary { background-color: var(--kaauh-teal) !important; }
|
||||
.bg-info { background-color: #17a2b8 !important; }
|
||||
.bg-success { background-color: #28a745 !important; }
|
||||
.bg-secondary { background-color: #6c757d !important; }
|
||||
.bg-warning { background-color: #ffc107 !important; }
|
||||
|
||||
/* Card enhancements */
|
||||
.card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-radius: 0.75rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
font-weight: 600;
|
||||
padding: 1rem 1.25rem;
|
||||
background-color: #f8f9fa; /* Light background */
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
/* Main Action Button Style */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 0.6rem 1.2rem;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.btn-main-action:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Secondary outline button */
|
||||
.btn-outline-secondary {
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal);
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn-outline-secondary:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
.table-hover tbody tr:hover {
|
||||
background-color: #f3f7f9;
|
||||
cursor: pointer;
|
||||
}
|
||||
.table thead th {
|
||||
color: #6c757d;
|
||||
font-weight: 600;
|
||||
border-bottom: 2px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
/* Card view specific styling */
|
||||
.card-view .card-header {
|
||||
background-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
}
|
||||
.card-view .card-footer {
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.4em 0.8em;
|
||||
border-radius: 0.4rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.7px;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1 fw-bold">
|
||||
<a href="{% url 'job_detail' job.slug %}" class="text-primary text-decoration-none">
|
||||
<i class="fas fa-arrow-left me-2"></i>
|
||||
</a>
|
||||
{% trans "Applicants for" %} "{{ job.title }}"
|
||||
</h1>
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb mb-0 small">
|
||||
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}" class="text-secondary">{% trans "Dashboard" %}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_list' %}" class="text-secondary">{% trans "Jobs" %}</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_detail' job.slug %}" class="text-secondary">{{ job.title }}</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page">{% trans "Applicants" %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus"></i> {% trans "Add New Applicant" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<h5 class="fw-bold mb-3 text-primary">{{ job.title }}</h5>
|
||||
<div class="row small text-secondary g-2">
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-building me-2 text-info"></i> <strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-map-pin me-2 text-info"></i> <strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-briefcase me-2 text-info"></i> <strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-globe me-2 text-info"></i> <strong>{% trans "Workplace:" %}</strong> {{ job.get_workplace_type_display }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="text-end">
|
||||
<span class="badge bg-{{ job.status|lower }} status-badge">
|
||||
<div class="col-md-4 text-end border-start ps-4">
|
||||
<span class="badge status-badge mb-2 bg-{% if job.status == 'ACTIVE' %}success{% elif job.status == 'DRAFT' or job.status == 'ARCHIVED' %}secondary{% elif job.status == 'CLOSED' %}warning{% else %}danger{% endif %}">
|
||||
{{ job.get_status_display }}
|
||||
</span>
|
||||
{% if candidates %}
|
||||
<div class="mt-2">
|
||||
<small class="text-muted">Total Applicants:</small>
|
||||
<h4 class="text-primary mb-0">{{ candidates.count }}</h4>
|
||||
<small class="text-muted d-block">{% trans "Total Applicants" %}:</small>
|
||||
<h3 class="text-primary fw-bold mb-0">{{ candidates.count }}</h3>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filters -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="search" class="form-label">Search Applicants</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
||||
<input type="text" class="form-control" id="search" name="search"
|
||||
placeholder="Search by name, email, phone, or stage..."
|
||||
value="{{ search_query }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-primary w-100">
|
||||
<i class="fas fa-filter"></i> Search
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<a href="{% url 'job_candidates_list' job.slug %}" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-times"></i> Clear
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<h5 class="mb-0">Applicants ({{ candidates.count }})</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select form-select-sm" style="width: auto;" onchange="window.location.href='?stage='+this.value+'&search={{ search_query }}'">
|
||||
<option value="">All Stages</option>
|
||||
<option value="Applied" {% if request.GET.stage == 'Applied' %}selected{% endif %}>Applied</option>
|
||||
<option value="Interview" {% if request.GET.stage == 'Interview' %}selected{% endif %}>Interview</option>
|
||||
<option value="Offer" {% if request.GET.stage == 'Offer' %}selected{% endif %}>Offer</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="card shadow-sm mb-4">
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<label for="search" class="form-label small text-muted">{% trans "Search Applicants" %}</label>
|
||||
<div class="input-group">
|
||||
<span class="input-group-text"><i class="fas fa-search text-primary"></i></span>
|
||||
<input type="text" class="form-control" id="search" name="search"
|
||||
placeholder="{% trans 'Search by name, email, phone, or stage...' %}"
|
||||
value="{{ search_query }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<input type="checkbox" class="form-check-input" id="selectAll">
|
||||
</th>
|
||||
<th scope="col">Name</th>
|
||||
<th scope="col">Email</th>
|
||||
<th scope="col">Phone</th>
|
||||
<th scope="col">Stage</th>
|
||||
<th scope="col">Applied Date</th>
|
||||
<th scope="col" class="text-center">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<button type="submit" class="btn btn-main-action w-100">
|
||||
<i class="fas fa-filter"></i> {% trans "Filter Results" %}
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-md-3 d-flex align-items-end">
|
||||
<a href="{% url 'job_candidates_list' job.slug %}" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-times"></i> {% trans "Clear Filters" %}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if candidates %}
|
||||
<div id="job-candidates-list">
|
||||
{# View Switcher (Assuming this template is customized to match the new UI style) #}
|
||||
{% include "includes/_list_view_switcher.html" with list_id="job-candidates-list" %}
|
||||
|
||||
{# Table View (Default) #}
|
||||
<div class="table-view active">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="mb-0">{% trans "Applicants" %} ({{ candidates.count }})</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<select class="form-select form-select-sm" style="width: auto;" onchange="window.location.href='?stage='+this.value+'&search={{ search_query }}'">
|
||||
<option value="">{% trans "All Stages" %}</option>
|
||||
<option value="Applied" {% if request.GET.stage == 'Applied' %}selected{% endif %}>{% trans "Applied" %}</option>
|
||||
<option value="Exam" {% if request.GET.stage == 'Exam' %}selected{% endif %}>{% trans "Exam" %}</option>
|
||||
<option value="Interview" {% if request.GET.stage == 'Interview' %}selected{% endif %}>{% trans "Interview" %}</option>
|
||||
<option value="Offer" {% if request.GET.stage == 'Offer' %}selected{% endif %}>{% trans "Offer" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="checkbox" class="form-check-input candidate-checkbox" value="{{ candidate.slug }}">
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
<strong>{{ candidate.first_name }} {{ candidate.last_name }}</strong>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ candidate.email }}</td>
|
||||
<td>{{ candidate.phone|default:"-" }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if candidate.stage == 'Applied' %}primary{% elif candidate.stage == 'Interview' %}info{% elif candidate.stage == 'Offer' %}success{% else %}secondary{% endif %}">
|
||||
{{ candidate.stage }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ candidate.created_at|date:"M d, Y" }}</td>
|
||||
<td class="text-center">
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-primary btn-sm" title="View">
|
||||
<i class="fas fa-eye"></i>
|
||||
<th scope="col" class="ps-3">
|
||||
<input type="checkbox" class="form-check-input" id="selectAll">
|
||||
</th>
|
||||
<th scope="col">{% trans "Name" %}</th>
|
||||
<th scope="col">{% trans "Email" %}</th>
|
||||
<th scope="col">{% trans "Phone" %}</th>
|
||||
<th scope="col">{% trans "Stage" %}</th>
|
||||
<th scope="col">{% trans "Applied Date" %}</th>
|
||||
<th scope="col" class="text-center pe-3">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
<tr>
|
||||
<td class="ps-3">
|
||||
<input type="checkbox" class="form-check-input candidate-checkbox" value="{{ candidate.slug }}">
|
||||
</td>
|
||||
<td>
|
||||
<strong class="text-primary">{{ candidate.first_name }} {{ candidate.last_name }}</strong>
|
||||
</td>
|
||||
<td>{{ candidate.email }}</td>
|
||||
<td>{{ candidate.phone|default:"-" }}</td>
|
||||
<td>
|
||||
<span class="badge bg-{% if candidate.stage == 'Applied' %}primary{% elif candidate.stage == 'Exam' %}info{% elif candidate.stage == 'Interview' %}warning text-dark{% elif candidate.stage == 'Offer' %}success{% else %}secondary{% endif %}">
|
||||
{{ candidate.stage }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-secondary small">{{ candidate.created_at|date:"M d, Y" }}</td>
|
||||
<td class="text-center pe-3">
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'View' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-secondary btn-sm" 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="modal" data-bs-target="#deleteModal"
|
||||
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="card-footer d-flex justify-content-between align-items-center bg-light">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="me-3 small text-muted">{% trans "Selected" %}: <strong id="selectedCount">0</strong></span>
|
||||
<button class="btn btn-sm btn-outline-secondary me-2" onclick="bulkAction('Interview')"
|
||||
{% if not user.is_staff %}disabled{% endif %}>
|
||||
<i class="fas fa-comments me-1"></i> {% trans "Mark Interview" %}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-main-action" onclick="bulkAction('Offer')"
|
||||
{% if not user.is_staff %}disabled{% endif %}>
|
||||
<i class="fas fa-handshake me-1"></i> {% trans "Mark Offer" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-secondary btn-sm" title="Edit">
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active"><span class="page-link bg-primary border-primary">{{ num }}</span></li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}{% if search_query %}&search={{ search_query }}{% endif %}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 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 shadow-sm">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<h5 class="h5 mb-1 text-white">{{ candidate.first_name }} {{ candidate.last_name }}</h5>
|
||||
<small class="text-white-50">{{ candidate.email }}</small>
|
||||
</div>
|
||||
<span class="badge status-badge bg-{% if candidate.stage == 'Applied' %}bg-secondary{% elif candidate.stage == 'Exam' %}bg-info{% elif candidate.stage == 'Interview' %}bg-warning text-dark{% elif candidate.stage == 'Offer' %}bg-success{% else %}bg-secondary{% endif %}">
|
||||
{{ candidate.stage }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="card-body small text-secondary">
|
||||
<p class="mb-2">
|
||||
<i class="fas fa-phone me-2 text-primary"></i> <strong>{% trans "Phone" %}:</strong> {{ candidate.phone|default:"N/A" }}
|
||||
</p>
|
||||
<p class="mb-0">
|
||||
<i class="fas fa-calendar-alt me-2 text-primary"></i> <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-outline-secondary w-100">
|
||||
<i class="fas fa-eye"></i> {% trans "View Profile" %}
|
||||
</a>
|
||||
{% if user.is_staff %}
|
||||
<div class="btn-group" 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="Delete"
|
||||
data-bs-toggle="deleteModal"
|
||||
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-delete-url="{% url 'candidate_delete' candidate.slug %}"
|
||||
data-item-name="{{ candidate.first_name }} {{ candidate.last_name }}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Bulk Actions -->
|
||||
<div class="card-footer d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="me-3">Selected: <span id="selectedCount">0</span></span>
|
||||
<button class="btn btn-sm btn-outline-primary me-2" onclick="bulkAction('interview')"
|
||||
{% if not user.is_staff %}disabled{% endif %}>
|
||||
<i class="fas fa-comments"></i> Mark as Interview
|
||||
</button>
|
||||
<button class="btn btn-sm btn-success" onclick="bulkAction('offer')"
|
||||
{% if not user.is_staff %}disabled{% endif %}>
|
||||
<i class="fas fa-handshake"></i> Mark as Offer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
{% if is_paginated %}
|
||||
<nav aria-label="Page navigation">
|
||||
<ul class="pagination pagination-sm mb-0">
|
||||
{% if page_obj.has_previous %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}" aria-label="Previous">
|
||||
<span aria-hidden="true">«</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
{% for num in page_obj.paginator.page_range %}
|
||||
{% if page_obj.number == num %}
|
||||
<li class="page-item active"><span class="page-link">{{ num }}</span></li>
|
||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ num }}{% if search_query %}&search={{ search_query }}{% endif %}">{{ num }}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% if page_obj.has_next %}
|
||||
<li class="page-item">
|
||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}" aria-label="Next">
|
||||
<span aria-hidden="true">»</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 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>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-user-slash fa-3x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">No applicants found</h4>
|
||||
<p class="text-muted">There are no candidates who have applied for this position yet.</p>
|
||||
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-primary">
|
||||
<i class="fas fa-user-plus"></i> Add First Applicant
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-user-slash fa-3x text-muted mb-3"></i>
|
||||
<h4 class="text-muted">{% trans "No applicants found" %}</h4>
|
||||
<p class="text-secondary">{% trans "There are no candidates who have applied for this position yet." %}</p>
|
||||
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action mt-3">
|
||||
<i class="fas fa-user-plus"></i> {% trans "Add First Applicant" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Delete Modal -->
|
||||
{% include "includes/delete_modal.html" %}
|
||||
{% include "includes/delete_modal.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
@ -320,7 +421,28 @@ function bulkAction(stage) {
|
||||
console.log('Updating candidates:', selectedCandidates, 'to stage:', stage);
|
||||
// For now, just show a message
|
||||
alert(`Bulk update functionality would mark ${selectedCandidates.length} candidates as ${stage}`);
|
||||
// After a successful AJAX request, you would likely reload the page or update the rows dynamically.
|
||||
}
|
||||
}
|
||||
|
||||
// Bootstrap Delete Modal Fix for dynamically loaded data attributes
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const deleteModal = document.getElementById('deleteModal');
|
||||
if (deleteModal) {
|
||||
deleteModal.addEventListener('show.bs.modal', function (event) {
|
||||
const button = event.relatedTarget;
|
||||
const deleteUrl = button.getAttribute('data-delete-url');
|
||||
const itemName = button.getAttribute('data-item-name');
|
||||
|
||||
const modalTitle = deleteModal.querySelector('.modal-title');
|
||||
const modalBody = deleteModal.querySelector('.modal-body p');
|
||||
const deleteForm = deleteModal.querySelector('form');
|
||||
|
||||
modalTitle.textContent = `Delete ${itemName}`;
|
||||
modalBody.innerHTML = `Are you sure you want to permanently delete the applicant <strong>${itemName}</strong>? This action cannot be undone.`;
|
||||
deleteForm.action = deleteUrl;
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -1,18 +1,39 @@
|
||||
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
{% block title %}{{ job.title }} - University ATS{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
|
||||
<style>
|
||||
/* Custom styles for the Job Detail Page (using variables from base.html) */
|
||||
/* ================================================= */
|
||||
/* THEME VARIABLES AND GLOBAL STYLES */
|
||||
/* ================================================= */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal: #00636e; /* Primary */
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-primary-text: #343a40;
|
||||
|
||||
/* Custom Stage Colors for Tracker */
|
||||
--stage-applied: var(--kaauh-teal); /* Teal */
|
||||
--stage-exam: #17a2b8; /* Info Cyan */
|
||||
--stage-interview: #ffc107; /* Warning Yellow */
|
||||
--stage-offer: #28a745; /* Success Green */
|
||||
--stage-inactive: #6c757d; /* Secondary Gray */
|
||||
}
|
||||
|
||||
/* Primary Color Overrides */
|
||||
.text-primary { color: var(--kaauh-teal) !important; }
|
||||
.text-info { color: var(--stage-exam) !important; }
|
||||
.text-success { color: var(--stage-offer) !important; }
|
||||
.text-secondary { color: var(--stage-inactive) !important; }
|
||||
.bg-success { background-color: var(--kaauh-teal) !important; }
|
||||
.bg-warning { background-color: #ffc107 !important; }
|
||||
.bg-secondary { background-color: #6c757d !important; }
|
||||
.bg-danger { background-color: #dc3545 !important; }
|
||||
|
||||
/* Header styling */
|
||||
.job-header-card {
|
||||
background: linear-gradient(135deg, var(--kaauh-teal), #004d57);
|
||||
@ -21,13 +42,11 @@
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.job-header-card h2 {
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
/* Status badge */
|
||||
.status-badge {
|
||||
font-size: 0.9rem;
|
||||
@ -40,21 +59,6 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Mapped color classes for status badges */
|
||||
.bg-success { background-color: var(--kaauh-teal) !important; }
|
||||
.bg-warning { background-color: #ffc107 !important; }
|
||||
.bg-secondary { background-color: #6c757d !important; }
|
||||
.bg-danger { background-color: #dc3545 !important; }
|
||||
|
||||
/* Fix for active tab text visibility */
|
||||
.nav-tabs .nav-link.active,
|
||||
.right-column-tabs .nav-link.active {
|
||||
color: var(--kaauh-teal-dark) !important;
|
||||
background-color: white !important;
|
||||
border-bottom: 3px solid var(--kaauh-teal) !important;
|
||||
font-weight: 600;
|
||||
z-index: 2;
|
||||
}
|
||||
/* Card enhancements */
|
||||
.card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
@ -69,25 +73,15 @@
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Standard Card Header (used for single cards or fallback) */
|
||||
/* Standard Card Header */
|
||||
.card-header {
|
||||
font-weight: 600;
|
||||
padding: 1rem 1.25rem;
|
||||
background-color: #f8f9fa; /* Light background */
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
}
|
||||
.card-header h5 {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
|
||||
.card-footer {
|
||||
padding: 1rem 1.25rem;
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
/* Left Column Tabs Theming (main details) */
|
||||
/* Left Column Tabs Theming */
|
||||
.nav-tabs {
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
background-color: #f8f9fa;
|
||||
@ -102,51 +96,20 @@
|
||||
margin-right: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.nav-tabs .nav-link:hover {
|
||||
color: var(--kaauh-teal);
|
||||
border-color: #e9ecef;
|
||||
background-color: #fff;
|
||||
}
|
||||
.nav-tabs .nav-link.active {
|
||||
color: var(--kaauh-teal-dark);
|
||||
background-color: white;
|
||||
border-bottom: 3px solid var(--kaauh-teal);
|
||||
color: var(--kaauh-teal-dark) !important;
|
||||
background-color: white !important;
|
||||
border-bottom: 3px solid var(--kaauh-teal) !important;
|
||||
font-weight: 600;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* ==================================== */
|
||||
/* RIGHT COLUMN TABS STYLING (IMPROVED) */
|
||||
/* ==================================== */
|
||||
|
||||
/* Right Column Tabs */
|
||||
.right-column-tabs {
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
}
|
||||
.right-column-tabs .nav-tabs {
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
}
|
||||
.right-column-tabs .nav-item {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
}
|
||||
.right-column-tabs .nav-link {
|
||||
padding: 0.9rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
border-radius: 0;
|
||||
border-right: 1px solid var(--kaauh-border);
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.right-column-tabs .nav-item:last-child .nav-link {
|
||||
border-right: none;
|
||||
}
|
||||
.right-column-tabs .nav-link.active {
|
||||
background-color: white;
|
||||
color: var(--kaauh-teal-dark);
|
||||
@ -154,23 +117,24 @@
|
||||
border-right-color: transparent;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
.right-column-tabs .nav-link:not(.active):hover {
|
||||
background-color: #f0f4f7;
|
||||
color: var(--kaauh-teal);
|
||||
|
||||
/* Main Action Button Style */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 0.6rem 1.2rem;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.right-column-tabs .tab-content {
|
||||
padding: 1.5rem 1.25rem;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Section styling */
|
||||
.job-section h5 {
|
||||
color: var(--kaauh-teal);
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 1.25rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
.btn-outline-secondary {
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
/* Applicant stats */
|
||||
@ -189,45 +153,6 @@
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Primary Color Overrides */
|
||||
.text-primary { color: var(--kaauh-teal) !important; }
|
||||
.text-info { color: #17a2b8 !important; }
|
||||
.text-success { color: #28a745 !important; }
|
||||
.text-secondary { color: #6c757d !important; }
|
||||
|
||||
/* Main Action Button Style */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 0.6rem 1.2rem;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.btn-main-action:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Secondary outline button (for forms/back links) */
|
||||
.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);
|
||||
}
|
||||
|
||||
/* Specific styling for the deadline box */
|
||||
.deadline-box {
|
||||
padding: 0.75rem;
|
||||
@ -235,18 +160,143 @@
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
/* Table styling for the Applicant preview */
|
||||
.table-applicants tbody tr:hover {
|
||||
background-color: #f3f9f9;
|
||||
/* ==================================== */
|
||||
/* MULTI-COLORED CANDIDATE STAGE TRACKER */
|
||||
/* ==================================== */
|
||||
|
||||
.progress-stages {
|
||||
position: relative;
|
||||
padding: 1.5rem 0;
|
||||
}
|
||||
.table-applicants td {
|
||||
border-top: 1px solid var(--kaauh-border);
|
||||
|
||||
.progress-stages a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.stage-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
min-width: 60px;
|
||||
transition: all 0.3s ease;
|
||||
color: var(--stage-inactive);
|
||||
}
|
||||
|
||||
.stage-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background-color: #e9ecef;
|
||||
color: var(--stage-inactive);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.9rem;
|
||||
z-index: 10;
|
||||
border: 2px solid white;
|
||||
box-shadow: 0 0 0 2px #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
/* ---------------- STAGE SPECIFIC COLORS ---------------- */
|
||||
|
||||
/* APPLIED STAGE (Teal) */
|
||||
.stage-item[data-stage="Applied"].completed .stage-icon,
|
||||
.stage-item[data-stage="Applied"].active .stage-icon {
|
||||
background-color: var(--stage-applied);
|
||||
color: white;
|
||||
}
|
||||
.stage-item[data-stage="Applied"].active { color: var(--stage-applied); }
|
||||
|
||||
/* EXAM STAGE (Cyan/Info) */
|
||||
.stage-item[data-stage="Exam"].completed .stage-icon,
|
||||
.stage-item[data-stage="Exam"].active .stage-icon {
|
||||
background-color: var(--stage-exam);
|
||||
color: white;
|
||||
}
|
||||
.stage-item[data-stage="Exam"].active { color: var(--stage-exam); }
|
||||
|
||||
/* INTERVIEW STAGE (Yellow/Warning) */
|
||||
.stage-item[data-stage="Interview"].completed .stage-icon,
|
||||
.stage-item[data-stage="Interview"].active .stage-icon {
|
||||
background-color: var(--stage-interview);
|
||||
color: var(--kaauh-primary-text); /* Dark text for light background */
|
||||
}
|
||||
.stage-item[data-stage="Interview"].active { color: var(--stage-interview); }
|
||||
|
||||
/* OFFER STAGE (Green/Success) */
|
||||
.stage-item[data-stage="Offer"].completed .stage-icon,
|
||||
.stage-item[data-stage="Offer"].active .stage-icon {
|
||||
background-color: var(--stage-offer);
|
||||
color: white;
|
||||
}
|
||||
.stage-item[data-stage="Offer"].active { color: var(--stage-offer); }
|
||||
|
||||
/* ---------------- GENERIC ACTIVE/COMPLETED STYLING ---------------- */
|
||||
|
||||
/* Active State (Applies glow/scale to current stage) */
|
||||
.stage-item.active .stage-icon {
|
||||
box-shadow: 0 0 0 4px rgba(0, 99, 110, 0.4);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
.stage-item.active .stage-count {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Completed State (Applies dark text color to completed stages) */
|
||||
.stage-item.completed {
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
|
||||
/* Connector Line */
|
||||
.stage-connector {
|
||||
flex-grow: 1;
|
||||
height: 3px;
|
||||
background-color: #e9ecef;
|
||||
margin: 0 0.5rem;
|
||||
position: relative;
|
||||
top: -18px;
|
||||
z-index: 1;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Line in completed state (Kept the line teal/primary for consistency) */
|
||||
.stage-connector.completed {
|
||||
background-color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
/* Labels and counts */
|
||||
.stage-label {
|
||||
font-size: 0.75rem;
|
||||
margin-top: 0.4rem;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.stage-count {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
margin-top: 0.1rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
|
||||
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}" class="text-secondary">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_list' %}" class="text-secondary">Jobs</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page" class="text-secondary">Job Detail</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="row g-4">
|
||||
|
||||
{# LEFT COLUMN: JOB DETAILS WITH TABS #}
|
||||
@ -335,6 +385,23 @@
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-edit me-2 text-primary"></i> <strong>{% trans "Updated At:" %}</strong> {{ job.updated_at|default:"N/A" }}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-outline-secondary"
|
||||
id="copyJobLinkButton"
|
||||
data-url="{{ job.application_url }}">
|
||||
<i class="fas fa-link me-1"></i>
|
||||
{% trans "Copy Public Link" %}
|
||||
</button>
|
||||
|
||||
<span id="copyFeedback" class="text-success ms-2 small" style="display:none;">
|
||||
{% trans "Copied!" %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
<h5 class="text-muted mb-3">{% trans "Financial & Timeline" %}</h5>
|
||||
<div class="row g-3">
|
||||
@ -409,18 +476,80 @@
|
||||
<i class="fas fa-edit"></i> {% trans "Edit Job" %}
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#myModalForm">
|
||||
<i class="fas fa-image me-1"></i> {% trans "Upload Image for Post" %}
|
||||
</button>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# RIGHT COLUMN: TABBED CARDS #}
|
||||
<div class="col-lg-4">
|
||||
<div class="card shadow-sm no-hover">
|
||||
<div class="col-lg-4 ">
|
||||
<div class="card shadow-sm no-hover mb-4">
|
||||
<div class="card-body p-4">
|
||||
<h6 class="text-muted mb-4">{% trans "Applicant Tracking" %}</h6>
|
||||
<div class="progress-stages">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
|
||||
{% comment %} STAGE 1: Applied {% endcomment %}
|
||||
<a href="{% url 'job_candidates_list' job.slug %}?stage=Applied"
|
||||
class="stage-item {% if current_stage == 'Applied' %}active{% endif %} {% if current_stage != 'Applied' and candidate.stage_history_has.Applied %}completed{% endif %}"
|
||||
data-stage="Applied">
|
||||
<div class="stage-icon">
|
||||
<i class="fas fa-file-signature"></i>
|
||||
</div>
|
||||
<div class="stage-label">{% trans "Applied" %}</div>
|
||||
<div class="stage-count">{{ applied_count|default:"0" }}</div>
|
||||
</a>
|
||||
|
||||
{% comment %} CONNECTOR 1 -> 2 {% endcomment %}
|
||||
<div class="stage-connector {% if current_stage != 'Applied' and candidate.stage_history_has.Exam %}completed{% endif %}"></div>
|
||||
|
||||
{% comment %} STAGE 2: Exam {% endcomment %}
|
||||
<a href="{% url 'job_candidates_list' job.slug %}?stage=Exam"
|
||||
class="stage-item {% if current_stage == 'Exam' %}active{% endif %} {% if current_stage != 'Exam' and candidate.stage_history_has.Exam %}completed{% endif %}"
|
||||
data-stage="Exam">
|
||||
<div class="stage-icon">
|
||||
<i class="fas fa-clipboard-check"></i>
|
||||
</div>
|
||||
<div class="stage-label">{% trans "Exam" %}</div>
|
||||
<div class="stage-count">{{ exam_count|default:"0" }}</div>
|
||||
</a>
|
||||
|
||||
{% comment %} CONNECTOR 2 -> 3 {% endcomment %}
|
||||
<div class="stage-connector {% if current_stage != 'Exam' and candidate.stage_history_has.Interview %}completed{% endif %}"></div>
|
||||
|
||||
{% comment %} STAGE 3: Interview {% endcomment %}
|
||||
<a href="{% url 'job_candidates_list' job.slug %}?stage=Interview"
|
||||
class="stage-item {% if current_stage == 'Interview' %}active{% endif %} {% if current_stage != 'Interview' and candidate.stage_history_has.Interview %}completed{% endif %}"
|
||||
data-stage="Interview">
|
||||
<div class="stage-icon">
|
||||
<i class="fas fa-comments"></i>
|
||||
</div>
|
||||
<div class="stage-label">{% trans "Interview" %}</div>
|
||||
<div class="stage-count">{{ interview_count|default:"0" }}</div>
|
||||
</a>
|
||||
|
||||
{% comment %} CONNECTOR 3 -> 4 {% endcomment %}
|
||||
<div class="stage-connector {% if current_stage != 'Interview' and candidate.stage_history_has.Offer %}completed{% endif %}"></div>
|
||||
|
||||
{% comment %} STAGE 4: Offer {% endcomment %}
|
||||
<a href="{% url 'job_candidates_list' job.slug %}?stage=Offer"
|
||||
class="stage-item {% if current_stage == 'Offer' %}active{% endif %} {% if current_stage != 'Offer' and candidate.stage_history_has.Offer %}completed{% endif %}"
|
||||
data-stage="Offer">
|
||||
<div class="stage-icon">
|
||||
<i class="fas fa-handshake"></i>
|
||||
</div>
|
||||
<div class="stage-label">{% trans "Offer" %}</div>
|
||||
<div class="stage-count">{{ offer_count|default:"0" }}</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm no-hover" style="height:400px;">
|
||||
|
||||
{# RIGHT TABS NAVIGATION #}
|
||||
<ul class="nav nav-tabs right-column-tabs" id="rightJobTabs" role="tablist">
|
||||
@ -431,7 +560,7 @@
|
||||
</li>
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
<button class="nav-link" id="manage-tab" data-bs-toggle="tab" data-bs-target="#manage-pane" type="button" role="tab" aria-controls="manage-pane" aria-selected="false">
|
||||
<i class="fas fa-cogs me-1 text-secondary"></i> {% trans "Manage" %}
|
||||
<i class="fas fa-cogs me-1 text-secondary"></i> {% trans "Form Template" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
@ -445,8 +574,8 @@
|
||||
|
||||
{# TAB 1: APPLICANTS CONTENT #}
|
||||
<div class="tab-pane fade show active" id="applicants-pane" role="tabpanel" aria-labelledby="applicants-tab">
|
||||
<h5 class="mb-3">{% trans "Candidates" %} (<span id="total_candidates">{{ total_candidates }}</span>)</h5>
|
||||
{% if total_candidates > 0 %}
|
||||
<h5 class="mb-3">{% trans "Total Applicants" %} (<span id="total_candidates">{{ total_applicants }}</span>)</h5>
|
||||
{% if total_applicants > 0 %}
|
||||
<div class="row mb-4 applicant-stats">
|
||||
<div class="col-4">
|
||||
<div class="stat-item">
|
||||
@ -469,15 +598,18 @@
|
||||
</div>
|
||||
<div class="col-12 mb-2">
|
||||
<a href="{% url 'job_candidates_list' job.slug %}" class="btn btn-outline-secondary w-100">
|
||||
{% trans "View All Applicants" %} ({{ total_candidates }})
|
||||
{% trans "View All Applicants" %} ({{ total_applicants }})
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
{% endif %}
|
||||
|
||||
|
||||
<div class="d-grid gap-2">
|
||||
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus"></i> {% trans "Create Candidate" %}
|
||||
<i class="fas fa-user-plus"></i> {% trans "Create Applicant" %}
|
||||
</a>
|
||||
<a href="{% url 'candidate_tier_management' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-layer-group"></i> {% trans "Manage Applicants" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@ -486,7 +618,7 @@
|
||||
<div class="tab-pane fade" id="manage-pane" role="tabpanel" aria-labelledby="manage-tab">
|
||||
|
||||
{# LinkedIn Integration (Content from old card) #}
|
||||
|
||||
|
||||
|
||||
{# Applicant Form Management (Content from old card) #}
|
||||
<h5 class="mb-3"><i class="fas fa-clipboard-list me-2 text-primary"></i>{% trans "Form Management" %}</h5>
|
||||
@ -494,7 +626,7 @@
|
||||
<p class="text-muted small mb-3">
|
||||
{% trans "Manage the custom application forms associated with this job posting." %}
|
||||
</p>
|
||||
|
||||
|
||||
{% if not job.form_template %}
|
||||
<a href="{% url 'create_form_template' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus-circle me-2"></i> {% trans "Create New Form Template" %}
|
||||
@ -505,7 +637,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -551,6 +683,41 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% comment %} {# Applicant Form Management (Content from old card) #}
|
||||
<h5 class="mb-3"><i class="fas fa-clipboard-list me-2 text-primary"></i>{% trans "Form Management" %}</h5> {% endcomment %}
|
||||
{% comment %} <div class="d-grid gap-2">
|
||||
<p class="text-muted small mb-3">
|
||||
{% trans "Manage the custom application forms associated with this job posting." %}
|
||||
</p> {% endcomment %}
|
||||
|
||||
{% comment %} <a href="{% url 'create_form_template' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus-circle me-2"></i> {% trans "Create New Form" %}
|
||||
</a>
|
||||
|
||||
<a href="" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-list-alt me-1"></i> {% trans "View All Existing Forms" %}
|
||||
</a> {% endcomment %}
|
||||
|
||||
{% comment %} <a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus"></i> {% trans "Create Applicant" %}
|
||||
</a> {% endcomment %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# TAB 3: INTERNAL INFO CONTENT #}
|
||||
<div class="tab-pane fade" id="internal-pane" role="tabpanel" aria-labelledby="internal-tab">
|
||||
<h5 class="mb-3"><i class="fas fa-info-circle me-2 text-secondary"></i>{% trans "Internal Information" %}</h5>
|
||||
<div class="small">
|
||||
<p class="mb-1"><strong>{% trans "Internal Job ID:" %}</strong> {{ job.internal_job_id }}</p>
|
||||
<p class="mb-1"><strong>{% trans "Created:" %}</strong> {{ job.created_at|date:"M d, Y" }}</p>
|
||||
<p class="mb-1"><strong>{% trans "Last Updated:" %}</strong> {{ job.updated_at|date:"M d, Y" }}</p>
|
||||
{% if job.reporting_to %}
|
||||
<p class="mb-0"><strong>{% trans "Reports To:" %}</strong> {{ job.reporting_to }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{% url 'job_list' %}" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to Jobs" %}
|
||||
@ -598,4 +765,34 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block customJS%}
|
||||
|
||||
|
||||
<script>
|
||||
document.getElementById('copyJobLinkButton').addEventListener('click', function() {
|
||||
// 1. Get the URL from the data attribute
|
||||
const urlToCopy = this.getAttribute('data-url');
|
||||
|
||||
// 2. Use the modern Clipboard API
|
||||
navigator.clipboard.writeText(urlToCopy).then(() => {
|
||||
|
||||
// 3. Show feedback message
|
||||
const feedback = document.getElementById('copyFeedback');
|
||||
feedback.style.display = 'inline';
|
||||
|
||||
// 4. Hide feedback after 2 seconds
|
||||
setTimeout(() => {
|
||||
feedback.style.display = 'none';
|
||||
}, 2000);
|
||||
|
||||
}).catch(err => {
|
||||
// Fallback for older browsers or security issues
|
||||
console.error('Could not copy text: ', err);
|
||||
alert("Copy failed. Please copy the URL manually: " + urlToCopy);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -116,7 +116,7 @@
|
||||
|
||||
/* Primary Action Button (Create/Submit) */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* ================================================= */
|
||||
/* THEME VARIABLES AND GLOBAL STYLES (FROM JOB DETAIL) */
|
||||
/* THEME VARIABLES AND GLOBAL STYLES */
|
||||
/* ================================================= */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
@ -21,7 +21,7 @@
|
||||
.text-success { color: #28a745 !important; }
|
||||
.text-secondary { color: #6c757d !important; }
|
||||
|
||||
/* Main Action Button Style */
|
||||
/* Main Action Button Style (Used for Download Resume) */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
@ -40,7 +40,7 @@
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
/* Outlined Button Styles */
|
||||
/* Outlined Button Styles (Only used variants kept) */
|
||||
.btn-outline-primary {
|
||||
color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
@ -58,14 +58,6 @@
|
||||
color: white;
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
.btn-outline-info {
|
||||
color: #17a2b8;
|
||||
border-color: #17a2b8;
|
||||
}
|
||||
.btn-outline-info:hover {
|
||||
background-color: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Card enhancements */
|
||||
.card {
|
||||
@ -121,70 +113,93 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Right Column Tabs (Parsed Data/Activity) */
|
||||
.right-column-card .nav-tabs {
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
border-bottom: none;
|
||||
background-color: transparent;
|
||||
}
|
||||
.right-column-card .nav-link {
|
||||
padding: 0.9rem 1rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
border-right: 1px solid var(--kaauh-border);
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.right-column-card .nav-item:last-child .nav-link {
|
||||
border-right: none;
|
||||
}
|
||||
.right-column-card .nav-link.active {
|
||||
background-color: white !important;
|
||||
color: var(--kaauh-teal-dark) !important;
|
||||
border-bottom: 3px solid var(--kaauh-teal);
|
||||
border-right-color: transparent;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
/* Right Column Card (General styling) */
|
||||
.right-column-card .tab-content {
|
||||
padding: 1.5rem 1.25rem;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
|
||||
/* ==================================== */
|
||||
/* NEW: PARSED DATA GRID OPTIMIZATION */
|
||||
/* NEW: Vertical Timeline Styling */
|
||||
/* ==================================== */
|
||||
.parsed-data-item {
|
||||
/* Ensure the border/bg container takes full width of the grid column */
|
||||
width: 100%;
|
||||
|
||||
/* Highlight box for the current stage */
|
||||
.current-stage {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
background-color: #f0f8ff; /* Light, subtle blue background */
|
||||
}
|
||||
.parsed-data-item .data-value {
|
||||
/* Allow data values to wrap without breaking the layout */
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
.current-stage .text-primary {
|
||||
color: var(--kaauh-teal) !important;
|
||||
}
|
||||
|
||||
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding-left: 2rem;
|
||||
}
|
||||
.timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 1.25rem;
|
||||
width: 2px;
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
margin-bottom: 2rem;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
.timeline-icon {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 0.8rem;
|
||||
z-index: 10;
|
||||
border: 4px solid white;
|
||||
}
|
||||
.timeline-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Custom Timeline Background Classes for Stages (Using Bootstrap color palette) */
|
||||
.timeline-bg-applied { background-color: var(--kaauh-teal) !important; }
|
||||
.timeline-bg-exam { background-color: #17a2b8 !important; }
|
||||
.timeline-bg-interview { background-color: #ffc107 !important; }
|
||||
.timeline-bg-offer { background-color: #28a745 !important; }
|
||||
.timeline-bg-rejected { background-color: #dc3545 !important; }
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="row g-4">
|
||||
<nav aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}" class="text-secondary">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_detail' candidate.job.slug %}" class="text-secondary">Job:({{candidate.job.title}})</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page" class="text-secondary">Applicant Detail</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{# LEFT COLUMN: MAIN CANDIDATE DETAILS AND TABS #}
|
||||
<div class="col-lg-8">
|
||||
<div class="card shadow-sm no-hover">
|
||||
|
||||
{# HEADER SECTION (The original Candidate Header Card content, redesigned) #}
|
||||
{# HEADER SECTION #}
|
||||
<div class="candidate-header-card">
|
||||
<div class="d-flex justify-content-between align-items-start flex-wrap">
|
||||
<div>
|
||||
<h1 class="h3 mb-2">{{ candidate.name }}</h1>
|
||||
<div class="d-flex align-items-center gap-2 mb-2">
|
||||
<span class="badge {% if candidate.applied %}bg-success{% else %}bg-warning{% endif %}">
|
||||
{{ candidate.applied|yesno:"Applied,Pending" }}
|
||||
</span>
|
||||
|
||||
<span id="stageDisplay" class="badge"
|
||||
data-class="{'bg-primary': $stage == 'Applied', 'bg-info': $stage == 'Exam', 'bg-warning': $stage == 'Interview', 'bg-success': $stage == 'Offer'}"
|
||||
data-signals-stage="'{{ candidate.stage }}'">
|
||||
@ -196,7 +211,7 @@
|
||||
{% trans "Applied for:" %} <strong>{{ candidate.job.title }}</strong>
|
||||
</small>
|
||||
</div>
|
||||
{# Action buttons moved to the right column, keeping only the most visible here for quick access if preferred #}
|
||||
{# Change Stage button #}
|
||||
{% if user.is_staff %}
|
||||
<button type="button" class="btn btn-outline-light btn-sm mt-1" data-bs-toggle="modal" data-bs-target="#stageUpdateModal">
|
||||
<i class="fas fa-exchange-alt"></i> {% trans "Change Stage" %}
|
||||
@ -222,7 +237,7 @@
|
||||
{% if candidate.parsed_summary %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="summary-tab" data-bs-toggle="tab" data-bs-target="#summary-pane" type="button" role="tab" aria-controls="summary-pane" aria-selected="false">
|
||||
<i class="fas fa-chart-bar me-1"></i> {% trans "Summary" %}
|
||||
<i class="fas fa-chart-bar me-1"></i> {% trans "Resume Summary" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
@ -231,7 +246,7 @@
|
||||
<div class="card-body">
|
||||
<div class="tab-content" id="candidateTabsContent">
|
||||
|
||||
{# TAB 1 CONTENT: CONTACT & DATES (Original Contact Card) #}
|
||||
{# TAB 1 CONTENT: CONTACT & DATES #}
|
||||
<div class="tab-pane fade show active" id="contact-pane" role="tabpanel" aria-labelledby="contact-tab">
|
||||
<h5 class="text-primary mb-4">{% trans "Core Details" %}</h5>
|
||||
<div class="row g-4">
|
||||
@ -271,7 +286,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# TAB 2 CONTENT: RESUME (Original Resume Card) #}
|
||||
{# TAB 2 CONTENT: RESUME #}
|
||||
{% if candidate.resume %}
|
||||
<div class="tab-pane fade" id="resume-pane" role="tabpanel" aria-labelledby="resume-tab">
|
||||
<h5 class="text-primary mb-4">{% trans "Resume Document" %}</h5>
|
||||
@ -288,12 +303,12 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# TAB 3 CONTENT: PARSED SUMMARY (Original Parsed Summary Card) #}
|
||||
{# TAB 3 CONTENT: PARSED SUMMARY #}
|
||||
{% if candidate.parsed_summary %}
|
||||
<div class="tab-pane fade" id="summary-pane" role="tabpanel" aria-labelledby="summary-tab">
|
||||
<h5 class="text-primary mb-4">{% trans "AI Generated Summary" %}</h5>
|
||||
<div class="border-start border-primary ps-3 pt-1 pb-1">
|
||||
<p class="mb-0 text-secondary">{{ candidate.parsed_summary|linebreaks }}</p>
|
||||
{% include 'includes/candidate_modal_body.html' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -303,18 +318,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# RIGHT COLUMN: ACTIONS AND PARSED DATA TABS #}
|
||||
{# RIGHT COLUMN: ACTIONS AND CANDIDATE TIMELINE #}
|
||||
<div class="col-lg-4">
|
||||
|
||||
{# ACTIONS CARD (The new consolidated action card) #}
|
||||
{# ACTIONS CARD #}
|
||||
{% if user.is_staff %}
|
||||
<div class="card shadow-sm mb-4 p-3">
|
||||
<h5 class="text-muted mb-3"><i class="fas fa-cog me-2"></i>{% trans "Management Actions" %}</h5>
|
||||
<div class="d-grid gap-2">
|
||||
{# MODAL TRIGGER #}
|
||||
{% comment %} <button type="button" class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#stageUpdateModal">
|
||||
<i class="fas fa-exchange-alt"></i> {% trans "Update Stage" %}
|
||||
</button> {% endcomment %}
|
||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-edit"></i> {% trans "Edit Details" %}
|
||||
</a>
|
||||
@ -328,52 +339,50 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# PARSED DATA TABS (Original Parsed Data Card, now tabbed) #}
|
||||
{% if parsed %}
|
||||
<div class="card right-column-card shadow-sm">
|
||||
<ul class="nav nav-tabs" id="parsedDataTabs" role="tablist">
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
<button class="nav-link active" id="data-tab" data-bs-toggle="tab" data-bs-target="#data-pane" type="button" role="tab" aria-controls="data-pane" aria-selected="true">
|
||||
<i class="fas fa-database me-1"></i> {% trans "Parsed Data" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
<button class="nav-link" id="activity-tab" data-bs-toggle="tab" data-bs-target="#activity-pane" type="button" role="tab" aria-controls="activity-pane" aria-selected="false">
|
||||
<i class="fas fa-history me-1"></i> {% trans "Activity" %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
{# ENHANCED: CANDIDATE JOURNEY TIMELINE CARD #}
|
||||
<div class="card shadow-sm timeline-card">
|
||||
<div class="card-header bg-white border-bottom py-3">
|
||||
<h5 class="mb-0 text-muted"><i class="fas fa-route me-2"></i>{% trans "Candidate Journey" %}</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
|
||||
<h6 class="text-uppercase text-secondary mb-3">{% trans "Current Stage" %}</h6>
|
||||
<div class="p-3 mb-4 rounded current-stage">
|
||||
<p class="mb-0 fw-bold fs-5 text-primary">{{ candidate.stage }}</p>
|
||||
<small class="text-muted d-block mt-1">
|
||||
{% trans "Latest status update:" %} {{ candidate.updated_at|date:"M d, Y" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="tab-content" id="parsedDataTabsContent">
|
||||
|
||||
{# TAB 1: PARSED DATA - UPDATED TO 2-COLUMN GRID #}
|
||||
<div class="tab-pane fade show active" id="data-pane" role="tabpanel" aria-labelledby="data-tab">
|
||||
<h6 class="text-muted small text-uppercase mb-3">{% trans "Structured Resume Data" %}</h6>
|
||||
<div class="row g-3">
|
||||
{% for key, value in parsed.items %}
|
||||
<div class="col-md-6 parsed-data-item">
|
||||
<div class="p-3 border rounded h-100 bg-light">
|
||||
<h6 class="text-muted small text-uppercase mb-1">
|
||||
<i class="fas fa-tag me-1 text-primary"></i> {{ key|title }}
|
||||
</h6>
|
||||
<p class="mb-0 text-dark small data-value">
|
||||
{{ value|default:"N/A"|linebreaksbr }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
<h6 class="text-uppercase text-secondary mb-3 pt-2 border-top">{% trans "Historical Timeline" %}</h6>
|
||||
<div class="timeline">
|
||||
|
||||
|
||||
{# Base Status: Application Submitted (Always required) #}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fa-file-signature"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Application Submitted" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.created_at|date:"M d, Y" }}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.created_at|date:"h:i A" }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# TAB 2: ACTIVITY (Placeholder) #}
|
||||
<div class="tab-pane fade" id="activity-pane" role="tabpanel" aria-labelledby="activity-tab">
|
||||
<p class="text-muted">
|
||||
{% trans "Activity feed (e.g., stage changes, notes, interview history) will appear here." %}
|
||||
{# Fallback if no history is explicitly logged #}
|
||||
{% if not candidate_stage_history %}
|
||||
<p class="text-muted mt-3 mb-0 ps-3">
|
||||
{% trans "Detailed stage history logs are currently unavailable." %}
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
232
templates/recruitment/candidate_tier_management.html
Normal file
232
templates/recruitment/candidate_tier_management.html
Normal file
@ -0,0 +1,232 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Candidate Management - {{ job.title }} - University ATS{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-primary-text: #343a40;
|
||||
--kaauh-success: #28a745; /* Standard success for positive actions */
|
||||
--kaauh-info: #17a2b8; /* Standard info/exam badge */
|
||||
}
|
||||
|
||||
/* 1. Main Container & Card Styling */
|
||||
.kaauh-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;
|
||||
}
|
||||
.tier-controls {
|
||||
background-color: var(--kaauh-border); /* Light background for control sections */
|
||||
border-radius: 0.5rem;
|
||||
padding: 1.25rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
/* 2. Button Styling (from reference) */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.btn-main-action:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.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);
|
||||
}
|
||||
|
||||
/* 3. Tab Styles (View Switcher) */
|
||||
.nav-pills .nav-link {
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-weight: 500;
|
||||
border-radius: 0.5rem;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.nav-pills .nav-link.active {
|
||||
background-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 4. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background-color: white;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
padding: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-bottom: 2px solid var(--kaauh-teal);
|
||||
}
|
||||
.candidate-table td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* 5. Badges (Status/Score) */
|
||||
.status-badge {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.3em 0.7em;
|
||||
border-radius: 0.35rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d; color: white; } /* Secondary */
|
||||
.bg-candidate { background-color: var(--kaauh-success); color: white; } /* Success */
|
||||
.bg-score { background-color: var(--kaauh-teal-dark); color: white; }
|
||||
.bg-exam-status { background-color: var(--kaauh-info); color: white; }
|
||||
|
||||
/* 6. Stage Badges (More distinct from KAAT-S reference) */
|
||||
.stage-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 0.3rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.stage-Applied { background-color: #e9ecef; color: #495057; }
|
||||
.stage-Exam { background-color: #d1ecf1; color: #0c5460; } /* Light cyan/info */
|
||||
.stage-Interview { background-color: #ffc107; color: #856404; } /* Yellow/warning */
|
||||
.stage-Offer { background-color: #d4edda; color: #155724; } /* Light green/success */
|
||||
|
||||
/* Candidate Indicator (used for the single Potential Candidates list) */
|
||||
.candidate-indicator-badge {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.15rem 0.4rem;
|
||||
border-radius: 0.25rem;
|
||||
font-weight: 700;
|
||||
margin-left: 0.5rem;
|
||||
background-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-layer-group me-2"></i>
|
||||
{% trans "Applicant Screening for Job:" %} - {{ job.title }} <small class="text-muted fs-6">{{job.internal_job_id}}<small>
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
Total Applicants: <span class="fw-bold">{{ total_candidates }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Job" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="tier-controls kaauh-card shadow-sm">
|
||||
<h4 class="h5 mb-3" style="color: var(--kaauh-teal-dark);">{% trans "Filter Potential Candidates" %}</h4>
|
||||
<form method="post" class="mb-0">
|
||||
{% csrf_token %}
|
||||
<div class="row g-3 align-items-end">
|
||||
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<label for="min_ai_score" class="form-label small text-muted">
|
||||
{% trans "Minimum AI Score" %}
|
||||
</label>
|
||||
<input type="number" name="min_ai_score" id="min_ai_score" class="form-control"
|
||||
value="{{ min_ai_score|default:'0' }}" min="0" max="100" step="1"
|
||||
placeholder="e.g., 75">
|
||||
</div>
|
||||
|
||||
<div class="col-md-3 col-sm-6">
|
||||
<label for="tier1_count" class="form-label small text-muted">
|
||||
{% trans "Number of Potential Candidates (Top N)" %}
|
||||
</label>
|
||||
<input type="number" name="tier1_count" id="tier1_count" class="form-control"
|
||||
value="{{ tier1_count }}" min="1" max="{{ total_candidates }}"
|
||||
placeholder="Enter N for top candidates">
|
||||
</div>
|
||||
|
||||
<div class="col-md-auto">
|
||||
<button type="submit" name="update_tiers" class="btn btn-main-action">
|
||||
<i class="fas fa-filter me-1"></i> {% trans "Apply Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="small text-muted mt-2 mb-0">
|
||||
{% trans "The candidates list is filtered by either the minimum AI Score OR the Top N candidates, whichever criteria results in a smaller set." %}
|
||||
</p>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<ul class="nav nav-pills mb-3 justify-content-center" id="candidateViewTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="all-applicants-tab" data-bs-toggle="tab" data-bs-target="#all-applicants" type="button"
|
||||
role="tab" aria-controls="all-applicants" aria-selected="true">
|
||||
<i class="fas fa-users me-1"></i> {% trans "All Applicants" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="potential-candidates-tab" data-bs-toggle="tab" data-bs-target="#potential-candidates" type="button"
|
||||
role="tab" aria-controls="potential-candidates" aria-selected="false">
|
||||
<i class="fas fa-trophy me-1"></i> {% trans "Potential Candidates" %}
|
||||
<span class="candidate-indicator-badge">{{ tier1_candidates|length }}</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content kaauh-card p-3 shadow-sm" id="candidateViewTabContent">
|
||||
|
||||
<div class="tab-pane fade show active" id="all-applicants" role="tabpanel" aria-labelledby="all-applicants-tab">
|
||||
<h2 class="h5 mb-3" style="color: var(--kaauh-teal-dark);">{% trans "Complete List of All Submissions" %}</h2>
|
||||
{% include "recruitment/partials/_candidate_table.html" with candidates=all_applicants_list is_potential_view=False only %}
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="potential-candidates" role="tabpanel" aria-labelledby="potential-candidates-tab">
|
||||
<h2 class="h5 mb-3" style="color: var(--kaauh-teal-dark);">
|
||||
{% trans "Top" %} {{ tier1_candidates|length }} {% trans "Candidates by AI Score" %}
|
||||
</h2>
|
||||
{% include "recruitment/partials/_candidate_table.html" with candidates=tier1_candidates is_potential_view=True only %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% comment %}
|
||||
Modals for candidate view and exam status update should be included here,
|
||||
e.g., {% include "includes/_modals_for_candidate_management.html" %}
|
||||
{% endcomment %}
|
||||
|
||||
{% endblock %}
|
||||
125
templates/recruitment/partials/_candidate_table.html
Normal file
125
templates/recruitment/partials/_candidate_table.html
Normal file
@ -0,0 +1,125 @@
|
||||
{% load i18n %}
|
||||
|
||||
{% if candidates %}
|
||||
<div class="candidate-table-responsive">
|
||||
<form method="post" id="candidate-action-form">
|
||||
{% csrf_token %}
|
||||
<table class="candidate-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Name / Contact" %}</th>
|
||||
<th scope="col">{% trans "AI Score" %}</th>
|
||||
<th scope="col">{% trans "Status" %}</th>
|
||||
<th scope="col">{% trans "Stage" %}</th>
|
||||
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for candidate in candidates %}
|
||||
<tr>
|
||||
<td>
|
||||
<div class="candidate-name">{{ candidate.name }}</div>
|
||||
<div class="candidate-details">
|
||||
<i class="fas fa-envelope me-1"></i> {{ candidate.email|default:"N/A" }}<br>
|
||||
<i class="fas fa-phone me-1"></i> {{ candidate.phone|default:"N/A" }}
|
||||
</div>
|
||||
{% if is_potential_view %}
|
||||
{# Show candidate ID in this view to help with bulk update ID entry #}
|
||||
<div class="candidate-details mt-1">
|
||||
ID: **{{ candidate.id }}**
|
||||
</div>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge bg-score">
|
||||
{{ candidate.match_score|default:"0" }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="status-badge
|
||||
{% if candidate.applicant_status == 'Candidate' %}bg-candidate{% else %}bg-applicant{% endif %}">
|
||||
{{ candidate.get_applicant_status_display }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span class="stage-badge stage-{{ candidate.stage }}">
|
||||
{{ candidate.get_stage_display }}
|
||||
</span>
|
||||
{% if candidate.stage == "Exam" and candidate.exam_status %}
|
||||
<br>
|
||||
<span class="status-badge bg-exam-status mt-1">
|
||||
{{ candidate.get_exam_status_display }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
|
||||
{# View Candidate Criteria/Details (Uses HTMX target #}
|
||||
<button type="button" class="btn btn-main-action"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="{% trans 'View Details and Score Breakdown' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
|
||||
{# Individual actions (Shown if it's the filtered view OR if the applicant needs to be promoted) #}
|
||||
{% if is_potential_view or candidate.applicant_status == 'Applicant' %}
|
||||
{% if candidate.applicant_status == 'Applicant' %}
|
||||
{# Mark as Candidate Button (Manual Selection/Promotion) #}
|
||||
<button type="submit" name="mark_as_candidate"
|
||||
class="btn btn-sm btn-success"
|
||||
formaction="?candidate_id={{ candidate.id }}&action=mark_as_candidate"
|
||||
title="{% trans 'Mark as Potential Candidate' %}">
|
||||
<i class="fas fa-user-check"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
{# Stage Progression Dropdown #}
|
||||
<div class="dropdown">
|
||||
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button"
|
||||
data-bs-toggle="dropdown" title="{% trans 'Move to Next Stage' %}">
|
||||
<i class="fas fa-tasks"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
{% for next_stage in candidate.get_available_stages %}
|
||||
<li>
|
||||
<button type="submit" name="update_stage"
|
||||
class="dropdown-item"
|
||||
formaction="?candidate_id={{ candidate.id }}&new_stage={{ next_stage }}">
|
||||
{% trans "Move to" %} {{ next_stage }}
|
||||
</button>
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% if candidate.stage == "Exam" %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#examModal{{ candidate.id }}">
|
||||
<i class="fas fa-clipboard-check me-1"></i> {% trans "Update Exam Status" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-4 text-muted kaauh-card p-4">
|
||||
<i class="fas fa-inbox fa-2x mb-2" style="color: var(--kaauh-teal);"></i>
|
||||
<p class="mb-0">{% trans "No candidates found in this list." %}</p>
|
||||
{% if is_potential_view %}
|
||||
<p class="small">{% trans "Adjust your 'Top N' filter in the controls above or check the All Applicants list." %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -4,8 +4,7 @@
|
||||
{% block title %}Create Training Material - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
{# 💡 Required for Summernote if you added it, otherwise remove this line #}
|
||||
{{ form.media.css }}
|
||||
|
||||
<style>
|
||||
/* ================================================= */
|
||||
/* THEME VARIABLES AND GLOBAL STYLES */
|
||||
@ -21,9 +20,11 @@
|
||||
/* Primary Color Overrides */
|
||||
.text-primary { color: var(--kaauh-teal) !important; }
|
||||
|
||||
|
||||
|
||||
/* Main Action Button Style */
|
||||
.btn-main-action, .btn-primary {
|
||||
background-color: var(--kaauh-teal);
|
||||
.btn-main-action{
|
||||
background-color: var(--kaauh-teal-dark); /* Changed to primary teal for main actions */
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
@ -34,7 +35,7 @@
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.btn-main-action:hover, .btn-primary:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
background-color: var(--kaauh-teal-dark); /* Darker on hover */
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
@ -129,15 +130,14 @@
|
||||
<form method="post" enctype="multipart/form-data" class="row">
|
||||
{% csrf_token %}
|
||||
|
||||
|
||||
{% crispy form %}
|
||||
|
||||
|
||||
{# Add the main action button here for consistency #}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 💡 Required for Summernote if you added it, otherwise remove this line #}
|
||||
{{ form.media.js }}
|
||||
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user