Merge pull request 'candidates pipeline status chart donout' (#20) from frontend into main

Reviewed-on: #20
This commit is contained in:
ismail 2025-10-23 13:29:09 +03:00
commit a181e84569
40 changed files with 2482 additions and 1177 deletions

View File

@ -135,9 +135,9 @@ WSGI_APPLICATION = 'NorahUniversity.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'norahuniversity',
'USER': 'norahuniversity',
'PASSWORD': 'norahuniversity',
'NAME': 'haikal_db',
'USER': 'faheed',
'PASSWORD': 'Faheed@215',
'HOST': '127.0.0.1',
'PORT': '5432',
}

View File

@ -23,9 +23,11 @@ urlpatterns = [
# path('', include('recruitment.urls')),
path("ckeditor5/", include('django_ckeditor_5.urls')),
path('form/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'),
path('form/<slug:template_slug>/submit/', views.submit_form, name='submit_form'),
path('application/<slug:template_slug>/', views.application_submit_form, name='application_submit_form'),
path('application/<slug:template_slug>/submit/', views.application_submit, name='application_submit'),
path('application/<slug:slug>/apply/', views.application_detail, name='application_detail'),
path('application/<slug:slug>/success/', views.application_success, name='application_success'),
path('api/templates/', views.list_form_templates, name='list_form_templates'),
path('api/templates/save/', views.save_form_template, name='save_form_template'),
path('api/templates/<slug:template_slug>/', views.load_form_template, name='load_form_template'),

View File

@ -5,16 +5,15 @@ from html import unescape
from urllib.parse import quote, urlencode
import requests
import logging
from django.conf import settings
import time
import random
from django.utils import timezone
from django.conf import settings
logger = logging.getLogger(__name__)
# Define a constant for the API version for better maintenance
# Define constants
LINKEDIN_API_VERSION = '2.0.0'
LINKEDIN_VERSION = '202409' # Modern API version for header control
LINKEDIN_VERSION = '202409'
MAX_POST_CHARS = 3000 # LinkedIn's maximum character limit for shareCommentary
class LinkedInService:
def __init__(self):
@ -23,11 +22,11 @@ class LinkedInService:
self.redirect_uri = settings.LINKEDIN_REDIRECT_URI
self.access_token = None
# Configuration for image processing wait time
self.ASSET_STATUS_TIMEOUT = 15 # Max time (seconds) to wait for image processing
self.ASSET_STATUS_INTERVAL = 2 # Check every 2 seconds
# --- AUTHENTICATION & PROFILE ---
self.ASSET_STATUS_TIMEOUT = 15
self.ASSET_STATUS_INTERVAL = 2
# ---------------- AUTHENTICATION & PROFILE ----------------
def get_auth_url(self):
"""Generate LinkedIn OAuth URL"""
params = {
@ -76,7 +75,7 @@ class LinkedInService:
logger.error(f"Error getting user profile: {e}")
raise
# --- ASSET UPLOAD & STATUS ---
# ---------------- ASSET UPLOAD & STATUS ----------------
def get_asset_status(self, asset_urn):
"""Checks the status of a registered asset (image) to ensure it's READY."""
@ -86,7 +85,7 @@ class LinkedInService:
'X-Restli-Protocol-Version': LINKEDIN_API_VERSION,
'LinkedIn-Version': LINKEDIN_VERSION,
}
try:
response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status()
@ -129,6 +128,7 @@ class LinkedInService:
"""Step 2: Upload image file and poll for 'READY' status."""
image_file.open()
image_content = image_file.read()
image_file.seek(0) # Reset pointer after reading
image_file.close()
headers = {
@ -137,8 +137,8 @@ class LinkedInService:
response = requests.post(upload_url, headers=headers, data=image_content, timeout=60)
response.raise_for_status()
# --- CRITICAL FIX: POLL FOR ASSET STATUS ---
# --- POLL FOR ASSET STATUS ---
start_time = time.time()
while time.time() - start_time < self.ASSET_STATUS_TIMEOUT:
try:
@ -149,56 +149,55 @@ class LinkedInService:
return True
if status == "FAILED":
raise Exception(f"LinkedIn image processing failed for asset {asset_urn}")
logger.info(f"Asset {asset_urn} status: {status}. Waiting...")
time.sleep(self.ASSET_STATUS_INTERVAL)
except Exception as e:
logger.warning(f"Error during asset status check for {asset_urn}: {e}. Retrying.")
time.sleep(self.ASSET_STATUS_INTERVAL * 2)
time.sleep(self.ASSET_STATUS_INTERVAL * 2)
# If the loop times out, return True to attempt post, but log warning
logger.warning(f"Asset {asset_urn} timed out, but upload succeeded. Forcing post attempt.")
return True
# --- POSTING LOGIC ---
# ---------------- POSTING UTILITIES ----------------
def clean_html_for_social_post(self, html_content):
"""Converts safe HTML to plain text with basic formatting (bullets, bold, newlines)."""
"""Converts safe HTML to plain text with basic formatting."""
if not html_content:
return ""
text = html_content
# 1. Convert Bolding tags to *Markdown*
text = re.sub(r'<strong>(.*?)</strong>', r'*\1*', text, flags=re.IGNORECASE)
text = re.sub(r'<b>(.*?)</b>', r'*\1*', text, flags=re.IGNORECASE)
# 2. Handle Lists: Convert <li> tags into a bullet point
text = re.sub(r'</(ul|ol|div)>', '\n', text, flags=re.IGNORECASE)
text = re.sub(r'<li[^>]*>', '', text, flags=re.IGNORECASE)
text = re.sub(r'</li>', '\n', text, flags=re.IGNORECASE)
text = re.sub(r'</(ul|ol|div)>', '\n', text, flags=re.IGNORECASE)
text = re.sub(r'<li[^>]*>', '', text, flags=re.IGNORECASE)
text = re.sub(r'</li>', '\n', text, flags=re.IGNORECASE)
# 3. Handle Paragraphs and Line Breaks
text = re.sub(r'</p>', '\n\n', text, flags=re.IGNORECASE)
text = re.sub(r'<br/?>', '\n', text, flags=re.IGNORECASE)
# 4. Strip all remaining, unsupported HTML tags
clean_text = re.sub(r'<[^>]+>', '', text)
# 5. Unescape HTML entities
clean_text = unescape(clean_text)
# 6. Clean up excessive whitespace/newlines
clean_text = re.sub(r'(\n\s*){3,}', '\n\n', clean_text).strip()
return clean_text
def hashtags_list(self, hash_tags_str):
"""Convert comma-separated hashtags string to list"""
if not hash_tags_str:
return ["#HigherEd", "#Hiring", "#UniversityJobs"]
tags = [tag.strip() for tag in hash_tags_str.split(',') if tag.strip()]
tags = [tag if tag.startswith('#') else f'#{tag}' for tag in tags]
@ -208,15 +207,18 @@ class LinkedInService:
return tags
def _build_post_message(self, job_posting):
"""Centralized logic to construct the professionally formatted text message."""
"""
Constructs the final text message.
Includes a unique suffix for duplicate content prevention (422 fix).
"""
message_parts = [
f"🔥 *Job Alert!* Were looking for a talented professional to join our team.",
f"👉 **{job_posting.title}** 👈",
]
if job_posting.department:
message_parts.append(f"*{job_posting.department}*")
message_parts.append(f"*{job_posting.department}*")
message_parts.append("\n" + "=" * 25 + "\n")
# KEY DETAILS SECTION
@ -229,7 +231,7 @@ class LinkedInService:
details_list.append(f"🏠 Workplace: {job_posting.get_workplace_type_display()}")
if job_posting.salary_range:
details_list.append(f"💰 Salary: {job_posting.salary_range}")
if details_list:
message_parts.append("*Key Information*:")
message_parts.extend(details_list)
@ -239,11 +241,13 @@ class LinkedInService:
clean_description = self.clean_html_for_social_post(job_posting.description)
if clean_description:
message_parts.append(f"🔎 *About the Role:*\n{clean_description}")
# CALL TO ACTION
if job_posting.application_url:
message_parts.append(f"\n\n---")
message_parts.append(f"🔗 **APPLY NOW:** {job_posting.application_url}")
# CRITICAL: Include the URL explicitly in the text body.
# When media_category is NONE, LinkedIn often makes these URLs clickable.
message_parts.append(f"🔗 **APPLY NOW:** {job_posting.application_url}")
# HASHTAGS
hashtags = self.hashtags_list(job_posting.hash_tags)
@ -252,19 +256,38 @@ class LinkedInService:
hashtags.insert(0, dept_hashtag)
message_parts.append("\n" + " ".join(hashtags))
if len(message_parts)>=3000:
message_parts=message_parts[0:2980]+"........"
return "\n".join(message_parts)
final_message = "\n".join(message_parts)
# --- FIX: ADD UNIQUE SUFFIX AND HANDLE LENGTH (422 fix) ---
unique_suffix = f"\n\n| Ref: {int(time.time())}"
available_length = MAX_POST_CHARS - len(unique_suffix)
if len(final_message) > available_length:
logger.warning("Post message truncated due to character limit.")
final_message = final_message[:available_length - 3] + "..."
return final_message + unique_suffix
# ---------------- MAIN POSTING METHODS ----------------
def _send_ugc_post(self, person_urn, job_posting, media_category="NONE", media_list=None):
"""
New private method to handle the final UGC post request (text or image).
This eliminates the duplication between create_job_post and create_job_post_with_image.
Private method to handle the final UGC post request.
CRITICAL FIX: Avoids ARTICLE category if not using an image to prevent 402 errors.
"""
message = self._build_post_message(job_posting)
# --- FIX FOR 402: Force NONE if no image is present. ---
if media_category != "IMAGE":
# We explicitly force pure text share to avoid LinkedIn's link crawler
# which triggers the commercial 402 error on job reposts.
media_category = "NONE"
media_list = None
# --------------------------------------------------------
url = "https://api.linkedin.com/v2/ugcPosts"
headers = {
@ -280,8 +303,8 @@ class LinkedInService:
"shareMediaCategory": media_category,
}
}
if media_list:
if media_list and media_category == "IMAGE":
specific_content["com.linkedin.ugc.ShareContent"]["media"] = media_list
payload = {
@ -294,8 +317,13 @@ class LinkedInService:
}
response = requests.post(url, headers=headers, json=payload, timeout=60)
# Log 402/422 details
if response.status_code in [402, 422]:
logger.error(f"{response.status_code} UGC Post Error Detail: {response.text}")
response.raise_for_status()
post_id = response.headers.get('x-restli-id', '')
post_url = f"https://www.linkedin.com/feed/update/{quote(post_id)}/" if post_id else ""
@ -308,18 +336,21 @@ class LinkedInService:
def create_job_post_with_image(self, job_posting, image_file, person_urn, asset_urn):
"""Step 3: Creates the final LinkedIn post payload with the image asset."""
"""Creates the final LinkedIn post payload with the image asset."""
if not job_posting.application_url:
raise ValueError("Application URL is required for image link share on LinkedIn.")
# Prepare the media list for the _send_ugc_post helper
# Media list for IMAGE category (retains link details)
# Note: This is an exception where we MUST provide link details for the image card
media_list = [{
"status": "READY",
"media": asset_urn,
"description": {"text": job_posting.title},
"originalUrl": job_posting.application_url,
"originalUrl": job_posting.application_url,
"title": {"text": "Apply Now"}
}]
# Use the helper method to send the post
return self._send_ugc_post(
person_urn=person_urn,
job_posting=job_posting,
@ -344,47 +375,44 @@ class LinkedInService:
# Check for image and attempt post
try:
# Assuming correct model path: job_posting.related_model_name.first().image_field_name
image_upload = job_posting.post_images.first().post_image
has_image = image_upload is not None
except Exception:
pass # No image available
pass
if has_image:
try:
# Step 1: Register
# Steps 1, 2, 3 for image post
upload_info = self.register_image_upload(person_urn)
asset_urn = upload_info['asset']
# Step 2: Upload and WAIT FOR READY (Crucial for 422 fix)
self.upload_image_to_linkedin(
upload_info['upload_url'],
image_upload,
image_upload,
asset_urn
)
# Step 3: Create post with image
return self.create_job_post_with_image(
job_posting, image_upload, person_urn, asset_urn
)
except Exception as e:
logger.error(f"Image post failed, falling back to text: {e}")
# Force fallback to text-only if image posting fails
has_image = False
has_image = False
# === FALLBACK TO PURE TEXT POST (shareMediaCategory: NONE) ===
# Use the single helper method here
# The _send_ugc_post method now ensures this is a PURE text post
# to avoid the 402/ARTICLE-related issues.
return self._send_ugc_post(
person_urn=person_urn,
job_posting=job_posting,
media_category="NONE"
media_category="NONE"
)
except Exception as e:
logger.error(f"Error creating LinkedIn post: {e}")
status_code = getattr(getattr(e, 'response', None), 'status_code', 500)
return {
'success': False,
'error': str(e),
'status_code': getattr(e.response, 'status_code', 500) if hasattr(e, 'response') else 500
'status_code': status_code
}

View File

@ -0,0 +1,476 @@
# Generated by Django 5.2.7 on 2025-10-22 16:33
import django.core.validators
import django.db.models.deletion
import django_ckeditor_5.fields
import django_countries.fields
import django_extensions.db.fields
import recruitment.validators
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='BreakTime',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start_time', models.TimeField(verbose_name='Start Time')),
('end_time', models.TimeField(verbose_name='End Time')),
],
),
migrations.CreateModel(
name='FormStage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('name', models.CharField(help_text='Name of the stage', max_length=200)),
('order', models.PositiveIntegerField(default=0, help_text='Order of the stage in the form')),
('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default resume stage')),
],
options={
'verbose_name': 'Form Stage',
'verbose_name_plural': 'Form Stages',
'ordering': ['order'],
},
),
migrations.CreateModel(
name='HiringAgency',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('name', models.CharField(max_length=200, unique=True, verbose_name='Agency Name')),
('contact_person', models.CharField(blank=True, max_length=150, verbose_name='Contact Person')),
('email', models.EmailField(blank=True, max_length=254)),
('phone', models.CharField(blank=True, max_length=20)),
('website', models.URLField(blank=True)),
('notes', models.TextField(blank=True, help_text='Internal notes about the agency')),
('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)),
('address', models.TextField(blank=True, null=True)),
],
options={
'verbose_name': 'Hiring Agency',
'verbose_name_plural': 'Hiring Agencies',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='Source',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('name', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, unique=True, verbose_name='Source Name')),
('source_type', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, verbose_name='Source Type')),
('description', models.TextField(blank=True, help_text='A description of the source', verbose_name='Description')),
('ip_address', models.GenericIPAddressField(blank=True, help_text='The IP address of the source', null=True, verbose_name='IP Address')),
('created_at', models.DateTimeField(auto_now_add=True)),
('api_key', models.CharField(blank=True, help_text='API key for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Key')),
('api_secret', models.CharField(blank=True, help_text='API secret for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Secret')),
('trusted_ips', models.TextField(blank=True, help_text='Comma-separated list of trusted IP addresses', null=True, verbose_name='Trusted IP Addresses')),
('is_active', models.BooleanField(default=True, help_text='Whether this source is active for integration', verbose_name='Active')),
('integration_version', models.CharField(blank=True, help_text='Version of the integration protocol', max_length=50, verbose_name='Integration Version')),
('last_sync_at', models.DateTimeField(blank=True, help_text='Timestamp of the last successful synchronization', null=True, verbose_name='Last Sync At')),
('sync_status', models.CharField(blank=True, choices=[('IDLE', 'Idle'), ('SYNCING', 'Syncing'), ('ERROR', 'Error'), ('DISABLED', 'Disabled')], default='IDLE', max_length=20, verbose_name='Sync Status')),
],
options={
'verbose_name': 'Source',
'verbose_name_plural': 'Sources',
'ordering': ['name'],
},
),
migrations.CreateModel(
name='ZoomMeeting',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('topic', models.CharField(max_length=255, verbose_name='Topic')),
('meeting_id', models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Meeting ID')),
('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')),
('duration', models.PositiveIntegerField(verbose_name='Duration')),
('timezone', models.CharField(max_length=50, verbose_name='Timezone')),
('join_url', models.URLField(verbose_name='Join URL')),
('participant_video', models.BooleanField(default=True, verbose_name='Participant Video')),
('password', models.CharField(blank=True, max_length=20, null=True, verbose_name='Password')),
('join_before_host', models.BooleanField(default=False, verbose_name='Join Before Host')),
('mute_upon_entry', models.BooleanField(default=False, verbose_name='Mute Upon Entry')),
('waiting_room', models.BooleanField(default=False, verbose_name='Waiting Room')),
('zoom_gateway_response', models.JSONField(blank=True, null=True, verbose_name='Zoom Gateway Response')),
('status', models.CharField(blank=True, db_index=True, default='waiting', max_length=20, null=True, verbose_name='Status')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='FormField',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('label', models.CharField(help_text='Label for the field', max_length=200)),
('field_type', models.CharField(choices=[('text', 'Text Input'), ('email', 'Email'), ('phone', 'Phone'), ('textarea', 'Text Area'), ('file', 'File Upload'), ('date', 'Date Picker'), ('select', 'Dropdown'), ('radio', 'Radio Buttons'), ('checkbox', 'Checkboxes')], help_text='Type of the field', max_length=20)),
('placeholder', models.CharField(blank=True, help_text='Placeholder text', max_length=200)),
('required', models.BooleanField(default=False, help_text='Whether the field is required')),
('order', models.PositiveIntegerField(default=0, help_text='Order of the field in the stage')),
('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default field')),
('options', models.JSONField(blank=True, default=list, help_text='Options for selection fields (stored as JSON array)')),
('file_types', models.CharField(blank=True, help_text="Allowed file types (comma-separated, e.g., '.pdf,.doc,.docx')", max_length=200)),
('max_file_size', models.PositiveIntegerField(default=5, help_text='Maximum file size in MB (default: 5MB)')),
('multiple_files', models.BooleanField(default=False, help_text='Allow multiple files to be uploaded')),
('max_files', models.PositiveIntegerField(default=1, help_text='Maximum number of files allowed (when multiple_files is True)')),
('stage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='recruitment.formstage')),
],
options={
'verbose_name': 'Form Field',
'verbose_name_plural': 'Form Fields',
'ordering': ['order'],
},
),
migrations.CreateModel(
name='FormTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('name', models.CharField(help_text='Name of the form template', max_length=200)),
('description', models.TextField(blank=True, help_text='Description of the form template')),
('is_active', models.BooleanField(default=False, help_text='Whether this template is active')),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='form_templates', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'Form Template',
'verbose_name_plural': 'Form Templates',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='FormSubmission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('submitted_at', models.DateTimeField(auto_now_add=True, db_index=True)),
('applicant_name', models.CharField(blank=True, help_text='Name of the applicant', max_length=200)),
('applicant_email', models.EmailField(blank=True, db_index=True, help_text='Email of the applicant', max_length=254)),
('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='form_submissions', to=settings.AUTH_USER_MODEL)),
('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.formtemplate')),
],
options={
'verbose_name': 'Form Submission',
'verbose_name_plural': 'Form Submissions',
'ordering': ['-submitted_at'],
},
),
migrations.AddField(
model_name='formstage',
name='template',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='recruitment.formtemplate'),
),
migrations.CreateModel(
name='Candidate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('first_name', models.CharField(max_length=255, verbose_name='First Name')),
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
('email', models.EmailField(db_index=True, max_length=254, verbose_name='Email')),
('phone', models.CharField(max_length=20, verbose_name='Phone')),
('address', models.TextField(max_length=200, verbose_name='Address')),
('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')),
('is_resume_parsed', models.BooleanField(default=False, verbose_name='Resume Parsed')),
('is_potential_candidate', models.BooleanField(default=False, verbose_name='Potential Candidate')),
('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')], db_index=True, 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.DateTimeField(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.DateTimeField(blank=True, null=True, verbose_name='Interview Date')),
('interview_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Interview Status')),
('offer_date', models.DateField(blank=True, null=True, verbose_name='Offer Date')),
('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Offer Status')),
('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')),
('ai_analysis_data', models.JSONField(default=dict, help_text='Full JSON output from the resume scoring model.', verbose_name='AI Analysis Data')),
('submitted_by_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_candidates', to='recruitment.hiringagency', verbose_name='Submitted by Agency')),
],
options={
'verbose_name': 'Candidate',
'verbose_name_plural': 'Candidates',
},
),
migrations.CreateModel(
name='JobPosting',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('title', models.CharField(max_length=200)),
('department', models.CharField(blank=True, max_length=100)),
('job_type', models.CharField(choices=[('FULL_TIME', 'Full-time'), ('PART_TIME', 'Part-time'), ('CONTRACT', 'Contract'), ('INTERNSHIP', 'Internship'), ('FACULTY', 'Faculty'), ('TEMPORARY', 'Temporary')], default='FULL_TIME', max_length=20)),
('workplace_type', models.CharField(choices=[('ON_SITE', 'On-site'), ('REMOTE', 'Remote'), ('HYBRID', 'Hybrid')], default='ON_SITE', max_length=20)),
('location_city', models.CharField(blank=True, max_length=100)),
('location_state', models.CharField(blank=True, max_length=100)),
('location_country', models.CharField(default='Saudia Arabia', max_length=100)),
('description', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Description')),
('qualifications', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
('salary_range', models.CharField(blank=True, help_text='e.g., $60,000 - $80,000', max_length=200)),
('benefits', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
('application_url', models.URLField(blank=True, help_text='URL where candidates apply', null=True, validators=[django.core.validators.URLValidator()])),
('application_deadline', models.DateField(db_index=True)),
('application_instructions', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
('internal_job_id', models.CharField(editable=False, max_length=50, primary_key=True, serialize=False)),
('created_by', models.CharField(blank=True, help_text='Name of person who created this job', max_length=100)),
('status', models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], db_index=True, default='DRAFT', max_length=20)),
('hash_tags', models.CharField(blank=True, help_text='Comma-separated hashtags for linkedin post like #hiring,#jobopening', max_length=200, validators=[recruitment.validators.validate_hash_tags])),
('linkedin_post_id', models.CharField(blank=True, help_text='LinkedIn post ID after posting', max_length=200)),
('linkedin_post_url', models.URLField(blank=True, help_text='Direct URL to LinkedIn post')),
('posted_to_linkedin', models.BooleanField(default=False)),
('linkedin_post_status', models.CharField(blank=True, help_text='Status of LinkedIn posting', max_length=50)),
('linkedin_posted_at', models.DateTimeField(blank=True, null=True)),
('published_at', models.DateTimeField(blank=True, db_index=True, null=True)),
('position_number', models.CharField(blank=True, help_text='University position number', max_length=50)),
('reporting_to', models.CharField(blank=True, help_text='Who this position reports to', max_length=100)),
('open_positions', models.PositiveIntegerField(default=1, help_text='Number of open positions for this job')),
('max_applications', models.PositiveIntegerField(blank=True, default=1000, help_text='Maximum number of applications allowed', null=True)),
('cancel_reason', models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason')),
('cancelled_by', models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By')),
('cancelled_at', models.DateTimeField(blank=True, null=True)),
('hiring_agency', models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')),
('source', models.ForeignKey(blank=True, help_text='The system or channel from which this job posting originated or was first published.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='job_postings', to='recruitment.source')),
],
options={
'verbose_name': 'Job Posting',
'verbose_name_plural': 'Job Postings',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='InterviewSchedule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('start_date', models.DateField(db_index=True, verbose_name='Start Date')),
('end_date', models.DateField(db_index=True, verbose_name='End Date')),
('working_days', models.JSONField(verbose_name='Working Days')),
('start_time', models.TimeField(verbose_name='Start Time')),
('end_time', models.TimeField(verbose_name='End Time')),
('break_start_time', models.TimeField(blank=True, null=True, verbose_name='Break Start Time')),
('break_end_time', models.TimeField(blank=True, null=True, verbose_name='Break End Time')),
('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')),
('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')),
('candidates', models.ManyToManyField(blank=True, null=True, related_name='interview_schedules', to='recruitment.candidate')),
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_schedules', to='recruitment.jobposting')),
],
),
migrations.AddField(
model_name='formtemplate',
name='job',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='form_template', to='recruitment.jobposting'),
),
migrations.AddField(
model_name='candidate',
name='job',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'),
),
migrations.CreateModel(
name='JobPostingImage',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('post_image', models.ImageField(upload_to='post/', validators=[recruitment.validators.validate_image_size])),
('job', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='post_images', to='recruitment.jobposting')),
],
),
migrations.CreateModel(
name='Profile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size])),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='SharedFormTemplate',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('is_public', models.BooleanField(default=False, help_text='Whether this template is publicly available')),
('shared_with', models.ManyToManyField(blank=True, related_name='shared_templates', to=settings.AUTH_USER_MODEL)),
('template', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='recruitment.formtemplate')),
],
options={
'verbose_name': 'Shared Form Template',
'verbose_name_plural': 'Shared Form Templates',
},
),
migrations.CreateModel(
name='IntegrationLog',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('action', models.CharField(choices=[('REQUEST', 'Request'), ('RESPONSE', 'Response'), ('ERROR', 'Error'), ('SYNC', 'Sync'), ('CREATE_JOB', 'Create Job'), ('UPDATE_JOB', 'Update Job')], max_length=20, verbose_name='Action')),
('endpoint', models.CharField(blank=True, max_length=255, verbose_name='Endpoint')),
('method', models.CharField(blank=True, max_length=10, verbose_name='HTTP Method')),
('request_data', models.JSONField(blank=True, null=True, verbose_name='Request Data')),
('response_data', models.JSONField(blank=True, null=True, verbose_name='Response Data')),
('status_code', models.CharField(blank=True, max_length=10, verbose_name='Status Code')),
('error_message', models.TextField(blank=True, verbose_name='Error Message')),
('ip_address', models.GenericIPAddressField(verbose_name='IP Address')),
('user_agent', models.CharField(blank=True, max_length=255, verbose_name='User Agent')),
('processing_time', models.FloatField(blank=True, null=True, verbose_name='Processing Time (seconds)')),
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integration_logs', to='recruitment.source', verbose_name='Source')),
],
options={
'verbose_name': 'Integration Log',
'verbose_name_plural': 'Integration Logs',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='TrainingMaterial',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('title', models.CharField(max_length=255, verbose_name='Title')),
('content', django_ckeditor_5.fields.CKEditor5Field(blank=True, verbose_name='Content')),
('video_link', models.URLField(blank=True, verbose_name='Video Link')),
('file', models.FileField(blank=True, upload_to='training_materials/', verbose_name='File')),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Created by')),
],
options={
'verbose_name': 'Training Material',
'verbose_name_plural': 'Training Materials',
},
),
migrations.CreateModel(
name='ScheduledInterview',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('interview_date', models.DateField(db_index=True, verbose_name='Interview Date')),
('interview_time', models.TimeField(verbose_name='Interview Time')),
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], db_index=True, default='scheduled', max_length=20)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.candidate')),
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.jobposting')),
('schedule', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')),
('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')),
],
),
migrations.CreateModel(
name='MeetingComment',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content')),
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meeting_comments', to=settings.AUTH_USER_MODEL, verbose_name='Author')),
('meeting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='recruitment.zoommeeting', verbose_name='Meeting')),
],
options={
'verbose_name': 'Meeting Comment',
'verbose_name_plural': 'Meeting Comments',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='FieldResponse',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('value', models.JSONField(blank=True, help_text='Response value (stored as JSON)', null=True)),
('uploaded_file', models.FileField(blank=True, null=True, upload_to='form_uploads/')),
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formfield')),
('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formsubmission')),
],
options={
'verbose_name': 'Field Response',
'verbose_name_plural': 'Field Responses',
'indexes': [models.Index(fields=['submission'], name='recruitment_submiss_474130_idx'), models.Index(fields=['field'], name='recruitment_field_i_097e5b_idx')],
},
),
migrations.AddIndex(
model_name='formsubmission',
index=models.Index(fields=['submitted_at'], name='recruitment_submitt_7946c8_idx'),
),
migrations.AddIndex(
model_name='interviewschedule',
index=models.Index(fields=['start_date'], name='recruitment_start_d_15d55e_idx'),
),
migrations.AddIndex(
model_name='interviewschedule',
index=models.Index(fields=['end_date'], name='recruitment_end_dat_aeb00e_idx'),
),
migrations.AddIndex(
model_name='interviewschedule',
index=models.Index(fields=['created_by'], name='recruitment_created_d0bdcc_idx'),
),
migrations.AddIndex(
model_name='formtemplate',
index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'),
),
migrations.AddIndex(
model_name='formtemplate',
index=models.Index(fields=['is_active'], name='recruitment_is_acti_ae5efb_idx'),
),
migrations.AddIndex(
model_name='candidate',
index=models.Index(fields=['stage'], name='recruitment_stage_f1c6eb_idx'),
),
migrations.AddIndex(
model_name='candidate',
index=models.Index(fields=['created_at'], name='recruitment_created_73590f_idx'),
),
migrations.AddIndex(
model_name='jobposting',
index=models.Index(fields=['status', 'created_at', 'title'], name='recruitment_status_8b77aa_idx'),
),
migrations.AddIndex(
model_name='jobposting',
index=models.Index(fields=['slug'], name='recruitment_slug_004045_idx'),
),
migrations.AddIndex(
model_name='scheduledinterview',
index=models.Index(fields=['job', 'status'], name='recruitment_job_id_f09e22_idx'),
),
migrations.AddIndex(
model_name='scheduledinterview',
index=models.Index(fields=['interview_date', 'interview_time'], name='recruitment_intervi_7f5877_idx'),
),
migrations.AddIndex(
model_name='scheduledinterview',
index=models.Index(fields=['candidate', 'job'], name='recruitment_candida_43d5b0_idx'),
),
]

