static file

This commit is contained in:
Faheed 2025-10-06 16:23:40 +03:00
parent a8a45195c1
commit 9b7c633a18
63 changed files with 2521 additions and 125 deletions

View File

@ -191,15 +191,6 @@ SOCIALACCOUNT_PROVIDERS = {
} }
} }
UNFOLD = {
"DASHBOARD_CALLBACK": "recruitment.utils.dashboard_callback",
"STYLES": [
lambda request: static("unfold/css/styles.css"),
],
"SCRIPTS": [
lambda request: static("unfold/js/app.js"),
],
}
ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A' ZOOM_ACCOUNT_ID = 'HoGikHXsQB2GNDC5Rvyw9A'
ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA' ZOOM_CLIENT_ID = 'brC39920R8C8azfudUaQgA'

Binary file not shown.

View File

@ -4,7 +4,7 @@ from crispy_forms.helper import FormHelper
from django.core.validators import URLValidator from django.core.validators import URLValidator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from crispy_forms.layout import Layout, Submit, HTML, Div, Field from crispy_forms.layout import Layout, Submit, HTML, Div, Field
from .models import ZoomMeeting, Candidate,Job,TrainingMaterial,JobPosting from .models import ZoomMeeting, Candidate,TrainingMaterial,JobPosting
class CandidateForm(forms.ModelForm): class CandidateForm(forms.ModelForm):
class Meta: class Meta:

View File

@ -0,0 +1,14 @@
# Generated by Django 5.2.7 on 2025-10-06 12:24
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0013_formfield_formstage_remove_formsubmission_form_and_more'),
('recruitment', '0018_alter_jobposting_hiring_agency'),
]
operations = [
]

View File

@ -16,22 +16,22 @@ class Base(models.Model):
class Meta: class Meta:
abstract = True abstract = True
# Create your models here. # # Create your models here.
class Job(Base): # class Job(Base):
title = models.CharField(max_length=255, verbose_name=_('Title')) # title = models.CharField(max_length=255, verbose_name=_('Title'))
description_en = models.TextField(verbose_name=_('Description English')) # description_en = models.TextField(verbose_name=_('Description English'))
description_ar = models.TextField(verbose_name=_('Description Arabic')) # description_ar = models.TextField(verbose_name=_('Description Arabic'))
is_published = models.BooleanField(default=False, verbose_name=_('Published')) # is_published = models.BooleanField(default=False, verbose_name=_('Published'))
posted_to_linkedin = models.BooleanField(default=False, verbose_name=_('Posted to LinkedIn')) # posted_to_linkedin = models.BooleanField(default=False, verbose_name=_('Posted to LinkedIn'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at')) # created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated at')) # updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated at'))
class Meta: # class Meta:
verbose_name = _('Job') # verbose_name = _('Job')
verbose_name_plural = _('Jobs') # verbose_name_plural = _('Jobs')
def __str__(self): # def __str__(self):
return self.title # return self.title
class JobPosting(Base): class JobPosting(Base):
# Basic Job Information # Basic Job Information
@ -506,4 +506,43 @@ class SharedFormTemplate(models.Model):
verbose_name_plural = 'Shared Form Templates' verbose_name_plural = 'Shared Form Templates'
def __str__(self): def __str__(self):
return f"Shared: {self.template.name}" return f"Shared: {self.template.name}"
class Source(models.Model):
name = models.CharField(
max_length=100,
unique=True,
verbose_name=_('Source Name'),
help_text=_("e.g., ATS, ERP ")
)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
class Meta:
verbose_name = _('Source')
verbose_name_plural = _('Sources')
ordering = ['name']
class HiringAgency(Base):
name = models.CharField(max_length=200, unique=True, verbose_name=_('Agency Name'))
contact_person = models.CharField(max_length=150, blank=True, verbose_name=_('Contact Person'))
email = models.EmailField(blank=True)
phone = models.CharField(max_length=20, blank=True)
website = models.URLField(blank=True)
notes = models.TextField(blank=True, help_text=_("Internal notes about the agency"))
country=CountryField(blank=True, null=True,blank_label=_('Select country'))
address=models.TextField(blank=True,null=True)
def __str__(self):
return self.name
class Meta:
verbose_name = _('Hiring Agency')
verbose_name_plural = _('Hiring Agencies')
ordering = ['name']

View File

@ -14,7 +14,7 @@ from django.contrib import messages
from django.core.paginator import Paginator from django.core.paginator import Paginator
from .linkedin_service import LinkedInService from .linkedin_service import LinkedInService
from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission from .models import FormTemplate, FormStage, FormField,FieldResponse,FormSubmission
from .models import ZoomMeeting, Job, Candidate, JobPosting from .models import ZoomMeeting, Candidate, JobPosting
from .serializers import JobPostingSerializer, CandidateSerializer from .serializers import JobPostingSerializer, CandidateSerializer
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404, render, redirect
from django.views.generic import CreateView,UpdateView,DetailView,ListView from django.views.generic import CreateView,UpdateView,DetailView,ListView

View File

View File

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class ApplicantConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'applicant'

View File

