candidates pipeline status chart

This commit is contained in:
Faheed 2025-10-23 01:44:33 +03:00
parent ec3c52579b
commit 3e99bb3dc9
38 changed files with 2381 additions and 1090 deletions

View File

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

View File

@ -23,9 +23,11 @@ urlpatterns = [
# path('', include('recruitment.urls')), # path('', include('recruitment.urls')),
path("ckeditor5/", include('django_ckeditor_5.urls')), path("ckeditor5/", include('django_ckeditor_5.urls')),
path('form/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'), path('application/<slug:template_slug>/', views.application_submit_form, name='application_submit_form'),
path('form/<slug:template_slug>/submit/', views.submit_form, name='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/', views.list_form_templates, name='list_form_templates'),
path('api/templates/save/', views.save_form_template, name='save_form_template'), 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'), 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 from urllib.parse import quote, urlencode
import requests import requests
import logging import logging
from django.conf import settings
import time import time
import random from django.conf import settings
from django.utils import timezone
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Define a constant for the API version for better maintenance # Define constants
LINKEDIN_API_VERSION = '2.0.0' 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: class LinkedInService:
def __init__(self): def __init__(self):
@ -23,11 +22,11 @@ class LinkedInService:
self.redirect_uri = settings.LINKEDIN_REDIRECT_URI self.redirect_uri = settings.LINKEDIN_REDIRECT_URI
self.access_token = None self.access_token = None
# Configuration for image processing wait time # Configuration for image processing wait time
self.ASSET_STATUS_TIMEOUT = 15 # Max time (seconds) to wait for image processing self.ASSET_STATUS_TIMEOUT = 15
self.ASSET_STATUS_INTERVAL = 2 # Check every 2 seconds self.ASSET_STATUS_INTERVAL = 2
# --- AUTHENTICATION & PROFILE ---
# ---------------- AUTHENTICATION & PROFILE ----------------
def get_auth_url(self): def get_auth_url(self):
"""Generate LinkedIn OAuth URL""" """Generate LinkedIn OAuth URL"""
params = { params = {
@ -76,7 +75,7 @@ class LinkedInService:
logger.error(f"Error getting user profile: {e}") logger.error(f"Error getting user profile: {e}")
raise raise
# --- ASSET UPLOAD & STATUS --- # ---------------- ASSET UPLOAD & STATUS ----------------
def get_asset_status(self, asset_urn): def get_asset_status(self, asset_urn):
"""Checks the status of a registered asset (image) to ensure it's READY.""" """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, 'X-Restli-Protocol-Version': LINKEDIN_API_VERSION,
'LinkedIn-Version': LINKEDIN_VERSION, 'LinkedIn-Version': LINKEDIN_VERSION,
} }
try: try:
response = requests.get(url, headers=headers, timeout=10) response = requests.get(url, headers=headers, timeout=10)
response.raise_for_status() response.raise_for_status()
@ -129,6 +128,7 @@ class LinkedInService:
"""Step 2: Upload image file and poll for 'READY' status.""" """Step 2: Upload image file and poll for 'READY' status."""
image_file.open() image_file.open()
image_content = image_file.read() image_content = image_file.read()
image_file.seek(0) # Reset pointer after reading
image_file.close() image_file.close()
headers = { headers = {
@ -137,8 +137,8 @@ class LinkedInService:
response = requests.post(upload_url, headers=headers, data=image_content, timeout=60) response = requests.post(upload_url, headers=headers, data=image_content, timeout=60)
response.raise_for_status() response.raise_for_status()
# --- CRITICAL FIX: POLL FOR ASSET STATUS --- # --- POLL FOR ASSET STATUS ---
start_time = time.time() start_time = time.time()
while time.time() - start_time < self.ASSET_STATUS_TIMEOUT: while time.time() - start_time < self.ASSET_STATUS_TIMEOUT:
try: try:
@ -149,56 +149,55 @@ class LinkedInService:
return True return True
if status == "FAILED": if status == "FAILED":
raise Exception(f"LinkedIn image processing failed for asset {asset_urn}") raise Exception(f"LinkedIn image processing failed for asset {asset_urn}")
logger.info(f"Asset {asset_urn} status: {status}. Waiting...") logger.info(f"Asset {asset_urn} status: {status}. Waiting...")
time.sleep(self.ASSET_STATUS_INTERVAL) time.sleep(self.ASSET_STATUS_INTERVAL)
except Exception as e: except Exception as e:
logger.warning(f"Error during asset status check for {asset_urn}: {e}. Retrying.") 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.") logger.warning(f"Asset {asset_urn} timed out, but upload succeeded. Forcing post attempt.")
return True return True
# --- POSTING LOGIC --- # ---------------- POSTING UTILITIES ----------------
def clean_html_for_social_post(self, html_content): 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: if not html_content:
return "" return ""
text = html_content text = html_content
# 1. Convert Bolding tags to *Markdown* # 1. Convert Bolding tags to *Markdown*
text = re.sub(r'<strong>(.*?)</strong>', r'*\1*', text, flags=re.IGNORECASE) text = re.sub(r'<strong>(.*?)</strong>', r'*\1*', text, flags=re.IGNORECASE)
text = re.sub(r'<b>(.*?)</b>', 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 # 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'</(ul|ol|div)>', '\n', text, flags=re.IGNORECASE)
text = re.sub(r'<li[^>]*>', '', 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'</li>', '\n', text, flags=re.IGNORECASE)
# 3. Handle Paragraphs and Line Breaks # 3. Handle Paragraphs and Line Breaks
text = re.sub(r'</p>', '\n\n', text, flags=re.IGNORECASE) text = re.sub(r'</p>', '\n\n', text, flags=re.IGNORECASE)
text = re.sub(r'<br/?>', '\n', text, flags=re.IGNORECASE) text = re.sub(r'<br/?>', '\n', text, flags=re.IGNORECASE)
# 4. Strip all remaining, unsupported HTML tags # 4. Strip all remaining, unsupported HTML tags
clean_text = re.sub(r'<[^>]+>', '', text) clean_text = re.sub(r'<[^>]+>', '', text)
# 5. Unescape HTML entities # 5. Unescape HTML entities
clean_text = unescape(clean_text) clean_text = unescape(clean_text)
# 6. Clean up excessive whitespace/newlines # 6. Clean up excessive whitespace/newlines
clean_text = re.sub(r'(\n\s*){3,}', '\n\n', clean_text).strip() clean_text = re.sub(r'(\n\s*){3,}', '\n\n', clean_text).strip()
return clean_text return clean_text
def hashtags_list(self, hash_tags_str): def hashtags_list(self, hash_tags_str):
"""Convert comma-separated hashtags string to list""" """Convert comma-separated hashtags string to list"""
if not hash_tags_str: if not hash_tags_str:
return ["#HigherEd", "#Hiring", "#UniversityJobs"] return ["#HigherEd", "#Hiring", "#UniversityJobs"]
tags = [tag.strip() for tag in hash_tags_str.split(',') if tag.strip()] 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] tags = [tag if tag.startswith('#') else f'#{tag}' for tag in tags]
@ -208,15 +207,18 @@ class LinkedInService:
return tags return tags
def _build_post_message(self, job_posting): 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 = [ message_parts = [
f"🔥 *Job Alert!* Were looking for a talented professional to join our team.", f"🔥 *Job Alert!* Were looking for a talented professional to join our team.",
f"👉 **{job_posting.title}** 👈", f"👉 **{job_posting.title}** 👈",
] ]
if job_posting.department: if job_posting.department:
message_parts.append(f"*{job_posting.department}*") message_parts.append(f"*{job_posting.department}*")
message_parts.append("\n" + "=" * 25 + "\n") message_parts.append("\n" + "=" * 25 + "\n")
# KEY DETAILS SECTION # KEY DETAILS SECTION
@ -229,7 +231,7 @@ class LinkedInService:
details_list.append(f"🏠 Workplace: {job_posting.get_workplace_type_display()}") details_list.append(f"🏠 Workplace: {job_posting.get_workplace_type_display()}")
if job_posting.salary_range: if job_posting.salary_range:
details_list.append(f"💰 Salary: {job_posting.salary_range}") details_list.append(f"💰 Salary: {job_posting.salary_range}")
if details_list: if details_list:
message_parts.append("*Key Information*:") message_parts.append("*Key Information*:")
message_parts.extend(details_list) message_parts.extend(details_list)
@ -239,11 +241,13 @@ class LinkedInService:
clean_description = self.clean_html_for_social_post(job_posting.description) clean_description = self.clean_html_for_social_post(job_posting.description)
if clean_description: if clean_description:
message_parts.append(f"🔎 *About the Role:*\n{clean_description}") message_parts.append(f"🔎 *About the Role:*\n{clean_description}")
# CALL TO ACTION # CALL TO ACTION
if job_posting.application_url: if job_posting.application_url:
message_parts.append(f"\n\n---") 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
hashtags = self.hashtags_list(job_posting.hash_tags) hashtags = self.hashtags_list(job_posting.hash_tags)
@ -252,19 +256,38 @@ class LinkedInService:
hashtags.insert(0, dept_hashtag) hashtags.insert(0, dept_hashtag)
message_parts.append("\n" + " ".join(hashtags)) message_parts.append("\n" + " ".join(hashtags))
if len(message_parts)>=3000: final_message = "\n".join(message_parts)
message_parts=message_parts[0:2980]+"........"
# --- FIX: ADD UNIQUE SUFFIX AND HANDLE LENGTH (422 fix) ---
return "\n".join(message_parts) 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): 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). Private method to handle the final UGC post request.
This eliminates the duplication between create_job_post and create_job_post_with_image. CRITICAL FIX: Avoids ARTICLE category if not using an image to prevent 402 errors.
""" """
message = self._build_post_message(job_posting) 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" url = "https://api.linkedin.com/v2/ugcPosts"
headers = { headers = {
@ -280,8 +303,8 @@ class LinkedInService:
"shareMediaCategory": media_category, "shareMediaCategory": media_category,
} }
} }
if media_list: if media_list and media_category == "IMAGE":
specific_content["com.linkedin.ugc.ShareContent"]["media"] = media_list specific_content["com.linkedin.ugc.ShareContent"]["media"] = media_list
payload = { payload = {
@ -294,8 +317,13 @@ class LinkedInService:
} }
response = requests.post(url, headers=headers, json=payload, timeout=60) 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() response.raise_for_status()
post_id = response.headers.get('x-restli-id', '') post_id = response.headers.get('x-restli-id', '')
post_url = f"https://www.linkedin.com/feed/update/{quote(post_id)}/" if post_id else "" 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): 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 = [{ media_list = [{
"status": "READY", "status": "READY",
"media": asset_urn, "media": asset_urn,
"description": {"text": job_posting.title}, "description": {"text": job_posting.title},
"originalUrl": job_posting.application_url, "originalUrl": job_posting.application_url,
"title": {"text": "Apply Now"} "title": {"text": "Apply Now"}
}] }]
# Use the helper method to send the post
return self._send_ugc_post( return self._send_ugc_post(
person_urn=person_urn, person_urn=person_urn,
job_posting=job_posting, job_posting=job_posting,
@ -344,47 +375,44 @@ class LinkedInService:
# Check for image and attempt post # Check for image and attempt post
try: try:
# Assuming correct model path: job_posting.related_model_name.first().image_field_name
image_upload = job_posting.post_images.first().post_image image_upload = job_posting.post_images.first().post_image
has_image = image_upload is not None has_image = image_upload is not None
except Exception: except Exception:
pass # No image available pass
if has_image: if has_image:
try: try:
# Step 1: Register # Steps 1, 2, 3 for image post
upload_info = self.register_image_upload(person_urn) upload_info = self.register_image_upload(person_urn)
asset_urn = upload_info['asset'] asset_urn = upload_info['asset']
# Step 2: Upload and WAIT FOR READY (Crucial for 422 fix)
self.upload_image_to_linkedin( self.upload_image_to_linkedin(
upload_info['upload_url'], upload_info['upload_url'],
image_upload, image_upload,
asset_urn asset_urn
) )
# Step 3: Create post with image
return self.create_job_post_with_image( return self.create_job_post_with_image(
job_posting, image_upload, person_urn, asset_urn job_posting, image_upload, person_urn, asset_urn
) )
except Exception as e: except Exception as e:
logger.error(f"Image post failed, falling back to text: {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) === # === 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( return self._send_ugc_post(
person_urn=person_urn, person_urn=person_urn,
job_posting=job_posting, job_posting=job_posting,
media_category="NONE" media_category="NONE"
) )
except Exception as e: except Exception as e:
logger.error(f"Error creating LinkedIn post: {e}") logger.error(f"Error creating LinkedIn post: {e}")
status_code = getattr(getattr(e, 'response', None), 'status_code', 500)
return { return {
'success': False, 'success': False,
'error': str(e), '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 schedule_type=Schedule.ONCE
).first() ).first()
if instance.is_active and instance.application_deadline: if instance.STATUS_CHOICES=='ACTIVE' and instance.application_deadline:
if not existing_schedule: if not existing_schedule:
# Create a new schedule if one does not exist # Create a new schedule if one does not exist
schedule( schedule(

View File

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

View File

@ -744,7 +744,7 @@ class AdvancedIntegrationTests(TransactionTestCase):
} }
response = self.client.post( response = self.client.post(
reverse('submit_form', kwargs={'template_id': template.id}), reverse('application_submit', kwargs={'template_id': template.id}),
submission_data submission_data
) )
self.assertEqual(response.status_code, 302) # Redirect to success page 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>/update/', views.edit_job, name='job_update'),
# path('jobs/<slug:slug>/delete/', views., name='job_delete'), # path('jobs/<slug:slug>/delete/', views., name='job_delete'),
path('jobs/<slug:slug>/', views.job_detail, name='job_detail'), 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'), path('careers/',views.kaauh_career,name='kaauh_career'),
# LinkedIn Integration URLs # LinkedIn Integration URLs
@ -83,8 +82,8 @@ urlpatterns = [
path('htmx/<slug:slug>/candidate_update_status/', views.candidate_update_status, name='candidate_update_status'), 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>/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>/', 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/<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/<slug:slug>/submissions/', views.form_template_submissions_list, name='form_template_submissions_list'),
path('forms/template/<int:template_id>/all-submissions/', views.form_template_all_submissions, name='form_template_all_submissions'), path('forms/template/<int:template_id>/all-submissions/', views.form_template_all_submissions, name='form_template_all_submissions'),
@ -140,6 +139,7 @@ urlpatterns = [
# Meeting Comments URLs # Meeting Comments URLs
path('meetings/<slug:slug>/comments/add/', views.add_meeting_comment, name='add_meeting_comment'), 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>/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>/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'), 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: try:
job = form.save(commit=False) job = form.save(commit=False)
job.save() 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_apply_url_absolute=request.build_absolute_uri(job_apply_url_relative)
job.application_url=job_apply_url_absolute job.application_url=job_apply_url_absolute
# FormTemplate.objects.create(job=job, is_active=False, name=job.title,created_by=request.user) # 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: # job detail facing the candidate:
def job_detail_candidate(request, slug): def application_detail(request, slug):
job = get_object_or_404(JobPosting, slug=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 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""" """Display the form as a step-by-step wizard"""
template = get_object_or_404(FormTemplate, slug=template_slug, is_active=True) template = get_object_or_404(FormTemplate, slug=template_slug, is_active=True)
job_id = template.job.internal_job_id job_id = template.job.internal_job_id
@ -811,24 +811,24 @@ def form_wizard_view(request, template_slug):
request, request,
'Application limit reached: This job is no longer accepting new applications. Please explore other available positions.' '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: if job.is_expired:
messages.error( messages.error(
request, request,
'Application deadline passed: This job is no longer accepting new applications. Please explore other available positions.' '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( return render(
request, request,
"forms/form_wizard.html", "forms/application_submit_form.html",
{"template_slug": template_slug, "job_id": job_id}, {"template_slug": template_slug, "job_id": job_id},
) )
@csrf_exempt @csrf_exempt
@require_POST @require_POST
def submit_form(request, template_slug): def application_submit(request, template_slug):
"""Handle form submission""" """Handle form submission"""
template = get_object_or_404(FormTemplate, slug=template_slug) template = get_object_or_404(FormTemplate, slug=template_slug)
job = template.job job = template.job
@ -2292,7 +2292,13 @@ def edit_meeting_comment(request, slug, comment_id):
return redirect('meeting_details', slug=slug) return redirect('meeting_details', slug=slug)
else: else:
form = MeetingCommentForm(instance=comment) form = MeetingCommentForm(instance=comment)
print("hi")
context = {
'form': form,
'meeting': meeting,
'comment':comment
}
return render(request, 'includes/edit_comment_form.html', context)
@login_required @login_required
def delete_meeting_comment(request, slug, comment_id): def delete_meeting_comment(request, slug, comment_id):

View File

@ -394,6 +394,27 @@ def dashboard_view(request):
).count() ).count()
high_potential_ratio = round((high_potential_count / total_candidates) * 100, 1) if total_candidates > 0 else 0 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 = { context = {
'total_jobs': total_jobs, 'total_jobs': total_jobs,
@ -409,6 +430,14 @@ def dashboard_view(request):
'high_potential_count': high_potential_count, 'high_potential_count': high_potential_count,
'high_potential_ratio': high_potential_ratio, 'high_potential_ratio': high_potential_ratio,
'scored_ratio': scored_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) return render(request, 'recruitment/dashboard.html', context)
@login_required @login_required

View File

@ -42,7 +42,7 @@
<p class="text-muted">{% trans "Review the job details, then apply below." %}</p> <p class="text-muted">{% trans "Review the job details, then apply below." %}</p>
{% if job.form_template %} {% 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" %} <i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
</a> </a>
{% endif %} {% endif %}
@ -102,7 +102,7 @@
<div class="mobile-fixed-apply-bar d-lg-none"> <div class="mobile-fixed-apply-bar d-lg-none">
{% if job.form_template %} {% 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" %} <i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
</a> </a>
{% endif %} {% endif %}

View File

@ -824,7 +824,7 @@
}); });
try { try {
const response = await fetch(`/form/${state.templateId}/submit/`, { const response = await fetch(`/application/${state.templateId}/submit/`, {
method: 'POST', method: 'POST',
body: formData body: formData
// IMPORTANT: Do NOT set Content-Type header when using 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="mt-auto pt-2 border-top">
<div class="d-flex gap-2 justify-content-end"> <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> <i class="fas fa-eye"></i>
</a> </a>
<a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'Edit' %}"> <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>{{ template.updated_at|date:"M d, Y" }}</td>
<td class="text-end"> <td class="text-end">
<div class="btn-group btn-group-sm" role="group"> <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> <i class="fas fa-eye"></i>
</a> </a>
<a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}"> <a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">

View File

@ -17,7 +17,7 @@
</div> </div>
<button type="submit" class="btn btn-primary">Add Comment</button> <button type="submit" class="btn btn-primary">Add Comment</button>
{% if 'HX-Request' in request.headers %} {% 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 %} {% endif %}
</form> </form>
</div> </div>

View File

@ -3,7 +3,7 @@
<div class="card-header text-primary-theme d-flex justify-content-between align-items-center"> <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> <h5 class="card-title mb-0">Comments ({{ comments.count }})</h5>
{% if 'HX-Request' in request.headers %} {% 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 <i class="bi bi-x-lg"></i> Close
</button> </button>
{% endif %} {% endif %}

View File

@ -9,7 +9,7 @@
{% csrf_token %} {% csrf_token %}
<button type="submit" class="btn btn-danger">Yes, Delete</button> <button type="submit" class="btn btn-danger">Yes, Delete</button>
{% if 'HX-Request' in request.headers %} {% 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 %} {% endif %}
</form> </form>
</div> </div>

View File

@ -15,7 +15,7 @@
</div> </div>
{% endif %} {% endif %}
</div> </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 %} {% 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> <button type="button" class="btn btn-secondary btn-sm" hx-get="{% url 'meeting_details' meeting.slug %}" hx-target="#comment-section">Cancel</button>
{% endif %} {% endif %}

View File

@ -248,7 +248,7 @@
<td class="col-link" data-label="{% trans 'Link' %}"> <td class="col-link" data-label="{% trans 'Link' %}">
<a style="background-color : #00636e;color : #FFF; padding : 4px 10px; white-space: nowrap;" <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"> target="_blank">
{% trans 'Apply' %} {% trans 'Apply' %}
</a> </a>

View File

@ -117,28 +117,21 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row g-4"> <div class="row g-4">
<div class="col-md-8"> <div class="col-md-12">
<div> <div>
<label for="{{ form.title.id_for_label }}" class="form-label">{% trans "Job Title" %} <span class="text-danger">*</span></label> <label for="{{ form.title.id_for_label }}" class="form-label">{% trans "Job Title" %} <span class="text-danger">*</span></label>
{{ form.title }} {{ form.title }}
{% if form.title.errors %}<div class="text-danger small mt-1">{{ form.title.errors }}</div>{% endif %} {% if form.title.errors %}<div class="text-danger small mt-1">{{ form.title.errors }}</div>{% endif %}
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-6">
<div> <div>
<label for="{{ form.job_type.id_for_label }}" class="form-label">{% trans "Job Type" %} <span class="text-danger">*</span></label> <label for="{{ form.job_type.id_for_label }}" class="form-label">{% trans "Job Type" %} <span class="text-danger">*</span></label>
{{ form.job_type }} {{ form.job_type }}
{% if form.job_type.errors %}<div class="text-danger small mt-1">{{ form.job_type.errors }}</div>{% endif %} {% if form.job_type.errors %}<div class="text-danger small mt-1">{{ form.job_type.errors }}</div>{% endif %}
</div> </div>
</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 class="col-md-6">
<div> <div>
<label for="{{ form.workplace_type.id_for_label }}" class="form-label">{% trans "Workplace Type" %} <span class="text-danger">*</span></label> <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 %} {% if form.workplace_type.errors %}<div class="text-danger small mt-1">{{ form.workplace_type.errors }}</div>{% endif %}
</div> </div>
</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 class="col-md-6">
<div> <div>
<label for="{{ form.position_number.id_for_label }}" class="form-label">{% trans "Position Number" %}</label> <label for="{{ form.application_deadline.id_for_label }}" class="form-label">{% trans "Application Deadline" %}<span class="text-danger">*</span></label>
{{ form.position_number }} {{ form.application_deadline }}
{% if form.position_number.errors %}<div class="text-danger small mt-1">{{ form.position_number.errors }}</div>{% endif %} {% if form.application_deadline.errors %}<div class="text-danger small mt-1">{{ form.application_deadline.errors }}</div>{% endif %}
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div> <div>
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">{% trans "Reports To" %}</label> <label for="{{ form.department.id_for_label }}" class="form-label">{% trans "Department" %}</label>
{{ form.reporting_to }} {{ form.department }}
{% if form.reporting_to.errors %}<div class="text-danger small mt-1">{{ form.reporting_to.errors }}</div>{% endif %} {% if form.department.errors %}<div class="text-danger small mt-1">{{ form.department.errors }}</div>{% endif %}
</div> </div>
</div> </div>
<div class="col-md-6"> <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 %} {% if form.max_applications.errors %}<div class="text-danger small mt-1">{{ form.max_applications.errors }}</div>{% endif %}
</div> </div>
</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> </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) #} {# SECTION 4: JOB CONTENT (CKEDITOR 5 Fields) #}
{# ================================================= #} {# ================================================= #}
@ -313,8 +233,90 @@
</div> </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 %}
</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 #} {# ACTION BUTTONS #}

View File

@ -117,28 +117,21 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row g-4"> <div class="row g-4">
<div class="col-md-8"> <div class="col-md-12">
<div> <div>
<label for="{{ form.title.id_for_label }}" class="form-label">{% trans "Job Title" %} <span class="text-danger">*</span></label> <label for="{{ form.title.id_for_label }}" class="form-label">{% trans "Job Title" %} <span class="text-danger">*</span></label>
{{ form.title }} {{ form.title }}
{% if form.title.errors %}<div class="text-danger small mt-1">{{ form.title.errors }}</div>{% endif %} {% if form.title.errors %}<div class="text-danger small mt-1">{{ form.title.errors }}</div>{% endif %}
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-6">
<div> <div>
<label for="{{ form.job_type.id_for_label }}" class="form-label">{% trans "Job Type" %} <span class="text-danger">*</span></label> <label for="{{ form.job_type.id_for_label }}" class="form-label">{% trans "Job Type" %} <span class="text-danger">*</span></label>
{{ form.job_type }} {{ form.job_type }}
{% if form.job_type.errors %}<div class="text-danger small mt-1">{{ form.job_type.errors }}</div>{% endif %} {% if form.job_type.errors %}<div class="text-danger small mt-1">{{ form.job_type.errors }}</div>{% endif %}
</div> </div>
</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 class="col-md-6">
<div> <div>
<label for="{{ form.workplace_type.id_for_label }}" class="form-label">{% trans "Workplace Type" %} <span class="text-danger">*</span></label> <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 %} {% if form.workplace_type.errors %}<div class="text-danger small mt-1">{{ form.workplace_type.errors }}</div>{% endif %}
</div> </div>
</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 class="col-md-6">
<div> <div>
<label for="{{ form.position_number.id_for_label }}" class="form-label">{% trans "Position Number" %}</label> <label for="{{ form.application_deadline.id_for_label }}" class="form-label">{% trans "Application Deadline" %}<span class="text-danger">*</span></label>
{{ form.position_number }} {{ form.application_deadline }}
{% if form.position_number.errors %}<div class="text-danger small mt-1">{{ form.position_number.errors }}</div>{% endif %} {% if form.application_deadline.errors %}<div class="text-danger small mt-1">{{ form.application_deadline.errors }}</div>{% endif %}
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<div> <div>
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">{% trans "Reports To" %}</label> <label for="{{ form.department.id_for_label }}" class="form-label">{% trans "Department" %}</label>
{{ form.reporting_to }} {{ form.department }}
{% if form.reporting_to.errors %}<div class="text-danger small mt-1">{{ form.reporting_to.errors }}</div>{% endif %} {% if form.department.errors %}<div class="text-danger small mt-1">{{ form.department.errors }}</div>{% endif %}
</div> </div>
</div> </div>
<div class="col-md-6"> <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 %} {% if form.max_applications.errors %}<div class="text-danger small mt-1">{{ form.max_applications.errors }}</div>{% endif %}
</div> </div>
</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> </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) #} {# SECTION 4: JOB CONTENT (CKEDITOR 5 Fields) #}
{# ================================================= #} {# ================================================= #}
@ -313,8 +233,90 @@
</div> </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 %}
</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 #} {# ACTION BUTTONS #}