View File

@ -26,7 +26,7 @@ def format_job(sender, instance, created, **kwargs):
schedule_type=Schedule.ONCE
).first()
if instance.is_active and instance.application_deadline:
if instance.STATUS_CHOICES=='ACTIVE' and instance.application_deadline:
if not existing_schedule:
# Create a new schedule if one does not exist
schedule(

View File

@ -248,7 +248,7 @@ class ViewTests(BaseTestCase):
}
response = self.client.post(
reverse('submit_form', kwargs={'template_id': template.id}),
reverse('application_submit', kwargs={'template_id': template.id}),
data
)
# After successful submission, should redirect to success page
@ -434,7 +434,7 @@ class IntegrationTests(BaseTestCase):
}
response = self.client.post(
reverse('submit_form', kwargs={'template_id': template.id}),
reverse('application_submit', kwargs={'template_id': template.id}),
form_data
)
@ -493,7 +493,7 @@ class AuthenticationTests(BaseTestCase):
)
response = self.client.post(
reverse('submit_form', kwargs={'template_id': template.id}),
reverse('application_submit', kwargs={'template_id': template.id}),
{}
)
# Should redirect to login page
@ -525,7 +525,7 @@ class EdgeCaseTests(BaseTestCase):
# Submit form twice
response1 = self.client.post(
reverse('submit_form', kwargs={'template_id': template.id}),
reverse('application_submit', kwargs={'template_id': template.id}),
{'field_1': 'John', 'field_2': 'Doe'}
)

View File

@ -744,7 +744,7 @@ class AdvancedIntegrationTests(TransactionTestCase):
}
response = self.client.post(
reverse('submit_form', kwargs={'template_id': template.id}),
reverse('application_submit', kwargs={'template_id': template.id}),
submission_data
)
self.assertEqual(response.status_code, 302) # Redirect to success page

View File