@ -0,0 +1,22 @@
from django import forms
from .models import ApplicantForm, FormField
class ApplicantFormCreateForm(forms.ModelForm):
class Meta:
model = ApplicantForm
fields = ['name', 'description']
widgets = {
'name': forms.TextInput(attrs={'class': 'form-control'}),
'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
class FormFieldForm(forms.ModelForm):
class Meta:
model = FormField
fields = ['label', 'field_type', 'required', 'help_text', 'choices']
widgets = {
'label': forms.TextInput(attrs={'class': 'form-control'}),
'field_type': forms.Select(attrs={'class': 'form-control'}),
'help_text': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
'choices': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Option1, Option2, Option3'}),
}

View File

@ -0,0 +1,49 @@
from django import forms
from .models import FormField
# applicant/forms_builder.py
def create_dynamic_form(form_instance):
fields = {}
for field in form_instance.fields.all():
field_kwargs = {
'label': field.label,
'required': field.required,
'help_text': field.help_text
}
# Use stable field_name instead of database ID
field_key = field.field_name
if field.field_type == 'text':
fields[field_key] = forms.CharField(**field_kwargs)
elif field.field_type == 'email':
fields[field_key] = forms.EmailField(**field_kwargs)
elif field.field_type == 'phone':
fields[field_key] = forms.CharField(**field_kwargs)
elif field.field_type == 'number':
fields[field_key] = forms.IntegerField(**field_kwargs)
elif field.field_type == 'date':
fields[field_key] = forms.DateField(**field_kwargs)
elif field.field_type == 'textarea':
fields[field_key] = forms.CharField(
widget=forms.Textarea,
**field_kwargs
)
elif field.field_type in ['select', 'radio']:
choices = [(c.strip(), c.strip()) for c in field.choices.split(',') if c.strip()]
if not choices:
choices = [('', '---')]
if field.field_type == 'select':
fields[field_key] = forms.ChoiceField(choices=choices, **field_kwargs)
else:
fields[field_key] = forms.ChoiceField(
choices=choices,
widget=forms.RadioSelect,
**field_kwargs
)
elif field.field_type == 'checkbox':
field_kwargs['required'] = False
fields[field_key] = forms.BooleanField(**field_kwargs)
return type('DynamicApplicantForm', (forms.Form,), fields)

View File

@ -0,0 +1,70 @@
# Generated by Django 5.2.6 on 2025-10-01 21:41
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('jobs', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ApplicantForm',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text="Form version name (e.g., 'Version A', 'Version B' etc)", max_length=200)),
('description', models.TextField(blank=True, help_text='Optional description of this form version')),
('is_active', models.BooleanField(default=False, help_text='Only one form can be active per job')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('job_posting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='applicant_forms', to='jobs.jobposting')),
],
options={
'verbose_name': 'Application Form',
'verbose_name_plural': 'Application Forms',
'ordering': ['-created_at'],
'unique_together': {('job_posting', 'name')},
},
),
migrations.CreateModel(
name='ApplicantSubmission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('submitted_at', models.DateTimeField(auto_now_add=True)),
('data', models.JSONField()),
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
('score', models.FloatField(default=0, help_text='Ranking score for the applicant submission')),
('form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='applicant.applicantform')),
('job_posting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='jobs.jobposting')),
],
options={
'verbose_name': 'Applicant Submission',
'verbose_name_plural': 'Applicant Submissions',
'ordering': ['-submitted_at'],
},
),
migrations.CreateModel(
name='FormField',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('label', models.CharField(max_length=255)),
('field_type', models.CharField(choices=[('text', 'Text'), ('email', 'Email'), ('phone', 'Phone'), ('number', 'Number'), ('date', 'Date'), ('select', 'Dropdown'), ('radio', 'Radio Buttons'), ('checkbox', 'Checkbox'), ('textarea', 'Paragraph Text'), ('file', 'File Upload'), ('image', 'Image Upload')], max_length=20)),
('required', models.BooleanField(default=True)),
('help_text', models.TextField(blank=True)),
('choices', models.TextField(blank=True, help_text='Comma-separated options for select/radio fields')),
('order', models.IntegerField(default=0)),
('field_name', models.CharField(blank=True, max_length=100)),
('form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='applicant.applicantform')),
],
options={
'verbose_name': 'Form Field',
'verbose_name_plural': 'Form Fields',
'ordering': ['order'],
},
),
]

View File

@ -0,0 +1,144 @@
# models.py
from django.db import models
from django.core.exceptions import ValidationError
from jobs.models import JobPosting
from django.urls import reverse
class ApplicantForm(models.Model):
"""Multiple dynamic forms per job posting, only one active at a time"""
job_posting = models.ForeignKey(
JobPosting,
on_delete=models.CASCADE,
related_name='applicant_forms'
)
name = models.CharField(
max_length=200,
help_text="Form version name (e.g., 'Version A', 'Version B' etc)"
)
description = models.TextField(
blank=True,
help_text="Optional description of this form version"
)
is_active = models.BooleanField(
default=False,
help_text="Only one form can be active per job"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
unique_together = ('job_posting', 'name')
ordering = ['-created_at']
verbose_name = "Application Form"
verbose_name_plural = "Application Forms"
def __str__(self):
status = "(Active)" if self.is_active else "(Inactive)"
return f"{self.name} for {self.job_posting.title} {status}"
def clean(self):
"""Ensure only one active form per job"""
if self.is_active:
existing_active = self.job_posting.applicant_forms.filter(
is_active=True
).exclude(pk=self.pk)
if existing_active.exists():
raise ValidationError(
"Only one active application form is allowed per job posting."
)
super().clean()
def activate(self):
"""Set this form as active and deactivate others"""
self.is_active = True
self.save()
# Deactivate other forms
self.job_posting.applicant_forms.exclude(pk=self.pk).update(
is_active=False
)
def get_public_url(self):
"""Returns the public application URL for this job's active form"""
return reverse('applicant:apply_form', args=[self.job_posting.internal_job_id])
class FormField(models.Model):
FIELD_TYPES = [
('text', 'Text'),
('email', 'Email'),
('phone', 'Phone'),
('number', 'Number'),
('date', 'Date'),
('select', 'Dropdown'),
('radio', 'Radio Buttons'),
('checkbox', 'Checkbox'),
('textarea', 'Paragraph Text'),
('file', 'File Upload'),
('image', 'Image Upload'),
]
form = models.ForeignKey(
ApplicantForm,
related_name='fields',
on_delete=models.CASCADE
)
label = models.CharField(max_length=255)
field_type = models.CharField(max_length=20, choices=FIELD_TYPES)
required = models.BooleanField(default=True)
help_text = models.TextField(blank=True)
choices = models.TextField(
blank=True,
help_text="Comma-separated options for select/radio fields"
)
order = models.IntegerField(default=0)
field_name = models.CharField(max_length=100, blank=True)
class Meta:
ordering = ['order']
verbose_name = "Form Field"
verbose_name_plural = "Form Fields"
def __str__(self):
return f"{self.label} ({self.field_type}) in {self.form.name}"
def save(self, *args, **kwargs):
if not self.field_name:
# Create a stable field name from label (e.g., "Full Name" → "full_name")
import re
# Use Unicode word characters, including Arabic, for field_name
self.field_name = re.sub(
r'[^\w]+',
'_',
self.label.lower(),
flags=re.UNICODE
).strip('_')
# Ensure uniqueness within the form
base_name = self.field_name
counter = 1
while FormField.objects.filter(
form=self.form,
field_name=self.field_name
).exists():
self.field_name = f"{base_name}_{counter}"
counter += 1
super().save(*args, **kwargs)
class ApplicantSubmission(models.Model):
job_posting = models.ForeignKey(JobPosting, on_delete=models.CASCADE)
form = models.ForeignKey(ApplicantForm, on_delete=models.CASCADE)
submitted_at = models.DateTimeField(auto_now_add=True)
data = models.JSONField()
ip_address = models.GenericIPAddressField(null=True, blank=True)
score = models.FloatField(
default=0,
help_text="Ranking score for the applicant submission"
)
class Meta:
ordering = ['-submitted_at']
verbose_name = "Applicant Submission"
verbose_name_plural = "Applicant Submissions"
def __str__(self):
return f"Submission for {self.job_posting.title} at {self.submitted_at}"

View File

@ -0,0 +1,94 @@
{% extends 'base.html' %}
{% block title %}
Apply: {{ job.title }}
{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-lg-8">
{# --- 1. Job Header and Overview (Fixed/Static Info) --- #}
<div class="card bg-light-subtle mb-4 p-4 border-0 rounded-3 shadow-sm">
<h1 class="h2 fw-bold text-primary mb-1">{{ job.title }}</h1>
<p class="mb-3 text-muted">
Your final step to apply for this position.
</p>
<div class="d-flex gap-4 small text-secondary">
<div>
<i class="fas fa-building me-1"></i>
<strong>Department:</strong> {{ job.department|default:"Not specified" }}
</div>
<div>
<i class="fas fa-map-marker-alt me-1"></i>
<strong>Location:</strong> {{ job.get_location_display }}
</div>
<div>
<i class="fas fa-briefcase me-1"></i>
<strong>Type:</strong> {{ job.get_job_type_display }} • {{ job.get_workplace_type_display }}
</div>
</div>
</div>
{# --- 2. Application Form Section --- #}
<div class="card p-5 border-0 rounded-3 shadow">
<h2 class="h3 fw-semibold mb-3">Application Details</h2>
{% if applicant_form.description %}
<p class="text-muted mb-4">{{ applicant_form.description }}</p>
{% endif %}
<form method="post" novalidate>
{% csrf_token %}
{% for field in form %}
<div class="form-group mb-4">
{# Label Tag #}
<label for="{{ field.id_for_label }}" class="form-label">
{{ field.label }}
{% if field.field.required %}<span class="text-danger">*</span>{% endif %}
</label>
{# The Field Widget (Assumes form-control is applied in backend) #}
{{ field }}
{# Field Errors #}
{% if field.errors %}
<div class="invalid-feedback d-block">{{ field.errors }}</div>
{% endif %}
{# Help Text #}
{% if field.help_text %}
<div class="form-text">{{ field.help_text }}</div>
{% endif %}
</div>
{% endfor %}
{# General Form Errors (Non-field errors) #}
{% if form.non_field_errors %}
<div class="alert alert-danger mb-4">
{{ form.non_field_errors }}
</div>
{% endif %}
<button type="submit" class="btn btn-primary btn-lg mt-3 w-100">
<i class="fas fa-paper-plane me-2"></i> Submit Application
</button>
</form>
</div>
<footer class="mt-4 text-center">
<a href="{% url 'applicant:review_job_detail' job.internal_job_id %}"
class="btn btn-link text-secondary">
&larr; Review Job Details
</a>
</footer>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,68 @@
{% extends 'base.html' %}
{% block title %}
Define Form for {{ job.title }}
{% endblock %}
{% block content %}
<div class="container my-5">
<div class="row justify-content-center">
<div class="col-lg-8 col-md-10">
<div class="card shadow-lg border-0 p-4 p-md-5">
<h2 class="card-title text-center mb-4 text-dark">
🛠️ New Application Form Configuration
</h2>
<p class="text-center text-muted mb-4 border-bottom pb-3">
You are creating a new form structure for job: <strong>{{ job.title }}</strong>
</p>
<form method="post" novalidate>
{% csrf_token %}
<fieldset class="mb-5">
<legend class="h5 mb-3 text-secondary">Form Metadata</legend>
<div class="form-group mb-4">
<label for="{{ form.name.id_for_label }}" class="form-label required">
Form Name
</label>
{# The field should already have form-control applied from the backend #}
{{ form.name }}
{% if form.name.errors %}
<div class="text-danger small mt-1">{{ form.name.errors }}</div>
{% endif %}
</div>
<div class="form-group mb-4">
<label for="{{ form.description.id_for_label }}" class="form-label">
Description
</label>
{# The field should already have form-control applied from the backend #}
{{ form.description}}
{% if form.description.errors %}
<div class="text-danger small mt-1">{{ form.description.errors }}</div>
{% endif %}
</div>
</fieldset>
<div class="d-flex justify-content-end gap-3 pt-3">
<a href="{% url 'applicant:job_forms_list' job.internal_job_id %}"
class="btn btn-outline-secondary">
Cancel
</a>
<button type="submit" class="btn univ-color btn-lg">
Create Form & Continue &rarr;
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,103 @@
{% extends 'base.html' %}
{% block title %}
Manage Forms | {{ job.title }}
{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-10">
<header class="mb-5 pb-3 border-bottom d-flex flex-column flex-md-row justify-content-between align-items-md-center">
<div>
<h2 class="h3 mb-1 ">
<i class="fas fa-clipboard-list me-2 text-secondary"></i>
Application Forms for <span class="text-success fw-bold">"{{ job.title }}"</span>
</h2>
<p class="text-muted small">
Internal Job ID: **{{ job.internal_job_id }}**
</p>
</div>
{# Primary Action Button using the theme color #}
<a href="{% url 'applicant:create_form' job_id=job.internal_job_id %}"
class="btn univ-color btn-lg shadow-sm mt-3 mt-md-0">
<i class="fas fa-plus me-1"></i> Create New Form
</a>
</header>
{% if forms %}
<div class="list-group">
{% for form in forms %}
{# Custom styling based on active state #}
<div class="list-group-item d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center p-3 mb-3 rounded shadow-sm
{% if form.is_active %}border-success border-3 bg-light{% else %}border-secondary border-1{% endif %}">
{# Left Section: Form Details #}
<div class="flex-grow-1 me-4 mb-2 mb-sm-0">
<h4 class="h5 mb-1 d-inline-block">
{{ form.name }}
</h4>
{# Status Badge #}
{% if form.is_active %}
<span class="badge bg-success ms-2">
<i class="fas fa-check-circle me-1"></i> Active Form
</span>
{% else %}
<span class="badge bg-secondary ms-2">
<i class="fas fa-times-circle me-1"></i> Inactive
</span>
{% endif %}
<p class="text-muted mt-1 mb-1 small">
{{ form.description|default:"— No description provided. —" }}
</p>
</div>
{# Right Section: Actions #}
<div class="d-flex gap-2 align-items-center flex-wrap">
{# Edit Structure Button #}
<a href="{% url 'applicant:edit_form' form.id %}"
class="btn btn-sm btn-outline-secondary">
<i class="fas fa-pen me-1"></i> Edit Structure
</a>
{# Conditional Activation Button #}
{% if not form.is_active %}
<a href="{% url 'applicant:activate_form' form.id %}"
class="btn btn-sm univ-color">
<i class="fas fa-bolt me-1"></i> Activate Form
</a>
{% else %}
{# Active indicator/Deactivate button placeholder #}
<a href="#" class="btn btn-sm btn-outline-success" disabled>
<i class="fas fa-star me-1"></i> Current Form
</a>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="text-center py-5 bg-light rounded shadow-sm">
<i class="fas fa-file-alt fa-4x text-muted mb-3"></i>
<p class="lead mb-0">No application forms have been created yet for this job.</p>
<p class="mt-2 mb-0 text-secondary">Click the button above to define a new form structure.</p>
</div>
{% endif %}
<footer class="text-end mt-5 pt-3 border-top">
<a href="{% url 'jobs:job_detail' job.internal_job_id %}"
class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> Back to Job Details
</a>
</footer>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,129 @@
{% extends "base.html" %}
{% block title %}{{ job.title }} - University ATS{% endblock %}
{% block content %}
<div class="row mb-5">
<div class="col-lg-8">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h2>{{ job.title }}</h2>
<span class="badge bg-{{ job.status|lower }} status-badge">
{{ job.get_status_display }}
</span>
</div>
<div class="card-body">
<!-- Job Details -->
<div class="row mb-3">
<div class="col-md-6">
<strong>Department:</strong> {{ job.department|default:"Not specified" }}
</div>
<div class="col-md-6">
<strong>Position Number:</strong> {{ job.position_number|default:"Not specified" }}
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>Job Type:</strong> {{ job.get_job_type_display }}
</div>
<div class="col-md-6">
<strong>Workplace:</strong> {{ job.get_workplace_type_display }}
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<strong>Location:</strong> {{ job.get_location_display }}
</div>
<div class="col-md-6">
<strong>Created By:</strong> {{ job.created_by|default:"Not specified" }}
</div>
</div>
{% if job.salary_range %}
<div class="row mb-3">
<div class="col-12">
<strong>Salary Range:</strong> {{ job.salary_range }}
</div>
</div>
{% endif %}
{% if job.start_date %}
<div class="row mb-3">
<div class="col-12">
<strong>Start Date:</strong> {{ job.start_date }}
</div>
</div>
{% endif %}
{% if job.application_deadline %}
<div class="row mb-3">
<div class="col-12">
<strong>Application Deadline:</strong> {{ job.application_deadline }}
{% if job.is_expired %}
<span class="badge bg-danger">EXPIRED</span>
{% endif %}
</div>
</div>
{% endif %}
<!-- Description -->
{% if job.description %}
<div class="mb-3">
<h5>Description</h5>
<div>{{ job.description|linebreaks }}</div>
</div>
{% endif %}
{% if job.qualifications %}
<div class="mb-3">
<h5>Qualifications</h5>
<div>{{ job.qualifications|linebreaks }}</div>
</div>
{% endif %}
{% if job.benefits %}
<div class="mb-3">
<h5>Benefits</h5>
<div>{{ job.benefits|linebreaks }}</div>
</div>
{% endif %}
{% if job.application_instructions %}
<div class="mb-3">
<h5>Application Instructions</h5>
<div>{{ job.application_instructions|linebreaks }}</div>
</div>
{% endif %}
</div>
</div>
</div>
<div class="col-lg-4">
<!-- Add this section below your existing job details -->
<div class="card mt-4">
<div class="card-header bg-success text-white">
<h5><i class="fas fa-file-signature"></i> Ready to Apply?</h5>
</div>
<div class="card-body">
<p>Review the job details on the left, then click the button below to submit your application.</p>
<a href="{% url 'applicant:apply_form' job.internal_job_id %}" class="btn btn-success btn-lg w-100">
<i class="fas fa-paper-plane"></i> Apply for this Position
</a>
<p class="text-muted mt-2">
<small>You'll be redirected to our secure application form where you can upload your resume and provide additional details.</small>
</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,35 @@
{% extends 'base.html' %}
{% block title %}Application Submitted - {{ job.title }}{% endblock %}
{% block content %}
<div class="card">
<div style="text-align: center; padding: 30px 0;">
<div style="width: 80px; height: 80px; background: #d4edda; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; margin-bottom: 20px;">
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="#28a745" viewBox="0 0 16 16">
<path d="M16 8A8 8 0 1 1 0 8a8 8 0 0 1 16 0zm-3.97-3.03a.75.75 0 0 0-1.08.022L7.477 9.417 5.384 7.323a.75.75 0 0 0-1.06 1.06L6.97 11.03a.75.75 0 0 0 1.079-.02l3.992-4.99a.75.75 0 0 0-.01-1.05z"/>
</svg>
</div>
<h1 style="color: #28a745; margin-bottom: 15px;">Thank You!</h1>
<h2>Your application has been submitted successfully</h2>
<div style="background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 25px 0; text-align: left;">
<p><strong>Position:</strong> {{ job.title }}</p>
<p><strong>Job ID:</strong> {{ job.internal_job_id }}</p>
<p><strong>Department:</strong> {{ job.department|default:"Not specified" }}</p>
{% if job.application_deadline %}
<p><strong>Application Deadline:</strong> {{ job.application_deadline|date:"F j, Y" }}</p>
{% endif %}
</div>
<p style="font-size: 18px; line-height: 1.6;">
We appreciate your interest in joining our team. Our hiring team will review your application
and contact you if there's a potential match for this position.
</p>
{% comment %} <div style="margin-top: 30px;">
<a href="/" class="btn btn-primary" style="margin-right: 10px;">Apply to Another Position</a>
<a href="{% url 'jobs:job_detail' job.internal_job_id %}" class="btn btn-outline">View Job Details</a>
</div> {% endcomment %}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,24 @@
import json
from django import template
register = template.Library()
@register.filter(name='from_json')
def from_json(json_string):
"""
Safely loads a JSON string into a Python object (list or dict).
"""
try:
# The JSON string comes from the context and needs to be parsed
return json.loads(json_string)
except (TypeError, json.JSONDecodeError):
# Handle cases where the string is invalid or None/empty
return []
@register.filter(name='split')
def split_string(value, key=None):
"""Splits a string by the given key (default is space)."""
if key is None:
return value.split()
return value.split(key)

View File

@ -0,0 +1,14 @@
# from django.db.models.signals import post_save
# from django.dispatch import receiver
# from . import models
#
# @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

View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,18 @@
from django.urls import path
from . import views
app_name = 'applicant'
urlpatterns = [
# Form Management
path('job/<str:job_id>/forms/', views.job_forms_list, name='job_forms_list'),
path('job/<str:job_id>/forms/create/', views.create_form_for_job, name='create_form'),
path('form/<int:form_id>/edit/', views.edit_form, name='edit_form'),
path('field/<int:field_id>/delete/', views.delete_field, name='delete_field'),
path('form/<int:form_id>/activate/', views.activate_form, name='activate_form'),
# Public Application
path('apply/<str:job_id>/', views.apply_form_view, name='apply_form'),
path('review/job/detail/<str:job_id>/',views.review_job_detail, name="review_job_detail"),
path('apply/<str:job_id>/thank-you/', views.thank_you_view, name='thank_you'),
]

View File

@ -0,0 +1,34 @@
import os
import fitz # PyMuPDF
import spacy
import requests
from recruitment import models
from django.conf import settings
nlp = spacy.load("en_core_web_sm")
def extract_text_from_pdf(pdf_path):
text = ""
with fitz.open(pdf_path) as doc:
for page in doc:
text += page.get_text()
return text
def extract_summary_from_pdf(pdf_path):
if not os.path.exists(pdf_path):
return {'error': 'File not found'}
text = extract_text_from_pdf(pdf_path)
doc = nlp(text)
summary = {
'name': doc.ents[0].text if doc.ents else '',
'skills': [chunk.text for chunk in doc.noun_chunks if len(chunk.text.split()) > 1],
'summary': text[:500]
}
return summary
def match_resume_with_job_description(resume, job_description,prompt=""):
resume_doc = nlp(resume)
job_doc = nlp(job_description)
similarity = resume_doc.similarity(job_doc)
return similarity

View File

@ -0,0 +1,175 @@
# applicant/views.py (Updated edit_form function)
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.http import Http404, JsonResponse # <-- Import JsonResponse
from django.views.decorators.csrf import csrf_exempt # <-- Needed for JSON POST if not using FormData
import json # <-- Import json
from django.db import transaction # <-- Import transaction
# (Keep all your existing imports)
from .models import ApplicantForm, FormField, ApplicantSubmission
from .forms import ApplicantFormCreateForm, FormFieldForm
from jobs.models import JobPosting
from .forms_builder import create_dynamic_form
# ... (Keep all other functions like job_forms_list, create_form_for_job, etc.)
# ...
# === FORM MANAGEMENT VIEWS ===
def job_forms_list(request, job_id):
"""List all forms for a specific job"""
job = get_object_or_404(JobPosting, internal_job_id=job_id)
forms = job.applicant_forms.all()
return render(request, 'applicant/job_forms_list.html', {
'job': job,
'forms': forms
})
def create_form_for_job(request, job_id):
"""Create a new form for a job"""
job = get_object_or_404(JobPosting, internal_job_id=job_id)
if request.method == 'POST':
form = ApplicantFormCreateForm(request.POST)
if form.is_valid():
applicant_form = form.save(commit=False)
applicant_form.job_posting = job
applicant_form.save()
messages.success(request, 'Form created successfully!')
return redirect('applicant:job_forms_list', job_id=job_id)
else:
form = ApplicantFormCreateForm()
return render(request, 'applicant/create_form.html', {
'job': job,
'form': form
})
@transaction.atomic # Ensures all fields are saved or none are
def edit_form(request, form_id):
"""Edit form details and manage fields, including dynamic builder save."""
applicant_form = get_object_or_404(ApplicantForm, id=form_id)
job = applicant_form.job_posting
if request.method == 'POST':
# --- 1. Handle JSON data from the Form Builder (JavaScript) ---
if request.content_type == 'application/json':
try:
field_data = json.loads(request.body)
# Clear existing fields for this form
applicant_form.fields.all().delete()
# Create new fields from the JSON data
for field_config in field_data:
# Sanitize/ensure required fields are present
FormField.objects.create(
form=applicant_form,
label=field_config.get('label', 'New Field'),
field_type=field_config.get('field_type', 'text'),
required=field_config.get('required', True),
help_text=field_config.get('help_text', ''),
choices=field_config.get('choices', ''),
order=field_config.get('order', 0),
# field_name will be auto-generated/re-generated on save() if needed
)
return JsonResponse({'status': 'success', 'message': 'Form structure saved successfully!'})
except json.JSONDecodeError:
return JsonResponse({'status': 'error', 'message': 'Invalid JSON data.'}, status=400)
except Exception as e:
return JsonResponse({'status': 'error', 'message': f'Server error: {str(e)}'}, status=500)
# --- 2. Handle standard POST requests (e.g., saving form details) ---
elif 'save_form_details' in request.POST: # Changed the button name for clarity
form_details = ApplicantFormCreateForm(request.POST, instance=applicant_form)
if form_details.is_valid():
form_details.save()
messages.success(request, 'Form details updated successfully!')
return redirect('applicant:edit_form', form_id=form_id)
# Note: The 'add_field' branch is now redundant since we use the builder,
# but you can keep it if you want the old manual way too.
# --- GET Request (or unsuccessful POST) ---
form_details = ApplicantFormCreateForm(instance=applicant_form)
# Get initial fields to load into the JS builder
initial_fields_json = list(applicant_form.fields.values(
'label', 'field_type', 'required', 'help_text', 'choices', 'order', 'field_name'
))
return render(request, 'applicant/edit_form.html', {
'applicant_form': applicant_form,
'job': job,
'form_details': form_details,
'initial_fields_json': json.dumps(initial_fields_json)
})
def delete_field(request, field_id):
"""Delete a form field"""
field = get_object_or_404(FormField, id=field_id)
form_id = field.form.id
field.delete()
messages.success(request, 'Field deleted successfully!')
return redirect('applicant:edit_form', form_id=form_id)
def activate_form(request, form_id):
"""Activate a form (deactivates others automatically)"""
applicant_form = get_object_or_404(ApplicantForm, id=form_id)
applicant_form.activate()
messages.success(request, f'Form "{applicant_form.name}" is now active!')
return redirect('applicant:job_forms_list', job_id=applicant_form.job_posting.internal_job_id)
# === PUBLIC VIEWS (for applicants) ===
def apply_form_view(request, job_id):
"""Public application form - serves active form"""
job = get_object_or_404(JobPosting, internal_job_id=job_id, status='ACTIVE')
if job.is_expired():
raise Http404("Application deadline has passed")
try:
applicant_form = job.applicant_forms.get(is_active=True)
except ApplicantForm.DoesNotExist:
raise Http404("No active application form configured for this job")
DynamicForm = create_dynamic_form(applicant_form)
if request.method == 'POST':
form = DynamicForm(request.POST)
if form.is_valid():
ApplicantSubmission.objects.create(
job_posting=job,
form=applicant_form,
data=form.cleaned_data,
ip_address=request.META.get('REMOTE_ADDR')
)
return redirect('applicant:thank_you', job_id=job_id)
else:
form = DynamicForm()
return render(request, 'applicant/apply_form.html', {
'form': form,
'job': job,
'applicant_form': applicant_form
})
def review_job_detail(request,job_id):
"""Public job detail view for applicants"""
job = get_object_or_404(JobPosting, internal_job_id=job_id, status='ACTIVE')
if job.is_expired():
raise Http404("This job posting has expired.")
return render(request,'applicant/review_job_detail.html',{'job':job})
def thank_you_view(request, job_id):
job = get_object_or_404(JobPosting, internal_job_id=job_id)
return render(request, 'applicant/thank_you.html', {'job': job})

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

8
static/image/vision.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Binary file not shown.

View File

@ -1,86 +0,0 @@
{% extends 'unfold/layouts/base.html' %}
{% load i18n unfold %}
{% block breadcrumbs %}{% endblock %}
{% block title %}
{% trans 'Dashboard' %} | {{ site_title|default:_('Django site admin') }}
{% endblock %}
{% block branding %}
<h1 id="site-name">
<a href="{% url 'admin:index' %}">
{{ site_header }}
</a>
</h1>
{% endblock %}
{% block content_before %}
{% component "unfold/helpers/header.html" %}
{% trans "Recruitment Dashboard" %}
{% endcomponent %}
{% endblock %}
{% block content %}
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
{% component "unfold/components/card.html" with title="Total Jobs Posted" %}
<p class="text-4xl font-bold text-blue-600">{{ total_jobs }}</p>
{% endcomponent %}
{% component "unfold/components/card.html" with title="Total Candidates" %}
<p class="text-4xl font-bold text-green-600">{{ total_candidates }}</p>
{% endcomponent %}
{% component "unfold/components/card.html" with title="Average Applications/Job" %}
<p class="text-4xl font-bold text-yellow-600">{{ average_applications }}</p>
{% endcomponent %}
</div>
<div class="bg-white p-6 rounded-xl shadow-md mb-8">
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-800">Applications Per Job</h2>
</div>
<canvas id="applicationsChart" height="300"></canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
const ctx = document.getElementById('applicationsChart').getContext('2d');
const applicationsChart = new Chart(ctx, {
type: 'bar',
data: {
labels: {{ job_titles|safe }},
datasets: [{
label: 'Applications',
data: {{ job_app_counts|safe }},
backgroundColor: 'rgba(34, 197, 94, 0.7)',
borderColor: 'rgba(34, 197, 94, 1)',
borderWidth: 1,
borderRadius: 4,
maxBarThickness: 40
}]
},
options: {
responsive: true,
plugins: {
legend: { display: false },
tooltip: {
callbacks: {
label: function(context) {
return `${context.raw} applications`;
}
}
}
},
scales: {
x: { ticks: { autoSkip: false } },
y: {
beginAtZero: true,
grace: '10%',
title: { display: true, text: 'Number of Applicants' }
}
}
}
});
</script>
{% endblock %}

View File

@ -1,4 +1,4 @@
{% load static i18n %} {% comment %} {% load static i18n %}
<!DOCTYPE html> <!DOCTYPE html>
<html lang="{% if request.LANGUAGE_CODE %}{{ request.LANGUAGE_CODE }}{% else %}en{% endif %}"> <html lang="{% if request.LANGUAGE_CODE %}{{ request.LANGUAGE_CODE }}{% else %}en{% endif %}">
<head> <head>
@ -9,16 +9,16 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.5/bundles/datastar.js"></script> <script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.5/bundles/datastar.js"></script>
{% comment %} <script src="https://unpkg.com/htmx.org@2.0.3/dist/htmx.min.js"></script> {% endcomment %} {% comment %} <script src="https://unpkg.com/htmx.org@2.0.3/dist/htmx.min.js"></script> {% endcomment %}
<meta name="csrf-token" content="{{ csrf_token }}"> {% comment %} <meta name="csrf-token" content="{{ csrf_token }}"> {% endcomment %}
<!-- FilePond CSS --> <!-- FilePond CSS -->
<link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet"> {% comment %} <link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
<link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" rel="stylesheet"> <link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/style.css' %}"> <link rel="stylesheet" href="{% static 'css/style.css' %}">
{% block extra_css %}{% endblock %} {% block extra_css %}{% endblock %}
</head> </head>
<body> <body> {% endcomment %}
<header class="header"> {% comment %} <header class="header">
<div class="container"> <div class="container">
<div class="d-flex justify-content-between align-items-center py-3"> <div class="d-flex justify-content-between align-items-center py-3">
<div class="logo h4 mb-0">NorahUniversity ATS</div> <div class="logo h4 mb-0">NorahUniversity ATS</div>
@ -37,8 +37,8 @@
</div> </div>
</div> </div>
</div> </div>
</header> </header> {% endcomment %}
{% comment %}
<nav class="navbar navbar-expand-lg navbar-light border-bottom"> <nav class="navbar navbar-expand-lg navbar-light border-bottom">
<div class="container"> <div class="container">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation"> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
@ -91,9 +91,9 @@
</ul> </ul>
</div> </div>
</div> </div>
</nav> </nav> {% endcomment %}
<main class="container my-4"> {% comment %} <main class="container my-4">
{% if messages %} {% if messages %}
<div class="messages"> <div class="messages">
{% for message in messages %} {% for message in messages %}
@ -107,12 +107,12 @@
{% block content %} {% block content %}
{% endblock %} {% endblock %}
</main> </main> {% endcomment %}
<!-- Delete Modal --> <!-- Delete Modal -->
{% include 'includes/delete_modal.html' %} {% comment %} {% include 'includes/delete_modal.html' %} {% endcomment %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script> {% comment %} <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script> <script>
const csrfToken = "{{ csrf_token }}"; const csrfToken = "{{ csrf_token }}";
@ -138,4 +138,423 @@
}); });
</script> </script>
{% block extra_js %}{% endblock %} {% block extra_js %}{% endblock %} {% endcomment %}
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="King Abdullah Academic University Hospital - Applicant Tracking System">
<title>{% block title %}University ATS{% 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">
<link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
<link href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css" rel="stylesheet">
<link rel="stylesheet" href="{% static 'css/style.css' %}">
<style>
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-light-bg: #f9fbfd;
--kaauh-border: #eaeff3;
}
/* === Top Bar === */
.top-bar {
background-color: white;
border-bottom: 1px solid var(--kaauh-border);
font-size: 0.825rem;
padding: 0.4rem 0;
}
.top-bar a { text-decoration: none; }
.top-bar .social-icons i {
color: var(--kaauh-teal);
transition: color 0.2s;
}
.top-bar .social-icons i:hover {
color: var(--kaauh-teal-dark);
}
.top-bar .contact-item {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.5rem;
}
.top-bar .logo-container img {
height: 50px;
object-fit: contain;
}
@media (max-width: 767.98px) {
.top-bar .logo-container {
order: -1;
margin-bottom: 0.5rem;
}
.top-bar .contact-info {
justify-content: center;
}
}
/* === Navbar === */
.navbar-brand {
font-weight: 700;
letter-spacing: -0.5px;
font-size: 1.35rem;
}
.navbar-dark {
background-color: var(--kaauh-teal) !important;
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
.nav-link {
font-weight: 500;
transition: all 0.2s ease;
}
.nav-link:hover,
.nav-link.active {
color: white !important;
background: rgba(255,255,255,0.12) !important;
border-radius: 4px;
}
/* Dropdown */
.dropdown-menu {
border: none;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
border-radius: 6px;
padding: 0.5rem 0;
min-width: 200px;
}
.dropdown-item {
padding: 0.5rem 1.25rem;
}
.dropdown-item:hover {
background-color: rgba(0, 99, 110, 0.08);
}
@media (min-width: 992px) {
/* This hover effect is what overrides the click for desktop */
/* For the profile dropdown, we generally want the click behavior,
so let's make sure it doesn't have the desktop hover override.
The current CSS applies to all .dropdown:hover, which is fine,
but we should rely on Bootstrap's JS for this one. */
/* Removed unnecessary desktop hover override for this specific context
or ensured it works by click */
.dropdown:hover > .dropdown-menu {
display: block;
margin-top: 0;
}
.dropdown-menu {
display: block !important;
opacity: 0;
visibility: hidden;
transform: translateY(8px);
transition: opacity 0.25s, transform 0.25s;
}
.dropdown:hover .dropdown-menu {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
}
/* === Job Table === */
.job-table-wrapper {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0,0,0,0.06);
margin-bottom: 2rem;
}
.job-table thead th {
background: var(--kaauh-teal);
color: white;
font-weight: 600;
padding: 1rem;
text-align: center;
}
.job-table td {
padding: 1rem;
vertical-align: middle;
text-align: center;
}
.job-table tr:hover td {
background-color: rgba(0, 99, 110, 0.03);
}
.btn-apply {
background: var(--kaauh-teal);
border: none;
color: white;
padding: 0.45rem 1rem;
font-weight: 600;
border-radius: 6px;
transition: all 0.2s;
}
.btn-apply:hover {
background: var(--kaauh-teal-dark);
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}
/* === Footer & Alerts === */
.footer {
background: var(--kaauh-light-bg);
padding: 2rem 0;
border-top: 1px solid var(--kaauh-border);
font-size: 0.95rem;
color: #555;
}
.alert {
border: none;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
}
main.container {
min-height: calc(100vh - 220px);
padding: 2rem 0;
}
/* === Profile Avatar === */
.profile-avatar {
width: 38px;
height: 38px;
border-radius: 50%;
background: white;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 0.9rem;
}
/* Ensures the dropdown-toggle style is minimal for the avatar */
.navbar-nav .dropdown-toggle.p-0::after {
display: none;
}
</style>
{% block customCSS %}{% endblock %}
</head>
<body class="d-flex flex-column min-vh-100">
<div class="top-bar">
<div class="container d-flex flex-wrap justify-content-between align-items-center gap-2">
<div class="d-flex align-items-center gap-3 social-icons">
<span class="text-muted d-none d-sm-inline">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>
</div>
<div class="contact-info d-flex flex-wrap justify-content-center gap-2">
<div class="contact-item">
<i class="fas fa-envelope text-primary"></i>
<span class="d-none d-sm-inline">info@kaauh.edu.sa</span>
</div>
<div class="contact-item">
<i class="fas fa-phone text-primary"></i>
<span class="d-none d-sm-inline">+966 11 820 0000</span>
</div>
</div>
<div class="logo-container d-flex gap-2">
<img src="{% static 'image/vision.svg' %}" alt="Saudi Vision 2030" loading="lazy">
<img src="{% static 'image/hospital_logo_3.png' %}" alt="King Abdullah Academic University Hospital" loading="lazy">
</div>
</div>
</div>
<nav class="navbar navbar-expand-lg navbar-dark sticky-top">
<div class="container d-flex align-items-center">
<a class="navbar-brand text-white" href="#">
<i class="fas fa-hospital me-1"></i> KAAUH ATS
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<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" %}
Dashboard
</span>
</a>
</li>
<li class="nav-item">
<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" %}
Jobs
</span>
</a>
</li>
<li class="nav-item">
<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" %}
Candidates
</span>
</a>
</li>
<li class="nav-item">
<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 class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
Training
</span>
</a>
</li>
<li class="nav-item">
<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">
{% include "icons/meeting.html" %}
Meetings
</span>
</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">Meeting & Schedule</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#"><i class="fas fa-calendar me-2"></i> Meetings</a></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-clock me-2"></i> Schedule</a></li>
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">Jobs</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#"><i class="fas fa-briefcase me-2"></i> Active Jobs</a></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-file-alt me-2"></i> Draft Jobs</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" href="#"><i class="fas fa-list me-1"></i> Job List</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">Candidates</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#"><i class="fas fa-users me-2"></i> All Candidates</a></li>
<li><a class="dropdown-item" href="#"><i class="fas fa-user-plus me-2"></i> New Candidates</a></li>
</ul>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<i class="fas fa-plus-circle me-1"></i> Create Job
</a>
</li>
</ul>
{% if not request.session.linkedin_authenticated %}
<ul class="navbar-nav ms-auto ms-lg-0">
<li class="nav-item">
<a class="nav-link" href="{% url 'linkedin_login' %}">
<i class="fab fa-linkedin me-1"></i> Connect LinkedIn
</a>
</li>
</ul>
{% else %}
<ul class="navbar-nav ms-auto ms-lg-0">
<li class="nav-item d-none d-lg-block">
<span class="nav-link text-success">
<i class="fab fa-linkedin me-1"></i> LinkedIn Connected
</span>
</li>
</ul>
{% endif %}
</div>
<ul class="navbar-nav ms-2 ms-lg-3">
<li class="nav-item dropdown">
<a class="nav-link p-0 dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
<div class="profile-avatar">
{{ user.username|first|upper }}
</div>
</a>
<ul class="dropdown-menu dropdown-menu-end py-2 shadow" style="min-width: 220px;">
<li class="px-3 py-2">
<div class="d-flex align-items-center">
<div class="me-3">
<div class="profile-avatar" style="width: 40px; height: 40px;">
{{ user.username|first|upper }}
</div>
</div>
<div>
<div class="fw-semibold">{{ 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" href="#"><i class="fas fa-user-circle me-2 text-primary"></i> My Profile</a></li>
<li><a class="dropdown-item py-2" href="#"><i class="fas fa-cog me-2 text-primary"></i> Settings</a></li>
<li><a class="dropdown-item py-2" href="#"><i class="fas fa-history me-2 text-primary"></i> Activity Log</a></li>
<li><a class="dropdown-item py-2" href="#"><i class="fas fa-question-circle me-2 text-primary"></i> Help & Support</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 text-danger">
<i class="fas fa-sign-out-alt me-2"></i> Sign Out
</button>
</form>
</li>
</ul>
</li>
</ul>
</div>
</nav>
<main class="container flex-grow-1">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% block content %}
{% endblock %}
</main>
<!-- Delete Modal -->
{% include 'includes/delete_modal.html' %}
<footer class="footer mt-auto">
<div class="container text-center">
<p class="mb-0">
&copy; {% now "Y" %} King Abdullah Academic University Hospital (KAAUH).<br>
<small>All rights reserved.</small>
</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const navbarCollapse = document.getElementById('navbarNav');
const navLinks = navbarCollapse.querySelectorAll('.nav-link:not(.dropdown-toggle)');
navLinks.forEach(link => {
link.addEventListener('click', () => {
const bsCollapse = bootstrap.Collapse.getInstance(navbarCollapse);
if (bsCollapse && navbarCollapse.classList.contains('show')) {
bsCollapse.hide();
}
});
});
});
</script>
{% block customJS %}{% endblock %}
</body>
</html>