View File

@ -4,7 +4,6 @@
{% block title %}{{ job.title }} - University ATS{% endblock %} {% block title %}{{ job.title }} - University ATS{% endblock %}
{% block customCSS %} {% block customCSS %}
<style> <style>
/* ================================================= */ /* ================================================= */
/* THEME VARIABLES AND GLOBAL STYLES */ /* THEME VARIABLES AND GLOBAL STYLES */
@ -12,41 +11,35 @@
:root { :root {
--kaauh-teal: #00636e; /* Primary */ --kaauh-teal: #00636e; /* Primary */
--kaauh-teal-dark: #004a53; --kaauh-teal-dark: #004a53;
--kaauh-teal-light: #4bb3be; /* For active glow */
--kaauh-border: #eaeff3; --kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40; --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 */ /* Primary Color Overrides for Bootstrap Classes */
--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 */
.text-primary { color: var(--kaauh-teal) !important; } .text-primary { color: var(--kaauh-teal) !important; }
.text-info { color: var(--stage-exam) !important; } .bg-primary { background-color: var(--kaauh-teal) !important; }
.text-success { color: var(--stage-offer) !important; }
.text-secondary { color: var(--stage-inactive) !important; } /* Status Badge Theme Mapping */
.bg-success { background-color: var(--kaauh-teal) !important; } .status-badge.bg-success { background-color: var(--color-active) !important; }
.bg-warning { background-color: #ffc107 !important; } .status-badge.bg-secondary { background-color: var(--color-draft) !important; }
.bg-secondary { background-color: #6c757d !important; } .status-badge.bg-warning { background-color: var(--color-closed) !important; }
.bg-danger { background-color: #dc3545 !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 */ /* Header styling */
.job-header-card { .job-header-card {
background: linear-gradient(135deg, var(--kaauh-teal), #004d57); background: linear-gradient(135deg, var(--kaauh-teal), var(--kaauh-teal-dark));
color: white; color: white;
border-radius: 0.75rem 0.75rem 0 0; border-radius: 0.75rem 0.75rem 0 0;
padding: 1.5rem; padding: 1.5rem;
@ -57,7 +50,7 @@
margin: 0; margin: 0;
font-size: 1.8rem; font-size: 1.8rem;
} }
/* Status badge */ /* Status badge - Consolidated style for all badges */
.status-badge { .status-badge {
font-size: 0.9rem; font-size: 0.9rem;
padding: 0.4em 0.8em; padding: 0.4em 0.8em;
@ -67,6 +60,7 @@
letter-spacing: 0.7px; letter-spacing: 0.7px;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
color: white; /* Ensure badge text is white */
} }
/* Card enhancements */ /* Card enhancements */
@ -91,12 +85,13 @@
border-bottom: 1px solid var(--kaauh-border); border-bottom: 1px solid var(--kaauh-border);
} }
/* Left Column Tabs Theming */ /* Tabs Theming - Applies to the right column */
.nav-tabs { .nav-tabs {
border-bottom: 1px solid var(--kaauh-border); border-bottom: 1px solid var(--kaauh-border);
background-color: #f8f9fa; background-color: #f8f9fa;
padding: 0 1.25rem; padding: 0;
} }
.nav-tabs .nav-link { .nav-tabs .nav-link {
border: none; border: none;
border-bottom: 3px solid transparent; border-bottom: 3px solid transparent;
@ -106,33 +101,22 @@
margin-right: 0.5rem; margin-right: 0.5rem;
transition: all 0.2s; transition: all 0.2s;
} }
/* Active Link */
.nav-tabs .nav-link.active { .nav-tabs .nav-link.active {
color: var(--kaauh-teal-dark) !important; color: var(--kaauh-teal-dark) !important;
background-color: white !important; background-color: white !important;
border-bottom: 3px solid var(--kaauh-teal) !important; border-bottom: 3px solid var(--kaauh-teal) !important;
font-weight: 600; font-weight: 600;
z-index: 2; z-index: 2;
} border-right-color: transparent !important;
margin-bottom: -1px;
/* 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;
} }
/* Main Action Button Style */ /* Main Action Button Style */
.btn-main-action { .btn-main-action, .btn-main-action:hover, .btn-main-action:active {
background-color: var(--kaauh-teal); background-color: var(--kaauh-teal) !important;
border-color: var(--kaauh-teal); border-color: var(--kaauh-teal) !important;
color: white; color: white !important;
font-weight: 600; font-weight: 600;
padding: 0.6rem 1.2rem; padding: 0.6rem 1.2rem;
transition: all 0.2s ease; transition: all 0.2s ease;
@ -142,92 +126,22 @@
justify-content: center; justify-content: center;
text-align: center; text-align: center;
} }
.btn-outline-secondary { .btn-outline-secondary { /* Apply primary colors for 'outline-secondary' used as theme buttons */
color: var(--kaauh-teal-dark); color: var(--kaauh-teal-dark) !important;
border-color: var(--kaauh-teal); border-color: var(--kaauh-teal) !important;
} }
.btn-outline-light { /* Fixing text color for the edit status button */
/* Applicant stats */ color: white !important;
.applicant-stats .stat-item { border-color: white !important;
padding: 0.75rem;
text-align: center;
border-radius: 0.5rem;
background-color: #f8f9fa;
border: 1px solid var(--kaauh-border);
} }
.applicant-stats .stat-item div:first-child { .btn-outline-light .text-primary { /* Override the edit icon's custom primary text class */
font-size: 1.6rem; color: white !important;
font-weight: 700;
} }
.applicant-stats .stat-item small { .kpi-card {
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 {
border-left: 4px solid var(--kaauh-teal); border-left: 4px solid var(--kaauh-teal);
background-color: #f0faff; background-color: #f0faff;
} }
.stats-grid .card-body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
</style> </style>
{% endblock %} {% endblock %}
@ -238,12 +152,12 @@
<ol class="breadcrumb"> <ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}" class="text-secondary">Home</a></li> <li class="breadcrumb-item"><a href="{% url '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"><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> </ol>
</nav> </nav>
<div class="row g-4"> <div class="row g-4">
{# LEFT COLUMN: JOB DETAILS WITH TABS #} {# LEFT COLUMN: JOB DETAILS (NO TABS) #}
<div class="col-lg-7"> <div class="col-lg-7">
<div class="card shadow-sm no-hover"> <div class="card shadow-sm no-hover">
@ -252,276 +166,136 @@
<div> <div>
<h2 class="mb-1">{{ job.title }}</h2> <h2 class="mb-1">{{ job.title }}</h2>
<small class="text-light">{% trans "JOB ID: "%}{{ job.internal_job_id }}</small> <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>
<div class="d-flex align-items-center gap-2"> <div class="d-flex align-items-center gap-2 mt-2 mt-md-0">
<span class="badge status-badge">
{# Corrected status badge logic to close the span correctly #} {# Status badge #}
{% if job.status == "ACTIVE" %} <div class="d-flex align-items-center">
<span class="badge bg-success status-badge"> <span class="status-badge
{% elif job.status == "DRAFT" %} {% if job.status == "ACTIVE" %}bg-success
<span class="badge bg-secondary status-badge"> {% elif job.status == "DRAFT" %}bg-secondary
{% elif job.status == "CLOSED" %} {% elif job.status == "CLOSED" %}bg-warning
<span class="badge bg-warning status-badge"> {% elif job.status == "CANCELLED" %}bg-danger
{% elif job.status == "CANCELLED" %} {% elif job.status == "ARCHIVED" %}bg-secondary
<span class="badge bg-danger status-badge"> {% else %}bg-secondary{% endif %}">
{% elif job.status == "ARCHIVED" %} {{ job.get_status_display }}
<span class="badge bg-secondary status-badge">
{% else %}
<span class="badge bg-secondary status-badge">
{% endif %}
{{ job.get_status_display }}
</span> </span>
<button type="button" class="btn btn-outline-light btn-sm ms-2" data-bs-toggle="modal" data-bs-target="#editStatusModal"> <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> </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> </span>
</div> </div>
</div> </div>
{# LEFT TABS NAVIGATION #} {# CONTENT: CORE DETAILS (No Tabs) #}
<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>
<div class="card-body"> <div class="card-body">
<div class="tab-content" id="jobTabsContent">
<h5 class="text-muted mb-3">{% trans "Administrative & Location" %}
{# TAB 1 CONTENT: CORE DETAILS #} <a href="{% url 'job_update' job.slug %}" class="btn btn-main-action btn-sm"><li class="fa fa-edit"></li>{% trans "Edit JOb" %}</a>
<div class="tab-pane fade show active" id="details" role="tabpanel" aria-labelledby="details-tab"> </h5>
<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="row g-3 mb-4 border-bottom pb-3 small text-secondary"> <div class="col-md-6">
<div class="col-md-6"> <i class="fas fa-building me-2 text-primary"></i> <strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }}
<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>
</div> </div>
<div class="col-md-6">
{# TAB 2 CONTENT: DESCRIPTION & REQUIREMENTS #} <i class="fas fa-hashtag me-2 text-primary"></i> <strong>{% trans "Position No:" %}</strong> {{ job.position_number|default:"N/A" }}
<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> </div>
<div class="col-md-6">
{# TAB 3 CONTENT: APPLICATION KPIS #} <i class="fas fa-briefcase me-2 text-primary"></i> <strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
<div class="tab-pane fade" id="kpis" role="tabpanel" aria-labelledby="kpis-tab"> </div>
<div class="row g-3 stats-grid"> <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 }}
{# 1. Job Avg. Score #} </div>
<div class="col-6 col-md-3"> <div class="col-md-6">
<div class="card text-center h-100 kpi-card"> <i class="fas fa-globe me-2 text-primary"></i> <strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
<div class="card-body p-2"> </div>
<i class="fas fa-star text-primary mb-1 d-block" style="font-size: 1.2rem;"></i> <div class="col-md-6">
<div class="h4 mb-0 text-primary fw-bold">{{ avg_match_score|floatformat:1 }}</div> <i class="fa-solid fa-money-bill me-2 text-primary"></i> <strong>{% trans "Salary:" %}</strong> {{ job.salary_range |default:"N/A" }}
<small class="text-muted d-block">{% trans "Avg. AI Score" %}</small> </div>
</div> <div class="col-md-6">
</div> <i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Created By:" %}</strong> {{ job.created_by|default:"N/A" }}
</div> </div>
<div class="col-md-6">
{# 2. High Potential Count #} <i class="fas fa-plus me-2 text-primary"></i> <strong>{% trans "Created At:" %}</strong> {{ job.created_at|default:"N/A" }}
<div class="col-6 col-md-3"> </div>
<div class="card text-center h-100"> <div class="col-md-6">
<div class="card-body p-2"> <i class="fas fa-edit me-2 text-primary"></i> <strong>{% trans "Updated At:" %}</strong> {{ job.updated_at|default:"N/A" }}
<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> </div>
</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> </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>
</div> </div>
{# RIGHT COLUMN: TABBED CARDS #} {# RIGHT COLUMN: TABBED CARDS #}
<div class="col-lg-5"> <div class="col-lg-5">
{# New Card for Candidate Category Chart #}
<div class="card shadow-sm no-hover mb-4"> <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 #} {# 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"> <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"> <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> </button>
</li> </li>
<li class="nav-item flex-fill" role="presentation"> <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"> <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> </button>
</li> </li>
<li class="nav-item flex-fill" role="presentation"> <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"> <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> </button>
</li> </li>
<li class="nav-item flex-fill" role="presentation"> <li class="nav-item flex-fill" role="presentation">
@ -531,70 +305,70 @@
</li> </li>
</ul> </ul>
<div class="tab-content mx-2 my-3" id="rightJobTabsContent"> <div class="tab-content p-3" id="rightJobTabsContent">
{# TAB 1: APPLICANTS CONTENT #} {# TAB 1: APPLICANTS CONTENT #}
<div class="tab-pane fade show active" id="applicants-pane" role="tabpanel" aria-labelledby="applicants-tab"> <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> <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"> <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>
<a href="{% url 'candidate_screening_view' job.slug %}" class="btn btn-main-action"> <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> </a>
</div> </div>
</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"> <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' %} {% 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> </div>
{# TAB 3: MANAGEMENT (Form Template) CONTENT #} {# TAB 3: MANAGEMENT (Form Template) CONTENT #}
<div class="tab-pane fade" id="manage-pane" role="tabpanel" aria-labelledby="manage-tab"> <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> <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"> <p class="text-muted small mb-3">
{% trans "Manage the custom application forms associated with this job posting." %} {% trans "Manage the custom application forms associated with this job posting." %}
</p> </p>
{% if not job.form_template %} {% if not job.form_template %}
<a href="{% url 'create_form_template' %}" class="btn btn-main-action"> <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> </a>
{% else %} {% 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" %} <i class="fas fa-list-alt me-1"></i> {% trans "View Form Template" %}
</a> </a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
{# TAB 4: LINKEDIN INTEGRATION CONTENT #} {# TAB 4: LINKEDIN INTEGRATION CONTENT #}
<div class="tab-pane fade" id="linkedin-pane" role="tabpanel" aria-labelledby="linkedin-tab"> <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> <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 %} {% 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!" %} <i class="fas fa-check-circle me-1"></i> {% trans "Posted successfully!" %}
</div> </div>
{% if job.linkedin_post_url %} {% 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" %} <i class="fab fa-linkedin me-1"></i> {% trans "View on LinkedIn" %}
</a> </a>
{% endif %} {% 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" }} {% trans "Posted on:" %} {{ job.linkedin_posted_at|date:"M d, Y" }}
</small> </small>
{% else %} {% 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 %} {% endif %}
<form method="post" action="{% url 'post_to_linkedin' job.slug %}" class="mt-2"> <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 %} {% if job.posted_to_linkedin %}{% trans "Re-post to LinkedIn" %}{% else %}{% trans "Post to LinkedIn" %}{% endif %}
</button> </button>
</form> </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 %} {% 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." %} {% trans "You need to" %} <a href="{% url 'linkedin_login' %}">{% trans "authenticate with LinkedIn" %}</a> {% trans "first." %}
</small> </small>
{% endif %} {% endif %}
@ -623,6 +401,82 @@
</div> </div>
</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> </div>
</div> </div>

View File

@ -311,7 +311,7 @@
<td class="text-center"> <td class="text-center">
<div class="btn-group btn-group-sm" role="group"> <div class="btn-group btn-group-sm" role="group">
{% if job.form_template %} {% 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> <i class="fas fa-eye"></i>
</a> </a>
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}"> <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-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-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 %} <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 %} {% else %}
{% trans "N/A" %} {% trans "N/A" %}
{% endif %} {% endif %}

View File

@ -3,7 +3,7 @@
{% block customCSS %} {% block customCSS %}
<style> <style>
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
/* KAAT-S Redesign CSS - Optimized Compact Detail View (Settings Removed) */ /* KAAT-S Redesign CSS - Optimized Compact Detail View (Comments Left) */
/* -------------------------------------------------------------------------- */ /* -------------------------------------------------------------------------- */
:root { :root {
@ -16,6 +16,11 @@
--kaauh-gray-light: #f8f9fa; /* Card Header/Footer Background */ --kaauh-gray-light: #f8f9fa; /* Card Header/Footer Background */
--kaauh-success: #198754; /* Success Green */ --kaauh-success: #198754; /* Success Green */
--kaauh-danger: #dc3545; /* Danger Red */ --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 { body {
@ -85,10 +90,6 @@ body {
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
} }
.detail-row-group {
padding: 0;
}
.detail-row { .detail-row {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@ -114,17 +115,34 @@ body {
flex-basis: 55%; 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 & Copy Button ------------------ */
.join-info-card { .join-info-card {
border-left: 5px solid var(--kaauh-teal); /* Highlight join info */ border-left: 5px solid var(--kaauh-teal); /* Highlight join info */
} }
/* Consolidated primary button style */
.btn-primary { .btn-primary {
background-color: var(--kaauh-teal); background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal); border-color: var(--kaauh-teal);
font-weight: 600; font-weight: 600;
padding: 0.6rem 1.25rem; padding: 0.6rem 1.25rem;
border-radius: 6px; border-radius: 6px;
transition: background-color 0.2s;
} }
.btn-primary:hover { .btn-primary:hover {
background-color: var(--kaauh-teal-dark); background-color: var(--kaauh-teal-dark);
@ -143,6 +161,11 @@ body {
.btn-copy { .btn-copy {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
background-color: var(--kaauh-teal-dark); 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 ------------------ */ /* ------------------ Footer & Actions ------------------ */
@ -150,32 +173,35 @@ body {
.action-bar-footer { .action-bar-footer {
border-top: 1px solid var(--kaauh-border); border-top: 1px solid var(--kaauh-border);
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
gap: 0.75rem;
background-color: var(--kaauh-gray-light); background-color: var(--kaauh-gray-light);
border-radius: 0 0 12px 12px; 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 { .btn-footer-action {
font-weight: 600; font-weight: 600;
padding: 0.5rem 1rem; /* Made buttons smaller and consistent */
padding: 0.4rem 0.8rem;
border-radius: 6px; border-radius: 6px;
font-size: 0.9rem; font-size: 0.85rem;
} }
/* ------------------ Comments Section ------------------ */ /* --- Comment Card Header Style --- */
#comments-card .card-header { #comments-card .card-header {
background-color: var(--kaauh-teal-light); background-color: white;
color: var(--kaauh-teal-dark); color: var(--kaauh-teal-dark);
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
font-weight: 600; font-weight: 600;
border-radius: 12px 12px 0 0; border-radius: 12px 12px 0 0;
} }
/* Comment card body/item styling is kept compact */
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<div class="container-fluid"> <div class="container-fluid py-4">
{# --- TOP BAR / BACK BUTTON --- #} {# --- TOP BAR / BACK BUTTON --- #}
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
@ -185,104 +211,202 @@ body {
</div> </div>
<div class="row g-4"> <div class="row g-4">
{# --- LEFT COLUMN (MAIN DETAILS) --- #}
<div class="col-lg-6"> {# --- LEFT COLUMN (COMMENTS & INTERNAL CONTEXT) - Takes 50% of the screen #}
<div class="card no-hover h-100"> <div class="col-lg-6 d-flex flex-column">
{# --- CONSOLIDATED HEADER --- #}
<div class="main-title-card"> {# --- 1. INTERNAL NOTES / DESCRIPTION CARD (New CRM Feature) --- #}
<div class="d-flex justify-content-between align-items-start"> {% if meeting.description %}
<div class="card-header-title-group"> <div class="card no-hover mb-4 flex-shrink-0">
<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 --- #}
<div class="card-body detail-section"> <div class="card-body detail-section">
<h2>{% trans "Core Details" %}</h2> <h2 class="d-flex align-items-center"><i class="fas fa-clipboard-list me-2"></i> {% trans "Internal Context" %}</h2>
<div class="detail-row-group"> <p class="text-muted small">{% trans "Meeting agenda, purpose, or interview details for internal team use." %}</p>
<div class="detail-row"><div class="detail-label">{% trans "Meeting ID" %}:</div><div class="detail-value">{{ meeting.meeting_id }}</div></div> <div class="p-3 bg-light rounded border">
<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> <p class="mb-0">{{ meeting.description|safe }}</p>
<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>
</div> </div>
</div>
{% endif %}
{# --- ACTION BAR AT THE BOTTOM OF THE MAIN CARD --- #} {# --- 2. Comments Section (Now in the Left Column) --- #}
<div class="card-footer action-bar-footer d-flex justify-content-end"> <div class="card no-hover flex-grow-1" id="comments-card">
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary btn-footer-action"> <div class="card-header d-flex justify-content-between align-items-center">
<i class="fas fa-edit me-1"></i> {% trans "Update" %} <h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
</a> <i class="fas fa-comments me-2"></i>
{% if meeting.zoom_gateway_response %} {% trans "Comments" %} ({{ meeting.comments.count }})
<button type="button" class="btn btn-secondary btn-footer-action" onclick="toggleGateway()"> </h5>
<i class="fas fa-code me-1"></i> {% trans "API Response" %} {% 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> </button>
{% endif %} {% endif %}
<button type="button" class="btn btn-danger btn-footer-action" title="{% trans 'Delete' %}" </div>
data-bs-toggle="modal" data-bs-target="#deleteModal" <div class="card-body overflow-auto">
hx-post="{% url 'delete_meeting' meeting.slug %}" <div id="comment-section">
hx-target="#deleteModalBody" {% if meeting.comments.all %}
hx-swap="outerHTML" {% for comment in meeting.comments.all|dictsortreversed:"created_at" %}
data-item-name="{{ meeting.topic }}"> <div class="card mb-3">
<i class="fas fa-trash-alt me-1"></i> <div class="card-header d-flex justify-content-between align-items-center">
Delete <div>
</button> <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> </div>
</div> </div>
{# --- RIGHT COLUMN (JOIN INFO) --- #} {# --- RIGHT COLUMN (MAIN DETAILS & JOIN INFO) - Takes 50% of the screen #}
<div class="col-lg-6"> <div class="col-lg-6">
{% if meeting.join_url %} <div class="d-flex flex-column h-100">
<div class="card no-hover join-info-card detail-section h-100">
<div class="card-body">
<h2>{% trans "Join Information" %}</h2>
<a href="{{ meeting.join_url }}" class="btn btn-primary w-100 mb-4" target="_blank"> {# --- CRM ASSOCIATED RECORD CARD (Elevated Importance) --- #}
<i class="fas fa-video me-1"></i> {% trans "Join Meeting Now" %} {% if meeting.interview %}
</a> <div class="card associated-record-card mb-4 flex-shrink-0">
<div class="card-body pt-3 pb-3">
<div class="join-url-container"> <div class="d-flex align-items-center">
<div id="copy-message" style="opacity: 0;">{% trans "Copied!" %}</div> <i class="fas fa-user-tag fa-2x me-3"></i>
<div>
<div class="join-url-display d-flex justify-content-between align-items-center"> <h6 class="mb-0 text-uppercase small fw-bold">{% trans "Associated Record" %}</h6>
<div class="text-truncate"> <span class="fw-bold fs-5 me-2">
<strong>{% trans "Join URL" %}:</strong> <a href="{% url 'candidate_detail' meeting.interview.candidate.slug %}" class="text-decoration-none">
<span id="meeting-join-url">{{ meeting.join_url }}</span> {{ meeting.interview.candidate.name }}
</a>
</span>
<span class="badge bg-secondary-subtle text-secondary small fw-normal">{{ meeting.interview.job_position }}</span>
</div> </div>
<button class="btn-copy ms-2" onclick="copyLink()" title="{% trans 'Copy URL' %}">
<i class="fas fa-copy"></i>
</button>
</div> </div>
</div> </div>
</div>
{% if meeting.password %} {% endif %}
<div class="detail-row" style="border: none; padding-top: 1rem;">
<div class="detail-label" style="font-size: 1rem;">{% trans "Password" %}:</div> {# --- 1. MAIN DETAILS CARD --- #}
<div class="detail-value fw-bolder text-danger">{{ meeting.password }}</div> <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> </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>
</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>
</div> </div>
@ -296,81 +420,22 @@ body {
</div> </div>
{% endif %} {% 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> </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 fade" id="commentModal" tabindex="-1" aria-labelledby="commentModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <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> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body" id="commentModalBody"> <div class="modal-body" id="commentModalBody">
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block customJS %} {% block customJS %}
<script> <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() { function copyLink() {
const urlElement = document.getElementById('meeting-join-url'); const urlElement = document.getElementById('meeting-join-url');
const displayContainer = urlElement.closest('.join-url-display');
const messageElement = document.getElementById('copy-message'); const messageElement = document.getElementById('copy-message');
const textToCopy = urlElement.textContent || urlElement.innerText; const textToCopy = urlElement.textContent || urlElement.innerText;
@ -396,6 +462,11 @@ body {
messageElement.style.backgroundColor = success ? 'var(--kaauh-success)' : 'var(--kaauh-danger)'; messageElement.style.backgroundColor = success ? 'var(--kaauh-success)' : 'var(--kaauh-danger)';
messageElement.style.opacity = '1'; 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(() => { window.copyMessageTimeout = setTimeout(() => {
messageElement.style.opacity = '0'; messageElement.style.opacity = '0';
}, 2000); }, 2000);

View File

@ -5,6 +5,7 @@
{% block customCSS %} {% block customCSS %}
<style> <style>
/* ... (Your existing CSS code is here) ... */
/* ================================================= */ /* ================================================= */
/* THEME VARIABLES AND GLOBAL STYLES */ /* THEME VARIABLES AND GLOBAL STYLES */
/* ================================================= */ /* ================================================= */
@ -225,25 +226,14 @@
<i class="fas fa-id-card me-1"></i> {% trans "Contact & Job" %} <i class="fas fa-id-card me-1"></i> {% trans "Contact & Job" %}
</button> </button>
</li> </li>
{% if candidate.resume %}
<li class="nav-item" role="presentation"> <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"> {# NEW TAB ADDED HERE #}
<i class="fas fa-file-pdf me-1"></i> {% trans "Resume" %} <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> </button>
</li> </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> </ul>
<div class="card-body"> <div class="card-body">
@ -290,35 +280,92 @@
</div> </div>
{# TAB 2 CONTENT: RESUME #} {# 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> {# NEW TAB 3 CONTENT: CANDIDATE JOURNEY TIMELINE #}
<div class="d-flex align-items-center justify-content-between p-3 border rounded"> <div class="tab-pane fade" id="timeline-pane" role="tabpanel" aria-labelledby="timeline-tab">
{% comment %} <div> {# ENHANCED: CANDIDATE JOURNEY TIMELINE CARD #}
<p class="mb-1"><strong>{{ candidate.resume.name }}</strong></p> <div class="card shadow-sm timeline-card">
<small class="text-muted">{{ candidate.resume.name|truncatechars:30 }}</small> <div class="card-header bg-white border-bottom py-3">
</div> {% endcomment %} <h5 class="mb-0 text-muted"><i class="fas fa-route me-2"></i>{% trans "Candidate Journey" %}</h5>
<div class="d-flex flex-column gap-2"> </div>
<div class="d-flex gap-2"> <div class="card-body p-4">
<a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
<i class="fas fa-eye me-1"></i> <h6 class="text-uppercase text-secondary mb-3">{% trans "Current Stage" %}</h6>
{% trans "View Actual Resume" %} <div class="p-3 mb-4 rounded current-stage">
</a> <p class="mb-0 fw-bold fs-5 text-primary">{{ candidate.stage }}</p>
<a href="{{ candidate.resume.url }}" download class="btn btn-main-action"> <small class="text-muted d-block mt-1">
<i class="fas fa-download me-1"></i> {% trans "Latest status update:" %} {{ candidate.updated_at|date:"M d, Y" }}
{% trans "Download Resume" %} </small>
</a>
</div> </div>
<a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
<i class="fas fa-file-alt me-1"></i> <h6 class="text-uppercase text-secondary mb-3 pt-2 border-top">{% trans "Historical Timeline" %}</h6>
{% trans "View Resume AI Overview" %} <div class="timeline">
</a>
{# 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> </div>
{% endif %}
{# TAB 3 CONTENT: PARSED SUMMARY #} {# TAB 4 CONTENT: PARSED SUMMARY #}
{% if candidate.parsed_summary %} {% if candidate.parsed_summary %}
<div class="tab-pane fade" id="summary-pane" role="tabpanel" aria-labelledby="summary-tab"> <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> <h5 class="text-primary mb-4">{% trans "AI Generated Summary" %}</h5>
@ -328,13 +375,12 @@
</div> </div>
{% endif %} {% endif %}
{# TAB 4 CONTENT: AI ANALYSIS #} {# TAB 5 CONTENT: AI ANALYSIS #}
{% if candidate.is_resume_parsed %} {% if candidate.is_resume_parsed %}
<div class="tab-pane fade" id="analysis-pane" role="tabpanel" aria-labelledby="analysis-tab"> <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> <h5 class="text-primary mb-4">{% trans "AI Analysis Report" %}</h5>
<div class="border-start border-primary ps-3 pt-1 pb-1"> <div class="border-start border-primary ps-3 pt-1 pb-1">
{% with analysis=candidate.ai_analysis_data %} {% with analysis=candidate.ai_analysis_data %}
{# Match Score Card #} {# 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="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"> <div class="d-flex justify-content-between align-items-center mb-2">
@ -482,9 +528,9 @@
{# RIGHT COLUMN: ACTIONS AND CANDIDATE TIMELINE #} {# RIGHT COLUMN: ACTIONS AND CANDIDATE TIMELINE #}
<div class="col-lg-4"> <div class="col-lg-4">
{# ACTIONS CARD #} {# ACTIONS CARD #}
{% if user.is_staff %}
<div class="card shadow-sm mb-4 p-3"> <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> <h5 class="text-muted mb-3"><i class="fas fa-cog me-2"></i>{% trans "Management Actions" %}</h5>
<div class="d-grid gap-2"> <div class="d-grid gap-2">
@ -497,95 +543,37 @@
<a href="{% url 'candidate_list' %}" class="btn btn-outline-secondary"> <a href="{% url 'candidate_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {% trans "Back to List" %} <i class="fas fa-arrow-left"></i> {% trans "Back to List" %}
</a> </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>
</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> </div>
</div> </div>
{% include 'recruitment/candidate_resume_template.html' %}
{% if user.is_staff %} {% if user.is_staff %}
{% include "recruitment/partials/stage_update_modal.html" with candidate=candidate form=stage_form %} {% include "recruitment/partials/stage_update_modal.html" with candidate=candidate form=stage_form %}
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -2,7 +2,169 @@
{% load static i18n %} {% load static i18n %}
{% block title %}Candidate Tier Management - {{ job.title }} - ATS{% endblock %} {% 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 %} {% block content %}
<div class="container-fluid py-4"> <div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
@ -31,28 +193,39 @@
</h2> </h2>
<div class="kaauh-card shadow-sm p-3"> <div class="kaauh-card shadow-sm p-3">
{% if candidates %} {% if candidates %}
<div class="bulk-action-bar"> <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"> <form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
{% csrf_token %} {% 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;"> {# Using d-flex for horizontal alignment and align-items-end to align items to the bottom baseline #}
<option selected> <div class="d-flex align-items-end gap-3">
----------
</option> {# Select Input Group #}
<option value="Interview"> <div>
{% trans "Interview Stage" %} <label for="update_status" class="form-label small mb-1 fw-bold">{% trans "Move Selected To:" %}</label>
</option> <select name="mark_as" id="update_status" class="form-select form-select-sm" style="min-width: 150px;">
<option value="Applied"> <option selected>
{% trans "Screening Stage" %} ----------
</option> </option>
</select> <option value="Interview">
<button type="submit" class="btn btn-main-action btn-sm"> {% trans "Interview Stage" %}
<i class="fas fa-arrow-right me-1"></i> </option>
</button> <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> </form>
</div> </div>
{% endif %} {% endif %}
<div class="table-responsive"> <div class="table-responsive">
<form id="candidate-form" method="post"> <form id="candidate-form" method="post">
{% csrf_token %} {% csrf_token %}

View File

@ -2,7 +2,170 @@
{% load static i18n %} {% load static i18n %}
{% block title %}- {{ job.title }} - ATS{% endblock %} {% 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 %} {% block content %}
<div class="container-fluid py-4"> <div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-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"> <div class="kaauh-card shadow-sm p-3">
{% if candidates %} {% 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"> {# Form 1: Status Update #}
{% csrf_token %} <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">
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;"> {% csrf_token %}
<option selected>
---------- {# Select Input Group - No label needed for this one, so we just flex the select and button #}
</option> <select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
<option value="Offer"> <option selected>
{% trans "To Offer" %} ----------
</option> </option>
<option value="Exam"> <option value="Offer">
{% trans "To Exam" %} {% trans "To Offer" %}
</option> </option>
</select> <option value="Exam">
<button type="submit" class="btn btn-main-action btn-sm"> {% trans "To Exam" %}
<i class="fas fa-arrow-right me-1"></i> </option>
</button> </select>
</form> <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"> {# Separator (Vertical Rule) - Aligns automatically at the bottom with align-items-end #}
<button type="submit" class="btn btn-main-action btn-sm"> <div class="vr" style="height: 28px;"></div>
<i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %}
</button> {# Form 2: Schedule Interviews #}
</form> <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> </div>
{% endif %} {% endif %}
<div class="table-responsive"> <div class="table-responsive">

View File

@ -2,6 +2,170 @@
{% load static i18n %} {% load static i18n %}
{% block title %}- {{ job.title }} - ATS{% endblock %} {% 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 %} {% block content %}
<div class="container-fluid py-4"> <div class="container-fluid py-4">
@ -24,29 +188,38 @@
</div> </div>
<div class="kaauh-card shadow-sm p-3"> <div class="kaauh-card shadow-sm p-3">
{% if candidates %} {% if candidates %}
<div class="bulk-action-bar"> <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"> {# Form: Hired/Rejected Status Update #}
{% csrf_token %} <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">
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;"> {% csrf_token %}
<option selected>
---------- {# Select element #}
</option> <select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
<option value="Hired"> <option selected>
{% trans "To Hired" %} ----------
</option> </option>
<option value="Rejected"> <option value="Hired">
{% trans "To Rejected" %} {% trans "To Hired" %}
</option> </option>
</select> <option value="Rejected">
<button type="submit" class="btn btn-main-action btn-sm"> {% trans "To Rejected" %}
<i class="fas fa-arrow-right me-1"></i> </option>
</button> </select>
</form>
{# 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> </div>
{% endif %} {% endif %}
<div class="table-responsive"> <div class="table-responsive">

View File

@ -41,6 +41,7 @@
margin: 0 auto; margin: 0 auto;
padding: 1rem; /* p-4 */ padding: 1rem; /* p-4 */
} }
.content-grid { .content-grid {
display: grid; display: grid;
@ -516,7 +517,7 @@
</style> </style>
</head> </head>
<body class="bg-kaauh-light-bg font-sans"> <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 Section -->
<header class="header-box"> <header class="header-box">
<div class="header-info"> <div class="header-info">
@ -535,14 +536,14 @@
<!-- GitHub and LinkedIn links for quick access (null in example but included for completeness) --> <!-- GitHub and LinkedIn links for quick access (null in example but included for completeness) -->
{% if candidate.resume_data.linkedin %} {% if candidate.resume_data.linkedin %}
<div class="contact-item"> <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> </div>
{% endif %} {% endif %}
{% if candidate.resume_data.github %} {% if candidate.resume_data.github %}
<div class="contact-item"> <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> </div>
{% endif %} {% endif %}
</div> </div>

View File

@ -2,7 +2,170 @@
{% load static i18n %} {% load static i18n %}
{% block title %}Candidate Management - {{ job.title }} - University ATS{% endblock %} {% 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 %} {% block content %}
<div class="container-fluid py-4"> <div class="container-fluid py-4">
@ -97,23 +260,35 @@
<div class="kaauh-card p-3"> <div class="kaauh-card p-3">
{% if candidates %} {% if candidates %}
<div class="bulk-action-bar"> <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"> <form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
{% csrf_token %} {% 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;"> {# MODIFIED: Using d-flex for horizontal alignment and align-items-end to align everything based on the baseline of the button/select #}
<option selected> <div class="d-flex align-items-end gap-3">
----------
</option> {# Select Input Group #}
<option value="Exam"> <div>
{% trans "Exam Stage" %} <label for="update_status" class="form-label small mb-1 fw-bold">{% trans "Move Selected To:" %}</label>
</option> <select name="mark_as" id="update_status" class="form-select form-select-sm" style="min-width: 150px;">
</select> <option selected>
<button type="submit" class="btn btn-main-action btn-sm"> ----------
<i class="fas fa-arrow-right me-1"></i> {% trans "Update Status" %} </option>
</button> <option value="Exam">
</form> {% trans "Exam Stage" %}
</div> </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 %} {% endif %}
<div class="table-responsive"> <div class="table-responsive">

View File

@ -7,12 +7,13 @@
<style> <style>
/* UI Variables for the KAAT-S Theme (Teal/Consistent Look) */ /* UI Variables for the KAAT-S Theme (Teal/Consistent Look) */
:root { :root {
--kaauh-teal: #00636e; --kaauh-teal: #00636e; /* Dark Teal */
--kaauh-teal-light: #0093a3; --kaauh-teal-light: #0093a3;
--kaauh-teal-dark: #004a53; --kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3; --kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40; --kaauh-primary-text: #343a40;
--color-success: #28a745; --color-success: #28a745;
--color-warning: #ffc107; /* Standardized warning color */
--color-info: #17a2b8; --color-info: #17a2b8;
} }
@ -29,14 +30,33 @@
box-shadow: 0 6px 16px rgba(0,0,0,0.1); box-shadow: 0 6px 16px rgba(0,0,0,0.1);
} }
/* Card Header and Icon Styling */
.card-header { .card-header {
font-weight: 600; font-weight: 600;
padding: 1.25rem; padding: 1.25rem;
border-bottom: 1px solid var(--kaauh-border); border-bottom: 1px solid var(--kaauh-border);
background-color: #f8f9fa; 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 { .stats {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
@ -59,21 +79,27 @@
color: #6c757d; color: #6c757d;
padding-bottom: 1rem; padding-bottom: 1rem;
} }
/* Header Styling */ /* Dropdown/Filter Styling */
.card-header h3, .card-header h2 { .job-filter-container {
display: flex; display: flex;
align-items: center; align-items: center;
color: var(--kaauh-primary-text); gap: 10px;
font-size: 1.25rem;
font-weight: 600;
margin: 0;
} }
.form-select {
.stat-icon { border-color: var(--kaauh-border);
color: var(--kaauh-teal); border-radius: 0.5rem;
font-size: 1.75rem; font-size: 0.9rem;
margin-right: 0.75rem; }
.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 */ /* Chart Container */
@ -81,6 +107,16 @@
padding: 2rem; 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> </style>
{% endblock %} {% endblock %}
@ -89,6 +125,9 @@
<h1 class="mb-4" style="color: var(--kaauh-teal-dark); font-weight: 700;">{% trans "Recruitment Intelligence" %} 🧠</h1> <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="stats">
<div class="card"> <div class="card">
@ -140,84 +179,183 @@
</div> </div>
</div> </div>
<div class="card shadow-lg"> {# -------------------------------------------------------------------------- #}
<div class="card-header"> {# CHARTS SECTION (Using a row/col layout for structure) #}
<h2 class="d-flex align-items-center mb-0"> {# -------------------------------------------------------------------------- #}
<i class="fas fa-chart-bar stat-icon"></i> <div class="row g-4">
{% trans "Top 5 Application Volume" %}
</h2> {# 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>
<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> </div>
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<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 // Pass context data safely to JavaScript
const jobTitles = JSON.parse('{{ job_titles|escapejs }}').slice(0, 5); // Take top 5 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 jobAppCounts = JSON.parse('{{ job_app_counts|escapejs }}').slice(0, 5); // Take top 5
const ctx = document.getElementById('applicationsChart').getContext('2d'); // BAR CHART configuration
const chart = new Chart(ctx, { const ctxBar = document.getElementById('applicationsChart').getContext('2d');
new Chart(ctxBar, {
type: 'bar', type: 'bar',
data: { data: {
// Use the parsed and sliced data
labels: jobTitles, labels: jobTitles,
datasets: [{ datasets: [{
label: '{% trans "Applications" %}', label: '{% trans "Applications" %}',
data: jobAppCounts, data: jobAppCounts,
// Use the defined CSS variable for consistency backgroundColor: 'var(--kaauh-teal)',
backgroundColor: ' #00636e', // Green theme borderColor: 'var(--kaauh-teal-dark)',
borderColor: ' #004a53',
borderWidth: 1, borderWidth: 1,
barThickness: 50 barThickness: 50
}] }]
}, },
options: { options: {
responsive: true, responsive: true,
aspectRatio: 2.5, // Make the chart wider aspectRatio: 2.5,
plugins: { plugins: {
legend: { legend: { display: false },
display: false // Hide legend since there's only one dataset
},
title: { title: {
display: true, display: true,
text: 'Top 5 Most Applied Jobs', text: '{% trans "Top 5 Most Applied Jobs" %}',
font: { font: { size: 16 },
size: 16
},
color: 'var(--kaauh-primary-text)' color: 'var(--kaauh-primary-text)'
} }
}, },
scales: { scales: {
y: { y: {
beginAtZero: true, beginAtZero: true,
title: { title: { display: true, text: '{% trans "Total Applications" %}' },
display: true, ticks: { color: '#333333', precision: 0 },
text: 'Total Applications' grid: { color: '#e0e0e0' }
},
ticks: {
color: '#333333',
precision: 0 // Ensure y-axis labels are integers
},
grid: {
color: '#e0e0e0'
}
}, },
x: { x: {
ticks: { ticks: { color: '#333333' },
color: '#333333' grid: { display: false }
},
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> </script>
{% endblock %} {% endblock %}