@ -14,8 +14,7 @@ urlpatterns = [
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
@ -83,8 +82,8 @@ urlpatterns = [
path('htmx/<slug:slug>/candidate_update_status/', views.candidate_update_status, name='candidate_update_status'),
path('forms/form/<slug:template_slug>/submit/', views.submit_form, name='submit_form'),
path('forms/form/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'),
# path('forms/form/<slug:template_slug>/submit/', views.submit_form, name='submit_form'),
# path('forms/form/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'),
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'),
@ -140,6 +139,7 @@ urlpatterns = [
# Meeting Comments URLs
path('meetings/<slug:slug>/comments/add/', views.add_meeting_comment, name='add_meeting_comment'),
path('meetings/<slug:slug>/comments/<int:comment_id>/edit/', views.edit_meeting_comment, name='edit_meeting_comment'),
path('meetings/<slug:slug>/comments/<int:comment_id>/delete/', views.delete_meeting_comment, name='delete_meeting_comment'),
path('meetings/<slug:slug>/set_meeting_candidate/', views.set_meeting_candidate, name='set_meeting_candidate'),

View File

@ -280,7 +280,7 @@ def create_job(request):
try:
job = form.save(commit=False)
job.save()
job_apply_url_relative=reverse('job_detail_candidate',kwargs={'slug':job.slug})
job_apply_url_relative=reverse('application_detail',kwargs={'slug':job.slug})
job_apply_url_absolute=request.build_absolute_uri(job_apply_url_relative)
job.application_url=job_apply_url_absolute
# FormTemplate.objects.create(job=job, is_active=False, name=job.title,created_by=request.user)
@ -512,9 +512,9 @@ def kaauh_career(request):
# job detail facing the candidate:
def job_detail_candidate(request, slug):
def application_detail(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
return render(request, "forms/job_detail_candidate.html", {"job": job})
return render(request, "forms/application_detail.html", {"job": job})
from django_q.tasks import async_task
@ -800,7 +800,7 @@ def delete_form_template(request, template_id):
)
def form_wizard_view(request, template_slug):
def application_submit_form(request, template_slug):
"""Display the form as a step-by-step wizard"""
template = get_object_or_404(FormTemplate, slug=template_slug, is_active=True)
job_id = template.job.internal_job_id
@ -811,24 +811,24 @@ def form_wizard_view(request, template_slug):
request,
'Application limit reached: This job is no longer accepting new applications. Please explore other available positions.'
)
return redirect('job_detail_candidate',slug=job.slug)
return redirect('application_detail',slug=job.slug)
if job.is_expired:
messages.error(
request,
'Application deadline passed: This job is no longer accepting new applications. Please explore other available positions.'
)
return redirect('job_detail_candidate',slug=job.slug)
return redirect('application_detail',slug=job.slug)
return render(
request,
"forms/form_wizard.html",
"forms/application_submit_form.html",
{"template_slug": template_slug, "job_id": job_id},
)
@csrf_exempt
@require_POST
def submit_form(request, template_slug):
def application_submit(request, template_slug):
"""Handle form submission"""
template = get_object_or_404(FormTemplate, slug=template_slug)
job = template.job
@ -2292,7 +2292,13 @@ def edit_meeting_comment(request, slug, comment_id):
return redirect('meeting_details', slug=slug)
else:
form = MeetingCommentForm(instance=comment)
print("hi")
context = {
'form': form,
'meeting': meeting,
'comment':comment
}
return render(request, 'includes/edit_comment_form.html', context)
@login_required
def delete_meeting_comment(request, slug, comment_id):

View File

@ -394,6 +394,27 @@ def dashboard_view(request):
).count()
high_potential_ratio = round((high_potential_count / total_candidates) * 100, 1) if total_candidates > 0 else 0
jobs=models.JobPosting.objects.all().order_by('internal_job_id')
selected_job_id=request.GET.get('selected_job_id','')
candidate_stage=['APPLIED','EXAM','INTERVIEW','OFFER']
apply_count,exam_count,interview_count,offer_count=[0]*4
if selected_job_id:
job=jobs.get(internal_job_id=selected_job_id)
apply_count=job.screening_candidates_count
exam_count=job.exam_candidates_count
interview_count=job.interview_candidates_count
offer_count=job.offer_candidates_count
all_candidates_count=job.all_candidates_count
else: #default job
job=jobs.first()
apply_count=job.screening_candidates_count
exam_count=job.exam_candidates_count
interview_count=job.interview_candidates_count
offer_count=job.offer_candidates_count
all_candidates_count=job.all_candidates_count
candidates_count=[ apply_count,exam_count,interview_count,offer_count ]
context = {
'total_jobs': total_jobs,
@ -409,6 +430,14 @@ def dashboard_view(request):
'high_potential_count': high_potential_count,
'high_potential_ratio': high_potential_ratio,
'scored_ratio': scored_ratio,
'current_job_id':selected_job_id,
'jobs':jobs,
'all_candidates_count':all_candidates_count,
'candidate_stage':json.dumps(candidate_stage),
'candidates_count':json.dumps(candidates_count)
,'my_job':job
}
return render(request, 'recruitment/dashboard.html', context)
@login_required

View File

@ -42,7 +42,7 @@
<p class="text-muted">{% trans "Review the job details, then apply below." %}</p>
{% if job.form_template %}
<a href="{% url 'form_wizard' job.form_template.pk %}" class="btn btn-main-action btn-lg w-100">
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100">
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
</a>
{% endif %}
@ -102,7 +102,7 @@
<div class="mobile-fixed-apply-bar d-lg-none">
{% if job.form_template %}
<a href="{% url 'form_wizard' job.form_template.pk %}" class="btn btn-main-action btn-lg w-100">
<a href="{% url 'application_submit_form' job.form_template.pk %}" class="btn btn-main-action btn-lg w-100">
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
</a>
{% endif %}

View File

@ -824,7 +824,7 @@
});
try {
const response = await fetch(`/form/${state.templateId}/submit/`, {
const response = await fetch(`/application/${state.templateId}/submit/`, {
method: 'POST',
body: formData
// IMPORTANT: Do NOT set Content-Type header when using FormData

View File

@ -231,7 +231,7 @@
<div class="mt-auto pt-2 border-top">
<div class="d-flex gap-2 justify-content-end">
<a href="{% url 'form_wizard' template.slug %}" class="btn btn-outline-primary btn-sm" title="{% trans 'Preview' %}">
<a href="{% url 'application_submit_form' template.slug %}" class="btn btn-outline-primary btn-sm" title="{% trans 'Preview' %}">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'Edit' %}">
@ -286,7 +286,7 @@
<td>{{ template.updated_at|date:"M d, Y" }}</td>
<td class="text-end">
<div class="btn-group btn-group-sm" role="group">
<a href="{% url 'form_wizard' template.slug %}" class="btn btn-outline-primary" title="{% trans 'Preview' %}">
<a href="{% url 'application_submit_form' template.slug %}" class="btn btn-outline-primary" title="{% trans 'Preview' %}">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">

View File

@ -17,7 +17,7 @@
</div>
<button type="submit" class="btn btn-primary">Add Comment</button>
{% if 'HX-Request' in request.headers %}
<button type="button" class="btn btn-secondary" hx-get="{% url 'meeting_details' meeting.slug %}" hx-target="#comment-section">Cancel</button>
<button type="button" class="btn btn-secondary" hx-get="{% url 'meeting_details' meeting.slug %}" hx-select="#comment-section" hx-target="#comment-section">Cancel</button>
{% endif %}
</form>
</div>

View File

@ -3,7 +3,7 @@
<div class="card-header text-primary-theme d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">Comments ({{ comments.count }})</h5>
{% if 'HX-Request' in request.headers %}
<button type="button" class="btn btn-light btn-sm" hx-get="{% url 'meeting_details' meeting.slug %}" hx-target="#comment-section">
<button type="button" class="btn btn-light btn-sm" hx-get="{% url 'meeting_details' meeting.slug %}" hx-select="#comment-section" hx-target="#comment-section">
<i class="bi bi-x-lg"></i> Close
</button>
{% endif %}

View File

@ -9,7 +9,7 @@
{% csrf_token %}
<button type="submit" class="btn btn-danger">Yes, Delete</button>
{% if 'HX-Request' in request.headers %}
<button type="button" class="btn btn-secondary" hx-get="{% url 'meeting_details' meeting.slug %}" hx-target="#comment-section">Cancel</button>
<button type="button" class="btn btn-secondary" hx-get="{% url 'meeting_details' meeting.slug %}" hx-select="#comment-section" hx-target="#comment-section">Cancel</button>
{% endif %}
</form>
</div>

View File

@ -15,7 +15,7 @@
</div>
{% endif %}
</div>
<button type="submit" class="btn bg-primary-theme btn-sm text-white">Update Comment</button>
<button type="submit" class="btn bg-primary btn-sm">Update Comment</button>
{% if 'HX-Request' in request.headers %}
<button type="button" class="btn btn-secondary btn-sm" hx-get="{% url 'meeting_details' meeting.slug %}" hx-target="#comment-section">Cancel</button>
{% endif %}

View File

@ -248,7 +248,7 @@
<td class="col-link" data-label="{% trans 'Link' %}">
<a style="background-color : #00636e;color : #FFF; padding : 4px 10px; white-space: nowrap;"
href="{% url 'job_detail_candidate' job.slug %}"
href="{% url 'application_detail' job.slug %}"
target="_blank">
{% trans 'Apply' %}
</a>

View File

@ -117,28 +117,21 @@
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-8">
<div class="col-md-12">
<div>
<label for="{{ form.title.id_for_label }}" class="form-label">{% trans "Job Title" %} <span class="text-danger">*</span></label>
{{ form.title }}
{% if form.title.errors %}<div class="text-danger small mt-1">{{ form.title.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="col-md-6">
<div>
<label for="{{ form.job_type.id_for_label }}" class="form-label">{% trans "Job Type" %} <span class="text-danger">*</span></label>
{{ form.job_type }}
{% if form.job_type.errors %}<div class="text-danger small mt-1">{{ form.job_type.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.department.id_for_label }}" class="form-label">{% trans "Department" %}</label>
{{ form.department }}
{% if form.department.errors %}<div class="text-danger small mt-1">{{ form.department.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.workplace_type.id_for_label }}" class="form-label">{% trans "Workplace Type" %} <span class="text-danger">*</span></label>
@ -146,33 +139,18 @@
{% if form.workplace_type.errors %}<div class="text-danger small mt-1">{{ form.workplace_type.errors }}</div>{% endif %}
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 2: INTERNAL AND PROMOTION #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-tags"></i> {% trans "Internal & Promotion" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<div>
<label for="{{ form.position_number.id_for_label }}" class="form-label">{% trans "Position Number" %}</label>
{{ form.position_number }}
{% if form.position_number.errors %}<div class="text-danger small mt-1">{{ form.position_number.errors }}</div>{% endif %}
<label for="{{ form.application_deadline.id_for_label }}" class="form-label">{% trans "Application Deadline" %}<span class="text-danger">*</span></label>
{{ form.application_deadline }}
{% if form.application_deadline.errors %}<div class="text-danger small mt-1">{{ form.application_deadline.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">{% trans "Reports To" %}</label>
{{ form.reporting_to }}
{% if form.reporting_to.errors %}<div class="text-danger small mt-1">{{ form.reporting_to.errors }}</div>{% endif %}
<label for="{{ form.department.id_for_label }}" class="form-label">{% trans "Department" %}</label>
{{ form.department }}
{% if form.department.errors %}<div class="text-danger small mt-1">{{ form.department.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
@ -189,72 +167,14 @@
{% if form.max_applications.errors %}<div class="text-danger small mt-1">{{ form.max_applications.errors }}</div>{% endif %}
</div>
</div>
<div class="col-12">
<div>
<label for="{{ form.hash_tags.id_for_label }}" class="form-label">{% trans "Hashtags (For Promotion/Search on Linkedin)" %}</label>
{{ form.hash_tags }}
{% if form.hash_tags.errors %}<div class="text-danger small mt-1">{{ form.hash_tags.errors }}</div>{% endif %}
<div class="form-text">{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}</div>
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 3: LOCATION AND DATES #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-map-marker-alt"></i> {% trans "Location, Dates, & Salary" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-4">
<div>
<label for="{{ form.location_city.id_for_label }}" class="form-label">{% trans "City" %}</label>
{{ form.location_city }}
{% if form.location_city.errors %}<div class="text-danger small mt-1">{{ form.location_city.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_state.id_for_label }}" class="form-label">{% trans "State/Province" %}</label>
{{ form.location_state }}
{% if form.location_state.errors %}<div class="text-danger small mt-1">{{ form.location_state.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_country.id_for_label }}" class="form-label">{% trans "Country" %}</label>
{{ form.location_country }}
{% if form.location_country.errors %}<div class="text-danger small mt-1">{{ form.location_country.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.application_deadline.id_for_label }}" class="form-label">{% trans "Application Deadline" %}<span class="text-danger">*</span></label>
{{ form.application_deadline }}
{% if form.application_deadline.errors %}<div class="text-danger small mt-1">{{ form.application_deadline.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.salary_range.id_for_label }}" class="form-label">{% trans "Salary Range" %}</label>
{{ form.salary_range }}
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 4: JOB CONTENT (CKEDITOR 5 Fields) #}
{# ================================================= #}
@ -313,8 +233,90 @@
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 2: INTERNAL AND PROMOTION #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-tags"></i> {% trans "Internal & Promotion" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<div>
<label for="{{ form.position_number.id_for_label }}" class="form-label">{% trans "Position Number" %}</label>
{{ form.position_number }}
{% if form.position_number.errors %}<div class="text-danger small mt-1">{{ form.position_number.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">{% trans "Reports To" %}</label>
{{ form.reporting_to }}
{% if form.reporting_to.errors %}<div class="text-danger small mt-1">{{ form.reporting_to.errors }}</div>{% endif %}
</div>
</div>
<div class="col-12">
<div>
<label for="{{ form.hash_tags.id_for_label }}" class="form-label">{% trans "Hashtags (For Promotion/Search on Linkedin)" %}</label>
{{ form.hash_tags }}
{% if form.hash_tags.errors %}<div class="text-danger small mt-1">{{ form.hash_tags.errors }}</div>{% endif %}
<div class="form-text">{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}</div>
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 3: LOCATION AND Salary #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-map-marker-alt"></i> {% trans "Location & Salary" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-4">
<div>
<label for="{{ form.location_city.id_for_label }}" class="form-label">{% trans "City" %}</label>
{{ form.location_city }}
{% if form.location_city.errors %}<div class="text-danger small mt-1">{{ form.location_city.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_state.id_for_label }}" class="form-label">{% trans "State/Province" %}</label>
{{ form.location_state }}
{% if form.location_state.errors %}<div class="text-danger small mt-1">{{ form.location_state.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_country.id_for_label }}" class="form-label">{% trans "Country" %}</label>
{{ form.location_country }}
{% if form.location_country.errors %}<div class="text-danger small mt-1">{{ form.location_country.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-12">
<div>
<label for="{{ form.salary_range.id_for_label }}" class="form-label">{% trans "Salary Range" %}</label>
{{ form.salary_range }}
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# ACTION BUTTONS #}

View File

@ -117,28 +117,21 @@
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-8">
<div class="col-md-12">
<div>
<label for="{{ form.title.id_for_label }}" class="form-label">{% trans "Job Title" %} <span class="text-danger">*</span></label>
{{ form.title }}
{% if form.title.errors %}<div class="text-danger small mt-1">{{ form.title.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div class="col-md-6">
<div>
<label for="{{ form.job_type.id_for_label }}" class="form-label">{% trans "Job Type" %} <span class="text-danger">*</span></label>
{{ form.job_type }}
{% if form.job_type.errors %}<div class="text-danger small mt-1">{{ form.job_type.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.department.id_for_label }}" class="form-label">{% trans "Department" %}</label>
{{ form.department }}
{% if form.department.errors %}<div class="text-danger small mt-1">{{ form.department.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.workplace_type.id_for_label }}" class="form-label">{% trans "Workplace Type" %} <span class="text-danger">*</span></label>
@ -146,33 +139,18 @@
{% if form.workplace_type.errors %}<div class="text-danger small mt-1">{{ form.workplace_type.errors }}</div>{% endif %}
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 2: INTERNAL AND PROMOTION #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-tags"></i> {% trans "Internal & Promotion" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<div>
<label for="{{ form.position_number.id_for_label }}" class="form-label">{% trans "Position Number" %}</label>
{{ form.position_number }}
{% if form.position_number.errors %}<div class="text-danger small mt-1">{{ form.position_number.errors }}</div>{% endif %}
<label for="{{ form.application_deadline.id_for_label }}" class="form-label">{% trans "Application Deadline" %}<span class="text-danger">*</span></label>
{{ form.application_deadline }}
{% if form.application_deadline.errors %}<div class="text-danger small mt-1">{{ form.application_deadline.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">{% trans "Reports To" %}</label>
{{ form.reporting_to }}
{% if form.reporting_to.errors %}<div class="text-danger small mt-1">{{ form.reporting_to.errors }}</div>{% endif %}
<label for="{{ form.department.id_for_label }}" class="form-label">{% trans "Department" %}</label>
{{ form.department }}
{% if form.department.errors %}<div class="text-danger small mt-1">{{ form.department.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
@ -189,72 +167,14 @@
{% if form.max_applications.errors %}<div class="text-danger small mt-1">{{ form.max_applications.errors }}</div>{% endif %}
</div>
</div>
<div class="col-12">
<div>
<label for="{{ form.hash_tags.id_for_label }}" class="form-label">{% trans "Hashtags (For Promotion/Search on Linkedin)" %}</label>
{{ form.hash_tags }}
{% if form.hash_tags.errors %}<div class="text-danger small mt-1">{{ form.hash_tags.errors }}</div>{% endif %}
<div class="form-text">{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}</div>
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 3: LOCATION AND DATES #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-map-marker-alt"></i> {% trans "Location, Dates, & Salary" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-4">
<div>
<label for="{{ form.location_city.id_for_label }}" class="form-label">{% trans "City" %}</label>
{{ form.location_city }}
{% if form.location_city.errors %}<div class="text-danger small mt-1">{{ form.location_city.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_state.id_for_label }}" class="form-label">{% trans "State/Province" %}</label>
{{ form.location_state }}
{% if form.location_state.errors %}<div class="text-danger small mt-1">{{ form.location_state.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_country.id_for_label }}" class="form-label">{% trans "Country" %}</label>
{{ form.location_country }}
{% if form.location_country.errors %}<div class="text-danger small mt-1">{{ form.location_country.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.application_deadline.id_for_label }}" class="form-label">{% trans "Application Deadline" %}<span class="text-danger">*</span></label>
{{ form.application_deadline }}
{% if form.application_deadline.errors %}<div class="text-danger small mt-1">{{ form.application_deadline.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.salary_range.id_for_label }}" class="form-label">{% trans "Salary Range" %}</label>
{{ form.salary_range }}
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 4: JOB CONTENT (CKEDITOR 5 Fields) #}
{# ================================================= #}
@ -313,8 +233,90 @@
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 2: INTERNAL AND PROMOTION #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-tags"></i> {% trans "Internal & Promotion" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<div>
<label for="{{ form.position_number.id_for_label }}" class="form-label">{% trans "Position Number" %}</label>
{{ form.position_number }}
{% if form.position_number.errors %}<div class="text-danger small mt-1">{{ form.position_number.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-6">
<div>
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">{% trans "Reports To" %}</label>
{{ form.reporting_to }}
{% if form.reporting_to.errors %}<div class="text-danger small mt-1">{{ form.reporting_to.errors }}</div>{% endif %}
</div>
</div>
<div class="col-12">
<div>
<label for="{{ form.hash_tags.id_for_label }}" class="form-label">{% trans "Hashtags (For Promotion/Search on Linkedin)" %}</label>
{{ form.hash_tags }}
{% if form.hash_tags.errors %}<div class="text-danger small mt-1">{{ form.hash_tags.errors }}</div>{% endif %}
<div class="form-text">{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}</div>
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# SECTION 3: LOCATION AND Salary #}
{# ================================================= #}
<div class="card mb-4 shadow-sm">
<div class="card-header-themed">
<h5><i class="fas fa-map-marker-alt"></i> {% trans "Location & Salary" %}</h5>
</div>
<div class="card-body">
<div class="row g-4">
<div class="col-md-4">
<div>
<label for="{{ form.location_city.id_for_label }}" class="form-label">{% trans "City" %}</label>
{{ form.location_city }}
{% if form.location_city.errors %}<div class="text-danger small mt-1">{{ form.location_city.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_state.id_for_label }}" class="form-label">{% trans "State/Province" %}</label>
{{ form.location_state }}
{% if form.location_state.errors %}<div class="text-danger small mt-1">{{ form.location_state.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-4">
<div>
<label for="{{ form.location_country.id_for_label }}" class="form-label">{% trans "Country" %}</label>
{{ form.location_country }}
{% if form.location_country.errors %}<div class="text-danger small mt-1">{{ form.location_country.errors }}</div>{% endif %}
</div>
</div>
<div class="col-md-12">
<div>
<label for="{{ form.salary_range.id_for_label }}" class="form-label">{% trans "Salary Range" %}</label>
{{ form.salary_range }}
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
</div>
</div>
</div>
</div>
</div>
{# ================================================= #}
{# ACTION BUTTONS #}

View File

@ -4,7 +4,6 @@
{% block title %}{{ job.title }} - University ATS{% endblock %}
{% block customCSS %}
<style>
/* ================================================= */
/* THEME VARIABLES AND GLOBAL STYLES */
@ -12,41 +11,35 @@
:root {
--kaauh-teal: #00636e; /* Primary */
--kaauh-teal-dark: #004a53;
--kaauh-teal-light: #4bb3be; /* For active glow */
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
/* Consistent Status/Color Map (aligning with theme/bootstrap defaults) */
--color-draft: #6c757d; /* Secondary Gray */
--color-active: var(--kaauh-teal); /* Primary Teal */
--color-closed: #ffc107; /* Warning Yellow */
--color-cancelled: #dc3545; /* Danger Red */
--color-archived: #343a40; /* Dark text/Muted */
}
/* 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 */
--kaauh-teal: #00636e; /* Primary Theme / Active Stage */
--kaauh-teal-light: #4bb3be; /* For active glow */
--color-created: #6c757d; /* Muted Initial State */
--color-active: #00636e; /* Teal for Active Flow */
--color-posted: #17a2b8; /* Info Blue for External Posting */
--color-closed: #ffc107; /* Warning Yellow for Soft End/Review */
--color-archived: #343a40; /* Darkest for Final Storage */
--color-canceled: #dc3545; /* Red for Negative/Canceled */
--color-line-default: #e9ecef; /* Light Gray for all inactive markers */
}
/* Primary Color Overrides */
/* Primary Color Overrides for Bootstrap Classes */
.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; }
.bg-primary { background-color: var(--kaauh-teal) !important; }
/* Status Badge Theme Mapping */
.status-badge.bg-success { background-color: var(--color-active) !important; }
.status-badge.bg-secondary { background-color: var(--color-draft) !important; }
.status-badge.bg-warning { background-color: var(--color-closed) !important; }
.status-badge.bg-danger { background-color: var(--color-cancelled) !important; }
/* Ensure text colors are consistent for standard BS classes */
.text-success { color: #28a745 !important; }
.text-info { color: #17a2b8 !important; }
.text-secondary { color: var(--kaauh-primary-text) !important; }
/* Header styling */
.job-header-card {
background: linear-gradient(135deg, var(--kaauh-teal), #004d57);
background: linear-gradient(135deg, var(--kaauh-teal), var(--kaauh-teal-dark));
color: white;
border-radius: 0.75rem 0.75rem 0 0;
padding: 1.5rem;
@ -57,7 +50,7 @@
margin: 0;
font-size: 1.8rem;
}
/* Status badge */
/* Status badge - Consolidated style for all badges */
.status-badge {
font-size: 0.9rem;
padding: 0.4em 0.8em;
@ -67,6 +60,7 @@
letter-spacing: 0.7px;
display: inline-flex;
align-items: center;
color: white; /* Ensure badge text is white */
}
/* Card enhancements */
@ -91,12 +85,13 @@
border-bottom: 1px solid var(--kaauh-border);
}
/* Left Column Tabs Theming */
/* Tabs Theming - Applies to the right column */
.nav-tabs {
border-bottom: 1px solid var(--kaauh-border);
background-color: #f8f9fa;
padding: 0 1.25rem;
padding: 0;
}
.nav-tabs .nav-link {
border: none;
border-bottom: 3px solid transparent;
@ -106,33 +101,22 @@
margin-right: 0.5rem;
transition: all 0.2s;
}
/* Active Link */
.nav-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;
}
/* Right Column Tabs */
.right-column-tabs {
padding: 0;
margin-bottom: 0;
border-bottom: 1px solid var(--kaauh-border);
}
.right-column-tabs .nav-link.active {
background-color: white;
color: var(--kaauh-teal-dark);
border-bottom: 3px solid var(--kaauh-teal);
border-right-color: transparent;
margin-bottom: -1px;
border-right-color: transparent !important;
margin-bottom: -1px;
}
/* Main Action Button Style */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
.btn-main-action, .btn-main-action:hover, .btn-main-action:active {
background-color: var(--kaauh-teal) !important;
border-color: var(--kaauh-teal) !important;
color: white !important;
font-weight: 600;
padding: 0.6rem 1.2rem;
transition: all 0.2s ease;
@ -142,92 +126,22 @@
justify-content: center;
text-align: center;
}
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
.btn-outline-secondary { /* Apply primary colors for 'outline-secondary' used as theme buttons */
color: var(--kaauh-teal-dark) !important;
border-color: var(--kaauh-teal) !important;
}
/* Applicant stats */
.applicant-stats .stat-item {
padding: 0.75rem;
text-align: center;
border-radius: 0.5rem;
background-color: #f8f9fa;
border: 1px solid var(--kaauh-border);
.btn-outline-light { /* Fixing text color for the edit status button */
color: white !important;
border-color: white !important;
}
.applicant-stats .stat-item div:first-child {
font-size: 1.6rem;
font-weight: 700;
.btn-outline-light .text-primary { /* Override the edit icon's custom primary text class */
color: white !important;
}
.applicant-stats .stat-item small {
font-size: 0.8rem;
}
/* Specific styling for the deadline box */
.deadline-box {
padding: 0.75rem;
border-radius: 0.5rem;
border: 1px solid;
}
/* Enhanced styling for the Job Avg. Score card */
.stats .card:first-child { /* Targets the first card in the .stats div (Job Avg. Score) */
border-left: 5px solid var(--kaauh-teal); /* A distinctive left border */
background: linear-gradient(to right bottom, rgba(0, 99, 110, 0.05), rgba(255, 255, 255, 0));
}
.stats .card:first-child .card-header {
background-color: rgba(0, 99, 110, 0.1); /* Subtle background for header */
border-bottom: 2px solid var(--kaauh-teal); /* Emphasized border */
border-left: none; /* Remove inherited border from parent if any */
border-top: none;
border-right: none;
}
.stats .card:first-child .card-header h3 {
color: var(--kaauh-teal); /* Match the theme color */
font-weight: 700;
font-size: 1.1rem; /* Slightly larger header */
margin-bottom: 0;
}
.stats .card:first-child .stat-icon {
color: var(--kaauh-teal); /* Match the theme color */
font-size: 1.2rem; /* Slightly larger icon */
vertical-align: middle;
}
.stats .card:first-child .stat-value {
font-size: 2.5rem; /* Larger, more prominent value */
font-weight: 700;
color: var(--kaauh-teal); /* Theme color for the value */
text-align: center;
margin: 1rem 0; /* Spacing around the value */
text-shadow: 0 1px 2px rgba(0,0,0,0.05); /* Subtle text shadow for depth */
}
.stats .card:first-child .stat-caption {
text-align: center;
color: #6c757d; /* Muted color for caption */
font-size: 0.9rem;
font-weight: 500;
margin-top: 0.5rem;
border-top: 1px solid rgba(0, 99, 110, 0.2); /* Subtle top border for caption area */
padding-top: 0.5rem;
}
/* Custom CSS for simplified stat card (from previous answer) */
.stats-grid .kpi-card {
.kpi-card {
border-left: 4px solid var(--kaauh-teal);
background-color: #f0faff;
}
.stats-grid .card-body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style>
{% endblock %}
@ -238,12 +152,12 @@
<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>
<li class="breadcrumb-item active" aria-current="page">Job Detail</li>
</ol>
</nav>
<div class="row g-4">
{# LEFT COLUMN: JOB DETAILS WITH TABS #}
{# LEFT COLUMN: JOB DETAILS (NO TABS) #}
<div class="col-lg-7">
<div class="card shadow-sm no-hover">
@ -252,276 +166,136 @@
<div>
<h2 class="mb-1">{{ job.title }}</h2>
<small class="text-light">{% trans "JOB ID: "%}{{ job.internal_job_id }}</small>
{# Deadline #}
{% if job.application_deadline %}
<div class="text-light mt-1">
<i class="fas fa-calendar-times me-2"></i>
<strong>{% trans "Deadline:" %}</strong> <span class="text-warning fw-bold">{{ job.application_deadline }}</span>
</div>
{% endif %}
</div>
<div class="d-flex align-items-center gap-2">
<span class="badge status-badge">
{# Corrected status badge logic to close the span correctly #}
{% if job.status == "ACTIVE" %}
<span class="badge bg-success status-badge">
{% elif job.status == "DRAFT" %}
<span class="badge bg-secondary status-badge">
{% elif job.status == "CLOSED" %}
<span class="badge bg-warning status-badge">
{% elif job.status == "CANCELLED" %}
<span class="badge bg-danger status-badge">
{% elif job.status == "ARCHIVED" %}
<span class="badge bg-secondary status-badge">
{% else %}
<span class="badge bg-secondary status-badge">
{% endif %}
{{ job.get_status_display }}
<div class="d-flex align-items-center gap-2 mt-2 mt-md-0">
{# Status badge #}
<div class="d-flex align-items-center">
<span class="status-badge
{% if job.status == "ACTIVE" %}bg-success
{% elif job.status == "DRAFT" %}bg-secondary
{% elif job.status == "CLOSED" %}bg-warning
{% elif job.status == "CANCELLED" %}bg-danger
{% elif job.status == "ARCHIVED" %}bg-secondary
{% else %}bg-secondary{% endif %}">
{{ job.get_status_display }}
</span>
<button type="button" class="btn btn-outline-light btn-sm ms-2" data-bs-toggle="modal" data-bs-target="#editStatusModal">
<i class="fas fa-edit text-primary"></i>
<i class="fas fa-edit"></i>
</button>
</div>
{# Share Public Link Button #}
<button
type="button"
class="btn btn-main-action btn-sm"
id="copyJobLinkButton"
data-url="{{ job.application_url }}">
<i class="fas fa-link"></i>
{% trans "Share Public Link" %}
</button>
<span id="copyFeedback" class="text-success ms-2 small" style="display:none;">
{% trans "Copied!" %}
</span>
</div>
</div>
{# LEFT TABS NAVIGATION #}
<ul class="nav nav-tabs" id="jobTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="details-tab" data-bs-toggle="tab" data-bs-target="#details" type="button" role="tab" aria-controls="details" aria-selected="true">
<i class="fas fa-info-circle me-1"></i> {% trans "Core Details" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="description-tab" data-bs-toggle="tab" data-bs-target="#description" type="button" role="tab" aria-controls="description" aria-selected="false">
<i class="fas fa-file-alt me-1"></i> {% trans "Description & Requirements" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="kpis-tab" data-bs-toggle="tab" data-bs-target="#kpis" type="button" role="tab" aria-controls="kpis" aria-selected="false">
<i class="fas fa-chart-line me-1"></i> {% trans "Application KPIs" %}
</button>
</li>
</ul>
{# CONTENT: CORE DETAILS (No Tabs) #}
<div class="card-body">
<div class="tab-content" id="jobTabsContent">
{# TAB 1 CONTENT: CORE DETAILS #}
<div class="tab-pane fade show active" id="details" role="tabpanel" aria-labelledby="details-tab">
<h5 class="text-muted mb-3">{% trans "Administrative & Location" %}</h5>
<div class="row g-3 mb-4 border-bottom pb-3 small text-secondary">
<div class="col-md-6">
<i class="fas fa-building me-2 text-primary"></i> <strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }}
</div>
<div class="col-md-6">
<i class="fas fa-hashtag me-2 text-primary"></i> <strong>{% trans "Position No:" %}</strong> {{ job.position_number|default:"N/A" }}
</div>
<div class="col-md-6">
<i class="fas fa-briefcase me-2 text-primary"></i> <strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
</div>
<div class="col-md-6">
<i class="fas fa-map-pin me-2 text-primary"></i> <strong>{% trans "Workplace:" %}</strong> {{ job.get_workplace_type_display }}
</div>
<div class="col-md-6">
<i class="fas fa-globe me-2 text-primary"></i> <strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
</div>
<div class="col-md-6">
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Created By:" %}</strong> {{ job.created_by|default:"N/A" }}
</div>
<div class="col-md-6">
<i class="fas fa-plus me-2 text-primary"></i> <strong>{% trans "Created At:" %}</strong> {{ job.created_at|default:"N/A" }}
</div>
<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-4">
<button
type="button"
class="btn btn-main-action btn-sm"
id="copyJobLinkButton"
data-url="{{ job.application_url }}">
{# Replaced bulky SVG with simpler Font Awesome icon #}
<i class="fas fa-link"></i>
{% trans "Share 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">
{% if job.salary_range %}
<div class="col-md-4">
<div class="deadline-box border-success">
<i class="fas fa-money-bill-wave me-2 text-success"></i>
<strong class="text-success">{% trans "Salary:" %}</strong> <span class="text-dark">{{ job.salary_range }}</span>
</div>
</div>
{% endif %}
{% if job.start_date %}
<div class="col-md-4">
<div class="deadline-box border-info">
<i class="far fa-calendar-alt me-2 text-info"></i>
<strong class="text-info">{% trans "Start Date:" %}</strong> <span class="text-dark">{{ job.start_date }}</span>
</div>
</div>
{% endif %}
{% if job.application_deadline %}
<div class="col-md-4">
<div class="deadline-box border-{% if job.is_expired %}danger{% else %}primary{% endif %} text-{% if job.is_expired %}danger{% else %}primary{% endif %}">
<i class="fas fa-calendar-times me-2"></i>
<strong>{% trans "Deadline:" %}</strong> <span class="text-dark">{{ job.application_deadline }}</span>
{% if job.is_expired %}
<span class="badge bg-danger ms-1">{% trans "EXPIRED" %}</span>
{% endif %}
</div>
</div>
{% endif %}
</div>
<h5 class="text-muted mb-3">{% trans "Administrative & Location" %}
<a href="{% url 'job_update' job.slug %}" class="btn btn-main-action btn-sm"><li class="fa fa-edit"></li>{% trans "Edit JOb" %}</a>
</h5>
<div class="row g-3 mb-4 border-bottom pb-3 small text-secondary">
<div class="col-md-6">
<i class="fas fa-building me-2 text-primary"></i> <strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }}
</div>
{# TAB 2 CONTENT: DESCRIPTION & REQUIREMENTS #}
<div class="tab-pane fade" id="description" role="tabpanel" aria-labelledby="description-tab">
{% if job.description %}
<div class="mb-4">
<h5>{% trans "Job Description" %}</h5>
<div class="text-secondary">{{ job.description|safe }}</div>
</div>
{% endif %}
{% if job.qualifications %}
<div class="mb-4">
<h5>{% trans "Required Qualifications" %}</h5>
<div class="text-secondary">{{ job.qualifications|safe }}</div>
</div>
{% endif %}
{% if job.benefits %}
<div class="mb-4">
<h5>{% trans "Benefits" %}</h5>
<div class="text-secondary">{{ job.benefits|safe}}</div>
</div>
{% endif %}
{% if job.application_instructions %}
<div class="mb-4">
<h5>{% trans "Application Instructions" %}</h5>
<div class="text-secondary">{{ job.application_instructions|safe }}</div>
</div>
{% endif %}
<div class="col-md-6">
<i class="fas fa-hashtag me-2 text-primary"></i> <strong>{% trans "Position No:" %}</strong> {{ job.position_number|default:"N/A" }}
</div>
{# TAB 3 CONTENT: APPLICATION KPIS #}
<div class="tab-pane fade" id="kpis" role="tabpanel" aria-labelledby="kpis-tab">
<div class="row g-3 stats-grid">
{# 1. Job Avg. Score #}
<div class="col-6 col-md-3">
<div class="card text-center h-100 kpi-card">
<div class="card-body p-2">
<i class="fas fa-star text-primary mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-primary fw-bold">{{ avg_match_score|floatformat:1 }}</div>
<small class="text-muted d-block">{% trans "Avg. AI Score" %}</small>
</div>
</div>
</div>
{# 2. High Potential Count #}
<div class="col-6 col-md-3">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-trophy text-success mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-success fw-bold">{{ high_potential_count }}</div>
<small class="text-muted d-block">{% trans "High Potential" %}</small>
</div>
</div>
</div>
{# 3. Avg. Time to Interview #}
<div class="col-6 col-md-3">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-calendar-alt text-info mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-info fw-bold">{{ avg_t2i_days|floatformat:1 }}d</div>
<small class="text-muted d-block">{% trans "Time to Interview" %}</small>
</div>
</div>
</div>
{# 4. Avg. Exam Review Time #}
<div class="col-6 col-md-3">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-hourglass-half text-secondary mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-secondary fw-bold">{{ avg_t_in_exam_days|floatformat:1 }}d</div>
<small class="text-muted d-block">{% trans "Avg. Exam Review" %}</small>
</div>
</div>
</div>
</div>
<p class="text-end text-muted small mt-3 me-2">
<i class="fas fa-info-circle me-1"></i> {% trans "KPIs based on completed applicant data." %}
</p>
<div class="col-md-6">
<i class="fas fa-briefcase me-2 text-primary"></i> <strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
</div>
<div class="col-md-6">
<i class="fas fa-map-pin me-2 text-primary"></i> <strong>{% trans "Workplace:" %}</strong> {{ job.get_workplace_type_display }}
</div>
<div class="col-md-6">
<i class="fas fa-globe me-2 text-primary"></i> <strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
</div>
<div class="col-md-6">
<i class="fa-solid fa-money-bill me-2 text-primary"></i> <strong>{% trans "Salary:" %}</strong> {{ job.salary_range |default:"N/A" }}
</div>
<div class="col-md-6">
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Created By:" %}</strong> {{ job.created_by|default:"N/A" }}
</div>
<div class="col-md-6">
<i class="fas fa-plus me-2 text-primary"></i> <strong>{% trans "Created At:" %}</strong> {{ job.created_at|default:"N/A" }}
</div>
<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>
{# Description Blocks (Main Content) #}
{% if job.description %}
<div class="mb-4">
<h5>{% trans "Job Description" %}</h5>
<div class="text-secondary">{{ job.description|safe }}</div>
</div>
{% endif %}
{% if job.qualifications %}
<div class="mb-4">
<h5>{% trans "Required Qualifications" %}</h5>
<div class="text-secondary">{{ job.qualifications|safe }}</div>
</div>
{% endif %}
{% if job.benefits %}
<div class="mb-4">
<h5>{% trans "Benefits" %}</h5>
<div class="text-secondary">{{ job.benefits|safe}}</div>
</div>
{% endif %}
{% if job.application_instructions %}
<div class="mb-4">
<h5>{% trans "Application Instructions" %}</h5>
<div class="text-secondary">{{ job.application_instructions|safe }}</div>
</div>
{% endif %}
</div>
{# FOOTER ACTIONS #}
<div class="card-footer d-flex flex-wrap gap-2">
<a href="{% url 'job_update' job.slug %}" class="btn btn-main-action">
<i class="fas fa-edit"></i> {% trans "Edit Job" %}
</a>
<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 #}
{# RIGHT COLUMN: TABBED CARDS #}
<div class="col-lg-5">
{# New Card for Candidate Category Chart #}
<div class="card shadow-sm no-hover mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-chart-pie me-2 text-primary"></i>
{% trans "Candidate Categories & Scores" %}
</h6>
</div>
<div class="card-body p-4">
<div style="height: 300px;">
<canvas id="jobCategoryMatchChart"></canvas>
</div>
</div>
</div>
{# REMOVED: Standalone Applicant Tracking Card (It is now in a tab) #}
<div class="card shadow-sm no-hover">
{# RIGHT TABS NAVIGATION #}
<ul class="nav nav-tabs right-column-tabs" id="rightJobTabs" role="tablist">
<ul class="nav nav-tabs" id="rightJobTabs" role="tablist">
<li class="nav-item flex-fill" role="presentation">
<button class="nav-link active" id="applicants-tab" data-bs-toggle="tab" data-bs-target="#applicants-pane" type="button" role="tab" aria-controls="applicants-pane" aria-selected="true">
<i class="fas fa-users me-1 text-primary"></i> {% trans "Applicants" %}
<i class="fas fa-users me-1"></i> {% trans "Applicants" %}
</button>
</li>
<li class="nav-item flex-fill" role="presentation">
<button class="nav-link" id="tracking-tab" data-bs-toggle="tab" data-bs-target="#tracking-pane" type="button" role="tab" aria-controls="tracking-pane" aria-selected="false">
<i class="fas fa-project-diagram me-1 text-primary"></i> {% trans "Tracking" %}
<i class="fas fa-project-diagram me-1"></i> {% trans "Tracking" %}
</button>
</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 "Form Template" %}
<i class="fas fa-cogs me-1"></i> {% trans "Form Template" %}
</button>
</li>
<li class="nav-item flex-fill" role="presentation">
@ -531,70 +305,70 @@
</li>
</ul>
<div class="tab-content mx-2 my-3" id="rightJobTabsContent">
<div class="tab-content p-3" id="rightJobTabsContent">
{# TAB 1: APPLICANTS CONTENT #}
<div class="tab-pane fade show active" id="applicants-pane" role="tabpanel" aria-labelledby="applicants-tab">
<h5 class="mb-3">{% trans "Total Applicants" %} (<span id="total_candidates">{{ total_applicants }}</span>)</h5>
<div class="d-grid gap-4">
<div class="d-grid gap-3">
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
<i class="fas fa-user-plus"></i> {% trans "Create Applicant" %}
<i class="fas fa-user-plus me-1"></i> {% trans "Create Applicant" %}
</a>
<a href="{% url 'candidate_screening_view' job.slug %}" class="btn btn-main-action">
<i class="fas fa-layer-group"></i> {% trans "Manage Applicants" %}
<i class="fas fa-layer-group me-1"></i> {% trans "Manage Applicants" %}
</a>
</div>
</div>
{# NEW TAB 2: APPLICANT TRACKING CONTENT #}
{# TAB 2: TRACKING CONTENT #}
<div class="tab-pane fade" id="tracking-pane" role="tabpanel" aria-labelledby="tracking-tab">
<h5 class="mb-3"><i class="fas fa-project-diagram me-2 text-primary"></i>{% trans "Pipeline Stages" %}</h5>
<h5 class="mb-3"><i class="fas fa-project-diagram me-2 text-primary"></i>{% trans "Applicant Stages" %}</h5>
{% include 'jobs/partials/applicant_tracking.html' %}
<p class="text-muted small mt-3">{% trans "View the number of candidates currently in each stage of the hiring pipeline." %}</p>
<p class="text-muted small">
{% trans "The applicant tracking flow is defined by the attached Form Template. View the Form Template tab to manage stages and fields." %}
</p>
{# Placeholder for stage tracker component #}
</div>
{# TAB 3: MANAGEMENT (Form Template) CONTENT #}
<div class="tab-pane fade" id="manage-pane" role="tabpanel" aria-labelledby="manage-tab">
<h5 class="mb-3"><i class="fas fa-clipboard-list me-2 text-primary"></i>{% trans "Form Management" %}</h5>
<div class="d-grid gap-2">
<div class="d-grid gap-3">
<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" %}
<i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %}
</a>
{% else %}
<a href="{% url 'form_wizard' job.form_template.pk %}" class="btn btn-outline-secondary">
<a href="{% url 'application_submit_form' job.form_template.pk %}" class="btn btn-outline-secondary w-100">
<i class="fas fa-list-alt me-1"></i> {% trans "View Form Template" %}
</a>
{% endif %}
</div>
</div>
{# TAB 4: LINKEDIN INTEGRATION CONTENT #}
<div class="tab-pane fade" id="linkedin-pane" role="tabpanel" aria-labelledby="linkedin-tab">
<h5 class="mb-3"><i class="fab fa-linkedin me-2 text-info"></i>{% trans "LinkedIn Integration" %}</h5>
<div class="mb-4">
<div class="d-grid gap-3">
{% if job.posted_to_linkedin %}
<div class="alert alert-success p-2 mb-3 small">
<div class="alert alert-success p-2 small mb-0">
<i class="fas fa-check-circle me-1"></i> {% trans "Posted successfully!" %}
</div>
{% if job.linkedin_post_url %}
<a href="{{ job.linkedin_post_url }}" target="_blank" class="btn btn-main-action w-100 mb-2">
<a href="{{ job.linkedin_post_url }}" target="_blank" class="btn btn-outline-secondary">
<i class="fab fa-linkedin me-1"></i> {% trans "View on LinkedIn" %}
</a>
{% endif %}
<small class="text-muted d-block text-center mb-3">
<small class="text-muted d-block text-center">
{% trans "Posted on:" %} {{ job.linkedin_posted_at|date:"M d, Y" }}
</small>
{% else %}
<p class="text-muted small mb-3">{% trans "This job has not been posted to LinkedIn yet." %}</p>
<p class="text-muted small mb-0">{% trans "This job has not been posted to LinkedIn yet." %}</p>
{% endif %}
<form method="post" action="{% url 'post_to_linkedin' job.slug %}" class="mt-2">
@ -605,9 +379,13 @@
{% if job.posted_to_linkedin %}{% trans "Re-post to LinkedIn" %}{% else %}{% trans "Post to LinkedIn" %}{% endif %}
</button>
</form>
<button type="button" class="btn btn-outline-secondary w-100" data-bs-toggle="modal" data-bs-target="#myModalForm">
<i class="fas fa-image me-1"></i> {% trans "Upload Image for Post" %}
</button>
{% if not request.session.linkedin_authenticated %}
<small class="text-muted d-block mt-2 text-center">
<small class="text-muted d-block text-center">
{% trans "You need to" %} <a href="{% url 'linkedin_login' %}">{% trans "authenticate with LinkedIn" %}</a> {% trans "first." %}
</small>
{% endif %}
@ -623,6 +401,82 @@
</div>
</div>
{# Card 2: Candidate Category Chart #}
<div class="card shadow-sm no-hover mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-chart-pie me-2 text-primary"></i>
{% trans "Candidate Categories & Scores" %}
</h6>
</div>
<div class="card-body p-4">
<div style="height: 300px;">
<canvas id="jobCategoryMatchChart"></canvas>
</div>
</div>
</div>
{# Card 3: KPIs #}
<div class="card shadow-sm no-hover mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-info-circle me-1 text-primary"></i>
{% trans "Key Performance Indicators" %}
</h6>
</div>
<div class="card-body p-4">
<div class="row g-3 stats-grid">
{# 1. Job Avg. Score #}
<div class="col-6">
<div class="card text-center h-100 kpi-card">
<div class="card-body p-2">
<i class="fas fa-star text-primary mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-primary fw-bold">{{ avg_match_score|floatformat:1 }}</div>
<small class="text-muted d-block">{% trans "Avg. AI Score" %}</small>
</div>
</div>
</div>
{# 2. High Potential Count #}
<div class="col-6">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-trophy text-success mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-success fw-bold">{{ high_potential_count }}</div>
<small class="text-muted d-block">{% trans "High Potential" %}</small>
</div>
</div>
</div>
{# 3. Avg. Time to Interview #}
<div class="col-6">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-calendar-alt text-info mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-info fw-bold">{{ avg_t2i_days|floatformat:1 }}d</div>
<small class="text-muted d-block">{% trans "Time to Interview" %}</small>
</div>
</div>
</div>
{# 4. Avg. Exam Review Time #}
<div class="col-6">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-hourglass-half text-secondary mb-1 d-block" style="font-size: 1.2rem;"></i>
<div class="h4 mb-0 text-secondary fw-bold">{{ avg_t_in_exam_days|floatformat:1 }}d</div>
<small class="text-muted d-block">{% trans "Avg. Exam Review" %}</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -311,7 +311,7 @@
<td class="text-center">
<div class="btn-group btn-group-sm" role="group">
{% if job.form_template %}
<a href="{% url 'form_wizard' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Preview' %}">
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Preview' %}">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
@ -354,7 +354,7 @@
<li><i class="fas fa-users text-primary-theme me-2"></i>{% trans "Applicants" %}:{{ job.metrics.applied|default:"0" }}</li>
<li><i class="fas fa-clipboard-check text-success me-2"></i> {% trans "Offers Made" %}: {{ job.metrics.offer|default:"0" }}</li>
<li><i class="fas fa-file-alt text-info me-2"></i> {% trans "Form" %}:{% if job.form_template %}
<a href="{% url 'form_wizard' job.form_template.pk %}" class="text-info">{{ job.form_template.name }}</a>
<a href="{% url 'application_submit_form' job.form_template.pk %}" class="text-info">{{ job.form_template.name }}</a>
{% else %}
{% trans "N/A" %}
{% endif %}

View File

@ -316,7 +316,7 @@
<tbody>
{% for meeting in meetings %}
<tr>
<td><strong class="text-primary">{{ meeting.topic }}</strong></td>
<td><strong class="text-primary"><a href="{% url 'meeting_details' meeting.slug %}" class="text-decoration-none text-secondary">{{ meeting.topic }}<a></strong></td>
<td>
{% if meeting.interview %}
<a class="text-primary text-decoration-none" href="{% url 'candidate_detail' meeting.interview.candidate.slug %}">{{ meeting.interview.candidate.name }} <i class="fas fa-link"></i></a>

View File

@ -3,7 +3,7 @@
{% block customCSS %}
<style>
/* -------------------------------------------------------------------------- */
/* KAAT-S Redesign CSS - Optimized Compact Detail View (Settings Removed) */
/* KAAT-S Redesign CSS - Optimized Compact Detail View (Comments Left) */
/* -------------------------------------------------------------------------- */
:root {
@ -16,6 +16,11 @@
--kaauh-gray-light: #f8f9fa; /* Card Header/Footer Background */
--kaauh-success: #198754; /* Success Green */
--kaauh-danger: #dc3545; /* Danger Red */
/* New CRM/ATS Specific Colors */
--kaauh-link: #007bff; /* Standard Blue Link */
--kaauh-link-hover: #0056b3;
--kaauh-accent-bg: #fff3cd; /* Light Yellow for Attention/Context */
--kaauh-accent-text: #664d03; /* Dark Yellow Text */
}
body {
@ -85,10 +90,6 @@ body {
padding-bottom: 0.5rem;
}
.detail-row-group {
padding: 0;
}
.detail-row {
display: flex;
justify-content: space-between;
@ -114,17 +115,34 @@ body {
flex-basis: 55%;
}
/* --- CRM ASSOCIATED RECORD STYLING --- */
.associated-record-card {
background-color: var(--kaauh-accent-bg);
color: var(--kaauh-accent-text);
border: 1px solid var(--kaauh-accent-text);
}
.associated-record-card a {
color: var(--kaauh-link);
font-weight: 700;
}
.associated-record-card a:hover {
color: var(--kaauh-link-hover);
}
/* ------------------ Join Info & Copy Button ------------------ */
.join-info-card {
border-left: 5px solid var(--kaauh-teal); /* Highlight join info */
}
/* Consolidated primary button style */
.btn-primary {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
font-weight: 600;
padding: 0.6rem 1.25rem;
border-radius: 6px;
transition: background-color 0.2s;
}
.btn-primary:hover {
background-color: var(--kaauh-teal-dark);
@ -143,6 +161,11 @@ body {
.btn-copy {
padding: 0.5rem 0.75rem;
background-color: var(--kaauh-teal-dark);
border: none;
color: white; /* Ensure copy button icon is white */
}
.btn-copy:hover {
background-color: var(--kaauh-teal);
}
/* ------------------ Footer & Actions ------------------ */
@ -150,32 +173,35 @@ body {
.action-bar-footer {
border-top: 1px solid var(--kaauh-border);
padding: 1rem 1.5rem;
gap: 0.75rem;
background-color: var(--kaauh-gray-light);
border-radius: 0 0 12px 12px;
/* Explicitly use flex for layout control */
display: flex;
justify-content: space-between; /* Separate the left/right groups */
align-items: center;
}
.btn-footer-action {
font-weight: 600;
padding: 0.5rem 1rem;
/* Made buttons smaller and consistent */
padding: 0.4rem 0.8rem;
border-radius: 6px;
font-size: 0.9rem;
font-size: 0.85rem;
}
/* ------------------ Comments Section ------------------ */
/* --- Comment Card Header Style --- */
#comments-card .card-header {
background-color: var(--kaauh-teal-light);
background-color: white;
color: var(--kaauh-teal-dark);
padding: 1rem 1.5rem;
font-weight: 600;
border-radius: 12px 12px 0 0;
}
/* Comment card body/item styling is kept compact */
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="container-fluid py-4">
{# --- TOP BAR / BACK BUTTON --- #}
<div class="d-flex justify-content-between align-items-center mb-3">
@ -185,104 +211,202 @@ body {
</div>
<div class="row g-4">
{# --- LEFT COLUMN (MAIN DETAILS) --- #}
<div class="col-lg-6">
<div class="card no-hover h-100">
{# --- CONSOLIDATED HEADER --- #}
<div class="main-title-card">
<div class="d-flex justify-content-between align-items-start">
<div class="card-header-title-group">
<h1 class="mb-1">
<svg class="heroicon me-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0 8.268-2.943-9.542-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
{{ meeting.topic }}
</h1>
<div class="d-flex align-items-center gap-3">
<span class="status-badge bg-{{ meeting.status }}">
{{ meeting.status|title }}
</span>
{% if meeting.interview %}
<span class="text-muted small">
{% trans "Candidate" %}: <a class="text-primary-theme fw-bold text-decoration-none" href="{% url 'candidate_detail' meeting.interview.candidate.slug %}">{{ meeting.interview.candidate.name }} </a>
</span>
{% endif %}
</div>
</div>
</div>
</div>
{# --- MAIN DETAIL BODY --- #}
{# --- LEFT COLUMN (COMMENTS & INTERNAL CONTEXT) - Takes 50% of the screen #}
<div class="col-lg-6 d-flex flex-column">
{# --- 1. INTERNAL NOTES / DESCRIPTION CARD (New CRM Feature) --- #}
{% if meeting.description %}
<div class="card no-hover mb-4 flex-shrink-0">
<div class="card-body detail-section">
<h2>{% trans "Core Details" %}</h2>
<div class="detail-row-group">
<div class="detail-row"><div class="detail-label">{% trans "Meeting ID" %}:</div><div class="detail-value">{{ meeting.meeting_id }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Start Time" %}:</div><div class="detail-value">{{ meeting.start_time|date:"M d, Y H:i" }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Duration" %}:</div><div class="detail-value">{{ meeting.duration }} {% trans "minutes" %}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Timezone" %}:</div><div class="detail-value">{{ meeting.timezone|default:"UTC" }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Host Email" %}:</div><div class="detail-value">{{ meeting.host_email|default:"N/A" }}</div></div>
<h2 class="d-flex align-items-center"><i class="fas fa-clipboard-list me-2"></i> {% trans "Internal Context" %}</h2>
<p class="text-muted small">{% trans "Meeting agenda, purpose, or interview details for internal team use." %}</p>
<div class="p-3 bg-light rounded border">
<p class="mb-0">{{ meeting.description|safe }}</p>
</div>
</div>
</div>
{% endif %}
{# --- ACTION BAR AT THE BOTTOM OF THE MAIN CARD --- #}
<div class="card-footer action-bar-footer d-flex justify-content-end">
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary btn-footer-action">
<i class="fas fa-edit me-1"></i> {% trans "Update" %}
</a>
{% if meeting.zoom_gateway_response %}
<button type="button" class="btn btn-secondary btn-footer-action" onclick="toggleGateway()">
<i class="fas fa-code me-1"></i> {% trans "API Response" %}
{# --- 2. Comments Section (Now in the Left Column) --- #}
<div class="card no-hover flex-grow-1" id="comments-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-comments me-2"></i>
{% trans "Comments" %} ({{ meeting.comments.count }})
</h5>
{% if user.is_authenticated %}
<button type="button" class="btn btn-primary btn-sm"
hx-get="{% url 'add_meeting_comment' meeting.slug %}"
hx-target="#comment-section"
>
<i class="fas fa-plus me-1"></i> {% trans "Add Comment" %}
</button>
{% endif %}
<button type="button" class="btn btn-danger btn-footer-action" title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal"
hx-post="{% url 'delete_meeting' meeting.slug %}"
hx-target="#deleteModalBody"
hx-swap="outerHTML"
data-item-name="{{ meeting.topic }}">
<i class="fas fa-trash-alt me-1"></i>
Delete
</button>
</div>
<div class="card-body overflow-auto">
<div id="comment-section">
{% if meeting.comments.all %}
{% for comment in meeting.comments.all|dictsortreversed:"created_at" %}
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong class="me-2">{{ comment.author.get_full_name|default:comment.author.username }}</strong>
{% if comment.author != user %}
<span class="badge bg-secondary ms-1">{% trans "Comment" %}</span>
{% endif %}
</div>
<small class="text-muted">{{ comment.created_at|date:"M d, Y P" }}</small>
</div>
<div class="card-body">
<p class="card-text">{{ comment.content|safe }}</p>
</div>
<div class="card-footer">
{% if comment.author == user or user.is_staff %}
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary"
hx-get="{% url 'edit_meeting_comment' meeting.slug comment.id %}"
hx-target="#comment-section"
title="{% trans 'Edit Comment' %}">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-outline-danger"
hx-get="{% url 'delete_meeting_comment' meeting.slug comment.id %}"
hx-target="#comment-section"
title="{% trans 'Delete Comment' %}">
<i class="fas fa-trash"></i>
</button>
</div>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<p class="text-muted">{% trans "No comments yet. Be the first to comment!" %}</p>
{% endif %}
</div>
</div>
</div>
</div>
{# --- RIGHT COLUMN (JOIN INFO) --- #}
{# --- RIGHT COLUMN (MAIN DETAILS & JOIN INFO) - Takes 50% of the screen #}
<div class="col-lg-6">
{% if meeting.join_url %}
<div class="card no-hover join-info-card detail-section h-100">
<div class="card-body">
<h2>{% trans "Join Information" %}</h2>
<div class="d-flex flex-column h-100">
<a href="{{ meeting.join_url }}" class="btn btn-primary w-100 mb-4" target="_blank">
<i class="fas fa-video me-1"></i> {% trans "Join Meeting Now" %}
</a>
<div class="join-url-container">
<div id="copy-message" style="opacity: 0;">{% trans "Copied!" %}</div>
<div class="join-url-display d-flex justify-content-between align-items-center">
<div class="text-truncate">
<strong>{% trans "Join URL" %}:</strong>
<span id="meeting-join-url">{{ meeting.join_url }}</span>
{# --- CRM ASSOCIATED RECORD CARD (Elevated Importance) --- #}
{% if meeting.interview %}
<div class="card associated-record-card mb-4 flex-shrink-0">
<div class="card-body pt-3 pb-3">
<div class="d-flex align-items-center">
<i class="fas fa-user-tag fa-2x me-3"></i>
<div>
<h6 class="mb-0 text-uppercase small fw-bold">{% trans "Associated Record" %}</h6>
<span class="fw-bold fs-5 me-2">
<a href="{% url 'candidate_detail' meeting.interview.candidate.slug %}" class="text-decoration-none">
{{ meeting.interview.candidate.name }}
</a>
</span>
<span class="badge bg-secondary-subtle text-secondary small fw-normal">{{ meeting.interview.job_position }}</span>
</div>
<button class="btn-copy ms-2" onclick="copyLink()" title="{% trans 'Copy URL' %}">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
{% if meeting.password %}
<div class="detail-row" style="border: none; padding-top: 1rem;">
<div class="detail-label" style="font-size: 1rem;">{% trans "Password" %}:</div>
<div class="detail-value fw-bolder text-danger">{{ meeting.password }}</div>
</div>
{% endif %}
{# --- 1. MAIN DETAILS CARD --- #}
<div class="card no-hover flex-grow-1 mb-4">
{# --- CONSOLIDATED HEADER --- #}
<div class="main-title-card">
<div class="d-flex justify-content-between align-items-start">
<div class="card-header-title-group">
<h1 class="mb-1">
<svg class="heroicon me-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0 8.268-2.943-9.542-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
{{ meeting.topic }}
</h1>
<div class="d-flex align-items-center gap-3">
<span class="status-badge bg-{{ meeting.status }}">
{{ meeting.status|title }}
</span>
</div>
</div>
{% endif %}
</div>
</div>
{# --- CONNECTION DETAIL BODY (Renamed from Core Details) --- #}
<div class="card-body detail-section">
<h2><i class="fas fa-calendar-alt me-2"></i> {% trans "Connection Details" %}</h2>
<div class="detail-row-group">
<div class="detail-row"><div class="detail-label">{% trans "Meeting ID" %}:</div><div class="detail-value">{{ meeting.meeting_id }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Start Time" %}:</div><div class="detail-value">{{ meeting.start_time|date:"M d, Y H:i" }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Duration" %}:</div><div class="detail-value">{{ meeting.duration }} {% trans "minutes" %}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Timezone" %}:</div><div class="detail-value">{{ meeting.timezone|default:"UTC" }}</div></div>
<div class="detail-row"><div class="detail-label">{% trans "Host Email" %}:</div><div class="detail-value">{{ meeting.host_email|default:"N/A" }}</div></div>
</div>
</div>
{# --- ACTION BAR AT THE BOTTOM OF THE MAIN CARD --- #}
<div class="card-footer action-bar-footer">
<div>
{# Placeholder for future left-aligned button #}
</div>
<div class="d-flex gap-2">
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary btn-footer-action">
<i class="fas fa-edit me-1"></i> {% trans "Update" %}
</a>
{% if meeting.zoom_gateway_response %}
<button type="button" class="btn btn-secondary btn-footer-action" onclick="toggleGateway()">
<i class="fas fa-code me-1"></i> {% trans "API Response" %}
</button>
{% endif %}
<button type="button" class="btn btn-danger btn-footer-action ms-3" title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal"
data-item-name="{{ meeting.topic }}">
<i class="fas fa-trash-alt me-1"></i>
Delete
</button>
</div>
</div>
</div>
{% endif %}
{# --- 2. JOIN INFO CARD (Separate from details, but in the same column) --- #}
{% if meeting.join_url %}
<div class="card no-hover join-info-card detail-section flex-shrink-0">
<div class="card-body">
<h2><i class="fas fa-link me-2"></i> {% trans "Join Information" %}</h2>
<a href="{{ meeting.join_url }}" class="btn btn-primary w-100 mb-4" target="_blank">
<i class="fas fa-video me-1"></i> {% trans "Join Meeting Now" %}
</a>
<div class="join-url-container">
{# Message should not be display: none; but opacity: 0; for smooth transition #}
<div id="copy-message" class="text-white rounded px-2 py-1 small fw-bold mb-2 text-center" style="opacity: 0; transition: opacity 0.3s; position: absolute; right: 0; top: -30px; background-color: var(--kaauh-success); z-index: 10;">{% trans "Copied!" %}</div>
<div class="join-url-display d-flex justify-content-between align-items-center position-relative">
<div class="text-truncate me-2">
<strong>{% trans "Join URL" %}:</strong>
<span id="meeting-join-url">{{ meeting.join_url }}</span>
</div>
<button class="btn-copy ms-2 flex-shrink-0" onclick="copyLink()" title="{% trans 'Copy URL' %}">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
{% if meeting.password %}
<div class="detail-row" style="border: none; padding-top: 1rem;">
<div class="detail-label" style="font-size: 1rem;">{% trans "Password" %}:</div>
<div class="detail-value fw-bolder text-danger">{{ meeting.password }}</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
</div>
@ -296,81 +420,22 @@ body {
</div>
{% endif %}
{# --- Comments Section (Full Width, below main content) --- #}
<div class="card no-hover mt-4" id="comments-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-comments me-2"></i>
{% trans "Comments" %} ({{ meeting.comments.count }})
</h5>
{% if user.is_authenticated %}
<button type="button" class="btn btn-primary btn-sm"
hx-get="{% url 'add_meeting_comment' meeting.slug %}"
hx-target="#comment-section"
>
<i class="fas fa-plus me-1"></i> {% trans "Add Comment" %}
</button>
{% endif %}
</div>
<div class="card-body">
<div id="comment-section">
{% if meeting.comments.all %}
{% for comment in meeting.comments.all|dictsortreversed:"created_at" %}
<div class="card mb-3">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<strong class="me-2">{{ comment.author.get_full_name|default:comment.author.username }}</strong>
{% if comment.author != user %}
<span class="badge bg-secondary ms-1">{% trans "Comment" %}</span>
{% endif %}
</div>
<small class="text-muted">{{ comment.created_at|date:"M d, Y P" }}</small>
</div>
<div class="card-body">
<p class="card-text">{{ comment.content|safe }}</p>
</div>
<div class="card-footer">
{% if comment.author == user or user.is_staff %}
<div class="btn-group btn-group-sm">
<button type="button" class="btn btn-outline-primary"
hx-get="{% url 'edit_meeting_comment' meeting.slug comment.id %}"
hx-target="#comment-section"
title="{% trans 'Edit Comment' %}">
<i class="fas fa-edit"></i>
</button>
<button type="button" class="btn btn-outline-danger"
hx-get="{% url 'delete_meeting_comment' meeting.slug comment.id %}"
hx-target="#comment-section"
title="{% trans 'Delete Comment' %}">
<i class="fas fa-trash"></i>
</button>
</div>
{% endif %}
</div>
</div>
{% endfor %}
{% else %}
<p class="text-muted">{% trans "No comments yet. Be the first to comment!" %}</p>
{% endif %}
</div>
</div>
</div>
</div>
{# MODALS (KEEP OUTSIDE OF THE MAIN LAYOUT ROWS) #}
{% comment %} {% include 'modals/delete_modal.html' with item_name="Meeting" delete_url_name='delete_meeting' %} {% endcomment %}
<div class="modal fade" id="commentModal" tabindex="-1" aria-labelledby="commentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="commentModalLabel">Add Comment</h5>
<h5 class="modal-title" id="commentModalLabel">Add Comment</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="commentModalBody">
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
@ -383,9 +448,10 @@ body {
}
}
// CopyLink function remains the same (as provided in the original code)
// CopyLink function implementation (slightly improved for message placement)
function copyLink() {
const urlElement = document.getElementById('meeting-join-url');
const displayContainer = urlElement.closest('.join-url-display');
const messageElement = document.getElementById('copy-message');
const textToCopy = urlElement.textContent || urlElement.innerText;
@ -396,6 +462,11 @@ body {
messageElement.style.backgroundColor = success ? 'var(--kaauh-success)' : 'var(--kaauh-danger)';
messageElement.style.opacity = '1';
// Position the message relative to the display container
const rect = displayContainer.getBoundingClientRect();
messageElement.style.left = (rect.width / 2) - (messageElement.offsetWidth / 2) + 'px';
messageElement.style.top = '-35px';
window.copyMessageTimeout = setTimeout(() => {
messageElement.style.opacity = '0';
}, 2000);

View File

@ -5,6 +5,7 @@
{% block customCSS %}
<style>
/* ... (Your existing CSS code is here) ... */
/* ================================================= */
/* THEME VARIABLES AND GLOBAL STYLES */
/* ================================================= */
@ -225,25 +226,14 @@
<i class="fas fa-id-card me-1"></i> {% trans "Contact & Job" %}
</button>
</li>
{% if candidate.resume %}
<li class="nav-item" role="presentation">
<button class="nav-link" id="resume-tab" data-bs-toggle="tab" data-bs-target="#resume-pane" type="button" role="tab" aria-controls="resume-pane" aria-selected="false">
<i class="fas fa-file-pdf me-1"></i> {% trans "Resume" %}
{# NEW TAB ADDED HERE #}
<button class="nav-link" id="timeline-tab" data-bs-toggle="tab" data-bs-target="#timeline-pane" type="button" role="tab" aria-controls="timeline-pane" aria-selected="false">
<i class="fas fa-route me-1"></i> {% trans "Journey Timeline" %}
</button>
</li>
{% endif %}
{% 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 "Resume Summary" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="analysis-tab" data-bs-toggle="tab" data-bs-target="#analysis-pane" type="button" role="tab" aria-controls="analysis-pane" aria-selected="false">
<i class="fas fa-brain me-1"></i> {% trans "AI Analysis" %}
</button>
</li>
{% endif %}
</ul>
<div class="card-body">
@ -290,35 +280,92 @@
</div>
{# 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>
<div class="d-flex align-items-center justify-content-between p-3 border rounded">
{% comment %} <div>
<p class="mb-1"><strong>{{ candidate.resume.name }}</strong></p>
<small class="text-muted">{{ candidate.resume.name|truncatechars:30 }}</small>
</div> {% endcomment %}
<div class="d-flex flex-column gap-2">
<div class="d-flex gap-2">
<a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
<i class="fas fa-eye me-1"></i>
{% trans "View Actual Resume" %}
</a>
<a href="{{ candidate.resume.url }}" download class="btn btn-main-action">
<i class="fas fa-download me-1"></i>
{% trans "Download Resume" %}
</a>
{# NEW TAB 3 CONTENT: CANDIDATE JOURNEY TIMELINE #}
<div class="tab-pane fade" id="timeline-pane" role="tabpanel" aria-labelledby="timeline-tab">
{# 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>
<a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
<i class="fas fa-file-alt me-1"></i>
{% trans "View Resume AI Overview" %}
</a>
<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>
{% if candidate.exam_date %}
<div class="timeline-item">
<div class="timeline-icon timeline-bg-applied"><i class="fas fa-clipboard-check"></i></div>
<div class="timeline-content">
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Exam" %}</p>
<small class="text-muted">
<i class="far fa-calendar-alt me-1"></i> {{ candidate.exam_date|date:"M d, Y" }}
<span class="ms-2">|</span>
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.exam_date|date:"h:i A" }}
</small>
</div>
</div>
{% endif %}
{% if candidate.interview_date %}
<div class="timeline-item">
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-comments"></i></div>
<div class="timeline-content">
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Interview" %}</p>
<small class="text-muted">
<i class="far fa-calendar-alt me-1"></i> {{ candidate.interview_date|date:"M d, Y" }}
<span class="ms-2">|</span>
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.interview_date|date:"h:i A" }}
</small>
</div>
</div>
{% endif %}
{% if candidate.offer_date %}
<div class="timeline-item">
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div>
<div class="timeline-content">
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
<small class="text-muted">
<i class="far fa-calendar-alt me-1"></i> {{ candidate.offer_date|date:"M d, Y" }}
<span class="ms-2">|</span>
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.offer_date|date:"h:i A" }}
</small>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endif %}
{# TAB 3 CONTENT: PARSED SUMMARY #}
{# TAB 4 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>
@ -328,13 +375,12 @@
</div>
{% endif %}
{# TAB 4 CONTENT: AI ANALYSIS #}
{# TAB 5 CONTENT: AI ANALYSIS #}
{% if candidate.is_resume_parsed %}
<div class="tab-pane fade" id="analysis-pane" role="tabpanel" aria-labelledby="analysis-tab">
<h5 class="text-primary mb-4">{% trans "AI Analysis Report" %}</h5>
<div class="border-start border-primary ps-3 pt-1 pb-1">
{% with analysis=candidate.ai_analysis_data %}
{# Match Score Card #}
<div class="mb-4 p-3 rounded" style="background-color: {% if analysis.match_score >= 70 %}rgba(40, 167, 69, 0.1){% elif analysis.match_score >= 40 %}rgba(255, 193, 7, 0.1){% else %}rgba(220, 53, 69, 0.1){% endif %}">
<div class="d-flex justify-content-between align-items-center mb-2">
@ -482,9 +528,9 @@
{# RIGHT COLUMN: ACTIONS AND CANDIDATE TIMELINE #}
<div class="col-lg-4">
{# 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">
@ -497,95 +543,37 @@
<a href="{% url 'candidate_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {% trans "Back to List" %}
</a>
{% if candidate.resume %}
<a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
<i class="fas fa-eye me-1"></i>
{% trans "View Actual Resume" %}
</a>
<a href="{{ candidate.resume.url }}" download class="btn btn-outline-primary">
<i class="fas fa-download me-1"></i>
{% trans "Download Resume" %}
</a>
<a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
<i class="fas fa-file-alt me-1"></i>
{% trans "View Resume AI Overview" %}
</a>
{% endif %}
</div>
</div>
{% endif %}
{# 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>
<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>
{% if candidate.exam_date %}
<div class="timeline-item">
<div class="timeline-icon timeline-bg-applied"><i class="fas fa-clipboard-check"></i></div>
<div class="timeline-content">
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Exam" %}</p>
<small class="text-muted">
<i class="far fa-calendar-alt me-1"></i> {{ candidate.exam_date|date:"M d, Y" }}
<span class="ms-2">|</span>
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.exam_date|date:"h:i A" }}
</small>
</div>
</div>
{% endif %}
{% if candidate.interview_date %}
<div class="timeline-item">
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-comments"></i></div>
<div class="timeline-content">
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Interview" %}</p>
<small class="text-muted">
<i class="far fa-calendar-alt me-1"></i> {{ candidate.interview_date|date:"M d, Y" }}
<span class="ms-2">|</span>
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.interview_date|date:"h:i A" }}
</small>
</div>
</div>
{% endif %}
{% if candidate.offer_date %}
<div class="timeline-item">
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div>
<div class="timeline-content">
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
<small class="text-muted">
<i class="far fa-calendar-alt me-1"></i> {{ candidate.offer_date|date:"M d, Y" }}
<span class="ms-2">|</span>
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.offer_date|date:"h:i A" }}
</small>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
{% include 'recruitment/candidate_resume_template.html' %}
{% if user.is_staff %}
{% include "recruitment/partials/stage_update_modal.html" with candidate=candidate form=stage_form %}
{% endif %}
{% endblock %}
{% endblock %}

View File

@ -2,7 +2,169 @@
{% load static i18n %}
{% block title %}Candidate Tier Management - {{ job.title }} - 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;
--kaauh-info: #17a2b8;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
/* Primary Color Overrides */
.text-primary-theme { color: var(--kaauh-teal) !important; }
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
/* 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;
}
/* Dedicated style for the filter block */
.filter-controls {
background-color: #f8f9fa;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
border: 1px solid var(--kaauh-border);
}
/* 2. Button Styling (Themed for Main Actions) */
.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);
}
/* Style for the Bulk Move button */
.btn-bulk-action {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
color: white;
font-weight: 500;
}
.btn-bulk-action:hover {
background-color: #00363e;
border-color: #00363e;
}
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
.candidate-table {
table-layout: fixed;
width: 100%;
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 1rem;
font-weight: 600;
color: var(--kaauh-teal-dark);
border-bottom: 2px solid var(--kaauh-teal);
font-size: 0.9rem;
vertical-align: middle;
}
.candidate-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--kaauh-border);
vertical-align: middle;
font-size: 0.9rem;
}
.candidate-table tbody tr:hover {
background-color: #f1f3f4;
}
.candidate-table thead th:nth-child(1) { width: 40px; }
.candidate-table thead th:nth-child(4) { width: 10%; }
.candidate-table thead th:nth-child(7) { width: 100px; }
.candidate-name {
font-weight: 600;
color: var(--kaauh-primary-text);
}
.candidate-details {
font-size: 0.8rem;
color: #6c757d;
}
/* 4. Badges and Statuses */
.ai-score-badge {
background-color: var(--kaauh-teal-dark) !important;
color: white;
font-weight: 700;
padding: 0.4em 0.8em;
border-radius: 0.4rem;
}
.status-badge {
font-size: 0.75rem;
padding: 0.3em 0.7em;
border-radius: 0.35rem;
font-weight: 700;
}
.bg-applicant { background-color: #6c757d !important; color: white; }
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
/* Stage Badges */
.stage-badge {
font-size: 0.75rem;
padding: 0.25rem 0.6rem;
border-radius: 0.3rem;
font-weight: 600;
display: inline-block;
margin-bottom: 0.2rem;
}
.stage-Applied { background-color: #e9ecef; color: #495057; }
.stage-Screening { background-color: var(--kaauh-info); color: white; }
.stage-Exam { background-color: var(--kaauh-warning); color: #856404; }
.stage-Interview { background-color: #17a2b8; color: white; }
.stage-Offer { background-color: var(--kaauh-success); color: white; }
/* Timeline specific container */
.applicant-tracking-timeline {
margin-bottom: 2rem;
}
/* --- CUSTOM HEIGHT OPTIMIZATION (MAKING INPUTS/BUTTONS SMALLER) --- */
.form-control-sm,
.btn-sm {
/* Reduce vertical padding even more than default Bootstrap 'sm' */
padding-top: 0.2rem !important;
padding-bottom: 0.2rem !important;
/* Ensure a consistent, small height for both */
height: 28px !important;
font-size: 0.8rem !important; /* Slightly smaller font */
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
@ -31,28 +193,39 @@
</h2>
<div class="kaauh-card shadow-sm p-3">
{% if candidates %}
<div class="bulk-action-bar">
{% if candidates %}
<div class="bulk-action-bar p-3 bg-light border-bottom">
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
{% csrf_token %}
<label for="update_status" class="form-label small mb-0 fw-bold">{% trans "Move Selected To:" %}</label>
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: auto;">
<option selected>
----------
</option>
<option value="Interview">
{% trans "Interview Stage" %}
</option>
<option value="Applied">
{% trans "Screening Stage" %}
</option>
</select>
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i>
</button>
{# Using d-flex for horizontal alignment and align-items-end to align items to the bottom baseline #}
<div class="d-flex align-items-end gap-3">
{# Select Input Group #}
<div>
<label for="update_status" class="form-label small mb-1 fw-bold">{% trans "Move Selected To:" %}</label>
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="min-width: 150px;">
<option selected>
----------
</option>
<option value="Interview">
{% trans "Interview Stage" %}
</option>
<option value="Applied">
{% trans "Screening Stage" %}
</option>
</select>
</div>
{# Button #}
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i> {% trans "Update Status" %}
</button>
</div>
</form>
</div>
{% endif %}
</div>
{% endif %}
<div class="table-responsive">
<form id="candidate-form" method="post">
{% csrf_token %}

View File

@ -2,7 +2,170 @@
{% load static i18n %}
{% block title %}- {{ job.title }} - 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;
--kaauh-info: #17a2b8;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
/* Primary Color Overrides */
.text-primary-theme { color: var(--kaauh-teal) !important; }
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
/* 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;
}
/* Dedicated style for the filter block */
.filter-controls {
background-color: #f8f9fa;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
border: 1px solid var(--kaauh-border);
}
/* 2. Button Styling (Themed for Main Actions) */
.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);
}
/* Style for the Bulk Move button */
.btn-bulk-action {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
color: white;
font-weight: 500;
}
.btn-bulk-action:hover {
background-color: #00363e;
border-color: #00363e;
}
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
.candidate-table {
table-layout: fixed;
width: 100%;
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 1rem;
font-weight: 600;
color: var(--kaauh-teal-dark);
border-bottom: 2px solid var(--kaauh-teal);
font-size: 0.9rem;
vertical-align: middle;
}
.candidate-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--kaauh-border);
vertical-align: middle;
font-size: 0.9rem;
}
.candidate-table tbody tr:hover {
background-color: #f1f3f4;
}
.candidate-table thead th:nth-child(1) { width: 40px; }
.candidate-table thead th:nth-child(4) { width: 10%; }
.candidate-table thead th:nth-child(7) { width: 100px; }
.candidate-name {
font-weight: 600;
color: var(--kaauh-primary-text);
}
.candidate-details {
font-size: 0.8rem;
color: #6c757d;
}
/* 4. Badges and Statuses */
.ai-score-badge {
background-color: var(--kaauh-teal-dark) !important;
color: white;
font-weight: 700;
padding: 0.4em 0.8em;
border-radius: 0.4rem;
}
.status-badge {
font-size: 0.75rem;
padding: 0.3em 0.7em;
border-radius: 0.35rem;
font-weight: 700;
}
.bg-applicant { background-color: #6c757d !important; color: white; }
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
/* Stage Badges */
.stage-badge {
font-size: 0.75rem;
padding: 0.25rem 0.6rem;
border-radius: 0.3rem;
font-weight: 600;
display: inline-block;
margin-bottom: 0.2rem;
}
.stage-Applied { background-color: #e9ecef; color: #495057; }
.stage-Screening { background-color: var(--kaauh-info); color: white; }
.stage-Exam { background-color: var(--kaauh-warning); color: #856404; }
.stage-Interview { background-color: #17a2b8; color: white; }
.stage-Offer { background-color: var(--kaauh-success); color: white; }
/* Timeline specific container */
.applicant-tracking-timeline {
margin-bottom: 2rem;
}
/* --- CUSTOM HEIGHT OPTIMIZATION (MAKING INPUTS/BUTTONS SMALLER) --- */
.form-control-sm,
.btn-sm {
/* Reduce vertical padding even more than default Bootstrap 'sm' */
padding-top: 0.2rem !important;
padding-bottom: 0.2rem !important;
/* Ensure a consistent, small height for both */
height: 28px !important;
font-size: 0.8rem !important; /* Slightly smaller font */
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
@ -25,32 +188,42 @@
<div class="kaauh-card shadow-sm p-3">
{% if candidates %}
<div class="bulk-action-bar">
<div class="bulk-action-bar p-3 bg-light border-bottom">
{# Use d-flex to align the entire contents (two forms and the separator) horizontally #}
<div class="d-flex align-items-end gap-3">
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
{% csrf_token %}
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
<option selected>
----------
</option>
<option value="Offer">
{% trans "To Offer" %}
</option>
<option value="Exam">
{% trans "To Exam" %}
</option>
</select>
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i>
</button>
</form>
{# Form 1: Status Update #}
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
{% csrf_token %}
{# Select Input Group - No label needed for this one, so we just flex the select and button #}
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
<option selected>
----------
</option>
<option value="Offer">
{% trans "To Offer" %}
</option>
<option value="Exam">
{% trans "To Exam" %}
</option>
</select>
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i> {% trans "Move" %}
</button>
</form>
<div class="vr" style="height: 28px;"></div> <form hx-boost="true" hx-include="#candidate-form" action="{% url 'schedule_interviews' job.slug %}" method="get" class="action-group">
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %}
</button>
</form>
{# Separator (Vertical Rule) - Aligns automatically at the bottom with align-items-end #}
<div class="vr" style="height: 28px;"></div>
{# Form 2: Schedule Interviews #}
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'schedule_interviews' job.slug %}" method="get" class="action-group">
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %}
</button>
</form>
</div>
</div>
{% endif %}
<div class="table-responsive">

View File

@ -2,6 +2,170 @@
{% load static i18n %}
{% block title %}- {{ job.title }} - 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;
--kaauh-info: #17a2b8;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
/* Primary Color Overrides */
.text-primary-theme { color: var(--kaauh-teal) !important; }
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
/* 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;
}
/* Dedicated style for the filter block */
.filter-controls {
background-color: #f8f9fa;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
border: 1px solid var(--kaauh-border);
}
/* 2. Button Styling (Themed for Main Actions) */
.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);
}
/* Style for the Bulk Move button */
.btn-bulk-action {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
color: white;
font-weight: 500;
}
.btn-bulk-action:hover {
background-color: #00363e;
border-color: #00363e;
}
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
.candidate-table {
table-layout: fixed;
width: 100%;
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 1rem;
font-weight: 600;
color: var(--kaauh-teal-dark);
border-bottom: 2px solid var(--kaauh-teal);
font-size: 0.9rem;
vertical-align: middle;
}
.candidate-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--kaauh-border);
vertical-align: middle;
font-size: 0.9rem;
}
.candidate-table tbody tr:hover {
background-color: #f1f3f4;
}
.candidate-table thead th:nth-child(1) { width: 40px; }
.candidate-table thead th:nth-child(4) { width: 10%; }
.candidate-table thead th:nth-child(7) { width: 100px; }
.candidate-name {
font-weight: 600;
color: var(--kaauh-primary-text);
}
.candidate-details {
font-size: 0.8rem;
color: #6c757d;
}
/* 4. Badges and Statuses */
.ai-score-badge {
background-color: var(--kaauh-teal-dark) !important;
color: white;
font-weight: 700;
padding: 0.4em 0.8em;
border-radius: 0.4rem;
}
.status-badge {
font-size: 0.75rem;
padding: 0.3em 0.7em;
border-radius: 0.35rem;
font-weight: 700;
}
.bg-applicant { background-color: #6c757d !important; color: white; }
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
/* Stage Badges */
.stage-badge {
font-size: 0.75rem;
padding: 0.25rem 0.6rem;
border-radius: 0.3rem;
font-weight: 600;
display: inline-block;
margin-bottom: 0.2rem;
}
.stage-Applied { background-color: #e9ecef; color: #495057; }
.stage-Screening { background-color: var(--kaauh-info); color: white; }
.stage-Exam { background-color: var(--kaauh-warning); color: #856404; }
.stage-Interview { background-color: #17a2b8; color: white; }
.stage-Offer { background-color: var(--kaauh-success); color: white; }
/* Timeline specific container */
.applicant-tracking-timeline {
margin-bottom: 2rem;
}
/* --- CUSTOM HEIGHT OPTIMIZATION (MAKING INPUTS/BUTTONS SMALLER) --- */
.form-control-sm,
.btn-sm {
/* Reduce vertical padding even more than default Bootstrap 'sm' */
padding-top: 0.2rem !important;
padding-bottom: 0.2rem !important;
/* Ensure a consistent, small height for both */
height: 28px !important;
font-size: 0.8rem !important; /* Slightly smaller font */
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
@ -24,29 +188,38 @@
</div>
<div class="kaauh-card shadow-sm p-3">
{% if candidates %}
<div class="bulk-action-bar">
{% if candidates %}
<div class="bulk-action-bar p-3 bg-light border-bottom">
{# Use d-flex and align-items-end on the container to align the form and the separator #}
<div class="d-flex align-items-end gap-3">
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
{% csrf_token %}
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
<option selected>
----------
</option>
<option value="Hired">
{% trans "To Hired" %}
</option>
<option value="Rejected">
{% trans "To Rejected" %}
</option>
</select>
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i>
</button>
</form>
{# Form: Hired/Rejected Status Update #}
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
{% csrf_token %}
{# Select element #}
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
<option selected>
----------
</option>
<option value="Hired">
{% trans "To Hired" %}
</option>
<option value="Rejected">
{% trans "To Rejected" %}
</option>
</select>
{# Button #}
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i> {% trans "Move" %}
</button>
</form>
<div class="vr" style="height: 28px;"></div>
{# Separator (Vertical Rule) #}
<div class="vr" style="height: 28px;"></div>
</div>
</div>
{% endif %}
<div class="table-responsive">

View File

@ -41,6 +41,7 @@
margin: 0 auto;
padding: 1rem; /* p-4 */
}
.content-grid {
display: grid;
@ -516,7 +517,7 @@
</style>
</head>
<body class="bg-kaauh-light-bg font-sans">
<div class="container">
<div class="container container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;">
<!-- Header Section -->
<header class="header-box">
<div class="header-info">
@ -535,14 +536,14 @@
<!-- GitHub and LinkedIn links for quick access (null in example but included for completeness) -->
{% if candidate.resume_data.linkedin %}
<div class="contact-item">
<i class="fab fa-linkedin"></i>
<a href="{{ candidate.resume_data.linkedin }}" target="_blank">LinkedIn</a>
<a href="{{ candidate.resume_data.linkedin }}" target="_blank"><i class="fab fa-linkedin text-white"></i></a>
</div>
{% endif %}
{% if candidate.resume_data.github %}
<div class="contact-item">
<i class="fab fa-github"></i>
<a href="{{ candidate.resume_data.github }}" target="_blank">GitHub</a>
<a href="{{ candidate.resume_data.github }}" target="_blank"><i class="fab fa-github text-white"></i></a>
</div>
{% endif %}
</div>

View File

@ -2,7 +2,170 @@
{% 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;
--kaauh-info: #17a2b8;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
/* Primary Color Overrides */
.text-primary-theme { color: var(--kaauh-teal) !important; }
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
/* 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;
}
/* Dedicated style for the filter block */
.filter-controls {
background-color: #f8f9fa;
border-radius: 0.75rem;
padding: 1.5rem;
margin-bottom: 2rem;
border: 1px solid var(--kaauh-border);
}
/* 2. Button Styling (Themed for Main Actions) */
.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);
}
/* Style for the Bulk Move button */
.btn-bulk-action {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
color: white;
font-weight: 500;
}
.btn-bulk-action:hover {
background-color: #00363e;
border-color: #00363e;
}
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
.candidate-table {
table-layout: fixed;
width: 100%;
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 1rem;
font-weight: 600;
color: var(--kaauh-teal-dark);
border-bottom: 2px solid var(--kaauh-teal);
font-size: 0.9rem;
vertical-align: middle;
}
.candidate-table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--kaauh-border);
vertical-align: middle;
font-size: 0.9rem;
}
.candidate-table tbody tr:hover {
background-color: #f1f3f4;
}
.candidate-table thead th:nth-child(1) { width: 40px; }
.candidate-table thead th:nth-child(4) { width: 10%; }
.candidate-table thead th:nth-child(7) { width: 100px; }
.candidate-name {
font-weight: 600;
color: var(--kaauh-primary-text);
}
.candidate-details {
font-size: 0.8rem;
color: #6c757d;
}
/* 4. Badges and Statuses */
.ai-score-badge {
background-color: var(--kaauh-teal-dark) !important;
color: white;
font-weight: 700;
padding: 0.4em 0.8em;
border-radius: 0.4rem;
}
.status-badge {
font-size: 0.75rem;
padding: 0.3em 0.7em;
border-radius: 0.35rem;
font-weight: 700;
}
.bg-applicant { background-color: #6c757d !important; color: white; }
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
/* Stage Badges */
.stage-badge {
font-size: 0.75rem;
padding: 0.25rem 0.6rem;
border-radius: 0.3rem;
font-weight: 600;
display: inline-block;
margin-bottom: 0.2rem;
}
.stage-Applied { background-color: #e9ecef; color: #495057; }
.stage-Screening { background-color: var(--kaauh-info); color: white; }
.stage-Exam { background-color: var(--kaauh-warning); color: #856404; }
.stage-Interview { background-color: #17a2b8; color: white; }
.stage-Offer { background-color: var(--kaauh-success); color: white; }
/* Timeline specific container */
.applicant-tracking-timeline {
margin-bottom: 2rem;
}
/* --- CUSTOM HEIGHT OPTIMIZATION (MAKING INPUTS/BUTTONS SMALLER) --- */
.form-control-sm,
.btn-sm {
/* Reduce vertical padding even more than default Bootstrap 'sm' */
padding-top: 0.2rem !important;
padding-bottom: 0.2rem !important;
/* Ensure a consistent, small height for both */
height: 28px !important;
font-size: 0.8rem !important; /* Slightly smaller font */
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
@ -97,23 +260,35 @@
<div class="kaauh-card p-3">
{% if candidates %}
<div class="bulk-action-bar">
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
{% csrf_token %}
<label for="update_status" class="form-label small mb-0 fw-bold">{% trans "Move Selected To:" %}</label>
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: auto;">
<option selected>
----------
</option>
<option value="Exam">
{% trans "Exam Stage" %}
</option>
</select>
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i> {% trans "Update Status" %}
</button>
</form>
</div>
<div class="bulk-action-bar p-3 bg-light border-bottom">
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
{% csrf_token %}
{# MODIFIED: Using d-flex for horizontal alignment and align-items-end to align everything based on the baseline of the button/select #}
<div class="d-flex align-items-end gap-3">
{# Select Input Group #}
<div>
<label for="update_status" class="form-label small mb-1 fw-bold">{% trans "Move Selected To:" %}</label>
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="min-width: 150px;">
<option selected>
----------
</option>
<option value="Exam">
{% trans "Exam Stage" %}
</option>
{# Include other options here, such as Interview, Offer, Rejected, etc. #}
</select>
</div>
{# Button #}
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i> {% trans "Update Status" %}
</button>
</div>
</form>
</div>
{% endif %}
<div class="table-responsive">

View File

@ -7,12 +7,13 @@
<style>
/* UI Variables for the KAAT-S Theme (Teal/Consistent Look) */
:root {
--kaauh-teal: #00636e;
--kaauh-teal: #00636e; /* Dark Teal */
--kaauh-teal-light: #0093a3;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--color-success: #28a745;
--color-warning: #ffc107; /* Standardized warning color */
--color-info: #17a2b8;
}
@ -29,14 +30,33 @@
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
}
/* Card Header and Icon Styling */
.card-header {
font-weight: 600;
padding: 1.25rem;
border-bottom: 1px solid var(--kaauh-border);
background-color: #f8f9fa;
display: flex; /* Ensure title and filter are aligned */
justify-content: space-between;
align-items: center;
}
/* Stats Grid Layout - Six columns for better detail display */
.card-header h3, .card-header h2 {
display: flex;
align-items: center;
color: var(--kaauh-primary-text);
font-size: 1.25rem;
font-weight: 600;
margin: 0;
}
.stat-icon {
color: var(--kaauh-teal);
font-size: 1.75rem;
margin-right: 0.75rem;
}
/* Stats Grid Layout */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
@ -59,21 +79,27 @@
color: #6c757d;
padding-bottom: 1rem;
}
/* Header Styling */
.card-header h3, .card-header h2 {
/* Dropdown/Filter Styling */
.job-filter-container {
display: flex;
align-items: center;
color: var(--kaauh-primary-text);
font-size: 1.25rem;
font-weight: 600;
margin: 0;
gap: 10px;
}
.stat-icon {
color: var(--kaauh-teal);
font-size: 1.75rem;
margin-right: 0.75rem;
.form-select {
border-color: var(--kaauh-border);
border-radius: 0.5rem;
font-size: 0.9rem;
}
.form-select:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
.job-filter-label {
font-weight: 500;
color: var(--kaauh-primary-text);
margin: 0;
white-space: nowrap;
}
/* Chart Container */
@ -81,6 +107,16 @@
padding: 2rem;
}
/* Bootstrap Overrides (Optional, for full consistency) */
.btn-primary {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
}
.btn-primary:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
}
</style>
{% endblock %}
@ -89,6 +125,9 @@
<h1 class="mb-4" style="color: var(--kaauh-teal-dark); font-weight: 700;">{% trans "Recruitment Intelligence" %} 🧠</h1>
{# -------------------------------------------------------------------------- #}
{# STATS CARDS SECTION #}
{# -------------------------------------------------------------------------- #}
<div class="stats">
<div class="card">
@ -140,84 +179,183 @@
</div>
</div>
<div class="card shadow-lg">
<div class="card-header">
<h2 class="d-flex align-items-center mb-0">
<i class="fas fa-chart-bar stat-icon"></i>
{% trans "Top 5 Application Volume" %}
</h2>
{# -------------------------------------------------------------------------- #}
{# CHARTS SECTION (Using a row/col layout for structure) #}
{# -------------------------------------------------------------------------- #}
<div class="row g-4">
{# BAR CHART - Application Volume #}
<div class="col-lg-12">
<div class="card shadow-lg h-100">
<div class="card-header">
<h2>
<i class="fas fa-chart-bar stat-icon"></i>
{% trans "Top 5 Application Volume" %}
</h2>
</div>
<div class="chart-container">
<canvas id="applicationsChart"></canvas>
</div>
</div>
</div>
<div class="chart-container">
<canvas id="applicationsChart"></canvas>
{# DONUT CHART - Candidate Pipeline Status #}
<div class="col-lg-12">
<div class="card shadow-lg h-100">
<div class="card-header">
<h2>
<i class="fas fa-filter stat-icon"></i>
{% trans "Candidate Pipeline Status for job: " %}
</h2>
<small>{{my_job}}</small>
{# Job Filter Dropdown - Consistent with Card Header Layout #}
<form method="get" action="." class="job-filter-container">
<label for="job-select" class="job-filter-label d-none d-md-inline">{% trans "Filter Job:" %}</label>
<select name="selected_job_id" id="job-select" class="form-select form-select-sm" onchange="this.form.submit()">
<option value="">{% trans "All Jobs (Default View)" %}</option>
{% for job in jobs%}
<option value="{{ job.internal_job_id }}" {% if selected_job_id == job.internal_job_id %}selected{% endif %}>
{{ job }}
</option>
{% endfor %}
</select>
</form>
</div>
<div class="chart-container d-flex justify-content-center align-items-center">
<canvas id="candidate_donout_chart"></canvas>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
// Get the all_candidates_count value from Django context
const ALL_CANDIDATES_COUNT = '{{ all_candidates_count|default:0 }}';
// --- 1. DONUT CHART CENTER TEXT PLUGIN (Custom) ---
const centerTextPlugin = {
id: 'centerText',
beforeDraw: (chart) => {
const { ctx } = chart;
// Convert to integer (handle case where all_candidates_count might be missing)
const total = parseInt(ALL_CANDIDATES_COUNT) || 0;
if (total === 0) return; // Don't draw if count is zero
// Get chart center coordinates
const xCenter = chart.getDatasetMeta(0).data[0].x;
const yCenter = chart.getDatasetMeta(0).data[0].y;
ctx.restore();
// --- First Line: The Total Count (Bold Number) ---
ctx.font = 'bold 28px sans-serif';
ctx.fillStyle = 'var(--kaauh-teal-dark)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(total.toString(), xCenter, yCenter - 10);
// --- Second Line: The Label ---
const labelText = '{% trans "Total" %}';
ctx.font = '12px sans-serif';
ctx.fillStyle = '#6c757d';
ctx.fillText(labelText, xCenter, yCenter + 20);
ctx.save();
}
};
// ------------------------------------------------
// Pass context data safely to JavaScript
const jobTitles = JSON.parse('{{ job_titles|escapejs }}').slice(0, 5); // Take top 5
const jobAppCounts = JSON.parse('{{ job_app_counts|escapejs }}').slice(0, 5); // Take top 5
const ctx = document.getElementById('applicationsChart').getContext('2d');
const chart = new Chart(ctx, {
// BAR CHART configuration
const ctxBar = document.getElementById('applicationsChart').getContext('2d');
new Chart(ctxBar, {
type: 'bar',
data: {
// Use the parsed and sliced data
labels: jobTitles,
datasets: [{
label: '{% trans "Applications" %}',
data: jobAppCounts,
// Use the defined CSS variable for consistency
backgroundColor: ' #00636e', // Green theme
borderColor: ' #004a53',
backgroundColor: 'var(--kaauh-teal)',
borderColor: 'var(--kaauh-teal-dark)',
borderWidth: 1,
barThickness: 50
}]
},
options: {
responsive: true,
aspectRatio: 2.5, // Make the chart wider
aspectRatio: 2.5,
plugins: {
legend: {
display: false // Hide legend since there's only one dataset
},
legend: { display: false },
title: {
display: true,
text: 'Top 5 Most Applied Jobs',
font: {
size: 16
},
text: '{% trans "Top 5 Most Applied Jobs" %}',
font: { size: 16 },
color: 'var(--kaauh-primary-text)'
}
},
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: 'Total Applications'
},
ticks: {
color: '#333333',
precision: 0 // Ensure y-axis labels are integers
},
grid: {
color: '#e0e0e0'
}
title: { display: true, text: '{% trans "Total Applications" %}' },
ticks: { color: '#333333', precision: 0 },
grid: { color: '#e0e0e0' }
},
x: {
ticks: {
color: '#333333'
},
grid: {
display: false
}
ticks: { color: '#333333' },
grid: { display: false }
}
}
}
});
// DONUT CHART configuration
const ctxDonut = document.getElementById('candidate_donout_chart').getContext('2d');
new Chart(ctxDonut, {
type: 'doughnut',
data: {
// Ensure these contexts are always output as valid JSON arrays
labels: JSON.parse('{{ candidate_stage|safe }}'),
datasets: [{
label: '{% trans "Candidate Count" %}',
data: JSON.parse('{{ candidates_count|safe }}'),
backgroundColor: [
'var(--kaauh-teal)', // Applied (Primary)
'rgb(255, 159, 64)', // Exam (Orange)
'rgb(54, 162, 235)', // Interview (Blue)
'rgb(75, 192, 192)' // Offer (Green)
],
hoverOffset: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: 'bottom',
labels: { padding: 20 }
},
title: {
display: true,
text: '{% trans "Pipeline Status Breakdown" %}',
font: { size: 16 }
}
}
},
// --- Register the custom plugin here ---
plugins: [centerTextPlugin]
});
</script>
{% endblock %}

View File

@ -4,7 +4,8 @@
{% load humanize %}
{% block title %}{% trans "Admin Settings" %} - KAAUH ATS{% endblock %}
{% block customCSS %}
{% block customCSS %}
<style>
/* Theme Variables for Consistency */
@ -12,17 +13,19 @@
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-light-bg: #f9fbfd;
/* Standard Bootstrap color overrides (for pagination/badges) */
--kaauh-border: #e9ecef;
/* Standardized Colors */
--color-primary: var(--kaauh-teal);
--color-primary-dark: var(--kaauh-teal-dark);
--color-background-light: #f4f6f9; /* Retaining this lighter shade for page background */
--color-success: #28a745;
--color-secondary: #6c757d;
}
/* Layout & Card Styling */
.dashboard-header {
color: var(--color-primary-dark);
border-bottom: 1px solid #e9ecef;
border-bottom: 1px solid var(--kaauh-border);
padding-bottom: 1rem;
margin-bottom: 2rem;
}
@ -31,16 +34,6 @@
.text-accent {
color: var(--color-primary) !important;
}
.feature-icon {
font-size: 2.5rem;
color: var(--color-primary);
margin-bottom: 1rem;
}
.feature-title {
color: var(--color-primary-dark);
font-weight: 600;
margin-bottom: 0.5rem;
}
/* Table Specific Styling */
.table-card {
@ -49,71 +42,99 @@
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
padding: 1.5rem;
}
/* Adjusted table stripe color to use theme variable */
/* Table Rows */
.table-striped > tbody > tr:nth-of-type(odd) > * {
background-color: rgba(0, 99, 110, 0.05); /* Light kaauh-teal stripe */
background-color: rgba(0, 99, 110, 0.03); /* Very light kaauh-teal stripe */
}
.page-link {
color: var(--color-primary-dark);
.table thead th {
border-bottom: 2px solid var(--kaauh-border);
font-weight: 600;
color: var(--kaauh-primary-text, #343a40);
}
.page-item.active .page-link {
background-color: var(--color-primary) !important;
border-color: var(--color-primary) !important;
color: #fff;
/* Status Badges */
.badge-active {
background-color: rgba(0, 99, 110, 0.1); /* Light Teal background */
color: var(--kaauh-teal);
font-weight: 600;
padding: 0.4em 0.8em;
}
.badge-inactive {
background-color: rgba(108, 117, 125, 0.1); /* Light Gray background */
color: var(--color-secondary);
font-weight: 600;
padding: 0.4em 0.8em;
}
/* Action button container for better alignment and spacing */
.action-btns {
display: flex;
justify-content: center;
gap: 5px; /* Small space between icons */
gap: 0.5rem; /* Increased space between buttons */
}
/* --- Main Action Button Style (Teal Theme) --- */
/* --- Main Action Button (Create User) --- */
.btn-main-action {
/* Use standard Bootstrap button styles as a base */
background-color: var(--kaauh-teal);
border: 1px solid var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem; /* Standard Bootstrap gap for icon/text */
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.375rem; /* Standard Bootstrap border radius */
text-decoration: none;
}
/* Functional Hover State */
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
color: white;
text-decoration: none;
}
/* Active/Focus State (optional but good for accessibility) */
.btn-main-action:focus,
.btn-main-action:active {
box-shadow: 0 0 0 0.25rem rgba(0, 99, 110, 0.5); /* Teal focus ring */
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
color: white;
color: white; /* Ensure text remains white on hover */
}
button i.fa-solid {
/* Re-enable display if it was hidden */
display: inline-block;
/* Ensure the correct font family is applied */
font-family: "Font Awesome 6 Free";
/* Inherit button color */
color: inherit;
}
.text-primary{
color:var(--kaauh-teal) !important;
/* --- Secondary Action Buttons (Table Row) --- */
.btn-table-action {
background: none;
border: none;
padding: 0.5rem; /* Make the click area larger */
color: var(--kaauh-teal); /* Default icon color */
transition: color 0.2s ease;
}
.btn-table-action:hover {
color: var(--kaauh-teal-dark);
box-shadow: none;
}
}
/* Toggle Status Button Styling */
.btn-toggle-status {
font-size: 0.85rem;
font-weight: 500;
padding: 0.375rem 0.75rem;
border-radius: 0.375rem;
transition: all 0.2s ease;
display: flex;
align-items: center;
gap: 0.3rem;
}
/* Deactivate (Red/Danger) */
.btn-deactivate {
background-color: #f8d7da; /* Light Red */
color: #721c24; /* Dark Red Text */
border: 1px solid #f5c6cb;
}
.btn-deactivate:hover {
background-color: #f5c6cb;
}
/* Activate (Teal/Primary) */
.btn-activate {
background-color: rgba(0, 99, 110, 0.15); /* Light Teal */
color: var(--kaauh-teal-dark);
border: 1px solid rgba(0, 99, 110, 0.2);
}
.btn-activate:hover {
background-color: rgba(0, 99, 110, 0.25);
}
</style>
{% endblock %}
@ -121,11 +142,11 @@
{% block content %}
<div class="container-fluid mt-5">
<div class="container-fluid py-4">
<div class="row px-lg-4">
<div class="col-12">
<h1 class="h3 fw-bold dashboard-header">
<i class="fas fa-cogs me-2 text-accent"></i>{% trans "Admin Settings Dashboard" %}
<i class="fas fa-cogs me-3 text-accent"></i>{% trans "Admin Settings Dashboard" %}
</h1>
</div>
</div>
@ -133,7 +154,7 @@
{# --- Paged User Table Section --- #}
<div class="row px-lg-4" id="user-list">
<div class="d-flex align-items-center justify-content-between mb-3">
<div class="d-flex align-items-center justify-content-between mb-4">
<h4 class="text-secondary fw-bold mb-0">{% trans "Staff User List" %}</h4>
@ -155,7 +176,7 @@
<th scope="col" class="text-center">{% trans "Status" %}</th>
<th scope="col" class="text-center">{% trans "First Join" %}</th>
<th scope="col" class="text-center">{% trans "Last Login" %}</th>
<th scope="col" class="text-center">{% trans "Actions" %}</th>
<th scope="col" class="text-center" style="min-width: 250px;">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
@ -164,11 +185,13 @@
<td>{{ user.pk }}</td>
<td>{{ user.get_full_name|default:user.first_name }}</td>
<td>{{ user.email }}</td>
{# Column: Status Badge (Improved Styling) #}
<td class="text-center">
{% if user.is_active %}
<span class="badge rounded-pill bg-success-subtle text-success border border-success-subtle">{% trans "Active" %}</span>
<span class="badge rounded-pill badge-active">{% trans "Active" %}</span>
{% else %}
<span class="badge rounded-pill bg-secondary-subtle text-secondary border border-secondary-subtle">{% trans "Inactive" %}</span>
<span class="badge rounded-pill badge-inactive">{% trans "Inactive" %}</span>
{% endif %}
</td>
@ -182,63 +205,54 @@
{% if user.last_login %}
<span title="{{ user.last_login|date:'DATETIME_FORMAT' }}">{{ user.last_login|naturaltime }}</span>
{% else %}
{% trans "Never" %}
{% endif %}
</td>
{# Column: Actions #}
<td class="text-center">
<div class="action-btns">
{% comment %} {# 1. Edit/Detail Button (Pencil Icon) - Added common action #}
<a href="{% url 'edit_staff_user' user.pk %}" class="btn btn-sm btn-outline-secondary" title="{% trans 'Edit User' %}">
<i class="fas fa-pencil"></i>
</a> {% endcomment %}
{# 2. Change Password Button (Key Icon) #}
<a href="{% url 'set_staff_password' user.pk %}" class="btn btn-sm btn-outline-info" title="{% trans 'Change Password' %}">
<i class="fas fa-key"></i>
{% trans 'Change Password' %}
</a>
{# 3. Delete Button (Trash Icon) #}
{# 3. Toggle Status Button (Improved Button Styling) #}
{% if user.is_active %}
<form method="post" action="{% url 'account_toggle_status' user.pk %}">
<form method="post" action="{% url 'account_toggle_status' user.pk %}" class="d-inline">
{% csrf_token %}
{# The button for DEACTIVATION #}
<button type="submit" >
<i class="fas fa-times-circle text-danger"></i> Deactivate
<button type="submit" class="btn-toggle-status btn-deactivate" title="{% trans 'Deactivate User' %}">
<i class="fas fa-times-circle"></i> {% trans "Deactivate" %}
</button>
</form>
{% else %}
<form method="post" action="{% url 'account_toggle_status' user.pk %}">
<form method="post" action="{% url 'account_toggle_status' user.pk %}" class="d-inline">
{% csrf_token %}
{# The button for REACTIVATION #}
<button type="submit bg-primary" >
<i class="fas fa-check-circle text-primary"></i> Activate
</button>
<button type="submit" class="btn-toggle-status btn-activate" title="{% trans 'Activate User' %}">
<i class="fas fa-check-circle"></i> {% trans "Activate" %}
</button>
</form>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="text-center text-muted py-4">{% trans "No staff users found." %}</td>
<td colspan="7" class="text-center text-muted py-4">{% trans "No staff users found." %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# NOTE: Add Pagination links here if the `staffs` object is a Paginator object #}
</div>
</div>
</div>