Compare commits

..

No commits in common. "a181e845698769d2dc6980ec56e9aa7fd0df8502" and "ec3c52579bdaccc8189342e36a0430e007db4bb7" have entirely different histories.

40 changed files with 1258 additions and 2563 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -14,7 +14,8 @@ urlpatterns = [
path('jobs/<slug:slug>/update/', views.edit_job, name='job_update'),
# path('jobs/<slug:slug>/delete/', views., name='job_delete'),
path('jobs/<slug:slug>/', views.job_detail, name='job_detail'),
path('jobs/<slug:slug>/candidate/', views.job_detail_candidate, name='job_detail_candidate'),
path('jobs/<slug:slug>/candidate/application/success', views.application_success, name='application_success'),
path('careers/',views.kaauh_career,name='kaauh_career'),
# LinkedIn Integration URLs
@ -82,8 +83,8 @@ urlpatterns = [
path('htmx/<slug:slug>/candidate_update_status/', views.candidate_update_status, name='candidate_update_status'),
# path('forms/form/<slug:template_slug>/submit/', views.submit_form, name='submit_form'),
# path('forms/form/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'),
path('forms/form/<slug:template_slug>/submit/', views.submit_form, name='submit_form'),
path('forms/form/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'),
path('forms/<int:template_id>/submissions/<slug:slug>/', views.form_submission_details, name='form_submission_details'),
path('forms/template/<slug:slug>/submissions/', views.form_template_submissions_list, name='form_template_submissions_list'),
path('forms/template/<int:template_id>/all-submissions/', views.form_template_all_submissions, name='form_template_all_submissions'),
@ -139,7 +140,6 @@ urlpatterns = [
# Meeting Comments URLs
path('meetings/<slug:slug>/comments/add/', views.add_meeting_comment, name='add_meeting_comment'),
path('meetings/<slug:slug>/comments/<int:comment_id>/edit/', views.edit_meeting_comment, name='edit_meeting_comment'),
path('meetings/<slug:slug>/comments/<int:comment_id>/delete/', views.delete_meeting_comment, name='delete_meeting_comment'),
path('meetings/<slug:slug>/set_meeting_candidate/', views.set_meeting_candidate, name='set_meeting_candidate'),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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