candidates pipeline status chart
This commit is contained in:
parent
ec3c52579b
commit
3e99bb3dc9
Binary file not shown.
Binary file not shown.
@ -135,9 +135,9 @@ WSGI_APPLICATION = 'NorahUniversity.wsgi.application'
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||
'NAME': 'norahuniversity',
|
||||
'USER': 'norahuniversity',
|
||||
'PASSWORD': 'norahuniversity',
|
||||
'NAME': 'haikal_db',
|
||||
'USER': 'faheed',
|
||||
'PASSWORD': 'Faheed@215',
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': '5432',
|
||||
}
|
||||
|
||||
@ -23,9 +23,11 @@ urlpatterns = [
|
||||
# path('', include('recruitment.urls')),
|
||||
path("ckeditor5/", include('django_ckeditor_5.urls')),
|
||||
|
||||
path('form/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'),
|
||||
path('form/<slug:template_slug>/submit/', views.submit_form, name='submit_form'),
|
||||
|
||||
path('application/<slug:template_slug>/', views.application_submit_form, name='application_submit_form'),
|
||||
path('application/<slug:template_slug>/submit/', views.application_submit, name='application_submit'),
|
||||
path('application/<slug:slug>/apply/', views.application_detail, name='application_detail'),
|
||||
path('application/<slug:slug>/success/', views.application_success, name='application_success'),
|
||||
|
||||
path('api/templates/', views.list_form_templates, name='list_form_templates'),
|
||||
path('api/templates/save/', views.save_form_template, name='save_form_template'),
|
||||
path('api/templates/<slug:template_slug>/', views.load_form_template, name='load_form_template'),
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -5,16 +5,15 @@ from html import unescape
|
||||
from urllib.parse import quote, urlencode
|
||||
import requests
|
||||
import logging
|
||||
from django.conf import settings
|
||||
import time
|
||||
import random
|
||||
from django.utils import timezone
|
||||
from django.conf import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Define a constant for the API version for better maintenance
|
||||
# Define constants
|
||||
LINKEDIN_API_VERSION = '2.0.0'
|
||||
LINKEDIN_VERSION = '202409' # Modern API version for header control
|
||||
LINKEDIN_VERSION = '202409'
|
||||
MAX_POST_CHARS = 3000 # LinkedIn's maximum character limit for shareCommentary
|
||||
|
||||
class LinkedInService:
|
||||
def __init__(self):
|
||||
@ -23,11 +22,11 @@ class LinkedInService:
|
||||
self.redirect_uri = settings.LINKEDIN_REDIRECT_URI
|
||||
self.access_token = None
|
||||
# Configuration for image processing wait time
|
||||
self.ASSET_STATUS_TIMEOUT = 15 # Max time (seconds) to wait for image processing
|
||||
self.ASSET_STATUS_INTERVAL = 2 # Check every 2 seconds
|
||||
|
||||
# --- AUTHENTICATION & PROFILE ---
|
||||
self.ASSET_STATUS_TIMEOUT = 15
|
||||
self.ASSET_STATUS_INTERVAL = 2
|
||||
|
||||
# ---------------- AUTHENTICATION & PROFILE ----------------
|
||||
|
||||
def get_auth_url(self):
|
||||
"""Generate LinkedIn OAuth URL"""
|
||||
params = {
|
||||
@ -76,7 +75,7 @@ class LinkedInService:
|
||||
logger.error(f"Error getting user profile: {e}")
|
||||
raise
|
||||
|
||||
# --- ASSET UPLOAD & STATUS ---
|
||||
# ---------------- ASSET UPLOAD & STATUS ----------------
|
||||
|
||||
def get_asset_status(self, asset_urn):
|
||||
"""Checks the status of a registered asset (image) to ensure it's READY."""
|
||||
@ -86,7 +85,7 @@ class LinkedInService:
|
||||
'X-Restli-Protocol-Version': LINKEDIN_API_VERSION,
|
||||
'LinkedIn-Version': LINKEDIN_VERSION,
|
||||
}
|
||||
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
@ -129,6 +128,7 @@ class LinkedInService:
|
||||
"""Step 2: Upload image file and poll for 'READY' status."""
|
||||
image_file.open()
|
||||
image_content = image_file.read()
|
||||
image_file.seek(0) # Reset pointer after reading
|
||||
image_file.close()
|
||||
|
||||
headers = {
|
||||
@ -137,8 +137,8 @@ class LinkedInService:
|
||||
|
||||
response = requests.post(upload_url, headers=headers, data=image_content, timeout=60)
|
||||
response.raise_for_status()
|
||||
|
||||
# --- CRITICAL FIX: POLL FOR ASSET STATUS ---
|
||||
|
||||
# --- POLL FOR ASSET STATUS ---
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < self.ASSET_STATUS_TIMEOUT:
|
||||
try:
|
||||
@ -149,56 +149,55 @@ class LinkedInService:
|
||||
return True
|
||||
if status == "FAILED":
|
||||
raise Exception(f"LinkedIn image processing failed for asset {asset_urn}")
|
||||
|
||||
|
||||
logger.info(f"Asset {asset_urn} status: {status}. Waiting...")
|
||||
time.sleep(self.ASSET_STATUS_INTERVAL)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error during asset status check for {asset_urn}: {e}. Retrying.")
|
||||
time.sleep(self.ASSET_STATUS_INTERVAL * 2)
|
||||
time.sleep(self.ASSET_STATUS_INTERVAL * 2)
|
||||
|
||||
# If the loop times out, return True to attempt post, but log warning
|
||||
logger.warning(f"Asset {asset_urn} timed out, but upload succeeded. Forcing post attempt.")
|
||||
return True
|
||||
|
||||
# --- POSTING LOGIC ---
|
||||
# ---------------- POSTING UTILITIES ----------------
|
||||
|
||||
def clean_html_for_social_post(self, html_content):
|
||||
"""Converts safe HTML to plain text with basic formatting (bullets, bold, newlines)."""
|
||||
"""Converts safe HTML to plain text with basic formatting."""
|
||||
if not html_content:
|
||||
return ""
|
||||
|
||||
text = html_content
|
||||
|
||||
|
||||
# 1. Convert Bolding tags to *Markdown*
|
||||
text = re.sub(r'<strong>(.*?)</strong>', r'*\1*', text, flags=re.IGNORECASE)
|
||||
text = re.sub(r'<b>(.*?)</b>', r'*\1*', text, flags=re.IGNORECASE)
|
||||
|
||||
# 2. Handle Lists: Convert <li> tags into a bullet point
|
||||
text = re.sub(r'</(ul|ol|div)>', '\n', text, flags=re.IGNORECASE)
|
||||
text = re.sub(r'<li[^>]*>', '• ', text, flags=re.IGNORECASE)
|
||||
text = re.sub(r'</li>', '\n', text, flags=re.IGNORECASE)
|
||||
|
||||
text = re.sub(r'</(ul|ol|div)>', '\n', text, flags=re.IGNORECASE)
|
||||
text = re.sub(r'<li[^>]*>', '• ', text, flags=re.IGNORECASE)
|
||||
text = re.sub(r'</li>', '\n', text, flags=re.IGNORECASE)
|
||||
|
||||
# 3. Handle Paragraphs and Line Breaks
|
||||
text = re.sub(r'</p>', '\n\n', text, flags=re.IGNORECASE)
|
||||
text = re.sub(r'<br/?>', '\n', text, flags=re.IGNORECASE)
|
||||
|
||||
|
||||
# 4. Strip all remaining, unsupported HTML tags
|
||||
clean_text = re.sub(r'<[^>]+>', '', text)
|
||||
|
||||
|
||||
# 5. Unescape HTML entities
|
||||
clean_text = unescape(clean_text)
|
||||
|
||||
|
||||
# 6. Clean up excessive whitespace/newlines
|
||||
clean_text = re.sub(r'(\n\s*){3,}', '\n\n', clean_text).strip()
|
||||
|
||||
|
||||
return clean_text
|
||||
|
||||
def hashtags_list(self, hash_tags_str):
|
||||
"""Convert comma-separated hashtags string to list"""
|
||||
if not hash_tags_str:
|
||||
return ["#HigherEd", "#Hiring", "#UniversityJobs"]
|
||||
|
||||
|
||||
tags = [tag.strip() for tag in hash_tags_str.split(',') if tag.strip()]
|
||||
tags = [tag if tag.startswith('#') else f'#{tag}' for tag in tags]
|
||||
|
||||
@ -208,15 +207,18 @@ class LinkedInService:
|
||||
return tags
|
||||
|
||||
def _build_post_message(self, job_posting):
|
||||
"""Centralized logic to construct the professionally formatted text message."""
|
||||
"""
|
||||
Constructs the final text message.
|
||||
Includes a unique suffix for duplicate content prevention (422 fix).
|
||||
"""
|
||||
message_parts = [
|
||||
f"🔥 *Job Alert!* We’re looking for a talented professional to join our team.",
|
||||
f"👉 **{job_posting.title}** 👈",
|
||||
]
|
||||
|
||||
|
||||
if job_posting.department:
|
||||
message_parts.append(f"*{job_posting.department}*")
|
||||
|
||||
message_parts.append(f"*{job_posting.department}*")
|
||||
|
||||
message_parts.append("\n" + "=" * 25 + "\n")
|
||||
|
||||
# KEY DETAILS SECTION
|
||||
@ -229,7 +231,7 @@ class LinkedInService:
|
||||
details_list.append(f"🏠 Workplace: {job_posting.get_workplace_type_display()}")
|
||||
if job_posting.salary_range:
|
||||
details_list.append(f"💰 Salary: {job_posting.salary_range}")
|
||||
|
||||
|
||||
if details_list:
|
||||
message_parts.append("*Key Information*:")
|
||||
message_parts.extend(details_list)
|
||||
@ -239,11 +241,13 @@ class LinkedInService:
|
||||
clean_description = self.clean_html_for_social_post(job_posting.description)
|
||||
if clean_description:
|
||||
message_parts.append(f"🔎 *About the Role:*\n{clean_description}")
|
||||
|
||||
|
||||
# CALL TO ACTION
|
||||
if job_posting.application_url:
|
||||
message_parts.append(f"\n\n---")
|
||||
message_parts.append(f"🔗 **APPLY NOW:** {job_posting.application_url}")
|
||||
# CRITICAL: Include the URL explicitly in the text body.
|
||||
# When media_category is NONE, LinkedIn often makes these URLs clickable.
|
||||
message_parts.append(f"🔗 **APPLY NOW:** {job_posting.application_url}")
|
||||
|
||||
# HASHTAGS
|
||||
hashtags = self.hashtags_list(job_posting.hash_tags)
|
||||
@ -252,19 +256,38 @@ class LinkedInService:
|
||||
hashtags.insert(0, dept_hashtag)
|
||||
|
||||
message_parts.append("\n" + " ".join(hashtags))
|
||||
|
||||
if len(message_parts)>=3000:
|
||||
message_parts=message_parts[0:2980]+"........"
|
||||
|
||||
return "\n".join(message_parts)
|
||||
|
||||
final_message = "\n".join(message_parts)
|
||||
|
||||
# --- FIX: ADD UNIQUE SUFFIX AND HANDLE LENGTH (422 fix) ---
|
||||
unique_suffix = f"\n\n| Ref: {int(time.time())}"
|
||||
|
||||
available_length = MAX_POST_CHARS - len(unique_suffix)
|
||||
|
||||
if len(final_message) > available_length:
|
||||
logger.warning("Post message truncated due to character limit.")
|
||||
final_message = final_message[:available_length - 3] + "..."
|
||||
|
||||
return final_message + unique_suffix
|
||||
|
||||
|
||||
# ---------------- MAIN POSTING METHODS ----------------
|
||||
|
||||
def _send_ugc_post(self, person_urn, job_posting, media_category="NONE", media_list=None):
|
||||
"""
|
||||
New private method to handle the final UGC post request (text or image).
|
||||
This eliminates the duplication between create_job_post and create_job_post_with_image.
|
||||
Private method to handle the final UGC post request.
|
||||
CRITICAL FIX: Avoids ARTICLE category if not using an image to prevent 402 errors.
|
||||
"""
|
||||
|
||||
|
||||
message = self._build_post_message(job_posting)
|
||||
|
||||
# --- FIX FOR 402: Force NONE if no image is present. ---
|
||||
if media_category != "IMAGE":
|
||||
# We explicitly force pure text share to avoid LinkedIn's link crawler
|
||||
# which triggers the commercial 402 error on job reposts.
|
||||
media_category = "NONE"
|
||||
media_list = None
|
||||
# --------------------------------------------------------
|
||||
|
||||
url = "https://api.linkedin.com/v2/ugcPosts"
|
||||
headers = {
|
||||
@ -280,8 +303,8 @@ class LinkedInService:
|
||||
"shareMediaCategory": media_category,
|
||||
}
|
||||
}
|
||||
|
||||
if media_list:
|
||||
|
||||
if media_list and media_category == "IMAGE":
|
||||
specific_content["com.linkedin.ugc.ShareContent"]["media"] = media_list
|
||||
|
||||
payload = {
|
||||
@ -294,8 +317,13 @@ class LinkedInService:
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=60)
|
||||
|
||||
# Log 402/422 details
|
||||
if response.status_code in [402, 422]:
|
||||
logger.error(f"{response.status_code} UGC Post Error Detail: {response.text}")
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
post_id = response.headers.get('x-restli-id', '')
|
||||
post_url = f"https://www.linkedin.com/feed/update/{quote(post_id)}/" if post_id else ""
|
||||
|
||||
@ -308,18 +336,21 @@ class LinkedInService:
|
||||
|
||||
|
||||
def create_job_post_with_image(self, job_posting, image_file, person_urn, asset_urn):
|
||||
"""Step 3: Creates the final LinkedIn post payload with the image asset."""
|
||||
"""Creates the final LinkedIn post payload with the image asset."""
|
||||
|
||||
if not job_posting.application_url:
|
||||
raise ValueError("Application URL is required for image link share on LinkedIn.")
|
||||
|
||||
# Prepare the media list for the _send_ugc_post helper
|
||||
# Media list for IMAGE category (retains link details)
|
||||
# Note: This is an exception where we MUST provide link details for the image card
|
||||
media_list = [{
|
||||
"status": "READY",
|
||||
"media": asset_urn,
|
||||
"description": {"text": job_posting.title},
|
||||
"originalUrl": job_posting.application_url,
|
||||
"originalUrl": job_posting.application_url,
|
||||
"title": {"text": "Apply Now"}
|
||||
}]
|
||||
|
||||
# Use the helper method to send the post
|
||||
return self._send_ugc_post(
|
||||
person_urn=person_urn,
|
||||
job_posting=job_posting,
|
||||
@ -344,47 +375,44 @@ class LinkedInService:
|
||||
|
||||
# Check for image and attempt post
|
||||
try:
|
||||
# Assuming correct model path: job_posting.related_model_name.first().image_field_name
|
||||
image_upload = job_posting.post_images.first().post_image
|
||||
has_image = image_upload is not None
|
||||
except Exception:
|
||||
pass # No image available
|
||||
pass
|
||||
|
||||
if has_image:
|
||||
try:
|
||||
# Step 1: Register
|
||||
# Steps 1, 2, 3 for image post
|
||||
upload_info = self.register_image_upload(person_urn)
|
||||
asset_urn = upload_info['asset']
|
||||
|
||||
# Step 2: Upload and WAIT FOR READY (Crucial for 422 fix)
|
||||
self.upload_image_to_linkedin(
|
||||
upload_info['upload_url'],
|
||||
image_upload,
|
||||
image_upload,
|
||||
asset_urn
|
||||
)
|
||||
|
||||
# Step 3: Create post with image
|
||||
|
||||
return self.create_job_post_with_image(
|
||||
job_posting, image_upload, person_urn, asset_urn
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Image post failed, falling back to text: {e}")
|
||||
# Force fallback to text-only if image posting fails
|
||||
has_image = False
|
||||
has_image = False
|
||||
|
||||
# === FALLBACK TO PURE TEXT POST (shareMediaCategory: NONE) ===
|
||||
# Use the single helper method here
|
||||
# The _send_ugc_post method now ensures this is a PURE text post
|
||||
# to avoid the 402/ARTICLE-related issues.
|
||||
return self._send_ugc_post(
|
||||
person_urn=person_urn,
|
||||
job_posting=job_posting,
|
||||
media_category="NONE"
|
||||
media_category="NONE"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating LinkedIn post: {e}")
|
||||
status_code = getattr(getattr(e, 'response', None), 'status_code', 500)
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'status_code': getattr(e.response, 'status_code', 500) if hasattr(e, 'response') else 500
|
||||
'status_code': status_code
|
||||
}
|
||||
Binary file not shown.
476
recruitment/migrations/0001_initial.py
Normal file
476
recruitment/migrations/0001_initial.py
Normal file
@ -0,0 +1,476 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-22 16:33
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import django_ckeditor_5.fields
|
||||
import django_countries.fields
|
||||
import django_extensions.db.fields
|
||||
import recruitment.validators
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BreakTime',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('start_time', models.TimeField(verbose_name='Start Time')),
|
||||
('end_time', models.TimeField(verbose_name='End Time')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormStage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('name', models.CharField(help_text='Name of the stage', max_length=200)),
|
||||
('order', models.PositiveIntegerField(default=0, help_text='Order of the stage in the form')),
|
||||
('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default resume stage')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Stage',
|
||||
'verbose_name_plural': 'Form Stages',
|
||||
'ordering': ['order'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HiringAgency',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('name', models.CharField(max_length=200, unique=True, verbose_name='Agency Name')),
|
||||
('contact_person', models.CharField(blank=True, max_length=150, verbose_name='Contact Person')),
|
||||
('email', models.EmailField(blank=True, max_length=254)),
|
||||
('phone', models.CharField(blank=True, max_length=20)),
|
||||
('website', models.URLField(blank=True)),
|
||||
('notes', models.TextField(blank=True, help_text='Internal notes about the agency')),
|
||||
('country', django_countries.fields.CountryField(blank=True, max_length=2, null=True)),
|
||||
('address', models.TextField(blank=True, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Hiring Agency',
|
||||
'verbose_name_plural': 'Hiring Agencies',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Source',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('name', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, unique=True, verbose_name='Source Name')),
|
||||
('source_type', models.CharField(help_text='e.g., ATS, ERP ', max_length=100, verbose_name='Source Type')),
|
||||
('description', models.TextField(blank=True, help_text='A description of the source', verbose_name='Description')),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, help_text='The IP address of the source', null=True, verbose_name='IP Address')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('api_key', models.CharField(blank=True, help_text='API key for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Key')),
|
||||
('api_secret', models.CharField(blank=True, help_text='API secret for authentication (will be encrypted)', max_length=255, null=True, verbose_name='API Secret')),
|
||||
('trusted_ips', models.TextField(blank=True, help_text='Comma-separated list of trusted IP addresses', null=True, verbose_name='Trusted IP Addresses')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Whether this source is active for integration', verbose_name='Active')),
|
||||
('integration_version', models.CharField(blank=True, help_text='Version of the integration protocol', max_length=50, verbose_name='Integration Version')),
|
||||
('last_sync_at', models.DateTimeField(blank=True, help_text='Timestamp of the last successful synchronization', null=True, verbose_name='Last Sync At')),
|
||||
('sync_status', models.CharField(blank=True, choices=[('IDLE', 'Idle'), ('SYNCING', 'Syncing'), ('ERROR', 'Error'), ('DISABLED', 'Disabled')], default='IDLE', max_length=20, verbose_name='Sync Status')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Source',
|
||||
'verbose_name_plural': 'Sources',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ZoomMeeting',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('topic', models.CharField(max_length=255, verbose_name='Topic')),
|
||||
('meeting_id', models.CharField(db_index=True, max_length=20, unique=True, verbose_name='Meeting ID')),
|
||||
('start_time', models.DateTimeField(db_index=True, verbose_name='Start Time')),
|
||||
('duration', models.PositiveIntegerField(verbose_name='Duration')),
|
||||
('timezone', models.CharField(max_length=50, verbose_name='Timezone')),
|
||||
('join_url', models.URLField(verbose_name='Join URL')),
|
||||
('participant_video', models.BooleanField(default=True, verbose_name='Participant Video')),
|
||||
('password', models.CharField(blank=True, max_length=20, null=True, verbose_name='Password')),
|
||||
('join_before_host', models.BooleanField(default=False, verbose_name='Join Before Host')),
|
||||
('mute_upon_entry', models.BooleanField(default=False, verbose_name='Mute Upon Entry')),
|
||||
('waiting_room', models.BooleanField(default=False, verbose_name='Waiting Room')),
|
||||
('zoom_gateway_response', models.JSONField(blank=True, null=True, verbose_name='Zoom Gateway Response')),
|
||||
('status', models.CharField(blank=True, db_index=True, default='waiting', max_length=20, null=True, verbose_name='Status')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormField',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('label', models.CharField(help_text='Label for the field', max_length=200)),
|
||||
('field_type', models.CharField(choices=[('text', 'Text Input'), ('email', 'Email'), ('phone', 'Phone'), ('textarea', 'Text Area'), ('file', 'File Upload'), ('date', 'Date Picker'), ('select', 'Dropdown'), ('radio', 'Radio Buttons'), ('checkbox', 'Checkboxes')], help_text='Type of the field', max_length=20)),
|
||||
('placeholder', models.CharField(blank=True, help_text='Placeholder text', max_length=200)),
|
||||
('required', models.BooleanField(default=False, help_text='Whether the field is required')),
|
||||
('order', models.PositiveIntegerField(default=0, help_text='Order of the field in the stage')),
|
||||
('is_predefined', models.BooleanField(default=False, help_text='Whether this is a default field')),
|
||||
('options', models.JSONField(blank=True, default=list, help_text='Options for selection fields (stored as JSON array)')),
|
||||
('file_types', models.CharField(blank=True, help_text="Allowed file types (comma-separated, e.g., '.pdf,.doc,.docx')", max_length=200)),
|
||||
('max_file_size', models.PositiveIntegerField(default=5, help_text='Maximum file size in MB (default: 5MB)')),
|
||||
('multiple_files', models.BooleanField(default=False, help_text='Allow multiple files to be uploaded')),
|
||||
('max_files', models.PositiveIntegerField(default=1, help_text='Maximum number of files allowed (when multiple_files is True)')),
|
||||
('stage', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='recruitment.formstage')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Field',
|
||||
'verbose_name_plural': 'Form Fields',
|
||||
'ordering': ['order'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('name', models.CharField(help_text='Name of the form template', max_length=200)),
|
||||
('description', models.TextField(blank=True, help_text='Description of the form template')),
|
||||
('is_active', models.BooleanField(default=False, help_text='Whether this template is active')),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='form_templates', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Template',
|
||||
'verbose_name_plural': 'Form Templates',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FormSubmission',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('submitted_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('applicant_name', models.CharField(blank=True, help_text='Name of the applicant', max_length=200)),
|
||||
('applicant_email', models.EmailField(blank=True, db_index=True, help_text='Email of the applicant', max_length=254)),
|
||||
('submitted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='form_submissions', to=settings.AUTH_USER_MODEL)),
|
||||
('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='submissions', to='recruitment.formtemplate')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Form Submission',
|
||||
'verbose_name_plural': 'Form Submissions',
|
||||
'ordering': ['-submitted_at'],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='formstage',
|
||||
name='template',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stages', to='recruitment.formtemplate'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Candidate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('first_name', models.CharField(max_length=255, verbose_name='First Name')),
|
||||
('last_name', models.CharField(max_length=255, verbose_name='Last Name')),
|
||||
('email', models.EmailField(db_index=True, max_length=254, verbose_name='Email')),
|
||||
('phone', models.CharField(max_length=20, verbose_name='Phone')),
|
||||
('address', models.TextField(max_length=200, verbose_name='Address')),
|
||||
('resume', models.FileField(upload_to='resumes/', verbose_name='Resume')),
|
||||
('is_resume_parsed', models.BooleanField(default=False, verbose_name='Resume Parsed')),
|
||||
('is_potential_candidate', models.BooleanField(default=False, verbose_name='Potential Candidate')),
|
||||
('parsed_summary', models.TextField(blank=True, verbose_name='Parsed Summary')),
|
||||
('applied', models.BooleanField(default=False, verbose_name='Applied')),
|
||||
('stage', models.CharField(choices=[('Applied', 'Applied'), ('Exam', 'Exam'), ('Interview', 'Interview'), ('Offer', 'Offer')], db_index=True, default='Applied', max_length=100, verbose_name='Stage')),
|
||||
('applicant_status', models.CharField(blank=True, choices=[('Applicant', 'Applicant'), ('Candidate', 'Candidate')], default='Applicant', max_length=100, null=True, verbose_name='Applicant Status')),
|
||||
('exam_date', models.DateTimeField(blank=True, null=True, verbose_name='Exam Date')),
|
||||
('exam_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Exam Status')),
|
||||
('interview_date', models.DateTimeField(blank=True, null=True, verbose_name='Interview Date')),
|
||||
('interview_status', models.CharField(blank=True, choices=[('Passed', 'Passed'), ('Failed', 'Failed')], max_length=100, null=True, verbose_name='Interview Status')),
|
||||
('offer_date', models.DateField(blank=True, null=True, verbose_name='Offer Date')),
|
||||
('offer_status', models.CharField(blank=True, choices=[('Accepted', 'Accepted'), ('Rejected', 'Rejected')], max_length=100, null=True, verbose_name='Offer Status')),
|
||||
('join_date', models.DateField(blank=True, null=True, verbose_name='Join Date')),
|
||||
('ai_analysis_data', models.JSONField(default=dict, help_text='Full JSON output from the resume scoring model.', verbose_name='AI Analysis Data')),
|
||||
('submitted_by_agency', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='submitted_candidates', to='recruitment.hiringagency', verbose_name='Submitted by Agency')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Candidate',
|
||||
'verbose_name_plural': 'Candidates',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='JobPosting',
|
||||
fields=[
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('title', models.CharField(max_length=200)),
|
||||
('department', models.CharField(blank=True, max_length=100)),
|
||||
('job_type', models.CharField(choices=[('FULL_TIME', 'Full-time'), ('PART_TIME', 'Part-time'), ('CONTRACT', 'Contract'), ('INTERNSHIP', 'Internship'), ('FACULTY', 'Faculty'), ('TEMPORARY', 'Temporary')], default='FULL_TIME', max_length=20)),
|
||||
('workplace_type', models.CharField(choices=[('ON_SITE', 'On-site'), ('REMOTE', 'Remote'), ('HYBRID', 'Hybrid')], default='ON_SITE', max_length=20)),
|
||||
('location_city', models.CharField(blank=True, max_length=100)),
|
||||
('location_state', models.CharField(blank=True, max_length=100)),
|
||||
('location_country', models.CharField(default='Saudia Arabia', max_length=100)),
|
||||
('description', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Description')),
|
||||
('qualifications', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
|
||||
('salary_range', models.CharField(blank=True, help_text='e.g., $60,000 - $80,000', max_length=200)),
|
||||
('benefits', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
|
||||
('application_url', models.URLField(blank=True, help_text='URL where candidates apply', null=True, validators=[django.core.validators.URLValidator()])),
|
||||
('application_deadline', models.DateField(db_index=True)),
|
||||
('application_instructions', django_ckeditor_5.fields.CKEditor5Field(blank=True, null=True)),
|
||||
('internal_job_id', models.CharField(editable=False, max_length=50, primary_key=True, serialize=False)),
|
||||
('created_by', models.CharField(blank=True, help_text='Name of person who created this job', max_length=100)),
|
||||
('status', models.CharField(choices=[('DRAFT', 'Draft'), ('ACTIVE', 'Active'), ('CLOSED', 'Closed'), ('CANCELLED', 'Cancelled'), ('ARCHIVED', 'Archived')], db_index=True, default='DRAFT', max_length=20)),
|
||||
('hash_tags', models.CharField(blank=True, help_text='Comma-separated hashtags for linkedin post like #hiring,#jobopening', max_length=200, validators=[recruitment.validators.validate_hash_tags])),
|
||||
('linkedin_post_id', models.CharField(blank=True, help_text='LinkedIn post ID after posting', max_length=200)),
|
||||
('linkedin_post_url', models.URLField(blank=True, help_text='Direct URL to LinkedIn post')),
|
||||
('posted_to_linkedin', models.BooleanField(default=False)),
|
||||
('linkedin_post_status', models.CharField(blank=True, help_text='Status of LinkedIn posting', max_length=50)),
|
||||
('linkedin_posted_at', models.DateTimeField(blank=True, null=True)),
|
||||
('published_at', models.DateTimeField(blank=True, db_index=True, null=True)),
|
||||
('position_number', models.CharField(blank=True, help_text='University position number', max_length=50)),
|
||||
('reporting_to', models.CharField(blank=True, help_text='Who this position reports to', max_length=100)),
|
||||
('open_positions', models.PositiveIntegerField(default=1, help_text='Number of open positions for this job')),
|
||||
('max_applications', models.PositiveIntegerField(blank=True, default=1000, help_text='Maximum number of applications allowed', null=True)),
|
||||
('cancel_reason', models.TextField(blank=True, help_text='Reason for canceling the job posting', verbose_name='Cancel Reason')),
|
||||
('cancelled_by', models.CharField(blank=True, help_text='Name of person who cancelled this job', max_length=100, verbose_name='Cancelled By')),
|
||||
('cancelled_at', models.DateTimeField(blank=True, null=True)),
|
||||
('hiring_agency', models.ManyToManyField(blank=True, help_text='External agency responsible for sourcing candidates for this role', related_name='jobs', to='recruitment.hiringagency', verbose_name='Hiring Agency')),
|
||||
('source', models.ForeignKey(blank=True, help_text='The system or channel from which this job posting originated or was first published.', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='job_postings', to='recruitment.source')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Job Posting',
|
||||
'verbose_name_plural': 'Job Postings',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='InterviewSchedule',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('start_date', models.DateField(db_index=True, verbose_name='Start Date')),
|
||||
('end_date', models.DateField(db_index=True, verbose_name='End Date')),
|
||||
('working_days', models.JSONField(verbose_name='Working Days')),
|
||||
('start_time', models.TimeField(verbose_name='Start Time')),
|
||||
('end_time', models.TimeField(verbose_name='End Time')),
|
||||
('break_start_time', models.TimeField(blank=True, null=True, verbose_name='Break Start Time')),
|
||||
('break_end_time', models.TimeField(blank=True, null=True, verbose_name='Break End Time')),
|
||||
('interview_duration', models.PositiveIntegerField(verbose_name='Interview Duration (minutes)')),
|
||||
('buffer_time', models.PositiveIntegerField(default=0, verbose_name='Buffer Time (minutes)')),
|
||||
('candidates', models.ManyToManyField(blank=True, null=True, related_name='interview_schedules', to='recruitment.candidate')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='interview_schedules', to='recruitment.jobposting')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='formtemplate',
|
||||
name='job',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='form_template', to='recruitment.jobposting'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='candidate',
|
||||
name='job',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='candidates', to='recruitment.jobposting', verbose_name='Job'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='JobPostingImage',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('post_image', models.ImageField(upload_to='post/', validators=[recruitment.validators.validate_image_size])),
|
||||
('job', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='post_images', to='recruitment.jobposting')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('profile_image', models.ImageField(blank=True, null=True, upload_to='profile_pic/', validators=[recruitment.validators.validate_image_size])),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SharedFormTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('is_public', models.BooleanField(default=False, help_text='Whether this template is publicly available')),
|
||||
('shared_with', models.ManyToManyField(blank=True, related_name='shared_templates', to=settings.AUTH_USER_MODEL)),
|
||||
('template', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='recruitment.formtemplate')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Shared Form Template',
|
||||
'verbose_name_plural': 'Shared Form Templates',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IntegrationLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('action', models.CharField(choices=[('REQUEST', 'Request'), ('RESPONSE', 'Response'), ('ERROR', 'Error'), ('SYNC', 'Sync'), ('CREATE_JOB', 'Create Job'), ('UPDATE_JOB', 'Update Job')], max_length=20, verbose_name='Action')),
|
||||
('endpoint', models.CharField(blank=True, max_length=255, verbose_name='Endpoint')),
|
||||
('method', models.CharField(blank=True, max_length=10, verbose_name='HTTP Method')),
|
||||
('request_data', models.JSONField(blank=True, null=True, verbose_name='Request Data')),
|
||||
('response_data', models.JSONField(blank=True, null=True, verbose_name='Response Data')),
|
||||
('status_code', models.CharField(blank=True, max_length=10, verbose_name='Status Code')),
|
||||
('error_message', models.TextField(blank=True, verbose_name='Error Message')),
|
||||
('ip_address', models.GenericIPAddressField(verbose_name='IP Address')),
|
||||
('user_agent', models.CharField(blank=True, max_length=255, verbose_name='User Agent')),
|
||||
('processing_time', models.FloatField(blank=True, null=True, verbose_name='Processing Time (seconds)')),
|
||||
('source', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integration_logs', to='recruitment.source', verbose_name='Source')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Integration Log',
|
||||
'verbose_name_plural': 'Integration Logs',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TrainingMaterial',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('title', models.CharField(max_length=255, verbose_name='Title')),
|
||||
('content', django_ckeditor_5.fields.CKEditor5Field(blank=True, verbose_name='Content')),
|
||||
('video_link', models.URLField(blank=True, verbose_name='Video Link')),
|
||||
('file', models.FileField(blank=True, upload_to='training_materials/', verbose_name='File')),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL, verbose_name='Created by')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Training Material',
|
||||
'verbose_name_plural': 'Training Materials',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ScheduledInterview',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('interview_date', models.DateField(db_index=True, verbose_name='Interview Date')),
|
||||
('interview_time', models.TimeField(verbose_name='Interview Time')),
|
||||
('status', models.CharField(choices=[('scheduled', 'Scheduled'), ('confirmed', 'Confirmed'), ('cancelled', 'Cancelled'), ('completed', 'Completed')], db_index=True, default='scheduled', max_length=20)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('candidate', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.candidate')),
|
||||
('job', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interviews', to='recruitment.jobposting')),
|
||||
('schedule', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='interviews', to='recruitment.interviewschedule')),
|
||||
('zoom_meeting', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='interview', to='recruitment.zoommeeting')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MeetingComment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('content', django_ckeditor_5.fields.CKEditor5Field(verbose_name='Content')),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='meeting_comments', to=settings.AUTH_USER_MODEL, verbose_name='Author')),
|
||||
('meeting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='comments', to='recruitment.zoommeeting', verbose_name='Meeting')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Meeting Comment',
|
||||
'verbose_name_plural': 'Meeting Comments',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FieldResponse',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
|
||||
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
|
||||
('value', models.JSONField(blank=True, help_text='Response value (stored as JSON)', null=True)),
|
||||
('uploaded_file', models.FileField(blank=True, null=True, upload_to='form_uploads/')),
|
||||
('field', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formfield')),
|
||||
('submission', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='responses', to='recruitment.formsubmission')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Field Response',
|
||||
'verbose_name_plural': 'Field Responses',
|
||||
'indexes': [models.Index(fields=['submission'], name='recruitment_submiss_474130_idx'), models.Index(fields=['field'], name='recruitment_field_i_097e5b_idx')],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='formsubmission',
|
||||
index=models.Index(fields=['submitted_at'], name='recruitment_submitt_7946c8_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='interviewschedule',
|
||||
index=models.Index(fields=['start_date'], name='recruitment_start_d_15d55e_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='interviewschedule',
|
||||
index=models.Index(fields=['end_date'], name='recruitment_end_dat_aeb00e_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='interviewschedule',
|
||||
index=models.Index(fields=['created_by'], name='recruitment_created_d0bdcc_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='formtemplate',
|
||||
index=models.Index(fields=['created_at'], name='recruitment_created_c21775_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='formtemplate',
|
||||
index=models.Index(fields=['is_active'], name='recruitment_is_acti_ae5efb_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='candidate',
|
||||
index=models.Index(fields=['stage'], name='recruitment_stage_f1c6eb_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='candidate',
|
||||
index=models.Index(fields=['created_at'], name='recruitment_created_73590f_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='jobposting',
|
||||
index=models.Index(fields=['status', 'created_at', 'title'], name='recruitment_status_8b77aa_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='jobposting',
|
||||
index=models.Index(fields=['slug'], name='recruitment_slug_004045_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='scheduledinterview',
|
||||
index=models.Index(fields=['job', 'status'], name='recruitment_job_id_f09e22_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='scheduledinterview',
|
||||
index=models.Index(fields=['interview_date', 'interview_time'], name='recruitment_intervi_7f5877_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='scheduledinterview',
|
||||
index=models.Index(fields=['candidate', 'job'], name='recruitment_candida_43d5b0_idx'),
|
||||
),
|
||||
]
|
||||
@ -26,7 +26,7 @@ def format_job(sender, instance, created, **kwargs):
|
||||
schedule_type=Schedule.ONCE
|
||||
).first()
|
||||
|
||||
if instance.is_active and instance.application_deadline:
|
||||
if instance.STATUS_CHOICES=='ACTIVE' and instance.application_deadline:
|
||||
if not existing_schedule:
|
||||
# Create a new schedule if one does not exist
|
||||
schedule(
|
||||
|
||||
@ -248,7 +248,7 @@ class ViewTests(BaseTestCase):
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
reverse('submit_form', kwargs={'template_id': template.id}),
|
||||
reverse('application_submit', kwargs={'template_id': template.id}),
|
||||
data
|
||||
)
|
||||
# After successful submission, should redirect to success page
|
||||
@ -434,7 +434,7 @@ class IntegrationTests(BaseTestCase):
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
reverse('submit_form', kwargs={'template_id': template.id}),
|
||||
reverse('application_submit', kwargs={'template_id': template.id}),
|
||||
form_data
|
||||
)
|
||||
|
||||
@ -493,7 +493,7 @@ class AuthenticationTests(BaseTestCase):
|
||||
)
|
||||
|
||||
response = self.client.post(
|
||||
reverse('submit_form', kwargs={'template_id': template.id}),
|
||||
reverse('application_submit', kwargs={'template_id': template.id}),
|
||||
{}
|
||||
)
|
||||
# Should redirect to login page
|
||||
@ -525,7 +525,7 @@ class EdgeCaseTests(BaseTestCase):
|
||||
|
||||
# Submit form twice
|
||||
response1 = self.client.post(
|
||||
reverse('submit_form', kwargs={'template_id': template.id}),
|
||||
reverse('application_submit', kwargs={'template_id': template.id}),
|
||||
{'field_1': 'John', 'field_2': 'Doe'}
|
||||
)
|
||||
|
||||
|
||||
@ -744,7 +744,7 @@ class AdvancedIntegrationTests(TransactionTestCase):
|
||||
}
|
||||
|
||||
response = self.client.post(
|
||||
reverse('submit_form', kwargs={'template_id': template.id}),
|
||||
reverse('application_submit', kwargs={'template_id': template.id}),
|
||||
submission_data
|
||||
)
|
||||
self.assertEqual(response.status_code, 302) # Redirect to success page
|
||||
|
||||
@ -14,8 +14,7 @@ urlpatterns = [
|
||||
path('jobs/<slug:slug>/update/', views.edit_job, name='job_update'),
|
||||
# path('jobs/<slug:slug>/delete/', views., name='job_delete'),
|
||||
path('jobs/<slug:slug>/', views.job_detail, name='job_detail'),
|
||||
path('jobs/<slug:slug>/candidate/', views.job_detail_candidate, name='job_detail_candidate'),
|
||||
path('jobs/<slug:slug>/candidate/application/success', views.application_success, name='application_success'),
|
||||
|
||||
path('careers/',views.kaauh_career,name='kaauh_career'),
|
||||
|
||||
# LinkedIn Integration URLs
|
||||
@ -83,8 +82,8 @@ urlpatterns = [
|
||||
|
||||
path('htmx/<slug:slug>/candidate_update_status/', views.candidate_update_status, name='candidate_update_status'),
|
||||
|
||||
path('forms/form/<slug:template_slug>/submit/', views.submit_form, name='submit_form'),
|
||||
path('forms/form/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'),
|
||||
# path('forms/form/<slug:template_slug>/submit/', views.submit_form, name='submit_form'),
|
||||
# path('forms/form/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'),
|
||||
path('forms/<int:template_id>/submissions/<slug:slug>/', views.form_submission_details, name='form_submission_details'),
|
||||
path('forms/template/<slug:slug>/submissions/', views.form_template_submissions_list, name='form_template_submissions_list'),
|
||||
path('forms/template/<int:template_id>/all-submissions/', views.form_template_all_submissions, name='form_template_all_submissions'),
|
||||
@ -140,6 +139,7 @@ urlpatterns = [
|
||||
# Meeting Comments URLs
|
||||
path('meetings/<slug:slug>/comments/add/', views.add_meeting_comment, name='add_meeting_comment'),
|
||||
path('meetings/<slug:slug>/comments/<int:comment_id>/edit/', views.edit_meeting_comment, name='edit_meeting_comment'),
|
||||
|
||||
path('meetings/<slug:slug>/comments/<int:comment_id>/delete/', views.delete_meeting_comment, name='delete_meeting_comment'),
|
||||
|
||||
path('meetings/<slug:slug>/set_meeting_candidate/', views.set_meeting_candidate, name='set_meeting_candidate'),
|
||||
|
||||
@ -280,7 +280,7 @@ def create_job(request):
|
||||
try:
|
||||
job = form.save(commit=False)
|
||||
job.save()
|
||||
job_apply_url_relative=reverse('job_detail_candidate',kwargs={'slug':job.slug})
|
||||
job_apply_url_relative=reverse('application_detail',kwargs={'slug':job.slug})
|
||||
job_apply_url_absolute=request.build_absolute_uri(job_apply_url_relative)
|
||||
job.application_url=job_apply_url_absolute
|
||||
# FormTemplate.objects.create(job=job, is_active=False, name=job.title,created_by=request.user)
|
||||
@ -512,9 +512,9 @@ def kaauh_career(request):
|
||||
|
||||
|
||||
# job detail facing the candidate:
|
||||
def job_detail_candidate(request, slug):
|
||||
def application_detail(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
return render(request, "forms/job_detail_candidate.html", {"job": job})
|
||||
return render(request, "forms/application_detail.html", {"job": job})
|
||||
|
||||
|
||||
from django_q.tasks import async_task
|
||||
@ -800,7 +800,7 @@ def delete_form_template(request, template_id):
|
||||
)
|
||||
|
||||
|
||||
def form_wizard_view(request, template_slug):
|
||||
def application_submit_form(request, template_slug):
|
||||
"""Display the form as a step-by-step wizard"""
|
||||
template = get_object_or_404(FormTemplate, slug=template_slug, is_active=True)
|
||||
job_id = template.job.internal_job_id
|
||||
@ -811,24 +811,24 @@ def form_wizard_view(request, template_slug):
|
||||
request,
|
||||
'Application limit reached: This job is no longer accepting new applications. Please explore other available positions.'
|
||||
)
|
||||
return redirect('job_detail_candidate',slug=job.slug)
|
||||
return redirect('application_detail',slug=job.slug)
|
||||
if job.is_expired:
|
||||
messages.error(
|
||||
request,
|
||||
'Application deadline passed: This job is no longer accepting new applications. Please explore other available positions.'
|
||||
)
|
||||
return redirect('job_detail_candidate',slug=job.slug)
|
||||
return redirect('application_detail',slug=job.slug)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"forms/form_wizard.html",
|
||||
"forms/application_submit_form.html",
|
||||
{"template_slug": template_slug, "job_id": job_id},
|
||||
)
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
def submit_form(request, template_slug):
|
||||
def application_submit(request, template_slug):
|
||||
"""Handle form submission"""
|
||||
template = get_object_or_404(FormTemplate, slug=template_slug)
|
||||
job = template.job
|
||||
@ -2292,7 +2292,13 @@ def edit_meeting_comment(request, slug, comment_id):
|
||||
return redirect('meeting_details', slug=slug)
|
||||
else:
|
||||
form = MeetingCommentForm(instance=comment)
|
||||
|
||||
print("hi")
|
||||
context = {
|
||||
'form': form,
|
||||
'meeting': meeting,
|
||||
'comment':comment
|
||||
}
|
||||
return render(request, 'includes/edit_comment_form.html', context)
|
||||
|
||||
@login_required
|
||||
def delete_meeting_comment(request, slug, comment_id):
|
||||
|
||||
@ -394,6 +394,27 @@ def dashboard_view(request):
|
||||
).count()
|
||||
high_potential_ratio = round((high_potential_count / total_candidates) * 100, 1) if total_candidates > 0 else 0
|
||||
|
||||
jobs=models.JobPosting.objects.all().order_by('internal_job_id')
|
||||
selected_job_id=request.GET.get('selected_job_id','')
|
||||
candidate_stage=['APPLIED','EXAM','INTERVIEW','OFFER']
|
||||
apply_count,exam_count,interview_count,offer_count=[0]*4
|
||||
|
||||
if selected_job_id:
|
||||
job=jobs.get(internal_job_id=selected_job_id)
|
||||
apply_count=job.screening_candidates_count
|
||||
exam_count=job.exam_candidates_count
|
||||
interview_count=job.interview_candidates_count
|
||||
offer_count=job.offer_candidates_count
|
||||
all_candidates_count=job.all_candidates_count
|
||||
|
||||
else: #default job
|
||||
job=jobs.first()
|
||||
apply_count=job.screening_candidates_count
|
||||
exam_count=job.exam_candidates_count
|
||||
interview_count=job.interview_candidates_count
|
||||
offer_count=job.offer_candidates_count
|
||||
all_candidates_count=job.all_candidates_count
|
||||
candidates_count=[ apply_count,exam_count,interview_count,offer_count ]
|
||||
|
||||
context = {
|
||||
'total_jobs': total_jobs,
|
||||
@ -409,6 +430,14 @@ def dashboard_view(request):
|
||||
'high_potential_count': high_potential_count,
|
||||
'high_potential_ratio': high_potential_ratio,
|
||||
'scored_ratio': scored_ratio,
|
||||
'current_job_id':selected_job_id,
|
||||
'jobs':jobs,
|
||||
'all_candidates_count':all_candidates_count,
|
||||
'candidate_stage':json.dumps(candidate_stage),
|
||||
'candidates_count':json.dumps(candidates_count)
|
||||
,'my_job':job
|
||||
|
||||
|
||||
}
|
||||
return render(request, 'recruitment/dashboard.html', context)
|
||||
@login_required
|
||||
|
||||
@ -42,7 +42,7 @@
|
||||
<p class="text-muted">{% trans "Review the job details, then apply below." %}</p>
|
||||
|
||||
{% if job.form_template %}
|
||||
<a href="{% url 'form_wizard' job.form_template.pk %}" class="btn btn-main-action btn-lg w-100">
|
||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100">
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -102,7 +102,7 @@
|
||||
|
||||
<div class="mobile-fixed-apply-bar d-lg-none">
|
||||
{% if job.form_template %}
|
||||
<a href="{% url 'form_wizard' job.form_template.pk %}" class="btn btn-main-action btn-lg w-100">
|
||||
<a href="{% url 'application_submit_form' job.form_template.pk %}" class="btn btn-main-action btn-lg w-100">
|
||||
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -824,7 +824,7 @@
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(`/form/${state.templateId}/submit/`, {
|
||||
const response = await fetch(`/application/${state.templateId}/submit/`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
// IMPORTANT: Do NOT set Content-Type header when using FormData
|
||||
@ -231,7 +231,7 @@
|
||||
<div class="mt-auto pt-2 border-top">
|
||||
<div class="d-flex gap-2 justify-content-end">
|
||||
|
||||
<a href="{% url 'form_wizard' template.slug %}" class="btn btn-outline-primary btn-sm" title="{% trans 'Preview' %}">
|
||||
<a href="{% url 'application_submit_form' template.slug %}" class="btn btn-outline-primary btn-sm" title="{% trans 'Preview' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'Edit' %}">
|
||||
@ -286,7 +286,7 @@
|
||||
<td>{{ template.updated_at|date:"M d, Y" }}</td>
|
||||
<td class="text-end">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
<a href="{% url 'form_wizard' template.slug %}" class="btn btn-outline-primary" title="{% trans 'Preview' %}">
|
||||
<a href="{% url 'application_submit_form' template.slug %}" class="btn btn-outline-primary" title="{% trans 'Preview' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
|
||||
@ -17,7 +17,7 @@
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Add Comment</button>
|
||||
{% if 'HX-Request' in request.headers %}
|
||||
<button type="button" class="btn btn-secondary" hx-get="{% url 'meeting_details' meeting.slug %}" hx-target="#comment-section">Cancel</button>
|
||||
<button type="button" class="btn btn-secondary" hx-get="{% url 'meeting_details' meeting.slug %}" hx-select="#comment-section" hx-target="#comment-section">Cancel</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
<div class="card-header text-primary-theme d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">Comments ({{ comments.count }})</h5>
|
||||
{% if 'HX-Request' in request.headers %}
|
||||
<button type="button" class="btn btn-light btn-sm" hx-get="{% url 'meeting_details' meeting.slug %}" hx-target="#comment-section">
|
||||
<button type="button" class="btn btn-light btn-sm" hx-get="{% url 'meeting_details' meeting.slug %}" hx-select="#comment-section" hx-target="#comment-section">
|
||||
<i class="bi bi-x-lg"></i> Close
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-danger">Yes, Delete</button>
|
||||
{% if 'HX-Request' in request.headers %}
|
||||
<button type="button" class="btn btn-secondary" hx-get="{% url 'meeting_details' meeting.slug %}" hx-target="#comment-section">Cancel</button>
|
||||
<button type="button" class="btn btn-secondary" hx-get="{% url 'meeting_details' meeting.slug %}" hx-select="#comment-section" hx-target="#comment-section">Cancel</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<button type="submit" class="btn bg-primary-theme btn-sm text-white">Update Comment</button>
|
||||
<button type="submit" class="btn bg-primary btn-sm">Update Comment</button>
|
||||
{% if 'HX-Request' in request.headers %}
|
||||
<button type="button" class="btn btn-secondary btn-sm" hx-get="{% url 'meeting_details' meeting.slug %}" hx-target="#comment-section">Cancel</button>
|
||||
{% endif %}
|
||||
|
||||
@ -248,7 +248,7 @@
|
||||
|
||||
<td class="col-link" data-label="{% trans 'Link' %}">
|
||||
<a style="background-color : #00636e;color : #FFF; padding : 4px 10px; white-space: nowrap;"
|
||||
href="{% url 'job_detail_candidate' job.slug %}"
|
||||
href="{% url 'application_detail' job.slug %}"
|
||||
target="_blank">
|
||||
{% trans 'Apply' %}
|
||||
</a>
|
||||
|
||||
@ -117,28 +117,21 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-8">
|
||||
<div class="col-md-12">
|
||||
<div>
|
||||
<label for="{{ form.title.id_for_label }}" class="form-label">{% trans "Job Title" %} <span class="text-danger">*</span></label>
|
||||
{{ form.title }}
|
||||
{% if form.title.errors %}<div class="text-danger small mt-1">{{ form.title.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.job_type.id_for_label }}" class="form-label">{% trans "Job Type" %} <span class="text-danger">*</span></label>
|
||||
{{ form.job_type }}
|
||||
{% if form.job_type.errors %}<div class="text-danger small mt-1">{{ form.job_type.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.department.id_for_label }}" class="form-label">{% trans "Department" %}</label>
|
||||
{{ form.department }}
|
||||
{% if form.department.errors %}<div class="text-danger small mt-1">{{ form.department.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.workplace_type.id_for_label }}" class="form-label">{% trans "Workplace Type" %} <span class="text-danger">*</span></label>
|
||||
@ -146,33 +139,18 @@
|
||||
{% if form.workplace_type.errors %}<div class="text-danger small mt-1">{{ form.workplace_type.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 2: INTERNAL AND PROMOTION #}
|
||||
{# ================================================= #}
|
||||
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header-themed">
|
||||
<h5><i class="fas fa-tags"></i> {% trans "Internal & Promotion" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.position_number.id_for_label }}" class="form-label">{% trans "Position Number" %}</label>
|
||||
{{ form.position_number }}
|
||||
{% if form.position_number.errors %}<div class="text-danger small mt-1">{{ form.position_number.errors }}</div>{% endif %}
|
||||
<label for="{{ form.application_deadline.id_for_label }}" class="form-label">{% trans "Application Deadline" %}<span class="text-danger">*</span></label>
|
||||
{{ form.application_deadline }}
|
||||
{% if form.application_deadline.errors %}<div class="text-danger small mt-1">{{ form.application_deadline.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">{% trans "Reports To" %}</label>
|
||||
{{ form.reporting_to }}
|
||||
{% if form.reporting_to.errors %}<div class="text-danger small mt-1">{{ form.reporting_to.errors }}</div>{% endif %}
|
||||
<label for="{{ form.department.id_for_label }}" class="form-label">{% trans "Department" %}</label>
|
||||
{{ form.department }}
|
||||
{% if form.department.errors %}<div class="text-danger small mt-1">{{ form.department.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@ -189,72 +167,14 @@
|
||||
{% if form.max_applications.errors %}<div class="text-danger small mt-1">{{ form.max_applications.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<label for="{{ form.hash_tags.id_for_label }}" class="form-label">{% trans "Hashtags (For Promotion/Search on Linkedin)" %}</label>
|
||||
{{ form.hash_tags }}
|
||||
{% if form.hash_tags.errors %}<div class="text-danger small mt-1">{{ form.hash_tags.errors }}</div>{% endif %}
|
||||
<div class="form-text">{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 3: LOCATION AND DATES #}
|
||||
{# ================================================= #}
|
||||
|
||||
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header-themed">
|
||||
<h5><i class="fas fa-map-marker-alt"></i> {% trans "Location, Dates, & Salary" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.location_city.id_for_label }}" class="form-label">{% trans "City" %}</label>
|
||||
{{ form.location_city }}
|
||||
{% if form.location_city.errors %}<div class="text-danger small mt-1">{{ form.location_city.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.location_state.id_for_label }}" class="form-label">{% trans "State/Province" %}</label>
|
||||
{{ form.location_state }}
|
||||
{% if form.location_state.errors %}<div class="text-danger small mt-1">{{ form.location_state.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.location_country.id_for_label }}" class="form-label">{% trans "Country" %}</label>
|
||||
{{ form.location_country }}
|
||||
{% if form.location_country.errors %}<div class="text-danger small mt-1">{{ form.location_country.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.application_deadline.id_for_label }}" class="form-label">{% trans "Application Deadline" %}<span class="text-danger">*</span></label>
|
||||
{{ form.application_deadline }}
|
||||
{% if form.application_deadline.errors %}<div class="text-danger small mt-1">{{ form.application_deadline.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.salary_range.id_for_label }}" class="form-label">{% trans "Salary Range" %}</label>
|
||||
{{ form.salary_range }}
|
||||
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 4: JOB CONTENT (CKEDITOR 5 Fields) #}
|
||||
{# ================================================= #}
|
||||
@ -313,8 +233,90 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{# ================================================= #}
|
||||
{# SECTION 2: INTERNAL AND PROMOTION #}
|
||||
{# ================================================= #}
|
||||
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header-themed">
|
||||
<h5><i class="fas fa-tags"></i> {% trans "Internal & Promotion" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.position_number.id_for_label }}" class="form-label">{% trans "Position Number" %}</label>
|
||||
{{ form.position_number }}
|
||||
{% if form.position_number.errors %}<div class="text-danger small mt-1">{{ form.position_number.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">{% trans "Reports To" %}</label>
|
||||
{{ form.reporting_to }}
|
||||
{% if form.reporting_to.errors %}<div class="text-danger small mt-1">{{ form.reporting_to.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<label for="{{ form.hash_tags.id_for_label }}" class="form-label">{% trans "Hashtags (For Promotion/Search on Linkedin)" %}</label>
|
||||
{{ form.hash_tags }}
|
||||
{% if form.hash_tags.errors %}<div class="text-danger small mt-1">{{ form.hash_tags.errors }}</div>{% endif %}
|
||||
<div class="form-text">{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 3: LOCATION AND Salary #}
|
||||
{# ================================================= #}
|
||||
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header-themed">
|
||||
<h5><i class="fas fa-map-marker-alt"></i> {% trans "Location & Salary" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.location_city.id_for_label }}" class="form-label">{% trans "City" %}</label>
|
||||
{{ form.location_city }}
|
||||
{% if form.location_city.errors %}<div class="text-danger small mt-1">{{ form.location_city.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.location_state.id_for_label }}" class="form-label">{% trans "State/Province" %}</label>
|
||||
{{ form.location_state }}
|
||||
{% if form.location_state.errors %}<div class="text-danger small mt-1">{{ form.location_state.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.location_country.id_for_label }}" class="form-label">{% trans "Country" %}</label>
|
||||
{{ form.location_country }}
|
||||
{% if form.location_country.errors %}<div class="text-danger small mt-1">{{ form.location_country.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="col-md-12">
|
||||
<div>
|
||||
<label for="{{ form.salary_range.id_for_label }}" class="form-label">{% trans "Salary Range" %}</label>
|
||||
{{ form.salary_range }}
|
||||
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================================================= #}
|
||||
{# ACTION BUTTONS #}
|
||||
|
||||
@ -117,28 +117,21 @@
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-8">
|
||||
<div class="col-md-12">
|
||||
<div>
|
||||
<label for="{{ form.title.id_for_label }}" class="form-label">{% trans "Job Title" %} <span class="text-danger">*</span></label>
|
||||
{{ form.title }}
|
||||
{% if form.title.errors %}<div class="text-danger small mt-1">{{ form.title.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.job_type.id_for_label }}" class="form-label">{% trans "Job Type" %} <span class="text-danger">*</span></label>
|
||||
{{ form.job_type }}
|
||||
{% if form.job_type.errors %}<div class="text-danger small mt-1">{{ form.job_type.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.department.id_for_label }}" class="form-label">{% trans "Department" %}</label>
|
||||
{{ form.department }}
|
||||
{% if form.department.errors %}<div class="text-danger small mt-1">{{ form.department.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.workplace_type.id_for_label }}" class="form-label">{% trans "Workplace Type" %} <span class="text-danger">*</span></label>
|
||||
@ -146,33 +139,18 @@
|
||||
{% if form.workplace_type.errors %}<div class="text-danger small mt-1">{{ form.workplace_type.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 2: INTERNAL AND PROMOTION #}
|
||||
{# ================================================= #}
|
||||
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header-themed">
|
||||
<h5><i class="fas fa-tags"></i> {% trans "Internal & Promotion" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.position_number.id_for_label }}" class="form-label">{% trans "Position Number" %}</label>
|
||||
{{ form.position_number }}
|
||||
{% if form.position_number.errors %}<div class="text-danger small mt-1">{{ form.position_number.errors }}</div>{% endif %}
|
||||
<label for="{{ form.application_deadline.id_for_label }}" class="form-label">{% trans "Application Deadline" %}<span class="text-danger">*</span></label>
|
||||
{{ form.application_deadline }}
|
||||
{% if form.application_deadline.errors %}<div class="text-danger small mt-1">{{ form.application_deadline.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">{% trans "Reports To" %}</label>
|
||||
{{ form.reporting_to }}
|
||||
{% if form.reporting_to.errors %}<div class="text-danger small mt-1">{{ form.reporting_to.errors }}</div>{% endif %}
|
||||
<label for="{{ form.department.id_for_label }}" class="form-label">{% trans "Department" %}</label>
|
||||
{{ form.department }}
|
||||
{% if form.department.errors %}<div class="text-danger small mt-1">{{ form.department.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
@ -189,72 +167,14 @@
|
||||
{% if form.max_applications.errors %}<div class="text-danger small mt-1">{{ form.max_applications.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<label for="{{ form.hash_tags.id_for_label }}" class="form-label">{% trans "Hashtags (For Promotion/Search on Linkedin)" %}</label>
|
||||
{{ form.hash_tags }}
|
||||
{% if form.hash_tags.errors %}<div class="text-danger small mt-1">{{ form.hash_tags.errors }}</div>{% endif %}
|
||||
<div class="form-text">{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 3: LOCATION AND DATES #}
|
||||
{# ================================================= #}
|
||||
|
||||
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header-themed">
|
||||
<h5><i class="fas fa-map-marker-alt"></i> {% trans "Location, Dates, & Salary" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.location_city.id_for_label }}" class="form-label">{% trans "City" %}</label>
|
||||
{{ form.location_city }}
|
||||
{% if form.location_city.errors %}<div class="text-danger small mt-1">{{ form.location_city.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.location_state.id_for_label }}" class="form-label">{% trans "State/Province" %}</label>
|
||||
{{ form.location_state }}
|
||||
{% if form.location_state.errors %}<div class="text-danger small mt-1">{{ form.location_state.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.location_country.id_for_label }}" class="form-label">{% trans "Country" %}</label>
|
||||
{{ form.location_country }}
|
||||
{% if form.location_country.errors %}<div class="text-danger small mt-1">{{ form.location_country.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.application_deadline.id_for_label }}" class="form-label">{% trans "Application Deadline" %}<span class="text-danger">*</span></label>
|
||||
{{ form.application_deadline }}
|
||||
{% if form.application_deadline.errors %}<div class="text-danger small mt-1">{{ form.application_deadline.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.salary_range.id_for_label }}" class="form-label">{% trans "Salary Range" %}</label>
|
||||
{{ form.salary_range }}
|
||||
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 4: JOB CONTENT (CKEDITOR 5 Fields) #}
|
||||
{# ================================================= #}
|
||||
@ -313,8 +233,90 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{# ================================================= #}
|
||||
{# SECTION 2: INTERNAL AND PROMOTION #}
|
||||
{# ================================================= #}
|
||||
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header-themed">
|
||||
<h5><i class="fas fa-tags"></i> {% trans "Internal & Promotion" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.position_number.id_for_label }}" class="form-label">{% trans "Position Number" %}</label>
|
||||
{{ form.position_number }}
|
||||
{% if form.position_number.errors %}<div class="text-danger small mt-1">{{ form.position_number.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.reporting_to.id_for_label }}" class="form-label">{% trans "Reports To" %}</label>
|
||||
{{ form.reporting_to }}
|
||||
{% if form.reporting_to.errors %}<div class="text-danger small mt-1">{{ form.reporting_to.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="col-12">
|
||||
<div>
|
||||
<label for="{{ form.hash_tags.id_for_label }}" class="form-label">{% trans "Hashtags (For Promotion/Search on Linkedin)" %}</label>
|
||||
{{ form.hash_tags }}
|
||||
{% if form.hash_tags.errors %}<div class="text-danger small mt-1">{{ form.hash_tags.errors }}</div>{% endif %}
|
||||
<div class="form-text">{% trans "Comma-separated list of hashtags, e.g., #hiring, #professor" %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 3: LOCATION AND Salary #}
|
||||
{# ================================================= #}
|
||||
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header-themed">
|
||||
<h5><i class="fas fa-map-marker-alt"></i> {% trans "Location & Salary" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.location_city.id_for_label }}" class="form-label">{% trans "City" %}</label>
|
||||
{{ form.location_city }}
|
||||
{% if form.location_city.errors %}<div class="text-danger small mt-1">{{ form.location_city.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.location_state.id_for_label }}" class="form-label">{% trans "State/Province" %}</label>
|
||||
{{ form.location_state }}
|
||||
{% if form.location_state.errors %}<div class="text-danger small mt-1">{{ form.location_state.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.location_country.id_for_label }}" class="form-label">{% trans "Country" %}</label>
|
||||
{{ form.location_country }}
|
||||
{% if form.location_country.errors %}<div class="text-danger small mt-1">{{ form.location_country.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="col-md-12">
|
||||
<div>
|
||||
<label for="{{ form.salary_range.id_for_label }}" class="form-label">{% trans "Salary Range" %}</label>
|
||||
{{ form.salary_range }}
|
||||
{% if form.salary_range.errors %}<div class="text-danger small mt-1">{{ form.salary_range.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================================================= #}
|
||||
{# ACTION BUTTONS #}
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
{% block title %}{{ job.title }} - University ATS{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
|
||||
<style>
|
||||
/* ================================================= */
|
||||
/* THEME VARIABLES AND GLOBAL STYLES */
|
||||
@ -12,41 +11,35 @@
|
||||
:root {
|
||||
--kaauh-teal: #00636e; /* Primary */
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-teal-light: #4bb3be; /* For active glow */
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-primary-text: #343a40;
|
||||
|
||||
/* Consistent Status/Color Map (aligning with theme/bootstrap defaults) */
|
||||
--color-draft: #6c757d; /* Secondary Gray */
|
||||
--color-active: var(--kaauh-teal); /* Primary Teal */
|
||||
--color-closed: #ffc107; /* Warning Yellow */
|
||||
--color-cancelled: #dc3545; /* Danger Red */
|
||||
--color-archived: #343a40; /* Dark text/Muted */
|
||||
}
|
||||
|
||||
/* Custom Stage Colors for Tracker */
|
||||
--stage-applied: var(--kaauh-teal); /* Teal */
|
||||
--stage-exam: #17a2b8; /* Info Cyan */
|
||||
--stage-interview: #ffc107; /* Warning Yellow */
|
||||
--stage-offer: #28a745; /* Success Green */
|
||||
--stage-inactive: #6c757d; /* Secondary Gray */
|
||||
|
||||
--kaauh-teal: #00636e; /* Primary Theme / Active Stage */
|
||||
--kaauh-teal-light: #4bb3be; /* For active glow */
|
||||
--color-created: #6c757d; /* Muted Initial State */
|
||||
--color-active: #00636e; /* Teal for Active Flow */
|
||||
--color-posted: #17a2b8; /* Info Blue for External Posting */
|
||||
--color-closed: #ffc107; /* Warning Yellow for Soft End/Review */
|
||||
--color-archived: #343a40; /* Darkest for Final Storage */
|
||||
--color-canceled: #dc3545; /* Red for Negative/Canceled */
|
||||
--color-line-default: #e9ecef; /* Light Gray for all inactive markers */
|
||||
}
|
||||
|
||||
|
||||
/* Primary Color Overrides */
|
||||
/* Primary Color Overrides for Bootstrap Classes */
|
||||
.text-primary { color: var(--kaauh-teal) !important; }
|
||||
.text-info { color: var(--stage-exam) !important; }
|
||||
.text-success { color: var(--stage-offer) !important; }
|
||||
.text-secondary { color: var(--stage-inactive) !important; }
|
||||
.bg-success { background-color: var(--kaauh-teal) !important; }
|
||||
.bg-warning { background-color: #ffc107 !important; }
|
||||
.bg-secondary { background-color: #6c757d !important; }
|
||||
.bg-danger { background-color: #dc3545 !important; }
|
||||
.bg-primary { background-color: var(--kaauh-teal) !important; }
|
||||
|
||||
/* Status Badge Theme Mapping */
|
||||
.status-badge.bg-success { background-color: var(--color-active) !important; }
|
||||
.status-badge.bg-secondary { background-color: var(--color-draft) !important; }
|
||||
.status-badge.bg-warning { background-color: var(--color-closed) !important; }
|
||||
.status-badge.bg-danger { background-color: var(--color-cancelled) !important; }
|
||||
/* Ensure text colors are consistent for standard BS classes */
|
||||
.text-success { color: #28a745 !important; }
|
||||
.text-info { color: #17a2b8 !important; }
|
||||
.text-secondary { color: var(--kaauh-primary-text) !important; }
|
||||
|
||||
/* Header styling */
|
||||
.job-header-card {
|
||||
background: linear-gradient(135deg, var(--kaauh-teal), #004d57);
|
||||
background: linear-gradient(135deg, var(--kaauh-teal), var(--kaauh-teal-dark));
|
||||
color: white;
|
||||
border-radius: 0.75rem 0.75rem 0 0;
|
||||
padding: 1.5rem;
|
||||
@ -57,7 +50,7 @@
|
||||
margin: 0;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
/* Status badge */
|
||||
/* Status badge - Consolidated style for all badges */
|
||||
.status-badge {
|
||||
font-size: 0.9rem;
|
||||
padding: 0.4em 0.8em;
|
||||
@ -67,6 +60,7 @@
|
||||
letter-spacing: 0.7px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
color: white; /* Ensure badge text is white */
|
||||
}
|
||||
|
||||
/* Card enhancements */
|
||||
@ -91,12 +85,13 @@
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
/* Left Column Tabs Theming */
|
||||
/* Tabs Theming - Applies to the right column */
|
||||
.nav-tabs {
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
background-color: #f8f9fa;
|
||||
padding: 0 1.25rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
border: none;
|
||||
border-bottom: 3px solid transparent;
|
||||
@ -106,33 +101,22 @@
|
||||
margin-right: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
/* Active Link */
|
||||
.nav-tabs .nav-link.active {
|
||||
color: var(--kaauh-teal-dark) !important;
|
||||
background-color: white !important;
|
||||
border-bottom: 3px solid var(--kaauh-teal) !important;
|
||||
font-weight: 600;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* Right Column Tabs */
|
||||
.right-column-tabs {
|
||||
padding: 0;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
}
|
||||
.right-column-tabs .nav-link.active {
|
||||
background-color: white;
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-bottom: 3px solid var(--kaauh-teal);
|
||||
border-right-color: transparent;
|
||||
margin-bottom: -1px;
|
||||
border-right-color: transparent !important;
|
||||
margin-bottom: -1px;
|
||||
}
|
||||
|
||||
/* Main Action Button Style */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
.btn-main-action, .btn-main-action:hover, .btn-main-action:active {
|
||||
background-color: var(--kaauh-teal) !important;
|
||||
border-color: var(--kaauh-teal) !important;
|
||||
color: white !important;
|
||||
font-weight: 600;
|
||||
padding: 0.6rem 1.2rem;
|
||||
transition: all 0.2s ease;
|
||||
@ -142,92 +126,22 @@
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.btn-outline-secondary {
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal);
|
||||
.btn-outline-secondary { /* Apply primary colors for 'outline-secondary' used as theme buttons */
|
||||
color: var(--kaauh-teal-dark) !important;
|
||||
border-color: var(--kaauh-teal) !important;
|
||||
}
|
||||
|
||||
/* Applicant stats */
|
||||
.applicant-stats .stat-item {
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
border-radius: 0.5rem;
|
||||
background-color: #f8f9fa;
|
||||
border: 1px solid var(--kaauh-border);
|
||||
.btn-outline-light { /* Fixing text color for the edit status button */
|
||||
color: white !important;
|
||||
border-color: white !important;
|
||||
}
|
||||
.applicant-stats .stat-item div:first-child {
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
.btn-outline-light .text-primary { /* Override the edit icon's custom primary text class */
|
||||
color: white !important;
|
||||
}
|
||||
.applicant-stats .stat-item small {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
/* Specific styling for the deadline box */
|
||||
.deadline-box {
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
/* Enhanced styling for the Job Avg. Score card */
|
||||
.stats .card:first-child { /* Targets the first card in the .stats div (Job Avg. Score) */
|
||||
border-left: 5px solid var(--kaauh-teal); /* A distinctive left border */
|
||||
background: linear-gradient(to right bottom, rgba(0, 99, 110, 0.05), rgba(255, 255, 255, 0));
|
||||
}
|
||||
|
||||
.stats .card:first-child .card-header {
|
||||
background-color: rgba(0, 99, 110, 0.1); /* Subtle background for header */
|
||||
border-bottom: 2px solid var(--kaauh-teal); /* Emphasized border */
|
||||
border-left: none; /* Remove inherited border from parent if any */
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.stats .card:first-child .card-header h3 {
|
||||
color: var(--kaauh-teal); /* Match the theme color */
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem; /* Slightly larger header */
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stats .card:first-child .stat-icon {
|
||||
color: var(--kaauh-teal); /* Match the theme color */
|
||||
font-size: 1.2rem; /* Slightly larger icon */
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.stats .card:first-child .stat-value {
|
||||
font-size: 2.5rem; /* Larger, more prominent value */
|
||||
font-weight: 700;
|
||||
color: var(--kaauh-teal); /* Theme color for the value */
|
||||
text-align: center;
|
||||
margin: 1rem 0; /* Spacing around the value */
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.05); /* Subtle text shadow for depth */
|
||||
}
|
||||
|
||||
.stats .card:first-child .stat-caption {
|
||||
text-align: center;
|
||||
color: #6c757d; /* Muted color for caption */
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
margin-top: 0.5rem;
|
||||
border-top: 1px solid rgba(0, 99, 110, 0.2); /* Subtle top border for caption area */
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Custom CSS for simplified stat card (from previous answer) */
|
||||
.stats-grid .kpi-card {
|
||||
.kpi-card {
|
||||
border-left: 4px solid var(--kaauh-teal);
|
||||
background-color: #f0faff;
|
||||
}
|
||||
.stats-grid .card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -238,12 +152,12 @@
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}" class="text-secondary">Home</a></li>
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_list' %}" class="text-secondary">Jobs</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page" class="text-secondary">Job Detail</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">Job Detail</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div class="row g-4">
|
||||
|
||||
{# LEFT COLUMN: JOB DETAILS WITH TABS #}
|
||||
{# LEFT COLUMN: JOB DETAILS (NO TABS) #}
|
||||
<div class="col-lg-7">
|
||||
<div class="card shadow-sm no-hover">
|
||||
|
||||
@ -252,276 +166,136 @@
|
||||
<div>
|
||||
<h2 class="mb-1">{{ job.title }}</h2>
|
||||
<small class="text-light">{% trans "JOB ID: "%}{{ job.internal_job_id }}</small>
|
||||
|
||||
{# Deadline #}
|
||||
{% if job.application_deadline %}
|
||||
<div class="text-light mt-1">
|
||||
<i class="fas fa-calendar-times me-2"></i>
|
||||
<strong>{% trans "Deadline:" %}</strong> <span class="text-warning fw-bold">{{ job.application_deadline }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge status-badge">
|
||||
{# Corrected status badge logic to close the span correctly #}
|
||||
{% if job.status == "ACTIVE" %}
|
||||
<span class="badge bg-success status-badge">
|
||||
{% elif job.status == "DRAFT" %}
|
||||
<span class="badge bg-secondary status-badge">
|
||||
{% elif job.status == "CLOSED" %}
|
||||
<span class="badge bg-warning status-badge">
|
||||
{% elif job.status == "CANCELLED" %}
|
||||
<span class="badge bg-danger status-badge">
|
||||
{% elif job.status == "ARCHIVED" %}
|
||||
<span class="badge bg-secondary status-badge">
|
||||
{% else %}
|
||||
<span class="badge bg-secondary status-badge">
|
||||
{% endif %}
|
||||
{{ job.get_status_display }}
|
||||
<div class="d-flex align-items-center gap-2 mt-2 mt-md-0">
|
||||
|
||||
{# Status badge #}
|
||||
<div class="d-flex align-items-center">
|
||||
<span class="status-badge
|
||||
{% if job.status == "ACTIVE" %}bg-success
|
||||
{% elif job.status == "DRAFT" %}bg-secondary
|
||||
{% elif job.status == "CLOSED" %}bg-warning
|
||||
{% elif job.status == "CANCELLED" %}bg-danger
|
||||
{% elif job.status == "ARCHIVED" %}bg-secondary
|
||||
{% else %}bg-secondary{% endif %}">
|
||||
{{ job.get_status_display }}
|
||||
</span>
|
||||
|
||||
<button type="button" class="btn btn-outline-light btn-sm ms-2" data-bs-toggle="modal" data-bs-target="#editStatusModal">
|
||||
<i class="fas fa-edit text-primary"></i>
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Share Public Link Button #}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-main-action btn-sm"
|
||||
id="copyJobLinkButton"
|
||||
data-url="{{ job.application_url }}">
|
||||
<i class="fas fa-link"></i>
|
||||
{% trans "Share Public Link" %}
|
||||
</button>
|
||||
|
||||
<span id="copyFeedback" class="text-success ms-2 small" style="display:none;">
|
||||
{% trans "Copied!" %}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# LEFT TABS NAVIGATION #}
|
||||
<ul class="nav nav-tabs" id="jobTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="details-tab" data-bs-toggle="tab" data-bs-target="#details" type="button" role="tab" aria-controls="details" aria-selected="true">
|
||||
<i class="fas fa-info-circle me-1"></i> {% trans "Core Details" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="description-tab" data-bs-toggle="tab" data-bs-target="#description" type="button" role="tab" aria-controls="description" aria-selected="false">
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans "Description & Requirements" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="kpis-tab" data-bs-toggle="tab" data-bs-target="#kpis" type="button" role="tab" aria-controls="kpis" aria-selected="false">
|
||||
<i class="fas fa-chart-line me-1"></i> {% trans "Application KPIs" %}
|
||||
</button>
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
{# CONTENT: CORE DETAILS (No Tabs) #}
|
||||
<div class="card-body">
|
||||
<div class="tab-content" id="jobTabsContent">
|
||||
|
||||
{# TAB 1 CONTENT: CORE DETAILS #}
|
||||
<div class="tab-pane fade show active" id="details" role="tabpanel" aria-labelledby="details-tab">
|
||||
<h5 class="text-muted mb-3">{% trans "Administrative & Location" %}</h5>
|
||||
<div class="row g-3 mb-4 border-bottom pb-3 small text-secondary">
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-building me-2 text-primary"></i> <strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-hashtag me-2 text-primary"></i> <strong>{% trans "Position No:" %}</strong> {{ job.position_number|default:"N/A" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-briefcase me-2 text-primary"></i> <strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-map-pin me-2 text-primary"></i> <strong>{% trans "Workplace:" %}</strong> {{ job.get_workplace_type_display }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-globe me-2 text-primary"></i> <strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Created By:" %}</strong> {{ job.created_by|default:"N/A" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-plus me-2 text-primary"></i> <strong>{% trans "Created At:" %}</strong> {{ job.created_at|default:"N/A" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-edit me-2 text-primary"></i> <strong>{% trans "Updated At:" %}</strong> {{ job.updated_at|default:"N/A" }}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-main-action btn-sm"
|
||||
id="copyJobLinkButton"
|
||||
data-url="{{ job.application_url }}">
|
||||
{# Replaced bulky SVG with simpler Font Awesome icon #}
|
||||
<i class="fas fa-link"></i>
|
||||
{% trans "Share Public Link" %}
|
||||
</button>
|
||||
|
||||
<span id="copyFeedback" class="text-success ms-2 small" style="display:none;">
|
||||
{% trans "Copied!" %}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<h5 class="text-muted mb-3">{% trans "Financial & Timeline" %}</h5>
|
||||
<div class="row g-3">
|
||||
{% if job.salary_range %}
|
||||
<div class="col-md-4">
|
||||
<div class="deadline-box border-success">
|
||||
<i class="fas fa-money-bill-wave me-2 text-success"></i>
|
||||
<strong class="text-success">{% trans "Salary:" %}</strong> <span class="text-dark">{{ job.salary_range }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if job.start_date %}
|
||||
<div class="col-md-4">
|
||||
<div class="deadline-box border-info">
|
||||
<i class="far fa-calendar-alt me-2 text-info"></i>
|
||||
<strong class="text-info">{% trans "Start Date:" %}</strong> <span class="text-dark">{{ job.start_date }}</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if job.application_deadline %}
|
||||
<div class="col-md-4">
|
||||
<div class="deadline-box border-{% if job.is_expired %}danger{% else %}primary{% endif %} text-{% if job.is_expired %}danger{% else %}primary{% endif %}">
|
||||
<i class="fas fa-calendar-times me-2"></i>
|
||||
<strong>{% trans "Deadline:" %}</strong> <span class="text-dark">{{ job.application_deadline }}</span>
|
||||
{% if job.is_expired %}
|
||||
<span class="badge bg-danger ms-1">{% trans "EXPIRED" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<h5 class="text-muted mb-3">{% trans "Administrative & Location" %}
|
||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-main-action btn-sm"><li class="fa fa-edit"></li>{% trans "Edit JOb" %}</a>
|
||||
</h5>
|
||||
<div class="row g-3 mb-4 border-bottom pb-3 small text-secondary">
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-building me-2 text-primary"></i> <strong>{% trans "Department:" %}</strong> {{ job.department|default:"N/A" }}
|
||||
</div>
|
||||
|
||||
{# TAB 2 CONTENT: DESCRIPTION & REQUIREMENTS #}
|
||||
<div class="tab-pane fade" id="description" role="tabpanel" aria-labelledby="description-tab">
|
||||
{% if job.description %}
|
||||
<div class="mb-4">
|
||||
<h5>{% trans "Job Description" %}</h5>
|
||||
<div class="text-secondary">{{ job.description|safe }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if job.qualifications %}
|
||||
<div class="mb-4">
|
||||
<h5>{% trans "Required Qualifications" %}</h5>
|
||||
<div class="text-secondary">{{ job.qualifications|safe }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if job.benefits %}
|
||||
<div class="mb-4">
|
||||
<h5>{% trans "Benefits" %}</h5>
|
||||
<div class="text-secondary">{{ job.benefits|safe}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if job.application_instructions %}
|
||||
<div class="mb-4">
|
||||
<h5>{% trans "Application Instructions" %}</h5>
|
||||
<div class="text-secondary">{{ job.application_instructions|safe }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-hashtag me-2 text-primary"></i> <strong>{% trans "Position No:" %}</strong> {{ job.position_number|default:"N/A" }}
|
||||
</div>
|
||||
|
||||
{# TAB 3 CONTENT: APPLICATION KPIS #}
|
||||
<div class="tab-pane fade" id="kpis" role="tabpanel" aria-labelledby="kpis-tab">
|
||||
<div class="row g-3 stats-grid">
|
||||
|
||||
{# 1. Job Avg. Score #}
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center h-100 kpi-card">
|
||||
<div class="card-body p-2">
|
||||
<i class="fas fa-star text-primary mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||
<div class="h4 mb-0 text-primary fw-bold">{{ avg_match_score|floatformat:1 }}</div>
|
||||
<small class="text-muted d-block">{% trans "Avg. AI Score" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 2. High Potential Count #}
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center h-100">
|
||||
<div class="card-body p-2">
|
||||
<i class="fas fa-trophy text-success mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||
<div class="h4 mb-0 text-success fw-bold">{{ high_potential_count }}</div>
|
||||
<small class="text-muted d-block">{% trans "High Potential" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 3. Avg. Time to Interview #}
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center h-100">
|
||||
<div class="card-body p-2">
|
||||
<i class="fas fa-calendar-alt text-info mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||
<div class="h4 mb-0 text-info fw-bold">{{ avg_t2i_days|floatformat:1 }}d</div>
|
||||
<small class="text-muted d-block">{% trans "Time to Interview" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 4. Avg. Exam Review Time #}
|
||||
<div class="col-6 col-md-3">
|
||||
<div class="card text-center h-100">
|
||||
<div class="card-body p-2">
|
||||
<i class="fas fa-hourglass-half text-secondary mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||
<div class="h4 mb-0 text-secondary fw-bold">{{ avg_t_in_exam_days|floatformat:1 }}d</div>
|
||||
<small class="text-muted d-block">{% trans "Avg. Exam Review" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="text-end text-muted small mt-3 me-2">
|
||||
<i class="fas fa-info-circle me-1"></i> {% trans "KPIs based on completed applicant data." %}
|
||||
</p>
|
||||
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-briefcase me-2 text-primary"></i> <strong>{% trans "Job Type:" %}</strong> {{ job.get_job_type_display }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-map-pin me-2 text-primary"></i> <strong>{% trans "Workplace:" %}</strong> {{ job.get_workplace_type_display }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-globe me-2 text-primary"></i> <strong>{% trans "Location:" %}</strong> {{ job.get_location_display }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fa-solid fa-money-bill me-2 text-primary"></i> <strong>{% trans "Salary:" %}</strong> {{ job.salary_range |default:"N/A" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Created By:" %}</strong> {{ job.created_by|default:"N/A" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-plus me-2 text-primary"></i> <strong>{% trans "Created At:" %}</strong> {{ job.created_at|default:"N/A" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<i class="fas fa-edit me-2 text-primary"></i> <strong>{% trans "Updated At:" %}</strong> {{ job.updated_at|default:"N/A" }}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
{# Description Blocks (Main Content) #}
|
||||
{% if job.description %}
|
||||
<div class="mb-4">
|
||||
<h5>{% trans "Job Description" %}</h5>
|
||||
<div class="text-secondary">{{ job.description|safe }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if job.qualifications %}
|
||||
<div class="mb-4">
|
||||
<h5>{% trans "Required Qualifications" %}</h5>
|
||||
<div class="text-secondary">{{ job.qualifications|safe }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if job.benefits %}
|
||||
<div class="mb-4">
|
||||
<h5>{% trans "Benefits" %}</h5>
|
||||
<div class="text-secondary">{{ job.benefits|safe}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if job.application_instructions %}
|
||||
<div class="mb-4">
|
||||
<h5>{% trans "Application Instructions" %}</h5>
|
||||
<div class="text-secondary">{{ job.application_instructions|safe }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# FOOTER ACTIONS #}
|
||||
<div class="card-footer d-flex flex-wrap gap-2">
|
||||
<a href="{% url 'job_update' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-edit"></i> {% trans "Edit Job" %}
|
||||
</a>
|
||||
|
||||
|
||||
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#myModalForm">
|
||||
<i class="fas fa-image me-1"></i> {% trans "Upload Image for Post" %}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# RIGHT COLUMN: TABBED CARDS #}
|
||||
{# RIGHT COLUMN: TABBED CARDS #}
|
||||
<div class="col-lg-5">
|
||||
|
||||
{# New Card for Candidate Category Chart #}
|
||||
<div class="card shadow-sm no-hover mb-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-chart-pie me-2 text-primary"></i>
|
||||
{% trans "Candidate Categories & Scores" %}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div style="height: 300px;">
|
||||
<canvas id="jobCategoryMatchChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# REMOVED: Standalone Applicant Tracking Card (It is now in a tab) #}
|
||||
|
||||
<div class="card shadow-sm no-hover">
|
||||
|
||||
{# RIGHT TABS NAVIGATION #}
|
||||
<ul class="nav nav-tabs right-column-tabs" id="rightJobTabs" role="tablist">
|
||||
<ul class="nav nav-tabs" id="rightJobTabs" role="tablist">
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
<button class="nav-link active" id="applicants-tab" data-bs-toggle="tab" data-bs-target="#applicants-pane" type="button" role="tab" aria-controls="applicants-pane" aria-selected="true">
|
||||
<i class="fas fa-users me-1 text-primary"></i> {% trans "Applicants" %}
|
||||
<i class="fas fa-users me-1"></i> {% trans "Applicants" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
<button class="nav-link" id="tracking-tab" data-bs-toggle="tab" data-bs-target="#tracking-pane" type="button" role="tab" aria-controls="tracking-pane" aria-selected="false">
|
||||
<i class="fas fa-project-diagram me-1 text-primary"></i> {% trans "Tracking" %}
|
||||
<i class="fas fa-project-diagram me-1"></i> {% trans "Tracking" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
<button class="nav-link" id="manage-tab" data-bs-toggle="tab" data-bs-target="#manage-pane" type="button" role="tab" aria-controls="manage-pane" aria-selected="false">
|
||||
<i class="fas fa-cogs me-1 text-secondary"></i> {% trans "Form Template" %}
|
||||
<i class="fas fa-cogs me-1"></i> {% trans "Form Template" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
@ -531,70 +305,70 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content mx-2 my-3" id="rightJobTabsContent">
|
||||
<div class="tab-content p-3" id="rightJobTabsContent">
|
||||
|
||||
{# TAB 1: APPLICANTS CONTENT #}
|
||||
<div class="tab-pane fade show active" id="applicants-pane" role="tabpanel" aria-labelledby="applicants-tab">
|
||||
<h5 class="mb-3">{% trans "Total Applicants" %} (<span id="total_candidates">{{ total_applicants }}</span>)</h5>
|
||||
|
||||
<div class="d-grid gap-4">
|
||||
<div class="d-grid gap-3">
|
||||
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus"></i> {% trans "Create Applicant" %}
|
||||
<i class="fas fa-user-plus me-1"></i> {% trans "Create Applicant" %}
|
||||
</a>
|
||||
<a href="{% url 'candidate_screening_view' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-layer-group"></i> {% trans "Manage Applicants" %}
|
||||
<i class="fas fa-layer-group me-1"></i> {% trans "Manage Applicants" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# NEW TAB 2: APPLICANT TRACKING CONTENT #}
|
||||
{# TAB 2: TRACKING CONTENT #}
|
||||
<div class="tab-pane fade" id="tracking-pane" role="tabpanel" aria-labelledby="tracking-tab">
|
||||
<h5 class="mb-3"><i class="fas fa-project-diagram me-2 text-primary"></i>{% trans "Pipeline Stages" %}</h5>
|
||||
<h5 class="mb-3"><i class="fas fa-project-diagram me-2 text-primary"></i>{% trans "Applicant Stages" %}</h5>
|
||||
{% include 'jobs/partials/applicant_tracking.html' %}
|
||||
<p class="text-muted small mt-3">{% trans "View the number of candidates currently in each stage of the hiring pipeline." %}</p>
|
||||
<p class="text-muted small">
|
||||
{% trans "The applicant tracking flow is defined by the attached Form Template. View the Form Template tab to manage stages and fields." %}
|
||||
</p>
|
||||
{# Placeholder for stage tracker component #}
|
||||
</div>
|
||||
|
||||
{# TAB 3: MANAGEMENT (Form Template) CONTENT #}
|
||||
<div class="tab-pane fade" id="manage-pane" role="tabpanel" aria-labelledby="manage-tab">
|
||||
|
||||
<h5 class="mb-3"><i class="fas fa-clipboard-list me-2 text-primary"></i>{% trans "Form Management" %}</h5>
|
||||
<div class="d-grid gap-2">
|
||||
<div class="d-grid gap-3">
|
||||
<p class="text-muted small mb-3">
|
||||
{% trans "Manage the custom application forms associated with this job posting." %}
|
||||
</p>
|
||||
|
||||
{% if not job.form_template %}
|
||||
<a href="{% url 'create_form_template' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus-circle me-2"></i> {% trans "Create New Form Template" %}
|
||||
<i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %}
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'form_wizard' job.form_template.pk %}" class="btn btn-outline-secondary">
|
||||
<a href="{% url 'application_submit_form' job.form_template.pk %}" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-list-alt me-1"></i> {% trans "View Form Template" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# TAB 4: LINKEDIN INTEGRATION CONTENT #}
|
||||
<div class="tab-pane fade" id="linkedin-pane" role="tabpanel" aria-labelledby="linkedin-tab">
|
||||
<h5 class="mb-3"><i class="fab fa-linkedin me-2 text-info"></i>{% trans "LinkedIn Integration" %}</h5>
|
||||
<div class="mb-4">
|
||||
<div class="d-grid gap-3">
|
||||
{% if job.posted_to_linkedin %}
|
||||
<div class="alert alert-success p-2 mb-3 small">
|
||||
<div class="alert alert-success p-2 small mb-0">
|
||||
<i class="fas fa-check-circle me-1"></i> {% trans "Posted successfully!" %}
|
||||
</div>
|
||||
{% if job.linkedin_post_url %}
|
||||
<a href="{{ job.linkedin_post_url }}" target="_blank" class="btn btn-main-action w-100 mb-2">
|
||||
<a href="{{ job.linkedin_post_url }}" target="_blank" class="btn btn-outline-secondary">
|
||||
<i class="fab fa-linkedin me-1"></i> {% trans "View on LinkedIn" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
<small class="text-muted d-block text-center mb-3">
|
||||
<small class="text-muted d-block text-center">
|
||||
{% trans "Posted on:" %} {{ job.linkedin_posted_at|date:"M d, Y" }}
|
||||
</small>
|
||||
{% else %}
|
||||
<p class="text-muted small mb-3">{% trans "This job has not been posted to LinkedIn yet." %}</p>
|
||||
<p class="text-muted small mb-0">{% trans "This job has not been posted to LinkedIn yet." %}</p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" action="{% url 'post_to_linkedin' job.slug %}" class="mt-2">
|
||||
@ -605,9 +379,13 @@
|
||||
{% if job.posted_to_linkedin %}{% trans "Re-post to LinkedIn" %}{% else %}{% trans "Post to LinkedIn" %}{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<button type="button" class="btn btn-outline-secondary w-100" data-bs-toggle="modal" data-bs-target="#myModalForm">
|
||||
<i class="fas fa-image me-1"></i> {% trans "Upload Image for Post" %}
|
||||
</button>
|
||||
|
||||
{% if not request.session.linkedin_authenticated %}
|
||||
<small class="text-muted d-block mt-2 text-center">
|
||||
<small class="text-muted d-block text-center">
|
||||
{% trans "You need to" %} <a href="{% url 'linkedin_login' %}">{% trans "authenticate with LinkedIn" %}</a> {% trans "first." %}
|
||||
</small>
|
||||
{% endif %}
|
||||
@ -623,6 +401,82 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Card 2: Candidate Category Chart #}
|
||||
<div class="card shadow-sm no-hover mb-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-chart-pie me-2 text-primary"></i>
|
||||
{% trans "Candidate Categories & Scores" %}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div style="height: 300px;">
|
||||
<canvas id="jobCategoryMatchChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Card 3: KPIs #}
|
||||
<div class="card shadow-sm no-hover mb-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">
|
||||
<i class="fas fa-info-circle me-1 text-primary"></i>
|
||||
{% trans "Key Performance Indicators" %}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
|
||||
<div class="row g-3 stats-grid">
|
||||
|
||||
{# 1. Job Avg. Score #}
|
||||
<div class="col-6">
|
||||
<div class="card text-center h-100 kpi-card">
|
||||
<div class="card-body p-2">
|
||||
<i class="fas fa-star text-primary mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||
<div class="h4 mb-0 text-primary fw-bold">{{ avg_match_score|floatformat:1 }}</div>
|
||||
<small class="text-muted d-block">{% trans "Avg. AI Score" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 2. High Potential Count #}
|
||||
<div class="col-6">
|
||||
<div class="card text-center h-100">
|
||||
<div class="card-body p-2">
|
||||
<i class="fas fa-trophy text-success mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||
<div class="h4 mb-0 text-success fw-bold">{{ high_potential_count }}</div>
|
||||
<small class="text-muted d-block">{% trans "High Potential" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 3. Avg. Time to Interview #}
|
||||
<div class="col-6">
|
||||
<div class="card text-center h-100">
|
||||
<div class="card-body p-2">
|
||||
<i class="fas fa-calendar-alt text-info mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||
<div class="h4 mb-0 text-info fw-bold">{{ avg_t2i_days|floatformat:1 }}d</div>
|
||||
<small class="text-muted d-block">{% trans "Time to Interview" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# 4. Avg. Exam Review Time #}
|
||||
<div class="col-6">
|
||||
<div class="card text-center h-100">
|
||||
<div class="card-body p-2">
|
||||
<i class="fas fa-hourglass-half text-secondary mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||
<div class="h4 mb-0 text-secondary fw-bold">{{ avg_t_in_exam_days|floatformat:1 }}d</div>
|
||||
<small class="text-muted d-block">{% trans "Avg. Exam Review" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -311,7 +311,7 @@
|
||||
<td class="text-center">
|
||||
<div class="btn-group btn-group-sm" role="group">
|
||||
{% if job.form_template %}
|
||||
<a href="{% url 'form_wizard' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Preview' %}">
|
||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Preview' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
@ -354,7 +354,7 @@
|
||||
<li><i class="fas fa-users text-primary-theme me-2"></i>{% trans "Applicants" %}:{{ job.metrics.applied|default:"0" }}</li>
|
||||
<li><i class="fas fa-clipboard-check text-success me-2"></i> {% trans "Offers Made" %}: {{ job.metrics.offer|default:"0" }}</li>
|
||||
<li><i class="fas fa-file-alt text-info me-2"></i> {% trans "Form" %}:{% if job.form_template %}
|
||||
<a href="{% url 'form_wizard' job.form_template.pk %}" class="text-info">{{ job.form_template.name }}</a>
|
||||
<a href="{% url 'application_submit_form' job.form_template.pk %}" class="text-info">{{ job.form_template.name }}</a>
|
||||
{% else %}
|
||||
{% trans "N/A" %}
|
||||
{% endif %}
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* KAAT-S Redesign CSS - Optimized Compact Detail View (Settings Removed) */
|
||||
/* KAAT-S Redesign CSS - Optimized Compact Detail View (Comments Left) */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
@ -16,6 +16,11 @@
|
||||
--kaauh-gray-light: #f8f9fa; /* Card Header/Footer Background */
|
||||
--kaauh-success: #198754; /* Success Green */
|
||||
--kaauh-danger: #dc3545; /* Danger Red */
|
||||
/* New CRM/ATS Specific Colors */
|
||||
--kaauh-link: #007bff; /* Standard Blue Link */
|
||||
--kaauh-link-hover: #0056b3;
|
||||
--kaauh-accent-bg: #fff3cd; /* Light Yellow for Attention/Context */
|
||||
--kaauh-accent-text: #664d03; /* Dark Yellow Text */
|
||||
}
|
||||
|
||||
body {
|
||||
@ -85,10 +90,6 @@ body {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-row-group {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@ -114,17 +115,34 @@ body {
|
||||
flex-basis: 55%;
|
||||
}
|
||||
|
||||
/* --- CRM ASSOCIATED RECORD STYLING --- */
|
||||
.associated-record-card {
|
||||
background-color: var(--kaauh-accent-bg);
|
||||
color: var(--kaauh-accent-text);
|
||||
border: 1px solid var(--kaauh-accent-text);
|
||||
}
|
||||
.associated-record-card a {
|
||||
color: var(--kaauh-link);
|
||||
font-weight: 700;
|
||||
}
|
||||
.associated-record-card a:hover {
|
||||
color: var(--kaauh-link-hover);
|
||||
}
|
||||
|
||||
|
||||
/* ------------------ Join Info & Copy Button ------------------ */
|
||||
|
||||
.join-info-card {
|
||||
border-left: 5px solid var(--kaauh-teal); /* Highlight join info */
|
||||
}
|
||||
/* Consolidated primary button style */
|
||||
.btn-primary {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
font-weight: 600;
|
||||
padding: 0.6rem 1.25rem;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
@ -143,6 +161,11 @@ body {
|
||||
.btn-copy {
|
||||
padding: 0.5rem 0.75rem;
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border: none;
|
||||
color: white; /* Ensure copy button icon is white */
|
||||
}
|
||||
.btn-copy:hover {
|
||||
background-color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
/* ------------------ Footer & Actions ------------------ */
|
||||
@ -150,32 +173,35 @@ body {
|
||||
.action-bar-footer {
|
||||
border-top: 1px solid var(--kaauh-border);
|
||||
padding: 1rem 1.5rem;
|
||||
gap: 0.75rem;
|
||||
background-color: var(--kaauh-gray-light);
|
||||
border-radius: 0 0 12px 12px;
|
||||
/* Explicitly use flex for layout control */
|
||||
display: flex;
|
||||
justify-content: space-between; /* Separate the left/right groups */
|
||||
align-items: center;
|
||||
}
|
||||
.btn-footer-action {
|
||||
font-weight: 600;
|
||||
padding: 0.5rem 1rem;
|
||||
/* Made buttons smaller and consistent */
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* ------------------ Comments Section ------------------ */
|
||||
/* --- Comment Card Header Style --- */
|
||||
#comments-card .card-header {
|
||||
background-color: var(--kaauh-teal-light);
|
||||
background-color: white;
|
||||
color: var(--kaauh-teal-dark);
|
||||
padding: 1rem 1.5rem;
|
||||
font-weight: 600;
|
||||
border-radius: 12px 12px 0 0;
|
||||
}
|
||||
/* Comment card body/item styling is kept compact */
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
{# --- TOP BAR / BACK BUTTON --- #}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
@ -185,104 +211,202 @@ body {
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
{# --- LEFT COLUMN (MAIN DETAILS) --- #}
|
||||
<div class="col-lg-6">
|
||||
<div class="card no-hover h-100">
|
||||
{# --- CONSOLIDATED HEADER --- #}
|
||||
<div class="main-title-card">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="card-header-title-group">
|
||||
<h1 class="mb-1">
|
||||
<svg class="heroicon me-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0 8.268-2.943-9.542-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
{{ meeting.topic }}
|
||||
</h1>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="status-badge bg-{{ meeting.status }}">
|
||||
{{ meeting.status|title }}
|
||||
</span>
|
||||
{% if meeting.interview %}
|
||||
<span class="text-muted small">
|
||||
{% trans "Candidate" %}: <a class="text-primary-theme fw-bold text-decoration-none" href="{% url 'candidate_detail' meeting.interview.candidate.slug %}">{{ meeting.interview.candidate.name }} </a>
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- MAIN DETAIL BODY --- #}
|
||||
|
||||
{# --- LEFT COLUMN (COMMENTS & INTERNAL CONTEXT) - Takes 50% of the screen #}
|
||||
<div class="col-lg-6 d-flex flex-column">
|
||||
|
||||
{# --- 1. INTERNAL NOTES / DESCRIPTION CARD (New CRM Feature) --- #}
|
||||
{% if meeting.description %}
|
||||
<div class="card no-hover mb-4 flex-shrink-0">
|
||||
<div class="card-body detail-section">
|
||||
<h2>{% trans "Core Details" %}</h2>
|
||||
<div class="detail-row-group">
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Meeting ID" %}:</div><div class="detail-value">{{ meeting.meeting_id }}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Start Time" %}:</div><div class="detail-value">{{ meeting.start_time|date:"M d, Y H:i" }}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Duration" %}:</div><div class="detail-value">{{ meeting.duration }} {% trans "minutes" %}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Timezone" %}:</div><div class="detail-value">{{ meeting.timezone|default:"UTC" }}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Host Email" %}:</div><div class="detail-value">{{ meeting.host_email|default:"N/A" }}</div></div>
|
||||
<h2 class="d-flex align-items-center"><i class="fas fa-clipboard-list me-2"></i> {% trans "Internal Context" %}</h2>
|
||||
<p class="text-muted small">{% trans "Meeting agenda, purpose, or interview details for internal team use." %}</p>
|
||||
<div class="p-3 bg-light rounded border">
|
||||
<p class="mb-0">{{ meeting.description|safe }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# --- ACTION BAR AT THE BOTTOM OF THE MAIN CARD --- #}
|
||||
<div class="card-footer action-bar-footer d-flex justify-content-end">
|
||||
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary btn-footer-action">
|
||||
<i class="fas fa-edit me-1"></i> {% trans "Update" %}
|
||||
</a>
|
||||
{% if meeting.zoom_gateway_response %}
|
||||
<button type="button" class="btn btn-secondary btn-footer-action" onclick="toggleGateway()">
|
||||
<i class="fas fa-code me-1"></i> {% trans "API Response" %}
|
||||
{# --- 2. Comments Section (Now in the Left Column) --- #}
|
||||
<div class="card no-hover flex-grow-1" id="comments-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
|
||||
<i class="fas fa-comments me-2"></i>
|
||||
{% trans "Comments" %} ({{ meeting.comments.count }})
|
||||
</h5>
|
||||
{% if user.is_authenticated %}
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
hx-get="{% url 'add_meeting_comment' meeting.slug %}"
|
||||
hx-target="#comment-section"
|
||||
>
|
||||
<i class="fas fa-plus me-1"></i> {% trans "Add Comment" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-danger btn-footer-action" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
hx-post="{% url 'delete_meeting' meeting.slug %}"
|
||||
hx-target="#deleteModalBody"
|
||||
hx-swap="outerHTML"
|
||||
data-item-name="{{ meeting.topic }}">
|
||||
<i class="fas fa-trash-alt me-1"></i>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body overflow-auto">
|
||||
<div id="comment-section">
|
||||
{% if meeting.comments.all %}
|
||||
{% for comment in meeting.comments.all|dictsortreversed:"created_at" %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong class="me-2">{{ comment.author.get_full_name|default:comment.author.username }}</strong>
|
||||
{% if comment.author != user %}
|
||||
<span class="badge bg-secondary ms-1">{% trans "Comment" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<small class="text-muted">{{ comment.created_at|date:"M d, Y P" }}</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">{{ comment.content|safe }}</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
{% if comment.author == user or user.is_staff %}
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-primary"
|
||||
hx-get="{% url 'edit_meeting_comment' meeting.slug comment.id %}"
|
||||
hx-target="#comment-section"
|
||||
title="{% trans 'Edit Comment' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
hx-get="{% url 'delete_meeting_comment' meeting.slug comment.id %}"
|
||||
hx-target="#comment-section"
|
||||
title="{% trans 'Delete Comment' %}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-muted">{% trans "No comments yet. Be the first to comment!" %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- RIGHT COLUMN (JOIN INFO) --- #}
|
||||
{# --- RIGHT COLUMN (MAIN DETAILS & JOIN INFO) - Takes 50% of the screen #}
|
||||
<div class="col-lg-6">
|
||||
{% if meeting.join_url %}
|
||||
<div class="card no-hover join-info-card detail-section h-100">
|
||||
<div class="card-body">
|
||||
<h2>{% trans "Join Information" %}</h2>
|
||||
<div class="d-flex flex-column h-100">
|
||||
|
||||
<a href="{{ meeting.join_url }}" class="btn btn-primary w-100 mb-4" target="_blank">
|
||||
<i class="fas fa-video me-1"></i> {% trans "Join Meeting Now" %}
|
||||
</a>
|
||||
|
||||
<div class="join-url-container">
|
||||
<div id="copy-message" style="opacity: 0;">{% trans "Copied!" %}</div>
|
||||
|
||||
<div class="join-url-display d-flex justify-content-between align-items-center">
|
||||
<div class="text-truncate">
|
||||
<strong>{% trans "Join URL" %}:</strong>
|
||||
<span id="meeting-join-url">{{ meeting.join_url }}</span>
|
||||
{# --- CRM ASSOCIATED RECORD CARD (Elevated Importance) --- #}
|
||||
{% if meeting.interview %}
|
||||
<div class="card associated-record-card mb-4 flex-shrink-0">
|
||||
<div class="card-body pt-3 pb-3">
|
||||
<div class="d-flex align-items-center">
|
||||
<i class="fas fa-user-tag fa-2x me-3"></i>
|
||||
<div>
|
||||
<h6 class="mb-0 text-uppercase small fw-bold">{% trans "Associated Record" %}</h6>
|
||||
<span class="fw-bold fs-5 me-2">
|
||||
<a href="{% url 'candidate_detail' meeting.interview.candidate.slug %}" class="text-decoration-none">
|
||||
{{ meeting.interview.candidate.name }}
|
||||
</a>
|
||||
</span>
|
||||
<span class="badge bg-secondary-subtle text-secondary small fw-normal">{{ meeting.interview.job_position }}</span>
|
||||
</div>
|
||||
<button class="btn-copy ms-2" onclick="copyLink()" title="{% trans 'Copy URL' %}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{% if meeting.password %}
|
||||
<div class="detail-row" style="border: none; padding-top: 1rem;">
|
||||
<div class="detail-label" style="font-size: 1rem;">{% trans "Password" %}:</div>
|
||||
<div class="detail-value fw-bolder text-danger">{{ meeting.password }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# --- 1. MAIN DETAILS CARD --- #}
|
||||
<div class="card no-hover flex-grow-1 mb-4">
|
||||
{# --- CONSOLIDATED HEADER --- #}
|
||||
<div class="main-title-card">
|
||||
<div class="d-flex justify-content-between align-items-start">
|
||||
<div class="card-header-title-group">
|
||||
<h1 class="mb-1">
|
||||
<svg class="heroicon me-2" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
<path d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0 8.268-2.943-9.542-7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
|
||||
</svg>
|
||||
{{ meeting.topic }}
|
||||
</h1>
|
||||
<div class="d-flex align-items-center gap-3">
|
||||
<span class="status-badge bg-{{ meeting.status }}">
|
||||
{{ meeting.status|title }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- CONNECTION DETAIL BODY (Renamed from Core Details) --- #}
|
||||
<div class="card-body detail-section">
|
||||
<h2><i class="fas fa-calendar-alt me-2"></i> {% trans "Connection Details" %}</h2>
|
||||
<div class="detail-row-group">
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Meeting ID" %}:</div><div class="detail-value">{{ meeting.meeting_id }}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Start Time" %}:</div><div class="detail-value">{{ meeting.start_time|date:"M d, Y H:i" }}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Duration" %}:</div><div class="detail-value">{{ meeting.duration }} {% trans "minutes" %}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Timezone" %}:</div><div class="detail-value">{{ meeting.timezone|default:"UTC" }}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Host Email" %}:</div><div class="detail-value">{{ meeting.host_email|default:"N/A" }}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# --- ACTION BAR AT THE BOTTOM OF THE MAIN CARD --- #}
|
||||
<div class="card-footer action-bar-footer">
|
||||
<div>
|
||||
{# Placeholder for future left-aligned button #}
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary btn-footer-action">
|
||||
<i class="fas fa-edit me-1"></i> {% trans "Update" %}
|
||||
</a>
|
||||
{% if meeting.zoom_gateway_response %}
|
||||
<button type="button" class="btn btn-secondary btn-footer-action" onclick="toggleGateway()">
|
||||
<i class="fas fa-code me-1"></i> {% trans "API Response" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button type="button" class="btn btn-danger btn-footer-action ms-3" title="{% trans 'Delete' %}"
|
||||
data-bs-toggle="modal" data-bs-target="#deleteModal"
|
||||
data-item-name="{{ meeting.topic }}">
|
||||
<i class="fas fa-trash-alt me-1"></i>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# --- 2. JOIN INFO CARD (Separate from details, but in the same column) --- #}
|
||||
{% if meeting.join_url %}
|
||||
<div class="card no-hover join-info-card detail-section flex-shrink-0">
|
||||
<div class="card-body">
|
||||
<h2><i class="fas fa-link me-2"></i> {% trans "Join Information" %}</h2>
|
||||
|
||||
<a href="{{ meeting.join_url }}" class="btn btn-primary w-100 mb-4" target="_blank">
|
||||
<i class="fas fa-video me-1"></i> {% trans "Join Meeting Now" %}
|
||||
</a>
|
||||
|
||||
<div class="join-url-container">
|
||||
{# Message should not be display: none; but opacity: 0; for smooth transition #}
|
||||
<div id="copy-message" class="text-white rounded px-2 py-1 small fw-bold mb-2 text-center" style="opacity: 0; transition: opacity 0.3s; position: absolute; right: 0; top: -30px; background-color: var(--kaauh-success); z-index: 10;">{% trans "Copied!" %}</div>
|
||||
|
||||
<div class="join-url-display d-flex justify-content-between align-items-center position-relative">
|
||||
<div class="text-truncate me-2">
|
||||
<strong>{% trans "Join URL" %}:</strong>
|
||||
<span id="meeting-join-url">{{ meeting.join_url }}</span>
|
||||
</div>
|
||||
<button class="btn-copy ms-2 flex-shrink-0" onclick="copyLink()" title="{% trans 'Copy URL' %}">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if meeting.password %}
|
||||
<div class="detail-row" style="border: none; padding-top: 1rem;">
|
||||
<div class="detail-label" style="font-size: 1rem;">{% trans "Password" %}:</div>
|
||||
<div class="detail-value fw-bolder text-danger">{{ meeting.password }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -296,81 +420,22 @@ body {
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{# --- Comments Section (Full Width, below main content) --- #}
|
||||
<div class="card no-hover mt-4" id="comments-card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
|
||||
<i class="fas fa-comments me-2"></i>
|
||||
{% trans "Comments" %} ({{ meeting.comments.count }})
|
||||
</h5>
|
||||
{% if user.is_authenticated %}
|
||||
<button type="button" class="btn btn-primary btn-sm"
|
||||
hx-get="{% url 'add_meeting_comment' meeting.slug %}"
|
||||
hx-target="#comment-section"
|
||||
>
|
||||
<i class="fas fa-plus me-1"></i> {% trans "Add Comment" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div id="comment-section">
|
||||
{% if meeting.comments.all %}
|
||||
{% for comment in meeting.comments.all|dictsortreversed:"created_at" %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong class="me-2">{{ comment.author.get_full_name|default:comment.author.username }}</strong>
|
||||
{% if comment.author != user %}
|
||||
<span class="badge bg-secondary ms-1">{% trans "Comment" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<small class="text-muted">{{ comment.created_at|date:"M d, Y P" }}</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="card-text">{{ comment.content|safe }}</p>
|
||||
</div>
|
||||
<div class="card-footer">
|
||||
{% if comment.author == user or user.is_staff %}
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-outline-primary"
|
||||
hx-get="{% url 'edit_meeting_comment' meeting.slug comment.id %}"
|
||||
hx-target="#comment-section"
|
||||
title="{% trans 'Edit Comment' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
hx-get="{% url 'delete_meeting_comment' meeting.slug comment.id %}"
|
||||
hx-target="#comment-section"
|
||||
title="{% trans 'Delete Comment' %}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-muted">{% trans "No comments yet. Be the first to comment!" %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# MODALS (KEEP OUTSIDE OF THE MAIN LAYOUT ROWS) #}
|
||||
{% comment %} {% include 'modals/delete_modal.html' with item_name="Meeting" delete_url_name='delete_meeting' %} {% endcomment %}
|
||||
<div class="modal fade" id="commentModal" tabindex="-1" aria-labelledby="commentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="commentModalLabel">Add Comment</h5>
|
||||
<h5 class="modal-title" id="commentModalLabel">Add Comment</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="commentModalBody">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% block customJS %}
|
||||
<script>
|
||||
@ -383,9 +448,10 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
// CopyLink function remains the same (as provided in the original code)
|
||||
// CopyLink function implementation (slightly improved for message placement)
|
||||
function copyLink() {
|
||||
const urlElement = document.getElementById('meeting-join-url');
|
||||
const displayContainer = urlElement.closest('.join-url-display');
|
||||
const messageElement = document.getElementById('copy-message');
|
||||
const textToCopy = urlElement.textContent || urlElement.innerText;
|
||||
|
||||
@ -396,6 +462,11 @@ body {
|
||||
messageElement.style.backgroundColor = success ? 'var(--kaauh-success)' : 'var(--kaauh-danger)';
|
||||
messageElement.style.opacity = '1';
|
||||
|
||||
// Position the message relative to the display container
|
||||
const rect = displayContainer.getBoundingClientRect();
|
||||
messageElement.style.left = (rect.width / 2) - (messageElement.offsetWidth / 2) + 'px';
|
||||
messageElement.style.top = '-35px';
|
||||
|
||||
window.copyMessageTimeout = setTimeout(() => {
|
||||
messageElement.style.opacity = '0';
|
||||
}, 2000);
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* ... (Your existing CSS code is here) ... */
|
||||
/* ================================================= */
|
||||
/* THEME VARIABLES AND GLOBAL STYLES */
|
||||
/* ================================================= */
|
||||
@ -225,25 +226,14 @@
|
||||
<i class="fas fa-id-card me-1"></i> {% trans "Contact & Job" %}
|
||||
</button>
|
||||
</li>
|
||||
{% if candidate.resume %}
|
||||
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="resume-tab" data-bs-toggle="tab" data-bs-target="#resume-pane" type="button" role="tab" aria-controls="resume-pane" aria-selected="false">
|
||||
<i class="fas fa-file-pdf me-1"></i> {% trans "Resume" %}
|
||||
{# NEW TAB ADDED HERE #}
|
||||
<button class="nav-link" id="timeline-tab" data-bs-toggle="tab" data-bs-target="#timeline-pane" type="button" role="tab" aria-controls="timeline-pane" aria-selected="false">
|
||||
<i class="fas fa-route me-1"></i> {% trans "Journey Timeline" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if candidate.parsed_summary %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="summary-tab" data-bs-toggle="tab" data-bs-target="#summary-pane" type="button" role="tab" aria-controls="summary-pane" aria-selected="false">
|
||||
<i class="fas fa-chart-bar me-1"></i> {% trans "Resume Summary" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="analysis-tab" data-bs-toggle="tab" data-bs-target="#analysis-pane" type="button" role="tab" aria-controls="analysis-pane" aria-selected="false">
|
||||
<i class="fas fa-brain me-1"></i> {% trans "AI Analysis" %}
|
||||
</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="card-body">
|
||||
@ -290,35 +280,92 @@
|
||||
</div>
|
||||
|
||||
{# TAB 2 CONTENT: RESUME #}
|
||||
{% if candidate.resume %}
|
||||
<div class="tab-pane fade" id="resume-pane" role="tabpanel" aria-labelledby="resume-tab">
|
||||
<h5 class="text-primary mb-4">{% trans "Resume Document" %}</h5>
|
||||
<div class="d-flex align-items-center justify-content-between p-3 border rounded">
|
||||
{% comment %} <div>
|
||||
<p class="mb-1"><strong>{{ candidate.resume.name }}</strong></p>
|
||||
<small class="text-muted">{{ candidate.resume.name|truncatechars:30 }}</small>
|
||||
</div> {% endcomment %}
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
|
||||
<i class="fas fa-eye me-1"></i>
|
||||
{% trans "View Actual Resume" %}
|
||||
</a>
|
||||
<a href="{{ candidate.resume.url }}" download class="btn btn-main-action">
|
||||
<i class="fas fa-download me-1"></i>
|
||||
{% trans "Download Resume" %}
|
||||
</a>
|
||||
|
||||
|
||||
{# NEW TAB 3 CONTENT: CANDIDATE JOURNEY TIMELINE #}
|
||||
<div class="tab-pane fade" id="timeline-pane" role="tabpanel" aria-labelledby="timeline-tab">
|
||||
{# ENHANCED: CANDIDATE JOURNEY TIMELINE CARD #}
|
||||
<div class="card shadow-sm timeline-card">
|
||||
<div class="card-header bg-white border-bottom py-3">
|
||||
<h5 class="mb-0 text-muted"><i class="fas fa-route me-2"></i>{% trans "Candidate Journey" %}</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
|
||||
<h6 class="text-uppercase text-secondary mb-3">{% trans "Current Stage" %}</h6>
|
||||
<div class="p-3 mb-4 rounded current-stage">
|
||||
<p class="mb-0 fw-bold fs-5 text-primary">{{ candidate.stage }}</p>
|
||||
<small class="text-muted d-block mt-1">
|
||||
{% trans "Latest status update:" %} {{ candidate.updated_at|date:"M d, Y" }}
|
||||
</small>
|
||||
</div>
|
||||
<a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
|
||||
<i class="fas fa-file-alt me-1"></i>
|
||||
{% trans "View Resume AI Overview" %}
|
||||
</a>
|
||||
|
||||
<h6 class="text-uppercase text-secondary mb-3 pt-2 border-top">{% trans "Historical Timeline" %}</h6>
|
||||
<div class="timeline">
|
||||
|
||||
|
||||
{# Base Status: Application Submitted (Always required) #}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fa-file-signature"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Application Submitted" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.created_at|date:"M d, Y" }}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.created_at|date:"h:i A" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% if candidate.exam_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fa-clipboard-check"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Exam" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.exam_date|date:"M d, Y" }}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.exam_date|date:"h:i A" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if candidate.interview_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-comments"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Interview" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.interview_date|date:"M d, Y" }}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.interview_date|date:"h:i A" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.offer_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.offer_date|date:"M d, Y" }}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.offer_date|date:"h:i A" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# TAB 3 CONTENT: PARSED SUMMARY #}
|
||||
{# TAB 4 CONTENT: PARSED SUMMARY #}
|
||||
{% if candidate.parsed_summary %}
|
||||
<div class="tab-pane fade" id="summary-pane" role="tabpanel" aria-labelledby="summary-tab">
|
||||
<h5 class="text-primary mb-4">{% trans "AI Generated Summary" %}</h5>
|
||||
@ -328,13 +375,12 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# TAB 4 CONTENT: AI ANALYSIS #}
|
||||
{# TAB 5 CONTENT: AI ANALYSIS #}
|
||||
{% if candidate.is_resume_parsed %}
|
||||
<div class="tab-pane fade" id="analysis-pane" role="tabpanel" aria-labelledby="analysis-tab">
|
||||
<h5 class="text-primary mb-4">{% trans "AI Analysis Report" %}</h5>
|
||||
<div class="border-start border-primary ps-3 pt-1 pb-1">
|
||||
{% with analysis=candidate.ai_analysis_data %}
|
||||
|
||||
{# Match Score Card #}
|
||||
<div class="mb-4 p-3 rounded" style="background-color: {% if analysis.match_score >= 70 %}rgba(40, 167, 69, 0.1){% elif analysis.match_score >= 40 %}rgba(255, 193, 7, 0.1){% else %}rgba(220, 53, 69, 0.1){% endif %}">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
@ -482,9 +528,9 @@
|
||||
|
||||
{# RIGHT COLUMN: ACTIONS AND CANDIDATE TIMELINE #}
|
||||
<div class="col-lg-4">
|
||||
|
||||
|
||||
{# ACTIONS CARD #}
|
||||
{% if user.is_staff %}
|
||||
|
||||
<div class="card shadow-sm mb-4 p-3">
|
||||
<h5 class="text-muted mb-3"><i class="fas fa-cog me-2"></i>{% trans "Management Actions" %}</h5>
|
||||
<div class="d-grid gap-2">
|
||||
@ -497,95 +543,37 @@
|
||||
<a href="{% url 'candidate_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to List" %}
|
||||
</a>
|
||||
{% if candidate.resume %}
|
||||
|
||||
<a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
|
||||
<i class="fas fa-eye me-1"></i>
|
||||
{% trans "View Actual Resume" %}
|
||||
</a>
|
||||
<a href="{{ candidate.resume.url }}" download class="btn btn-outline-primary">
|
||||
<i class="fas fa-download me-1"></i>
|
||||
{% trans "Download Resume" %}
|
||||
</a>
|
||||
|
||||
<a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
|
||||
<i class="fas fa-file-alt me-1"></i>
|
||||
{% trans "View Resume AI Overview" %}
|
||||
</a>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# ENHANCED: CANDIDATE JOURNEY TIMELINE CARD #}
|
||||
<div class="card shadow-sm timeline-card">
|
||||
<div class="card-header bg-white border-bottom py-3">
|
||||
<h5 class="mb-0 text-muted"><i class="fas fa-route me-2"></i>{% trans "Candidate Journey" %}</h5>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
|
||||
<h6 class="text-uppercase text-secondary mb-3">{% trans "Current Stage" %}</h6>
|
||||
<div class="p-3 mb-4 rounded current-stage">
|
||||
<p class="mb-0 fw-bold fs-5 text-primary">{{ candidate.stage }}</p>
|
||||
<small class="text-muted d-block mt-1">
|
||||
{% trans "Latest status update:" %} {{ candidate.updated_at|date:"M d, Y" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<h6 class="text-uppercase text-secondary mb-3 pt-2 border-top">{% trans "Historical Timeline" %}</h6>
|
||||
<div class="timeline">
|
||||
|
||||
|
||||
{# Base Status: Application Submitted (Always required) #}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fa-file-signature"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Application Submitted" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.created_at|date:"M d, Y" }}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.created_at|date:"h:i A" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% if candidate.exam_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fa-clipboard-check"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Exam" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.exam_date|date:"M d, Y" }}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.exam_date|date:"h:i A" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if candidate.interview_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-comments"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Interview" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.interview_date|date:"M d, Y" }}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.interview_date|date:"h:i A" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.offer_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
|
||||
<small class="text-muted">
|
||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.offer_date|date:"M d, Y" }}
|
||||
<span class="ms-2">|</span>
|
||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.offer_date|date:"h:i A" }}
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{% include 'recruitment/candidate_resume_template.html' %}
|
||||
|
||||
|
||||
{% if user.is_staff %}
|
||||
{% include "recruitment/partials/stage_update_modal.html" with candidate=candidate form=stage_form %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -2,7 +2,169 @@
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Candidate Tier Management - {{ job.title }} - ATS{% endblock %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-primary-text: #343a40;
|
||||
--kaauh-success: #28a745;
|
||||
--kaauh-info: #17a2b8;
|
||||
--kaauh-danger: #dc3545;
|
||||
--kaauh-warning: #ffc107;
|
||||
}
|
||||
|
||||
/* Primary Color Overrides */
|
||||
.text-primary-theme { color: var(--kaauh-teal) !important; }
|
||||
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
|
||||
|
||||
/* 1. Main Container & Card Styling */
|
||||
.kaauh-card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Dedicated style for the filter block */
|
||||
.filter-controls {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
/* 2. Button Styling (Themed for Main Actions) */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.btn-main-action:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.btn-outline-secondary {
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal);
|
||||
}
|
||||
.btn-outline-secondary:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
/* Style for the Bulk Move button */
|
||||
.btn-bulk-action {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn-bulk-action:hover {
|
||||
background-color: #00363e;
|
||||
border-color: #00363e;
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background-color: white;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-bottom: 2px solid var(--kaauh-teal);
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* 4. Badges and Statuses */
|
||||
.ai-score-badge {
|
||||
background-color: var(--kaauh-teal-dark) !important;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
padding: 0.4em 0.8em;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
.status-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.3em 0.7em;
|
||||
border-radius: 0.35rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 0.3rem;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.stage-Applied { background-color: #e9ecef; color: #495057; }
|
||||
.stage-Screening { background-color: var(--kaauh-info); color: white; }
|
||||
.stage-Exam { background-color: var(--kaauh-warning); color: #856404; }
|
||||
.stage-Interview { background-color: #17a2b8; color: white; }
|
||||
.stage-Offer { background-color: var(--kaauh-success); color: white; }
|
||||
|
||||
/* Timeline specific container */
|
||||
.applicant-tracking-timeline {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* --- CUSTOM HEIGHT OPTIMIZATION (MAKING INPUTS/BUTTONS SMALLER) --- */
|
||||
.form-control-sm,
|
||||
.btn-sm {
|
||||
/* Reduce vertical padding even more than default Bootstrap 'sm' */
|
||||
padding-top: 0.2rem !important;
|
||||
padding-bottom: 0.2rem !important;
|
||||
/* Ensure a consistent, small height for both */
|
||||
height: 28px !important;
|
||||
font-size: 0.8rem !important; /* Slightly smaller font */
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
@ -31,28 +193,39 @@
|
||||
</h2>
|
||||
|
||||
<div class="kaauh-card shadow-sm p-3">
|
||||
{% if candidates %}
|
||||
<div class="bulk-action-bar">
|
||||
{% if candidates %}
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
|
||||
{% csrf_token %}
|
||||
<label for="update_status" class="form-label small mb-0 fw-bold">{% trans "Move Selected To:" %}</label>
|
||||
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: auto;">
|
||||
<option selected>
|
||||
----------
|
||||
</option>
|
||||
<option value="Interview">
|
||||
{% trans "Interview Stage" %}
|
||||
</option>
|
||||
<option value="Applied">
|
||||
{% trans "Screening Stage" %}
|
||||
</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-arrow-right me-1"></i>
|
||||
</button>
|
||||
|
||||
{# Using d-flex for horizontal alignment and align-items-end to align items to the bottom baseline #}
|
||||
<div class="d-flex align-items-end gap-3">
|
||||
|
||||
{# Select Input Group #}
|
||||
<div>
|
||||
<label for="update_status" class="form-label small mb-1 fw-bold">{% trans "Move Selected To:" %}</label>
|
||||
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="min-width: 150px;">
|
||||
<option selected>
|
||||
----------
|
||||
</option>
|
||||
<option value="Interview">
|
||||
{% trans "Interview Stage" %}
|
||||
</option>
|
||||
<option value="Applied">
|
||||
{% trans "Screening Stage" %}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Button #}
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Update Status" %}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
<form id="candidate-form" method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
@ -2,7 +2,170 @@
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}- {{ job.title }} - ATS{% endblock %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-primary-text: #343a40;
|
||||
--kaauh-success: #28a745;
|
||||
--kaauh-info: #17a2b8;
|
||||
--kaauh-danger: #dc3545;
|
||||
--kaauh-warning: #ffc107;
|
||||
}
|
||||
|
||||
/* Primary Color Overrides */
|
||||
.text-primary-theme { color: var(--kaauh-teal) !important; }
|
||||
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
|
||||
|
||||
/* 1. Main Container & Card Styling */
|
||||
.kaauh-card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Dedicated style for the filter block */
|
||||
.filter-controls {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
/* 2. Button Styling (Themed for Main Actions) */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.btn-main-action:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.btn-outline-secondary {
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal);
|
||||
}
|
||||
.btn-outline-secondary:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
/* Style for the Bulk Move button */
|
||||
.btn-bulk-action {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn-bulk-action:hover {
|
||||
background-color: #00363e;
|
||||
border-color: #00363e;
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background-color: white;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-bottom: 2px solid var(--kaauh-teal);
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* 4. Badges and Statuses */
|
||||
.ai-score-badge {
|
||||
background-color: var(--kaauh-teal-dark) !important;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
padding: 0.4em 0.8em;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
.status-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.3em 0.7em;
|
||||
border-radius: 0.35rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 0.3rem;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.stage-Applied { background-color: #e9ecef; color: #495057; }
|
||||
.stage-Screening { background-color: var(--kaauh-info); color: white; }
|
||||
.stage-Exam { background-color: var(--kaauh-warning); color: #856404; }
|
||||
.stage-Interview { background-color: #17a2b8; color: white; }
|
||||
.stage-Offer { background-color: var(--kaauh-success); color: white; }
|
||||
|
||||
/* Timeline specific container */
|
||||
.applicant-tracking-timeline {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* --- CUSTOM HEIGHT OPTIMIZATION (MAKING INPUTS/BUTTONS SMALLER) --- */
|
||||
.form-control-sm,
|
||||
.btn-sm {
|
||||
/* Reduce vertical padding even more than default Bootstrap 'sm' */
|
||||
padding-top: 0.2rem !important;
|
||||
padding-bottom: 0.2rem !important;
|
||||
/* Ensure a consistent, small height for both */
|
||||
height: 28px !important;
|
||||
font-size: 0.8rem !important; /* Slightly smaller font */
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
@ -25,32 +188,42 @@
|
||||
|
||||
<div class="kaauh-card shadow-sm p-3">
|
||||
{% if candidates %}
|
||||
<div class="bulk-action-bar">
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
{# Use d-flex to align the entire contents (two forms and the separator) horizontally #}
|
||||
<div class="d-flex align-items-end gap-3">
|
||||
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
|
||||
{% csrf_token %}
|
||||
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
|
||||
<option selected>
|
||||
----------
|
||||
</option>
|
||||
<option value="Offer">
|
||||
{% trans "To Offer" %}
|
||||
</option>
|
||||
<option value="Exam">
|
||||
{% trans "To Exam" %}
|
||||
</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-arrow-right me-1"></i>
|
||||
</button>
|
||||
</form>
|
||||
{# Form 1: Status Update #}
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Select Input Group - No label needed for this one, so we just flex the select and button #}
|
||||
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
|
||||
<option selected>
|
||||
----------
|
||||
</option>
|
||||
<option value="Offer">
|
||||
{% trans "To Offer" %}
|
||||
</option>
|
||||
<option value="Exam">
|
||||
{% trans "To Exam" %}
|
||||
</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Move" %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="vr" style="height: 28px;"></div> <form hx-boost="true" hx-include="#candidate-form" action="{% url 'schedule_interviews' job.slug %}" method="get" class="action-group">
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %}
|
||||
</button>
|
||||
</form>
|
||||
{# Separator (Vertical Rule) - Aligns automatically at the bottom with align-items-end #}
|
||||
<div class="vr" style="height: 28px;"></div>
|
||||
|
||||
{# Form 2: Schedule Interviews #}
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'schedule_interviews' job.slug %}" method="get" class="action-group">
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
|
||||
@ -2,6 +2,170 @@
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}- {{ job.title }} - ATS{% endblock %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-primary-text: #343a40;
|
||||
--kaauh-success: #28a745;
|
||||
--kaauh-info: #17a2b8;
|
||||
--kaauh-danger: #dc3545;
|
||||
--kaauh-warning: #ffc107;
|
||||
}
|
||||
|
||||
/* Primary Color Overrides */
|
||||
.text-primary-theme { color: var(--kaauh-teal) !important; }
|
||||
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
|
||||
|
||||
/* 1. Main Container & Card Styling */
|
||||
.kaauh-card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Dedicated style for the filter block */
|
||||
.filter-controls {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
/* 2. Button Styling (Themed for Main Actions) */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.btn-main-action:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.btn-outline-secondary {
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal);
|
||||
}
|
||||
.btn-outline-secondary:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
/* Style for the Bulk Move button */
|
||||
.btn-bulk-action {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn-bulk-action:hover {
|
||||
background-color: #00363e;
|
||||
border-color: #00363e;
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background-color: white;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-bottom: 2px solid var(--kaauh-teal);
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* 4. Badges and Statuses */
|
||||
.ai-score-badge {
|
||||
background-color: var(--kaauh-teal-dark) !important;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
padding: 0.4em 0.8em;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
.status-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.3em 0.7em;
|
||||
border-radius: 0.35rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 0.3rem;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.stage-Applied { background-color: #e9ecef; color: #495057; }
|
||||
.stage-Screening { background-color: var(--kaauh-info); color: white; }
|
||||
.stage-Exam { background-color: var(--kaauh-warning); color: #856404; }
|
||||
.stage-Interview { background-color: #17a2b8; color: white; }
|
||||
.stage-Offer { background-color: var(--kaauh-success); color: white; }
|
||||
|
||||
/* Timeline specific container */
|
||||
.applicant-tracking-timeline {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* --- CUSTOM HEIGHT OPTIMIZATION (MAKING INPUTS/BUTTONS SMALLER) --- */
|
||||
.form-control-sm,
|
||||
.btn-sm {
|
||||
/* Reduce vertical padding even more than default Bootstrap 'sm' */
|
||||
padding-top: 0.2rem !important;
|
||||
padding-bottom: 0.2rem !important;
|
||||
/* Ensure a consistent, small height for both */
|
||||
height: 28px !important;
|
||||
font-size: 0.8rem !important; /* Slightly smaller font */
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
@ -24,29 +188,38 @@
|
||||
</div>
|
||||
|
||||
<div class="kaauh-card shadow-sm p-3">
|
||||
{% if candidates %}
|
||||
<div class="bulk-action-bar">
|
||||
{% if candidates %}
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
{# Use d-flex and align-items-end on the container to align the form and the separator #}
|
||||
<div class="d-flex align-items-end gap-3">
|
||||
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
|
||||
{% csrf_token %}
|
||||
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
|
||||
<option selected>
|
||||
----------
|
||||
</option>
|
||||
<option value="Hired">
|
||||
{% trans "To Hired" %}
|
||||
</option>
|
||||
<option value="Rejected">
|
||||
{% trans "To Rejected" %}
|
||||
</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-arrow-right me-1"></i>
|
||||
</button>
|
||||
</form>
|
||||
{# Form: Hired/Rejected Status Update #}
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="d-flex align-items-end gap-2 action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# Select element #}
|
||||
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
|
||||
<option selected>
|
||||
----------
|
||||
</option>
|
||||
<option value="Hired">
|
||||
{% trans "To Hired" %}
|
||||
</option>
|
||||
<option value="Rejected">
|
||||
{% trans "To Rejected" %}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
{# Button #}
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Move" %}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="vr" style="height: 28px;"></div>
|
||||
{# Separator (Vertical Rule) #}
|
||||
<div class="vr" style="height: 28px;"></div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="table-responsive">
|
||||
|
||||
@ -41,6 +41,7 @@
|
||||
margin: 0 auto;
|
||||
padding: 1rem; /* p-4 */
|
||||
}
|
||||
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
@ -516,7 +517,7 @@
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-kaauh-light-bg font-sans">
|
||||
<div class="container">
|
||||
<div class="container container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;">
|
||||
<!-- Header Section -->
|
||||
<header class="header-box">
|
||||
<div class="header-info">
|
||||
@ -535,14 +536,14 @@
|
||||
<!-- GitHub and LinkedIn links for quick access (null in example but included for completeness) -->
|
||||
{% if candidate.resume_data.linkedin %}
|
||||
<div class="contact-item">
|
||||
<i class="fab fa-linkedin"></i>
|
||||
<a href="{{ candidate.resume_data.linkedin }}" target="_blank">LinkedIn</a>
|
||||
|
||||
<a href="{{ candidate.resume_data.linkedin }}" target="_blank"><i class="fab fa-linkedin text-white"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if candidate.resume_data.github %}
|
||||
<div class="contact-item">
|
||||
<i class="fab fa-github"></i>
|
||||
<a href="{{ candidate.resume_data.github }}" target="_blank">GitHub</a>
|
||||
|
||||
<a href="{{ candidate.resume_data.github }}" target="_blank"><i class="fab fa-github text-white"></i></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -2,7 +2,170 @@
|
||||
{% load static i18n %}
|
||||
|
||||
{% block title %}Candidate Management - {{ job.title }} - University ATS{% endblock %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* KAAT-S UI Variables */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-primary-text: #343a40;
|
||||
--kaauh-success: #28a745;
|
||||
--kaauh-info: #17a2b8;
|
||||
--kaauh-danger: #dc3545;
|
||||
--kaauh-warning: #ffc107;
|
||||
}
|
||||
|
||||
/* Primary Color Overrides */
|
||||
.text-primary-theme { color: var(--kaauh-teal) !important; }
|
||||
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
|
||||
|
||||
/* 1. Main Container & Card Styling */
|
||||
.kaauh-card {
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
/* Dedicated style for the filter block */
|
||||
.filter-controls {
|
||||
background-color: #f8f9fa;
|
||||
border-radius: 0.75rem;
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
border: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
/* 2. Button Styling (Themed for Main Actions) */
|
||||
.btn-main-action {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.btn-main-action:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.btn-outline-secondary {
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal);
|
||||
}
|
||||
.btn-outline-secondary:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
/* Style for the Bulk Move button */
|
||||
.btn-bulk-action {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
color: white;
|
||||
font-weight: 500;
|
||||
}
|
||||
.btn-bulk-action:hover {
|
||||
background-color: #00363e;
|
||||
border-color: #00363e;
|
||||
}
|
||||
|
||||
/* 3. Candidate Table Styling (Aligned with KAAT-S) */
|
||||
.candidate-table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
background-color: white;
|
||||
border-radius: 0.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.candidate-table thead {
|
||||
background-color: var(--kaauh-border);
|
||||
}
|
||||
.candidate-table th {
|
||||
padding: 0.75rem 1rem;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
border-bottom: 2px solid var(--kaauh-teal);
|
||||
font-size: 0.9rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.candidate-table td {
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
vertical-align: middle;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.candidate-table tbody tr:hover {
|
||||
background-color: #f1f3f4;
|
||||
}
|
||||
.candidate-table thead th:nth-child(1) { width: 40px; }
|
||||
.candidate-table thead th:nth-child(4) { width: 10%; }
|
||||
.candidate-table thead th:nth-child(7) { width: 100px; }
|
||||
|
||||
.candidate-name {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-primary-text);
|
||||
}
|
||||
.candidate-details {
|
||||
font-size: 0.8rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
/* 4. Badges and Statuses */
|
||||
.ai-score-badge {
|
||||
background-color: var(--kaauh-teal-dark) !important;
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
padding: 0.4em 0.8em;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
.status-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.3em 0.7em;
|
||||
border-radius: 0.35rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.bg-applicant { background-color: #6c757d !important; color: white; }
|
||||
.bg-candidate { background-color: var(--kaauh-success) !important; color: white; }
|
||||
|
||||
/* Stage Badges */
|
||||
.stage-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 0.3rem;
|
||||
font-weight: 600;
|
||||
display: inline-block;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
.stage-Applied { background-color: #e9ecef; color: #495057; }
|
||||
.stage-Screening { background-color: var(--kaauh-info); color: white; }
|
||||
.stage-Exam { background-color: var(--kaauh-warning); color: #856404; }
|
||||
.stage-Interview { background-color: #17a2b8; color: white; }
|
||||
.stage-Offer { background-color: var(--kaauh-success); color: white; }
|
||||
|
||||
/* Timeline specific container */
|
||||
.applicant-tracking-timeline {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* --- CUSTOM HEIGHT OPTIMIZATION (MAKING INPUTS/BUTTONS SMALLER) --- */
|
||||
.form-control-sm,
|
||||
.btn-sm {
|
||||
/* Reduce vertical padding even more than default Bootstrap 'sm' */
|
||||
padding-top: 0.2rem !important;
|
||||
padding-bottom: 0.2rem !important;
|
||||
/* Ensure a consistent, small height for both */
|
||||
height: 28px !important;
|
||||
font-size: 0.8rem !important; /* Slightly smaller font */
|
||||
}
|
||||
|
||||
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
<div class="container-fluid py-4">
|
||||
@ -97,23 +260,35 @@
|
||||
|
||||
<div class="kaauh-card p-3">
|
||||
{% if candidates %}
|
||||
<div class="bulk-action-bar">
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
|
||||
{% csrf_token %}
|
||||
<label for="update_status" class="form-label small mb-0 fw-bold">{% trans "Move Selected To:" %}</label>
|
||||
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: auto;">
|
||||
<option selected>
|
||||
----------
|
||||
</option>
|
||||
<option value="Exam">
|
||||
{% trans "Exam Stage" %}
|
||||
</option>
|
||||
</select>
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Update Status" %}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="bulk-action-bar p-3 bg-light border-bottom">
|
||||
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
|
||||
{% csrf_token %}
|
||||
|
||||
{# MODIFIED: Using d-flex for horizontal alignment and align-items-end to align everything based on the baseline of the button/select #}
|
||||
<div class="d-flex align-items-end gap-3">
|
||||
|
||||
{# Select Input Group #}
|
||||
<div>
|
||||
<label for="update_status" class="form-label small mb-1 fw-bold">{% trans "Move Selected To:" %}</label>
|
||||
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="min-width: 150px;">
|
||||
<option selected>
|
||||
----------
|
||||
</option>
|
||||
<option value="Exam">
|
||||
{% trans "Exam Stage" %}
|
||||
</option>
|
||||
{# Include other options here, such as Interview, Offer, Rejected, etc. #}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{# Button #}
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-arrow-right me-1"></i> {% trans "Update Status" %}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="table-responsive">
|
||||
|
||||
@ -7,12 +7,13 @@
|
||||
<style>
|
||||
/* UI Variables for the KAAT-S Theme (Teal/Consistent Look) */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal: #00636e; /* Dark Teal */
|
||||
--kaauh-teal-light: #0093a3;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-primary-text: #343a40;
|
||||
--color-success: #28a745;
|
||||
--color-warning: #ffc107; /* Standardized warning color */
|
||||
--color-info: #17a2b8;
|
||||
}
|
||||
|
||||
@ -29,14 +30,33 @@
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* Card Header and Icon Styling */
|
||||
.card-header {
|
||||
font-weight: 600;
|
||||
padding: 1.25rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
background-color: #f8f9fa;
|
||||
display: flex; /* Ensure title and filter are aligned */
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Stats Grid Layout - Six columns for better detail display */
|
||||
.card-header h3, .card-header h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--kaauh-primary-text);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
color: var(--kaauh-teal);
|
||||
font-size: 1.75rem;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
|
||||
/* Stats Grid Layout */
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
@ -59,21 +79,27 @@
|
||||
color: #6c757d;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Header Styling */
|
||||
.card-header h3, .card-header h2 {
|
||||
|
||||
/* Dropdown/Filter Styling */
|
||||
.job-filter-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--kaauh-primary-text);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
color: var(--kaauh-teal);
|
||||
font-size: 1.75rem;
|
||||
margin-right: 0.75rem;
|
||||
.form-select {
|
||||
border-color: var(--kaauh-border);
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.form-select:focus {
|
||||
border-color: var(--kaauh-teal);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
|
||||
}
|
||||
.job-filter-label {
|
||||
font-weight: 500;
|
||||
color: var(--kaauh-primary-text);
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Chart Container */
|
||||
@ -81,6 +107,16 @@
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
/* Bootstrap Overrides (Optional, for full consistency) */
|
||||
.btn-primary {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -89,6 +125,9 @@
|
||||
|
||||
<h1 class="mb-4" style="color: var(--kaauh-teal-dark); font-weight: 700;">{% trans "Recruitment Intelligence" %} 🧠</h1>
|
||||
|
||||
{# -------------------------------------------------------------------------- #}
|
||||
{# STATS CARDS SECTION #}
|
||||
{# -------------------------------------------------------------------------- #}
|
||||
<div class="stats">
|
||||
|
||||
<div class="card">
|
||||
@ -140,84 +179,183 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-header">
|
||||
<h2 class="d-flex align-items-center mb-0">
|
||||
<i class="fas fa-chart-bar stat-icon"></i>
|
||||
{% trans "Top 5 Application Volume" %}
|
||||
</h2>
|
||||
{# -------------------------------------------------------------------------- #}
|
||||
{# CHARTS SECTION (Using a row/col layout for structure) #}
|
||||
{# -------------------------------------------------------------------------- #}
|
||||
<div class="row g-4">
|
||||
|
||||
{# BAR CHART - Application Volume #}
|
||||
<div class="col-lg-12">
|
||||
<div class="card shadow-lg h-100">
|
||||
<div class="card-header">
|
||||
<h2>
|
||||
<i class="fas fa-chart-bar stat-icon"></i>
|
||||
{% trans "Top 5 Application Volume" %}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="applicationsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="applicationsChart"></canvas>
|
||||
|
||||
{# DONUT CHART - Candidate Pipeline Status #}
|
||||
<div class="col-lg-12">
|
||||
<div class="card shadow-lg h-100">
|
||||
<div class="card-header">
|
||||
<h2>
|
||||
<i class="fas fa-filter stat-icon"></i>
|
||||
{% trans "Candidate Pipeline Status for job: " %}
|
||||
</h2>
|
||||
<small>{{my_job}}</small>
|
||||
|
||||
{# Job Filter Dropdown - Consistent with Card Header Layout #}
|
||||
<form method="get" action="." class="job-filter-container">
|
||||
<label for="job-select" class="job-filter-label d-none d-md-inline">{% trans "Filter Job:" %}</label>
|
||||
<select name="selected_job_id" id="job-select" class="form-select form-select-sm" onchange="this.form.submit()">
|
||||
|
||||
<option value="">{% trans "All Jobs (Default View)" %}</option>
|
||||
{% for job in jobs%}
|
||||
<option value="{{ job.internal_job_id }}" {% if selected_job_id == job.internal_job_id %}selected{% endif %}>
|
||||
{{ job }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="chart-container d-flex justify-content-center align-items-center">
|
||||
<canvas id="candidate_donout_chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
// Get the all_candidates_count value from Django context
|
||||
const ALL_CANDIDATES_COUNT = '{{ all_candidates_count|default:0 }}';
|
||||
|
||||
// --- 1. DONUT CHART CENTER TEXT PLUGIN (Custom) ---
|
||||
const centerTextPlugin = {
|
||||
id: 'centerText',
|
||||
beforeDraw: (chart) => {
|
||||
const { ctx } = chart;
|
||||
|
||||
// Convert to integer (handle case where all_candidates_count might be missing)
|
||||
const total = parseInt(ALL_CANDIDATES_COUNT) || 0;
|
||||
if (total === 0) return; // Don't draw if count is zero
|
||||
|
||||
// Get chart center coordinates
|
||||
const xCenter = chart.getDatasetMeta(0).data[0].x;
|
||||
const yCenter = chart.getDatasetMeta(0).data[0].y;
|
||||
|
||||
ctx.restore();
|
||||
|
||||
// --- First Line: The Total Count (Bold Number) ---
|
||||
ctx.font = 'bold 28px sans-serif';
|
||||
ctx.fillStyle = 'var(--kaauh-teal-dark)';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
ctx.fillText(total.toString(), xCenter, yCenter - 10);
|
||||
|
||||
// --- Second Line: The Label ---
|
||||
const labelText = '{% trans "Total" %}';
|
||||
ctx.font = '12px sans-serif';
|
||||
ctx.fillStyle = '#6c757d';
|
||||
ctx.fillText(labelText, xCenter, yCenter + 20);
|
||||
|
||||
ctx.save();
|
||||
}
|
||||
};
|
||||
// ------------------------------------------------
|
||||
|
||||
// Pass context data safely to JavaScript
|
||||
const jobTitles = JSON.parse('{{ job_titles|escapejs }}').slice(0, 5); // Take top 5
|
||||
const jobAppCounts = JSON.parse('{{ job_app_counts|escapejs }}').slice(0, 5); // Take top 5
|
||||
|
||||
const ctx = document.getElementById('applicationsChart').getContext('2d');
|
||||
const chart = new Chart(ctx, {
|
||||
// BAR CHART configuration
|
||||
const ctxBar = document.getElementById('applicationsChart').getContext('2d');
|
||||
new Chart(ctxBar, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
// Use the parsed and sliced data
|
||||
labels: jobTitles,
|
||||
datasets: [{
|
||||
label: '{% trans "Applications" %}',
|
||||
data: jobAppCounts,
|
||||
// Use the defined CSS variable for consistency
|
||||
backgroundColor: ' #00636e', // Green theme
|
||||
borderColor: ' #004a53',
|
||||
backgroundColor: 'var(--kaauh-teal)',
|
||||
borderColor: 'var(--kaauh-teal-dark)',
|
||||
borderWidth: 1,
|
||||
barThickness: 50
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
aspectRatio: 2.5, // Make the chart wider
|
||||
aspectRatio: 2.5,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false // Hide legend since there's only one dataset
|
||||
},
|
||||
legend: { display: false },
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Top 5 Most Applied Jobs',
|
||||
font: {
|
||||
size: 16
|
||||
},
|
||||
text: '{% trans "Top 5 Most Applied Jobs" %}',
|
||||
font: { size: 16 },
|
||||
color: 'var(--kaauh-primary-text)'
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Total Applications'
|
||||
},
|
||||
ticks: {
|
||||
color: '#333333',
|
||||
precision: 0 // Ensure y-axis labels are integers
|
||||
},
|
||||
grid: {
|
||||
color: '#e0e0e0'
|
||||
}
|
||||
title: { display: true, text: '{% trans "Total Applications" %}' },
|
||||
ticks: { color: '#333333', precision: 0 },
|
||||
grid: { color: '#e0e0e0' }
|
||||
},
|
||||
x: {
|
||||
ticks: {
|
||||
color: '#333333'
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
ticks: { color: '#333333' },
|
||||
grid: { display: false }
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// DONUT CHART configuration
|
||||
const ctxDonut = document.getElementById('candidate_donout_chart').getContext('2d');
|
||||
|
||||
new Chart(ctxDonut, {
|
||||
type: 'doughnut',
|
||||
data: {
|
||||
// Ensure these contexts are always output as valid JSON arrays
|
||||
labels: JSON.parse('{{ candidate_stage|safe }}'),
|
||||
datasets: [{
|
||||
label: '{% trans "Candidate Count" %}',
|
||||
data: JSON.parse('{{ candidates_count|safe }}'),
|
||||
backgroundColor: [
|
||||
'var(--kaauh-teal)', // Applied (Primary)
|
||||
'rgb(255, 159, 64)', // Exam (Orange)
|
||||
'rgb(54, 162, 235)', // Interview (Blue)
|
||||
'rgb(75, 192, 192)' // Offer (Green)
|
||||
],
|
||||
hoverOffset: 4
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'bottom',
|
||||
labels: { padding: 20 }
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: '{% trans "Pipeline Status Breakdown" %}',
|
||||
font: { size: 16 }
|
||||
}
|
||||
}
|
||||
},
|
||||
// --- Register the custom plugin here ---
|
||||
plugins: [centerTextPlugin]
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user