Compare commits
13 Commits
c2773d9b4e
...
1f3c38d687
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f3c38d687 | |||
| a1558a6b22 | |||
| f33cf97975 | |||
| ee78018a5a | |||
| fec156fab2 | |||
| e6daa39f1e | |||
| 3086b38a23 | |||
| ef8616c088 | |||
| d97ad030f1 | |||
| 65c25a1a57 | |||
| 2dd90e4d38 | |||
| 5921e30396 | |||
| f0ae8f46d9 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -148,7 +148,7 @@ DATABASES = {
|
||||
# 'ENGINE': 'django.db.backends.sqlite3',
|
||||
# 'NAME': BASE_DIR / 'db.sqlite3',
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators
|
||||
@ -171,12 +171,12 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||
]
|
||||
|
||||
|
||||
ACCOUNT_LOGIN_METHODS = ['email']
|
||||
ACCOUNT_LOGIN_METHODS = ['email']
|
||||
ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*']
|
||||
|
||||
ACCOUNT_UNIQUE_EMAIL = True
|
||||
ACCOUNT_EMAIL_VERIFICATION = 'none'
|
||||
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
|
||||
|
||||
ACCOUNT_UNIQUE_EMAIL = True
|
||||
ACCOUNT_EMAIL_VERIFICATION = 'none'
|
||||
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
|
||||
|
||||
|
||||
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
||||
@ -274,7 +274,6 @@ LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='
|
||||
LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/'
|
||||
|
||||
|
||||
|
||||
Q_CLUSTER = {
|
||||
'name': 'KAAUH_CLUSTER',
|
||||
'workers': 8,
|
||||
|
||||
@ -16,25 +16,25 @@ urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/', include(router.urls)),
|
||||
path('accounts/', include('allauth.urls')),
|
||||
|
||||
|
||||
path('i18n/', include('django.conf.urls.i18n')),
|
||||
# path('summernote/', include('django_summernote.urls')),
|
||||
# path('', include('recruitment.urls')),
|
||||
path("ckeditor5/", include('django_ckeditor_5.urls')),
|
||||
|
||||
path('<int:template_id>/', views.form_wizard_view, name='form_wizard'),
|
||||
path('<int:template_id>/submit/', views.submit_form, name='submit_form'),
|
||||
path('form_wizard/<slug:template_slug>/', views.form_wizard_view, name='form_wizard'),
|
||||
path('form/<slug:template_slug>/submit/', views.submit_form, name='submit_form'),
|
||||
|
||||
path('api/templates/', views.list_form_templates, name='list_form_templates'),
|
||||
path('api/templates/save/', views.save_form_template, name='save_form_template'),
|
||||
path('api/templates/<int:template_id>/', views.load_form_template, name='load_form_template'),
|
||||
path('api/templates/<int:template_id>/delete/', views.delete_form_template, name='delete_form_template'),
|
||||
path('api/templates/<slug:template_slug>/', views.load_form_template, name='load_form_template'),
|
||||
path('api/templates/<slug:template_slug>/delete/', views.delete_form_template, name='delete_form_template'),
|
||||
path('api/webhook/',views.zoom_webhook_view,name='zoom_webhook_view')
|
||||
]
|
||||
|
||||
urlpatterns += i18n_patterns(
|
||||
path('', include('recruitment.urls')),
|
||||
|
||||
|
||||
)
|
||||
# 2. URLs that DO have a language prefix (user-facing views)
|
||||
# This includes the root path (''), which is handled by 'recruitment.urls'
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -116,7 +116,7 @@ class JobPostingAdmin(admin.ModelAdmin):
|
||||
'fields': ('internal_job_id', 'created_by', 'created_at', 'updated_at')
|
||||
}),
|
||||
('Integration', {
|
||||
'fields': ('source', 'open_positions', 'position_number', 'reporting_to', 'start_date')
|
||||
'fields': ('source', 'open_positions', 'position_number', 'reporting_to',)
|
||||
}),
|
||||
('LinkedIn Integration', {
|
||||
'fields': ('posted_to_linkedin', 'linkedin_post_id', 'linkedin_post_url', 'linkedin_posted_at')
|
||||
|
||||
@ -197,10 +197,10 @@ class JobPostingForm(forms.ModelForm):
|
||||
fields = [
|
||||
'title', 'department', 'job_type', 'workplace_type',
|
||||
'location_city', 'location_state', 'location_country',
|
||||
'description', 'qualifications', 'salary_range', 'benefits','application_start_date'
|
||||
,'application_deadline', 'application_instructions',
|
||||
'position_number', 'reporting_to', 'joining_date',
|
||||
'created_by','open_positions','hash_tags','max_applications'
|
||||
'description', 'qualifications', 'salary_range', 'benefits',
|
||||
'application_deadline', 'application_instructions',
|
||||
'position_number', 'reporting_to',
|
||||
'open_positions','hash_tags','max_applications'
|
||||
]
|
||||
widgets = {
|
||||
# Basic Information
|
||||
@ -243,6 +243,15 @@ class JobPostingForm(forms.ModelForm):
|
||||
}),
|
||||
|
||||
|
||||
# Application Information
|
||||
# 'application_url': forms.URLInput(attrs={
|
||||
# 'class': 'form-control',
|
||||
# 'placeholder': 'https://university.edu/careers/job123',
|
||||
# 'required': True
|
||||
# }),
|
||||
|
||||
|
||||
|
||||
# Application Information
|
||||
# 'application_url': forms.URLInput(attrs={
|
||||
# 'class': 'form-control',
|
||||
@ -255,7 +264,8 @@ class JobPostingForm(forms.ModelForm):
|
||||
}),
|
||||
'application_deadline': forms.DateInput(attrs={
|
||||
'class': 'form-control',
|
||||
'type': 'date'
|
||||
'type': 'date',
|
||||
'required':True
|
||||
}),
|
||||
|
||||
'open_positions': forms.NumberInput(attrs={
|
||||
@ -278,15 +288,8 @@ class JobPostingForm(forms.ModelForm):
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Department Chair, Director, etc.'
|
||||
}),
|
||||
'joining_date': forms.DateInput(attrs={
|
||||
'class': 'form-control',
|
||||
'type': 'date'
|
||||
}),
|
||||
|
||||
'created_by': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'University Administrator'
|
||||
}),
|
||||
|
||||
|
||||
'max_applications': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'min': 1,
|
||||
@ -296,19 +299,15 @@ class JobPostingForm(forms.ModelForm):
|
||||
|
||||
def __init__(self,*args,**kwargs):
|
||||
|
||||
# Extract your custom argument BEFORE calling super()
|
||||
self.is_anonymous_user = kwargs.pop('is_anonymous_user', False)
|
||||
# Now call the parent __init__ with remaining args
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
if not self.instance.pk:# Creating new job posting
|
||||
if not self.is_anonymous_user:
|
||||
self.fields['created_by'].initial = 'University Administrator'
|
||||
# self.fields['status'].initial = 'Draft'
|
||||
self.fields['location_city'].initial='Riyadh'
|
||||
self.fields['location_state'].initial='Riyadh Province'
|
||||
self.fields['location_country'].initial='Saudi Arabia'
|
||||
|
||||
|
||||
|
||||
def clean_hash_tags(self):
|
||||
hash_tags=self.cleaned_data.get('hash_tags')
|
||||
|
||||
@ -8,7 +8,7 @@ import logging
|
||||
from django.conf import settings
|
||||
import time
|
||||
import random
|
||||
from django.utils import timezone
|
||||
from django.utils import timezone
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -27,7 +27,7 @@ class LinkedInService:
|
||||
self.ASSET_STATUS_INTERVAL = 2 # Check every 2 seconds
|
||||
|
||||
# --- AUTHENTICATION & PROFILE ---
|
||||
|
||||
|
||||
def get_auth_url(self):
|
||||
"""Generate LinkedIn OAuth URL"""
|
||||
params = {
|
||||
@ -86,7 +86,7 @@ class LinkedInService:
|
||||
'X-Restli-Protocol-Version': LINKEDIN_API_VERSION,
|
||||
'LinkedIn-Version': LINKEDIN_VERSION,
|
||||
}
|
||||
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=10)
|
||||
response.raise_for_status()
|
||||
@ -137,7 +137,7 @@ class LinkedInService:
|
||||
|
||||
response = requests.post(upload_url, headers=headers, data=image_content, timeout=60)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
# --- CRITICAL FIX: POLL FOR ASSET STATUS ---
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < self.ASSET_STATUS_TIMEOUT:
|
||||
@ -149,13 +149,13 @@ 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.")
|
||||
@ -169,36 +169,36 @@ class LinkedInService:
|
||||
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]
|
||||
|
||||
@ -213,10 +213,10 @@ class LinkedInService:
|
||||
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 +229,7 @@ class LinkedInService:
|
||||
details_list.append(f"🏠 Workplace: {job_posting.get_workplace_type_display()}")
|
||||
if job_posting.salary_range:
|
||||
details_list.append(f"💰 Salary: {job_posting.salary_range}")
|
||||
|
||||
|
||||
if details_list:
|
||||
message_parts.append("*Key Information*:")
|
||||
message_parts.extend(details_list)
|
||||
@ -239,7 +239,7 @@ 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---")
|
||||
@ -255,17 +255,17 @@ class LinkedInService:
|
||||
|
||||
if len(message_parts)>=3000:
|
||||
message_parts=message_parts[0:2980]+"........"
|
||||
|
||||
|
||||
return "\n".join(message_parts)
|
||||
|
||||
|
||||
def _send_ugc_post(self, person_urn, job_posting, media_category="NONE", media_list=None):
|
||||
"""
|
||||
New private method to handle the final UGC post request (text or image).
|
||||
This eliminates the duplication between create_job_post and create_job_post_with_image.
|
||||
"""
|
||||
|
||||
|
||||
message = self._build_post_message(job_posting)
|
||||
|
||||
|
||||
url = "https://api.linkedin.com/v2/ugcPosts"
|
||||
headers = {
|
||||
'Authorization': f'Bearer {self.access_token}',
|
||||
@ -280,7 +280,7 @@ class LinkedInService:
|
||||
"shareMediaCategory": media_category,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if media_list:
|
||||
specific_content["com.linkedin.ugc.ShareContent"]["media"] = media_list
|
||||
|
||||
@ -295,7 +295,7 @@ class LinkedInService:
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=60)
|
||||
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 ""
|
||||
|
||||
@ -309,13 +309,13 @@ 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."""
|
||||
|
||||
|
||||
# Prepare the media list for the _send_ugc_post helper
|
||||
media_list = [{
|
||||
"status": "READY",
|
||||
"media": asset_urn,
|
||||
"description": {"text": job_posting.title},
|
||||
"originalUrl": job_posting.application_url,
|
||||
"originalUrl": job_posting.application_url,
|
||||
"title": {"text": "Apply Now"}
|
||||
}]
|
||||
|
||||
@ -371,7 +371,7 @@ class LinkedInService:
|
||||
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
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-19 15:50
|
||||
# Generated by Django 5.2.7 on 2025-10-21 22:26
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
@ -236,8 +236,8 @@ class Migration(migrations.Migration):
|
||||
('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_start_date', models.DateField(blank=True, null=True)),
|
||||
('application_deadline', models.DateField(blank=True, db_index=True, null=True)),
|
||||
('application_start_date', models.DateField()),
|
||||
('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)),
|
||||
@ -251,7 +251,6 @@ class Migration(migrations.Migration):
|
||||
('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)),
|
||||
('joining_date', models.DateField(blank=True, help_text='Desired start date', null=True)),
|
||||
('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')),
|
||||
@ -309,7 +308,7 @@ class Migration(migrations.Migration):
|
||||
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/')),
|
||||
('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)),
|
||||
],
|
||||
),
|
||||
|
||||
@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-21 23:25
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='jobposting',
|
||||
name='application_start_date',
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -2,8 +2,9 @@ from django.db import models
|
||||
from django.urls import reverse
|
||||
from typing import List,Dict,Any
|
||||
from django.utils import timezone
|
||||
from django.db.models import FloatField,CharField
|
||||
from django.db.models.functions import Cast
|
||||
from django.db.models import FloatField,CharField,IntegerField
|
||||
from django.db.models.functions import Cast,Coalesce
|
||||
from django.db.models import F
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.validators import URLValidator
|
||||
from django_countries.fields import CountryField
|
||||
@ -83,8 +84,8 @@ class JobPosting(Base):
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
application_start_date=models.DateField(null=True, blank=True)
|
||||
application_deadline = models.DateField(db_index=True, null=True, blank=True) # Added index
|
||||
|
||||
application_deadline = models.DateField(db_index=True) # Added index
|
||||
application_instructions =CKEditor5Field(
|
||||
blank=True, null=True,config_name='extends'
|
||||
)
|
||||
@ -136,7 +137,7 @@ class JobPosting(Base):
|
||||
reporting_to = models.CharField(
|
||||
max_length=100, blank=True, help_text="Who this position reports to"
|
||||
)
|
||||
joining_date = models.DateField(null=True, blank=True, help_text="Desired start date")
|
||||
|
||||
open_positions = models.PositiveIntegerField(
|
||||
default=1, help_text="Number of open positions for this job"
|
||||
)
|
||||
@ -225,6 +226,7 @@ class JobPosting(Base):
|
||||
parts.append(self.location_country)
|
||||
return ", ".join(parts) if parts else "Not specified"
|
||||
|
||||
@property
|
||||
def is_expired(self):
|
||||
"""Check if application deadline has passed"""
|
||||
if self.application_deadline:
|
||||
@ -289,7 +291,7 @@ class JobPosting(Base):
|
||||
return self.current_applications_count >= self.max_applications
|
||||
@property
|
||||
def all_candidates(self):
|
||||
return self.candidates.annotate(sortable_score=Cast('ai_analysis_data__match_score',output_field=CharField())).order_by('-sortable_score')
|
||||
return self.candidates.annotate(sortable_score=Coalesce(Cast('ai_analysis_data__analysis_data__match_score',output_field=IntegerField()),0)).order_by('-sortable_score')
|
||||
@property
|
||||
def screening_candidates(self):
|
||||
return self.all_candidates.filter(stage="Applied")
|
||||
@ -304,7 +306,7 @@ class JobPosting(Base):
|
||||
@property
|
||||
def offer_candidates(self):
|
||||
return self.all_candidates.filter(stage="Offer")
|
||||
|
||||
|
||||
|
||||
#counts
|
||||
@property
|
||||
@ -451,94 +453,99 @@ class Candidate(Base):
|
||||
Generic method to set any single key-value pair and save.
|
||||
"""
|
||||
self.ai_analysis_data[key] = value
|
||||
self.save(update_fields=['ai_analysis_data'])
|
||||
# self.save(update_fields=['ai_analysis_data'])
|
||||
|
||||
# ====================================================================
|
||||
# ✨ PROPERTIES (GETTERS)
|
||||
# ====================================================================
|
||||
|
||||
@property
|
||||
def resume_data(self):
|
||||
return self.ai_analysis_data.get('resume_data', {})
|
||||
@property
|
||||
def analysis_data(self):
|
||||
return self.ai_analysis_data.get('analysis_data', {})
|
||||
@property
|
||||
def match_score(self) -> int:
|
||||
"""1. A score from 0 to 100 representing how well the candidate fits the role."""
|
||||
return self.ai_analysis_data.get('match_score', 0)
|
||||
return self.analysis_data.get('match_score', 0)
|
||||
|
||||
@property
|
||||
def years_of_experience(self) -> float:
|
||||
"""4. The total number of years of professional experience as a numerical value."""
|
||||
return self.ai_analysis_data.get('years_of_experience', 0.0)
|
||||
return self.analysis_data.get('years_of_experience', 0.0)
|
||||
|
||||
@property
|
||||
def soft_skills_score(self) -> int:
|
||||
"""15. A score (0-100) for inferred non-technical skills."""
|
||||
return self.ai_analysis_data.get('soft_skills_score', 0)
|
||||
return self.analysis_data.get('soft_skills_score', 0)
|
||||
|
||||
@property
|
||||
def industry_match_score(self) -> int:
|
||||
"""16. A score (0-100) for the relevance of the candidate's industry experience."""
|
||||
# Renamed to clarify: experience_industry_match
|
||||
return self.ai_analysis_data.get('experience_industry_match', 0)
|
||||
return self.analysis_data.get('experience_industry_match', 0)
|
||||
|
||||
# --- Properties for Funnel & Screening Efficiency ---
|
||||
|
||||
@property
|
||||
def min_requirements_met(self) -> bool:
|
||||
"""14. Boolean (true/false) indicating if all non-negotiable minimum requirements are met."""
|
||||
return self.ai_analysis_data.get('min_req_met_bool', False)
|
||||
return self.analysis_data.get('min_req_met_bool', False)
|
||||
|
||||
@property
|
||||
def screening_stage_rating(self) -> str:
|
||||
"""13. A standardized rating (e.g., "A - Highly Qualified", "B - Qualified")."""
|
||||
return self.ai_analysis_data.get('screening_stage_rating', 'N/A')
|
||||
return self.analysis_data.get('screening_stage_rating', 'N/A')
|
||||
|
||||
@property
|
||||
def top_3_keywords(self) -> List[str]:
|
||||
"""10. A list of the three most dominant and relevant technical skills or technologies."""
|
||||
return self.ai_analysis_data.get('top_3_keywords', [])
|
||||
return self.analysis_data.get('top_3_keywords', [])
|
||||
|
||||
@property
|
||||
def most_recent_job_title(self) -> str:
|
||||
"""8. The candidate's most recent or current professional job title."""
|
||||
return self.ai_analysis_data.get('most_recent_job_title', 'N/A')
|
||||
return self.analysis_data.get('most_recent_job_title', 'N/A')
|
||||
|
||||
# --- Properties for Structured Detail ---
|
||||
|
||||
@property
|
||||
def criteria_checklist(self) -> Dict[str, str]:
|
||||
"""5 & 6. An object rating the candidate's match for each specific criterion."""
|
||||
return self.ai_analysis_data.get('criteria_checklist', {})
|
||||
return self.analysis_data.get('criteria_checklist', {})
|
||||
|
||||
@property
|
||||
def professional_category(self) -> str:
|
||||
"""7. The most fitting professional field or category for the individual."""
|
||||
return self.ai_analysis_data.get('category', 'N/A')
|
||||
return self.analysis_data.get('category', 'N/A')
|
||||
|
||||
@property
|
||||
def language_fluency(self) -> List[Dict[str, str]]:
|
||||
"""12. A list of languages and their fluency levels mentioned."""
|
||||
return self.ai_analysis_data.get('language_fluency', [])
|
||||
return self.analysis_data.get('language_fluency', [])
|
||||
|
||||
# --- Properties for Summaries and Narrative ---
|
||||
|
||||
@property
|
||||
def strengths(self) -> str:
|
||||
"""2. A brief summary of why the candidate is a strong fit."""
|
||||
return self.ai_analysis_data.get('strengths', '')
|
||||
return self.analysis_data.get('strengths', '')
|
||||
|
||||
@property
|
||||
def weaknesses(self) -> str:
|
||||
"""3. A brief summary of where the candidate falls short or what criteria are missing."""
|
||||
return self.ai_analysis_data.get('weaknesses', '')
|
||||
return self.analysis_data.get('weaknesses', '')
|
||||
|
||||
@property
|
||||
def job_fit_narrative(self) -> str:
|
||||
"""11. A single, concise sentence summarizing the core fit."""
|
||||
return self.ai_analysis_data.get('job_fit_narrative', '')
|
||||
return self.analysis_data.get('job_fit_narrative', '')
|
||||
|
||||
@property
|
||||
def recommendation(self) -> str:
|
||||
"""9. Provide a detailed final recommendation for the candidate."""
|
||||
# Using a more descriptive name to avoid conflict with potential built-in methods
|
||||
return self.ai_analysis_data.get('recommendation', '')
|
||||
return self.analysis_data.get('recommendation', '')
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
@ -725,8 +732,10 @@ class FormTemplate(Base):
|
||||
blank=True, help_text="Description of the form template"
|
||||
)
|
||||
created_by = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, related_name="form_templates",null=True,blank=True, db_index=True
|
||||
)
|
||||
User, on_delete=models.CASCADE, related_name="form_templates",null=True,blank=True, db_index=True
|
||||
)
|
||||
# FIXME: on Delete model SETNULl
|
||||
|
||||
is_active = models.BooleanField(
|
||||
default=False, help_text="Whether this template is active"
|
||||
)
|
||||
|
||||
@ -7,10 +7,17 @@ from .models import FormField,FormStage,FormTemplate,Candidate,JobPosting
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# @receiver(post_save, sender=JobPosting)
|
||||
# def create_form_for_job(sender, instance, created, **kwargs):
|
||||
# if created:
|
||||
# FormTemplate.objects.create(job=instance, is_active=True, name=instance.title)
|
||||
@receiver(post_save, sender=JobPosting)
|
||||
def format_job(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
# FormTemplate.objects.create(job=instance, is_active=True, name=instance.title)
|
||||
async_task(
|
||||
'recruitment.tasks.format_job_description',
|
||||
instance.pk,
|
||||
# hook='myapp.tasks.email_sent_callback' # Optional callback
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Candidate)
|
||||
def score_candidate_resume(sender, instance, created, **kwargs):
|
||||
if not instance.is_resume_parsed:
|
||||
@ -75,238 +82,246 @@ def create_default_stages(sender, instance, created, **kwargs):
|
||||
order=4,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=contact_stage,
|
||||
label='National ID / Iqama Number',
|
||||
field_type='text',
|
||||
required=False,
|
||||
order=5,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=contact_stage,
|
||||
label='Resume Upload',
|
||||
field_type='file',
|
||||
required=True,
|
||||
order=5,
|
||||
order=6,
|
||||
is_predefined=True,
|
||||
file_types='.pdf,.doc,.docx',
|
||||
max_file_size=1
|
||||
)
|
||||
|
||||
# Stage 2: Resume Objective
|
||||
objective_stage = FormStage.objects.create(
|
||||
template=instance,
|
||||
name='Resume Objective',
|
||||
order=1,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=objective_stage,
|
||||
label='Career Objective',
|
||||
field_type='textarea',
|
||||
required=False,
|
||||
order=0,
|
||||
is_predefined=True
|
||||
)
|
||||
# # Stage 2: Resume Objective
|
||||
# objective_stage = FormStage.objects.create(
|
||||
# template=instance,
|
||||
# name='Resume Objective',
|
||||
# order=1,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=objective_stage,
|
||||
# label='Career Objective',
|
||||
# field_type='textarea',
|
||||
# required=False,
|
||||
# order=0,
|
||||
# is_predefined=True
|
||||
# )
|
||||
|
||||
# Stage 3: Education
|
||||
education_stage = FormStage.objects.create(
|
||||
template=instance,
|
||||
name='Education',
|
||||
order=2,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=education_stage,
|
||||
label='Degree',
|
||||
field_type='text',
|
||||
required=True,
|
||||
order=0,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=education_stage,
|
||||
label='Institution',
|
||||
field_type='text',
|
||||
required=True,
|
||||
order=1,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=education_stage,
|
||||
label='Location',
|
||||
field_type='text',
|
||||
required=False,
|
||||
order=2,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=education_stage,
|
||||
label='Graduation Date',
|
||||
field_type='date',
|
||||
required=False,
|
||||
order=3,
|
||||
is_predefined=True
|
||||
)
|
||||
# # Stage 3: Education
|
||||
# education_stage = FormStage.objects.create(
|
||||
# template=instance,
|
||||
# name='Education',
|
||||
# order=2,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=education_stage,
|
||||
# label='Degree',
|
||||
# field_type='text',
|
||||
# required=True,
|
||||
# order=0,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=education_stage,
|
||||
# label='Institution',
|
||||
# field_type='text',
|
||||
# required=True,
|
||||
# order=1,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=education_stage,
|
||||
# label='Location',
|
||||
# field_type='text',
|
||||
# required=False,
|
||||
# order=2,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=education_stage,
|
||||
# label='Graduation Date',
|
||||
# field_type='date',
|
||||
# required=False,
|
||||
# order=3,
|
||||
# is_predefined=True
|
||||
# )
|
||||
|
||||
# Stage 4: Experience
|
||||
experience_stage = FormStage.objects.create(
|
||||
template=instance,
|
||||
name='Experience',
|
||||
order=3,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=experience_stage,
|
||||
label='Position Title',
|
||||
field_type='text',
|
||||
required=True,
|
||||
order=0,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=experience_stage,
|
||||
label='Company Name',
|
||||
field_type='text',
|
||||
required=True,
|
||||
order=1,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=experience_stage,
|
||||
label='Location',
|
||||
field_type='text',
|
||||
required=False,
|
||||
order=2,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=experience_stage,
|
||||
label='Start Date',
|
||||
field_type='date',
|
||||
required=True,
|
||||
order=3,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=experience_stage,
|
||||
label='End Date',
|
||||
field_type='date',
|
||||
required=True,
|
||||
order=4,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=experience_stage,
|
||||
label='Responsibilities & Achievements',
|
||||
field_type='textarea',
|
||||
required=False,
|
||||
order=5,
|
||||
is_predefined=True
|
||||
)
|
||||
# # Stage 4: Experience
|
||||
# experience_stage = FormStage.objects.create(
|
||||
# template=instance,
|
||||
# name='Experience',
|
||||
# order=3,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=experience_stage,
|
||||
# label='Position Title',
|
||||
# field_type='text',
|
||||
# required=True,
|
||||
# order=0,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=experience_stage,
|
||||
# label='Company Name',
|
||||
# field_type='text',
|
||||
# required=True,
|
||||
# order=1,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=experience_stage,
|
||||
# label='Location',
|
||||
# field_type='text',
|
||||
# required=False,
|
||||
# order=2,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=experience_stage,
|
||||
# label='Start Date',
|
||||
# field_type='date',
|
||||
# required=True,
|
||||
# order=3,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=experience_stage,
|
||||
# label='End Date',
|
||||
# field_type='date',
|
||||
# required=True,
|
||||
# order=4,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=experience_stage,
|
||||
# label='Responsibilities & Achievements',
|
||||
# field_type='textarea',
|
||||
# required=False,
|
||||
# order=5,
|
||||
# is_predefined=True
|
||||
# )
|
||||
|
||||
# Stage 5: Skills
|
||||
skills_stage = FormStage.objects.create(
|
||||
template=instance,
|
||||
name='Skills',
|
||||
order=4,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=skills_stage,
|
||||
label='Technical Skills',
|
||||
field_type='checkbox',
|
||||
required=False,
|
||||
order=0,
|
||||
is_predefined=True,
|
||||
options=['Programming Languages', 'Frameworks', 'Tools & Technologies']
|
||||
)
|
||||
# # Stage 5: Skills
|
||||
# skills_stage = FormStage.objects.create(
|
||||
# template=instance,
|
||||
# name='Skills',
|
||||
# order=4,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=skills_stage,
|
||||
# label='Technical Skills',
|
||||
# field_type='checkbox',
|
||||
# required=False,
|
||||
# order=0,
|
||||
# is_predefined=True,
|
||||
# options=['Programming Languages', 'Frameworks', 'Tools & Technologies']
|
||||
# )
|
||||
|
||||
# Stage 6: Summary
|
||||
summary_stage = FormStage.objects.create(
|
||||
template=instance,
|
||||
name='Summary',
|
||||
order=5,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=summary_stage,
|
||||
label='Professional Summary',
|
||||
field_type='textarea',
|
||||
required=False,
|
||||
order=0,
|
||||
is_predefined=True
|
||||
)
|
||||
# # Stage 6: Summary
|
||||
# summary_stage = FormStage.objects.create(
|
||||
# template=instance,
|
||||
# name='Summary',
|
||||
# order=5,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=summary_stage,
|
||||
# label='Professional Summary',
|
||||
# field_type='textarea',
|
||||
# required=False,
|
||||
# order=0,
|
||||
# is_predefined=True
|
||||
# )
|
||||
|
||||
# Stage 7: Certifications
|
||||
certifications_stage = FormStage.objects.create(
|
||||
template=instance,
|
||||
name='Certifications',
|
||||
order=6,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=certifications_stage,
|
||||
label='Certification Name',
|
||||
field_type='text',
|
||||
required=False,
|
||||
order=0,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=certifications_stage,
|
||||
label='Issuing Organization',
|
||||
field_type='text',
|
||||
required=False,
|
||||
order=1,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=certifications_stage,
|
||||
label='Issue Date',
|
||||
field_type='date',
|
||||
required=False,
|
||||
order=2,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=certifications_stage,
|
||||
label='Expiration Date',
|
||||
field_type='date',
|
||||
required=False,
|
||||
order=3,
|
||||
is_predefined=True
|
||||
)
|
||||
# # Stage 7: Certifications
|
||||
# certifications_stage = FormStage.objects.create(
|
||||
# template=instance,
|
||||
# name='Certifications',
|
||||
# order=6,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=certifications_stage,
|
||||
# label='Certification Name',
|
||||
# field_type='text',
|
||||
# required=False,
|
||||
# order=0,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=certifications_stage,
|
||||
# label='Issuing Organization',
|
||||
# field_type='text',
|
||||
# required=False,
|
||||
# order=1,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=certifications_stage,
|
||||
# label='Issue Date',
|
||||
# field_type='date',
|
||||
# required=False,
|
||||
# order=2,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=certifications_stage,
|
||||
# label='Expiration Date',
|
||||
# field_type='date',
|
||||
# required=False,
|
||||
# order=3,
|
||||
# is_predefined=True
|
||||
# )
|
||||
|
||||
# Stage 8: Awards and Recognitions
|
||||
awards_stage = FormStage.objects.create(
|
||||
template=instance,
|
||||
name='Awards and Recognitions',
|
||||
order=7,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=awards_stage,
|
||||
label='Award Name',
|
||||
field_type='text',
|
||||
required=False,
|
||||
order=0,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=awards_stage,
|
||||
label='Issuing Organization',
|
||||
field_type='text',
|
||||
required=False,
|
||||
order=1,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=awards_stage,
|
||||
label='Date Received',
|
||||
field_type='date',
|
||||
required=False,
|
||||
order=2,
|
||||
is_predefined=True
|
||||
)
|
||||
FormField.objects.create(
|
||||
stage=awards_stage,
|
||||
label='Description',
|
||||
field_type='textarea',
|
||||
required=False,
|
||||
order=3,
|
||||
is_predefined=True
|
||||
)
|
||||
# # Stage 8: Awards and Recognitions
|
||||
# awards_stage = FormStage.objects.create(
|
||||
# template=instance,
|
||||
# name='Awards and Recognitions',
|
||||
# order=7,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=awards_stage,
|
||||
# label='Award Name',
|
||||
# field_type='text',
|
||||
# required=False,
|
||||
# order=0,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=awards_stage,
|
||||
# label='Issuing Organization',
|
||||
# field_type='text',
|
||||
# required=False,
|
||||
# order=1,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=awards_stage,
|
||||
# label='Date Received',
|
||||
# field_type='date',
|
||||
# required=False,
|
||||
# order=2,
|
||||
# is_predefined=True
|
||||
# )
|
||||
# FormField.objects.create(
|
||||
# stage=awards_stage,
|
||||
# label='Description',
|
||||
# field_type='textarea',
|
||||
# required=False,
|
||||
# order=3,
|
||||
# is_predefined=True
|
||||
# )
|
||||
@ -1,3 +1,4 @@
|
||||
import re
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
@ -11,22 +12,43 @@ from . linkedin_service import LinkedInService
|
||||
from django.shortcuts import get_object_or_404
|
||||
from . models import JobPosting
|
||||
from django.utils import timezone
|
||||
from . models import InterviewSchedule,ScheduledInterview,ZoomMeeting
|
||||
|
||||
from .models import ScheduledInterview, ZoomMeeting, Candidate, JobPosting, InterviewSchedule
|
||||
# Add python-docx import for Word document processing
|
||||
try:
|
||||
from docx import Document
|
||||
DOCX_AVAILABLE = True
|
||||
except ImportError:
|
||||
DOCX_AVAILABLE = False
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning("python-docx not available. Word document processing will be disabled.")
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
OPENROUTER_API_KEY ='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'
|
||||
# OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free'
|
||||
OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free'
|
||||
|
||||
# OPENROUTER_MODEL = 'openai/gpt-oss-20b:free'
|
||||
OPENROUTER_MODEL = 'openai/gpt-oss-20b'
|
||||
# OPENROUTER_MODEL = 'openai/gpt-oss-20b'
|
||||
# OPENROUTER_MODEL = 'mistralai/mistral-small-3.2-24b-instruct:free'
|
||||
|
||||
# from google import genai
|
||||
|
||||
# client = genai.Client(api_key="AIzaSyDkwYmvRe5ieTjQi1ClSzD5z5roTwaFsmY")
|
||||
|
||||
# def google_ai(text):
|
||||
# response = client.models.generate_content(
|
||||
# model="gemini-2.5-flash", contents=text
|
||||
# )
|
||||
# return response
|
||||
|
||||
|
||||
if not OPENROUTER_API_KEY:
|
||||
logger.warning("OPENROUTER_API_KEY not set. Resume scoring will be skipped.")
|
||||
|
||||
def extract_text_from_pdf(file_path):
|
||||
print("text extraction")
|
||||
"""Extract text from PDF files"""
|
||||
print("PDF text extraction")
|
||||
text = ""
|
||||
try:
|
||||
with open(file_path, "rb") as f:
|
||||
@ -38,6 +60,95 @@ def extract_text_from_pdf(file_path):
|
||||
raise
|
||||
return text.strip()
|
||||
|
||||
def extract_text_from_word(file_path):
|
||||
"""Extract text from Word documents (.docx)"""
|
||||
if not DOCX_AVAILABLE:
|
||||
raise ImportError("python-docx is not installed. Please install it with: pip install python-docx")
|
||||
|
||||
print("Word text extraction")
|
||||
text = ""
|
||||
try:
|
||||
doc = Document(file_path)
|
||||
|
||||
# Extract text from paragraphs
|
||||
for paragraph in doc.paragraphs:
|
||||
text += paragraph.text + "\n"
|
||||
|
||||
# Extract text from tables
|
||||
for table in doc.tables:
|
||||
for row in table.rows:
|
||||
for cell in row.cells:
|
||||
text += cell.text + "\t"
|
||||
text += "\n"
|
||||
|
||||
# Extract text from headers and footers
|
||||
for section in doc.sections:
|
||||
# Header
|
||||
if section.header:
|
||||
for paragraph in section.header.paragraphs:
|
||||
text += "[HEADER] " + paragraph.text + "\n"
|
||||
|
||||
# Footer
|
||||
if section.footer:
|
||||
for paragraph in section.footer.paragraphs:
|
||||
text += "[FOOTER] " + paragraph.text + "\n"
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Word extraction failed: {e}")
|
||||
raise
|
||||
return text.strip()
|
||||
|
||||
def extract_text_from_document(file_path):
|
||||
"""Extract text from documents based on file type"""
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"File not found: {file_path}")
|
||||
|
||||
file_ext = os.path.splitext(file_path)[1].lower()
|
||||
|
||||
if file_ext == '.pdf':
|
||||
return extract_text_from_pdf(file_path)
|
||||
elif file_ext == '.docx':
|
||||
return extract_text_from_word(file_path)
|
||||
else:
|
||||
raise ValueError(f"Unsupported file type: {file_ext}. Only .pdf and .docx files are supported.")
|
||||
|
||||
def format_job_description(pk):
|
||||
job_posting = JobPosting.objects.get(pk=pk)
|
||||
print(job_posting)
|
||||
prompt = f"""
|
||||
Can you please organize and format this unformatted job description and qualifications into clear, readable sections using headings and bullet points?
|
||||
Format the Content: You need to convert the clear, formatted job description and qualifications into a 2 blocks of HTML code.
|
||||
**JOB DESCRIPTION:**
|
||||
{job_posting.description}
|
||||
|
||||
**QUALIFICATIONS:**
|
||||
{job_posting.qualifications}
|
||||
|
||||
**STRICT JSON OUTPUT INSTRUCTIONS:**
|
||||
Output a single, valid JSON object with ONLY the following two top-level keys:
|
||||
|
||||
'job_description': 'A HTML containing the formatted job description',
|
||||
'job_qualifications': 'A HTML containing the formatted job qualifications',
|
||||
|
||||
|
||||
Do not include any other text except for the JSON output.
|
||||
"""
|
||||
result = ai_handler(prompt)
|
||||
|
||||
if result['status'] == 'error':
|
||||
logger.error(f"AI handler returned error for candidate {job_posting.pk}")
|
||||
print(f"AI handler returned error for candidate {job_posting.pk}")
|
||||
return
|
||||
data = result['data']
|
||||
if isinstance(data, str):
|
||||
data = json.loads(data)
|
||||
print(data)
|
||||
|
||||
job_posting.description = data.get('job_description')
|
||||
job_posting.qualifications = data.get('job_qualifications')
|
||||
job_posting.save(update_fields=['description', 'qualifications'])
|
||||
|
||||
|
||||
def ai_handler(prompt):
|
||||
print("model call")
|
||||
response = requests.post(
|
||||
@ -71,142 +182,19 @@ def ai_handler(prompt):
|
||||
return {"status": "error", "data": response.json()}
|
||||
|
||||
|
||||
# def handle_reume_parsing_and_scoring(pk):
|
||||
# from django.db import transaction
|
||||
|
||||
# logger.info(f"Scoring resume for candidate {pk}")
|
||||
# instance = Candidate.objects.get(pk=pk)
|
||||
# try:
|
||||
# file_path = instance.resume.path
|
||||
# with transaction.atomic():
|
||||
# if not os.path.exists(file_path):
|
||||
# logger.warning(f"Resume file not found: {file_path}")
|
||||
# return
|
||||
|
||||
# resume_text = extract_text_from_pdf(file_path)
|
||||
# job_detail= f"{instance.job.description} {instance.job.qualifications}"
|
||||
# resume_parser_prompt = f"""
|
||||
# You are an expert resume parser and summarizer. Given a resume in plain text format, extract and organize the following key-value information into a clean, valid JSON object:
|
||||
|
||||
# full_name: Full name of the candidate
|
||||
# current_title: Most recent or current job title
|
||||
# location: City and state (or country if outside the U.S.)
|
||||
# contact: Phone number and email (as a single string or separate fields)
|
||||
# linkedin: LinkedIn profile URL (if present)
|
||||
# github: GitHub or portfolio URL (if present)
|
||||
# summary: Brief professional profile or summary (1–2 sentences)
|
||||
# education: List of degrees, each with:
|
||||
# institution
|
||||
# degree
|
||||
# year
|
||||
# gpa (if provided)
|
||||
# relevant_courses (as a list, if mentioned)
|
||||
# skills: Grouped by category if possible (e.g., programming, big data, visualization), otherwise as a flat list of technologies/tools
|
||||
# experience: List of roles, each with:
|
||||
# company
|
||||
# job_title
|
||||
# location
|
||||
# start_date and end_date (or "Present" if applicable)
|
||||
# key_achievements (as a list of concise bullet points)
|
||||
# projects: List of notable projects (if clearly labeled), each with:
|
||||
# name
|
||||
# year
|
||||
# technologies_used
|
||||
# brief_description
|
||||
# Instructions:
|
||||
|
||||
# Be concise but preserve key details.
|
||||
# Normalize formatting (e.g., “Jun. 2014” → “2014-06”).
|
||||
# Omit redundant or promotional language.
|
||||
# If a section is missing, omit the key or set it to null/empty list as appropriate.
|
||||
# Output only valid JSON—no markdown, no extra text.
|
||||
# Now, process the following resume text:
|
||||
# {resume_text}
|
||||
# """
|
||||
# resume_parser_result = ai_handler(resume_parser_prompt)
|
||||
# resume_scoring_prompt = f"""
|
||||
# You are an expert technical recruiter. Your task is to score the following candidate for the role based on the provided job criteria.
|
||||
|
||||
# **Job Criteria:**
|
||||
# {job_detail}
|
||||
|
||||
# **Candidate's Extracted Resume Json:**
|
||||
# \"\"\"
|
||||
# {resume_parser_result}
|
||||
# \"\"\"
|
||||
|
||||
# **Your Task:**
|
||||
# Provide a response in strict JSON format with the following keys:
|
||||
# 1. 'match_score': A score from 0 to 100 representing how well the candidate fits the role.
|
||||
# 2. 'strengths': A brief summary of why the candidate is a strong fit, referencing specific criteria.
|
||||
# 3. 'weaknesses': A brief summary of where the candidate falls short or what criteria are missing.
|
||||
# 4. 'years_of_experience': The total number of years of professional experience mentioned in the resume as a numerical value (e.g., 6.5).
|
||||
# 5. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}).
|
||||
# 6. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}).
|
||||
# 7. 'category': Based on the content provided, determine the most fitting professional field or category for the individual. (e.g., {{"category" : "Data Science"}}) only output the category name and no other text example ('Software Development', 'correct') , ('Software Development and devops','wrong').
|
||||
# 8. 'most_recent_job_title': The candidate's most recent or current professional job title.
|
||||
# 9. 'recommendation': Provide a recommendation for the candidate (e.g., {{"recommendation": "
|
||||
# Conclusion and Minor Considerations
|
||||
# Overall Assessment: Highly Recommended Candidate.
|
||||
|
||||
# [Candidate] is an exceptionally strong candidate for this role. His proven track record with the core technology stack (Django, Python, Docker, CI/CD) and relevant experience in large-scale, high-impact enterprise projects (Telecom BPM/MDM) make him an excellent technical fit. His fluency in Arabic and English directly addresses a major non-negotiable requirement.
|
||||
|
||||
# The only minor area not explicitly mentioned is the mentoring aspect, but his senior level of experience and technical breadth strongly suggest he possesses the capability to mentor junior engineers.
|
||||
|
||||
# The hiring manager should move forward with this candidate with high confidence.
|
||||
# ."}}).
|
||||
# 10. 'top_3_keywords': A list of the three most dominant and relevant technical skills or technologies from the resume that match the job criteria.
|
||||
# 11. 'job_fit_narrative': A single, concise sentence summarizing the core fit.
|
||||
# 12. 'language_fluency': A list of languages and their fluency levels mentioned.
|
||||
# 13. 'screening_stage_rating': A standardized rating (e.g., "A - Highly Qualified", "B - Qualified").
|
||||
# 14. 'min_req_met_bool': Boolean (true/false) indicating if all non-negotiable minimum requirements are met.
|
||||
# 15. 'soft_skills_score': A score (0-100) for inferred non-technical skills like leadership and communication.
|
||||
# 16. 'experience_industry_match': A score (0-100) for the relevance of the candidate's industry experience.
|
||||
|
||||
# Only output valid JSON. Do not include any other text.
|
||||
# """
|
||||
|
||||
# resume_scoring_result = ai_handler(resume_scoring_prompt)
|
||||
|
||||
# print(resume_scoring_result)
|
||||
|
||||
# instance.parsed_summary = str(resume_parser_result)
|
||||
|
||||
|
||||
# # Core Scores
|
||||
# instance.set_field('match_score', resume_scoring_result.get('match_score', 0)) # Set default for int
|
||||
# instance.set_field('years_of_experience', resume_scoring_result.get('years_of_experience', 0.0)) # Set default for float
|
||||
# instance.set_field('soft_skills_score', resume_scoring_result.get('soft_skills_score', 0))
|
||||
# instance.set_field('experience_industry_match', resume_scoring_result.get('experience_industry_match', 0))
|
||||
|
||||
# # Screening & Funnel
|
||||
# instance.set_field('min_req_met_bool', resume_scoring_result.get('min_req_met_bool', False)) # Set default for bool
|
||||
# instance.set_field('screening_stage_rating', resume_scoring_result.get('screening_stage_rating', 'N/A'))
|
||||
# instance.set_field('most_recent_job_title', resume_scoring_result.get('most_recent_job_title', 'N/A'))
|
||||
# instance.set_field('top_3_keywords', resume_scoring_result.get('top_3_keywords', [])) # Set default for list
|
||||
|
||||
# # Summaries & Narrative
|
||||
# instance.set_field('strengths', resume_scoring_result.get('strengths', ''))
|
||||
# instance.set_field('weaknesses', resume_scoring_result.get('weaknesses', ''))
|
||||
# instance.set_field('job_fit_narrative', resume_scoring_result.get('job_fit_narrative', ''))
|
||||
# instance.set_field('recommendation', resume_scoring_result.get('recommendation', ''))
|
||||
|
||||
# # Structured Data
|
||||
# instance.set_field('criteria_checklist', resume_scoring_result.get('criteria_checklist', {})) # Set default for dict
|
||||
# instance.set_field('language_fluency', resume_scoring_result.get('language_fluency', [])) # Set default for list
|
||||
|
||||
# instance.set_field('category', resume_scoring_result.get('category', 'Uncategorized')) # Use 'category' key
|
||||
|
||||
# instance.is_resume_parsed = True
|
||||
|
||||
# instance.save(update_fields=['ai_analysis_data', 'is_resume_parsed','parsed_summary'])
|
||||
|
||||
# logger.info(f"Successfully scored resume for candidate {instance.id}")
|
||||
|
||||
# except Exception as e:
|
||||
# instance.is_resume_parsed = False
|
||||
# instance.save(update_fields=['is_resume_parsed'])
|
||||
# logger.error(f"Failed to score resume for candidate:{instance.pk} {e}")
|
||||
def safe_cast_to_float(value, default=0.0):
|
||||
"""Safely converts a value (int, float, or string) to a float."""
|
||||
if isinstance(value, (int, float)):
|
||||
return float(value)
|
||||
if isinstance(value, str):
|
||||
# Remove non-numeric characters except the decimal point
|
||||
cleaned_value = re.sub(r'[^\d.]', '', value)
|
||||
try:
|
||||
# Ensure we handle empty strings after cleaning
|
||||
return float(cleaned_value) if cleaned_value else default
|
||||
except ValueError:
|
||||
return default
|
||||
return default
|
||||
|
||||
def handle_reume_parsing_and_scoring(pk):
|
||||
"""
|
||||
@ -235,7 +223,8 @@ def handle_reume_parsing_and_scoring(pk):
|
||||
# Consider marking the task as unsuccessful but don't re-queue
|
||||
return
|
||||
|
||||
resume_text = extract_text_from_pdf(file_path)
|
||||
# Use the new unified document parser
|
||||
resume_text = extract_text_from_document(file_path)
|
||||
job_detail = f"{instance.job.description} {instance.job.qualifications}"
|
||||
|
||||
except Exception as e:
|
||||
@ -248,8 +237,8 @@ def handle_reume_parsing_and_scoring(pk):
|
||||
You are an expert AI system functioning as both a Resume Parser and a Technical Recruiter.
|
||||
|
||||
Your task is to:
|
||||
1. **PARSE**: Extract all key-value information from the provided RESUME TEXT into a clean JSON structure under the key 'parsed_data'.
|
||||
2. **SCORE**: Analyze the parsed data against the JOB CRITERIA and generate a comprehensive score and analysis under the key 'scoring_data'.
|
||||
1. **PARSE**: Extract all key-value information from the provided RESUME TEXT into a clean JSON structure under the key 'resume_data', preserving the original text and it's formatting and dont add any extra text.
|
||||
2. **SCORE**: Analyze the parsed data against the JOB CRITERIA and generate a comprehensive score and analysis under the key 'analysis_data'.
|
||||
|
||||
**JOB CRITERIA:**
|
||||
{job_detail}
|
||||
@ -260,7 +249,8 @@ def handle_reume_parsing_and_scoring(pk):
|
||||
**STRICT JSON OUTPUT INSTRUCTIONS:**
|
||||
Output a single, valid JSON object with ONLY the following two top-level keys:
|
||||
|
||||
1. "parsed_data": {{
|
||||
|
||||
1. "resume_data": {{
|
||||
"full_name": "Full name of the candidate",
|
||||
"current_title": "Most recent or current job title",
|
||||
"location": "City and state",
|
||||
@ -271,9 +261,9 @@ def handle_reume_parsing_and_scoring(pk):
|
||||
"education": [{{
|
||||
"institution": "Institution name",
|
||||
"degree": "Degree name",
|
||||
"year": "Year of graduation",
|
||||
"year": "Year of graduation" (if provided) or '',
|
||||
"gpa": "GPA (if provided)",
|
||||
"relevant_courses": ["list", "of", "courses"]
|
||||
"relevant_courses": ["list", "of", "courses"](if provided) or []
|
||||
}}],
|
||||
"skills": {{
|
||||
"category_1": ["skill_a", "skill_b"],
|
||||
@ -285,32 +275,37 @@ def handle_reume_parsing_and_scoring(pk):
|
||||
"location": "Location",
|
||||
"start_date": "YYYY-MM",
|
||||
"end_date": "YYYY-MM or Present",
|
||||
"key_achievements": ["concise", "bullet", "points"]
|
||||
"key_achievements": ["concise bullet points"] (if provided) or []
|
||||
}}],
|
||||
"projects": [{{
|
||||
"name": "Project name",
|
||||
"year": "Year",
|
||||
"technologies_used": ["list", "of", "tech"],
|
||||
"technologies_used": ["list", "of", "tech"] (if provided) or [],
|
||||
"brief_description": "description"
|
||||
}}]
|
||||
}}
|
||||
|
||||
2. "scoring_data": {{
|
||||
"match_score": "Score 0-100",
|
||||
2. "analysis_data": {{
|
||||
"match_score": "Integer Score 0-100",
|
||||
"strengths": "Brief summary of strengths",
|
||||
"weaknesses": "Brief summary of weaknesses",
|
||||
"years_of_experience": "Total years of experience (float, e.g., 6.5)",
|
||||
"criteria_checklist": {{ "Python": "Met", "AWS": "Not Mentioned"}},
|
||||
"category": "Most fitting professional field (e.g., Data Science)",
|
||||
"category": "Most fitting professional field (e.g., Data Science), only output the category name and no other text example ('Software Development', 'correct') , ('Software Development and devops','wrong') ('Software Development / Backend Development','wrong')",
|
||||
"most_recent_job_title": "Candidate's most recent job title",
|
||||
"recommendation": "Detailed hiring recommendation narrative",
|
||||
"top_3_keywords": ["keyword1", "keyword2", "keyword3"],
|
||||
"job_fit_narrative": "Single, concise summary sentence",
|
||||
"language_fluency": ["language: fluency_level"],
|
||||
"screening_stage_rating": "Standardized rating (e.g., A - Highly Qualified)",
|
||||
"screening_stage_rating": "Standardized rating (Highly Qualified, Qualified , Partially Qualified, Not Qualified)",
|
||||
"min_req_met_bool": "Boolean (true/false)",
|
||||
"soft_skills_score": "Score 0-100 for inferred non-technical skills",
|
||||
"experience_industry_match": "Score 0-100 for industry relevance"
|
||||
"soft_skills_score": "Integer Score 0-100 for inferred non-technical skills",
|
||||
"experience_industry_match": "Integer Score 0-100 for industry relevance",
|
||||
"seniority_level_match": "Integer Score 0-100 for alignment with JD's seniority level",
|
||||
"red_flags": ["List of any potential concerns (if any): e.g., 'Employment gap 1 year', 'Frequent job hopping', 'Missing required certification'"],
|
||||
"employment_stability_score": "Integer Score 0-100 (Higher is more stable/longer tenure) (if possible)",
|
||||
"transferable_skills_narrative": "A brief sentence describing the relevance of non-core experience (if applicable).",
|
||||
"cultural_fit_keywords": ["A list of 3-5 keywords extracted from the resume (if possible) (e.g., 'team-player', 'mentored', 'cross-functional')"]
|
||||
}}
|
||||
|
||||
If a top-level key or its required fields are missing, set the field to null, an empty list, or an empty object as appropriate.
|
||||
@ -330,8 +325,8 @@ def handle_reume_parsing_and_scoring(pk):
|
||||
data = json.loads(data)
|
||||
print(data)
|
||||
|
||||
parsed_summary = data.get('parsed_data', {})
|
||||
scoring_result = data.get('scoring_data', {})
|
||||
# parsed_summary = data.get('parsed_data', {})
|
||||
# scoring_result = data.get('scoring_data', {})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"AI handler failed for candidate {instance.pk}: {e}")
|
||||
@ -342,37 +337,64 @@ def handle_reume_parsing_and_scoring(pk):
|
||||
with transaction.atomic():
|
||||
|
||||
# Map JSON keys to model fields with appropriate defaults
|
||||
update_map = {
|
||||
'match_score': ('match_score', 0),
|
||||
'years_of_experience': ('years_of_experience', 0.0),
|
||||
'soft_skills_score': ('soft_skills_score', 0),
|
||||
'experience_industry_match': ('experience_industry_match', 0),
|
||||
# update_map = {
|
||||
# 'match_score': ('match_score', 0),
|
||||
# 'years_of_experience': ('years_of_experience', 0.0),
|
||||
# 'soft_skills_score': ('soft_skills_score', 0),
|
||||
# 'experience_industry_match': ('experience_industry_match', 0),
|
||||
|
||||
'min_req_met_bool': ('min_req_met_bool', False),
|
||||
'screening_stage_rating': ('screening_stage_rating', 'N/A'),
|
||||
'most_recent_job_title': ('most_recent_job_title', 'N/A'),
|
||||
'top_3_keywords': ('top_3_keywords', []),
|
||||
# 'min_req_met_bool': ('min_req_met_bool', False),
|
||||
# 'screening_stage_rating': ('screening_stage_rating', 'N/A'),
|
||||
# 'most_recent_job_title': ('most_recent_job_title', 'N/A'),
|
||||
# 'top_3_keywords': ('top_3_keywords', []),
|
||||
|
||||
'strengths': ('strengths', ''),
|
||||
'weaknesses': ('weaknesses', ''),
|
||||
'job_fit_narrative': ('job_fit_narrative', ''),
|
||||
'recommendation': ('recommendation', ''),
|
||||
# 'strengths': ('strengths', ''),
|
||||
# 'weaknesses': ('weaknesses', ''),
|
||||
# 'job_fit_narrative': ('job_fit_narrative', ''),
|
||||
# 'recommendation': ('recommendation', ''),
|
||||
|
||||
'criteria_checklist': ('criteria_checklist', {}),
|
||||
'language_fluency': ('language_fluency', []),
|
||||
'category': ('category', 'N/A'),
|
||||
}
|
||||
# 'criteria_checklist': ('criteria_checklist', {}),
|
||||
# 'language_fluency': ('language_fluency', []),
|
||||
# 'category': ('category', 'N/A'),
|
||||
# }
|
||||
|
||||
# Apply scoring results to the instance
|
||||
for model_field, (json_key, default_value) in update_map.items():
|
||||
instance.ai_analysis_data[model_field] = scoring_result.get(json_key, default_value)
|
||||
# for model_field, (json_key, default_value) in update_map.items():
|
||||
# instance.ai_analysis_data[model_field] = scoring_result.get(json_key, default_value)
|
||||
# instance.set_field(model_field, scoring_result.get(json_key, default_value))
|
||||
# instance.set_field("match_score" , int(safe_cast_to_float(scoring_result.get('match_score', 0), default=0)))
|
||||
# instance.set_field("years_of_experience" , safe_cast_to_float(scoring_result.get('years_of_experience', 0.0)))
|
||||
# instance.set_field("soft_skills_score" , int(safe_cast_to_float(scoring_result.get('soft_skills_score', 0), default=0)))
|
||||
# instance.set_field("experience_industry_match" , int(safe_cast_to_float(scoring_result.get('experience_industry_match', 0), default=0)))
|
||||
|
||||
# # Other Model Fields
|
||||
# instance.set_field("min_req_met_bool" , scoring_result.get('min_req_met_bool', False))
|
||||
# instance.set_field("screening_stage_rating" , scoring_result.get('screening_stage_rating', 'N/A'))
|
||||
# instance.set_field("category" , scoring_result.get('category', 'N/A'))
|
||||
# instance.set_field("most_recent_job_title" , scoring_result.get('most_recent_job_title', 'N/A'))
|
||||
# instance.set_field("top_3_keywords" , scoring_result.get('top_3_keywords', []))
|
||||
# instance.set_field("strengths" , scoring_result.get('strengths', ''))
|
||||
# instance.set_field("weaknesses" , scoring_result.get('weaknesses', ''))
|
||||
# instance.set_field("job_fit_narrative" , scoring_result.get('job_fit_narrative', ''))
|
||||
# instance.set_field("recommendation" , scoring_result.get('recommendation', ''))
|
||||
# instance.set_field("criteria_checklist" , scoring_result.get('criteria_checklist', {}))
|
||||
# instance.set_field("language_fluency" , scoring_result.get('language_fluency', []))
|
||||
|
||||
|
||||
# 2. Update the Full JSON Field (ai_analysis_data)
|
||||
if instance.ai_analysis_data is None:
|
||||
instance.ai_analysis_data = {}
|
||||
|
||||
# Save both structured outputs into the single JSONField for completeness
|
||||
instance.ai_analysis_data = data
|
||||
# instance.ai_analysis_data['parsed_data'] = parsed_summary
|
||||
# instance.ai_analysis_data['scoring_data'] = scoring_result
|
||||
|
||||
# Apply parsing results
|
||||
instance.parsed_summary = json.dumps(parsed_summary)
|
||||
# instance.parsed_summary = json.dumps(parsed_summary)
|
||||
instance.is_resume_parsed = True
|
||||
|
||||
instance.save(update_fields=['ai_analysis_data','parsed_summary', 'is_resume_parsed'])
|
||||
instance.save(update_fields=['ai_analysis_data', 'is_resume_parsed'])
|
||||
|
||||
logger.info(f"Successfully scored and saved analysis for candidate {instance.id}")
|
||||
print(f"Successfully scored and saved analysis for candidate {instance.id}")
|
||||
@ -530,7 +552,3 @@ def linkedin_post_task(job_slug, access_token):
|
||||
job.linkedin_post_status = f"CRITICAL_ERROR: {str(e)}"
|
||||
job.save()
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
11
recruitment/templatetags/candidate_filters.py
Normal file
11
recruitment/templatetags/candidate_filters.py
Normal file
@ -0,0 +1,11 @@
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter(name='split_language')
|
||||
def split_language(value):
|
||||
"""Split language string and return proficiency level"""
|
||||
if ':' in value:
|
||||
parts = value.split(':', 1) # Split only on first colon
|
||||
return parts[1].strip() if len(parts) > 1 else value
|
||||
return value
|
||||
10
recruitment/templatetags/safe_dict.py
Normal file
10
recruitment/templatetags/safe_dict.py
Normal file
@ -0,0 +1,10 @@
|
||||
from django import template
|
||||
|
||||
register = template.Library()
|
||||
|
||||
@register.filter(name='safe_get')
|
||||
def safe_get(dictionary, key):
|
||||
"""Safely get a value from a dictionary, returning empty string if key doesn't exist"""
|
||||
if dictionary and key in dictionary:
|
||||
return dictionary[key]
|
||||
return ""
|
||||
@ -32,6 +32,7 @@ urlpatterns = [
|
||||
path('candidates/<slug:slug>/update/', views_frontend.CandidateUpdateView.as_view(), name='candidate_update'),
|
||||
path('candidates/<slug:slug>/delete/', views_frontend.CandidateDeleteView.as_view(), name='candidate_delete'),
|
||||
path('candidate/<slug:slug>/view/', views_frontend.candidate_detail, name='candidate_detail'),
|
||||
path('candidate/<slug:slug>/resume-template/', views_frontend.candidate_resume_template_view, name='candidate_resume_template'),
|
||||
path('candidate/<slug:slug>/update-stage/', views_frontend.candidate_update_stage, name='candidate_update_stage'),
|
||||
|
||||
# Training URLs
|
||||
@ -61,7 +62,7 @@ urlpatterns = [
|
||||
# Form Preview URLs
|
||||
# path('forms/', views.form_list, name='form_list'),
|
||||
path('forms/builder/', views.form_builder, name='form_builder'),
|
||||
path('forms/builder/<int:template_id>/', views.form_builder, name='form_builder'),
|
||||
path('forms/builder/<slug:template_slug>/', views.form_builder, name='form_builder'),
|
||||
path('forms/', views.form_templates_list, name='form_templates_list'),
|
||||
path('forms/create-template/', views.create_form_template, name='create_form_template'),
|
||||
|
||||
@ -81,8 +82,8 @@ urlpatterns = [
|
||||
|
||||
path('htmx/<slug:slug>/candidate_update_status/', views.candidate_update_status, name='candidate_update_status'),
|
||||
|
||||
path('forms/form/<int:template_id>/submit/', views.submit_form, name='submit_form'),
|
||||
path('forms/form/<int:template_id>/', 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'),
|
||||
@ -95,6 +96,11 @@ urlpatterns = [
|
||||
# path('api/forms/save/', views.save_form_builder, name='save_form_builder'),
|
||||
# path('api/forms/<int:form_id>/load/', views.load_form, name='load_form'),
|
||||
# path('api/forms/<int:form_id>/update/', views.update_form_builder, name='update_form_builder'),
|
||||
# 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'),
|
||||
# path('api/templates/<slug:template_slug>/delete/', views.delete_form_template, name='delete_form_template'),
|
||||
|
||||
|
||||
path('jobs/<slug:slug>/calendar/', views.interview_calendar_view, name='interview_calendar'),
|
||||
path('jobs/<slug:slug>/calendar/interview/<int:interview_id>/', views.interview_detail_view, name='interview_detail'),
|
||||
|
||||
@ -1,21 +1,25 @@
|
||||
import json
|
||||
import requests
|
||||
|
||||
from django.utils.translation import gettext as _
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django import forms
|
||||
|
||||
from rich import print
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from django.views.decorators.http import require_http_methods
|
||||
from django.http import HttpResponse, JsonResponse
|
||||
from datetime import datetime,time,timedelta
|
||||
from django.views import View
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.db.models import FloatField,CharField, DurationField
|
||||
from django.db.models.functions import Cast
|
||||
from django.db.models.fields.json import KeyTextTransform
|
||||
from django.db.models.expressions import ExpressionWrapper
|
||||
from django.db.models import Count, Avg, F,Q
|
||||
from .forms import (
|
||||
CandidateExamDateForm,
|
||||
InterviewForm,
|
||||
@ -269,21 +273,12 @@ def create_job(request):
|
||||
|
||||
if request.method == "POST":
|
||||
form = JobPostingForm(
|
||||
request.POST, is_anonymous_user=not request.user.is_authenticated
|
||||
request.POST
|
||||
)
|
||||
# to check user is authenticated or not
|
||||
if form.is_valid():
|
||||
try:
|
||||
job = form.save(commit=False)
|
||||
if request.user.is_authenticated:
|
||||
job.created_by = (
|
||||
request.user.get_full_name() or request.user.username
|
||||
)
|
||||
else:
|
||||
job.created_by = request.POST.get("created_by", "").strip()
|
||||
if not job.created_by:
|
||||
job.created_by = request.user.username
|
||||
|
||||
job.save()
|
||||
job_apply_url_relative=reverse('job_detail_candidate',kwargs={'slug':job.slug})
|
||||
job_apply_url_absolute=request.build_absolute_uri(job_apply_url_relative)
|
||||
@ -298,7 +293,7 @@ def create_job(request):
|
||||
else:
|
||||
messages.error(request, f"Please correct the errors below.{form.errors}")
|
||||
else:
|
||||
form = JobPostingForm(is_anonymous_user=not request.user.is_authenticated)
|
||||
form = JobPostingForm()
|
||||
return render(request, "jobs/create_job.html", {"form": form})
|
||||
|
||||
|
||||
@ -309,21 +304,11 @@ def edit_job(request, slug):
|
||||
if request.method == "POST":
|
||||
form = JobPostingForm(
|
||||
request.POST,
|
||||
instance=job,
|
||||
is_anonymous_user=not request.user.is_authenticated,
|
||||
instance=job
|
||||
)
|
||||
if form.is_valid():
|
||||
try:
|
||||
job = form.save(commit=False)
|
||||
if request.user.is_authenticated:
|
||||
job.created_by = (
|
||||
request.user.get_full_name() or request.user.username
|
||||
)
|
||||
else:
|
||||
job.created_by = request.POST.get("created_by", "").strip()
|
||||
if not job.created_by:
|
||||
job.created_by = "University Administrator"
|
||||
job.save()
|
||||
form.save()
|
||||
messages.success(request, f'Job "{job.title}" updated successfully!')
|
||||
return redirect("job_list")
|
||||
except Exception as e:
|
||||
@ -334,7 +319,7 @@ def edit_job(request, slug):
|
||||
else:
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
form = JobPostingForm(
|
||||
instance=job, is_anonymous_user=not request.user.is_authenticated
|
||||
instance=job
|
||||
)
|
||||
return render(request, "jobs/edit_job.html", {"form": form, "job": job})
|
||||
|
||||
@ -386,24 +371,83 @@ def job_detail(request, slug):
|
||||
messages.error(request, "Failed to update status due to validation errors.")
|
||||
|
||||
|
||||
category_data = applicants.filter(
|
||||
major_category_name__isnull=False,
|
||||
ai_analysis_data__match_score__isnull=False # This was part of the original query, ensure it's intentional
|
||||
).values('major_category_name').annotate(
|
||||
candidate_count=Count('id'),
|
||||
ai_analysis_data__avg_match_score=Avg('match_score', output_field=FloatField())
|
||||
).order_by('major_category_name')
|
||||
# --- 2. Quality Metrics (JSON Aggregation) ---
|
||||
|
||||
# Filter for candidates who have been scored and annotate with a sortable score
|
||||
candidates_with_score = applicants.filter(is_resume_parsed=True).annotate(
|
||||
# Extract the score as TEXT
|
||||
score_as_text=KeyTextTransform(
|
||||
'match_score',
|
||||
KeyTextTransform('resume_data', F('ai_analysis_data'))
|
||||
)
|
||||
).annotate(
|
||||
# Cast the extracted text score to a FloatField for numerical operations
|
||||
sortable_score=Cast('score_as_text', output_field=FloatField())
|
||||
)
|
||||
|
||||
# Aggregate: Average Match Score
|
||||
avg_match_score_result = candidates_with_score.aggregate(
|
||||
avg_score=Avg('sortable_score')
|
||||
)['avg_score']
|
||||
avg_match_score = round(avg_match_score_result or 0, 1)
|
||||
|
||||
# Metric: High Potential Count (Score >= 75)
|
||||
high_potential_count = candidates_with_score.filter(
|
||||
sortable_score__gte=75
|
||||
).count()
|
||||
high_potential_ratio = round((high_potential_count / total_applicant) * 100, 1) if total_applicant > 0 else 0
|
||||
|
||||
# --- 3. Time Metrics (Duration Aggregation) ---
|
||||
|
||||
# Metric: Average Time from Applied to Interview (T2I)
|
||||
t2i_candidates = applicants.filter(
|
||||
interview_date__isnull=False
|
||||
).annotate(
|
||||
time_to_interview=ExpressionWrapper(
|
||||
F('interview_date') - F('created_at'),
|
||||
output_field=DurationField()
|
||||
)
|
||||
)
|
||||
avg_t2i_duration = t2i_candidates.aggregate(
|
||||
avg_t2i=Avg('time_to_interview')
|
||||
)['avg_t2i']
|
||||
|
||||
# Convert timedelta to days
|
||||
avg_t2i_days = round(avg_t2i_duration.total_seconds() / (60*60*24), 1) if avg_t2i_duration else 0
|
||||
|
||||
# Metric: Average Time in Exam Stage
|
||||
t_in_exam_candidates = applicants.filter(
|
||||
exam_date__isnull=False, interview_date__isnull=False
|
||||
).annotate(
|
||||
time_in_exam=ExpressionWrapper(
|
||||
F('interview_date') - F('exam_date'),
|
||||
output_field=DurationField()
|
||||
)
|
||||
)
|
||||
avg_t_in_exam_duration = t_in_exam_candidates.aggregate(
|
||||
avg_t_in_exam=Avg('time_in_exam')
|
||||
)['avg_t_in_exam']
|
||||
|
||||
# Convert timedelta to days
|
||||
avg_t_in_exam_days = round(avg_t_in_exam_duration.total_seconds() / (60*60*24), 1) if avg_t_in_exam_duration else 0
|
||||
|
||||
category_data = applicants.filter(
|
||||
ai_analysis_data__analysis_data__category__isnull=False
|
||||
).values('ai_analysis_data__analysis_data__category').annotate(
|
||||
candidate_count=Count('id'),
|
||||
category=Cast('ai_analysis_data__analysis_data__category',output_field=CharField())
|
||||
).order_by('ai_analysis_data__analysis_data__category')
|
||||
# Prepare data for Chart.js
|
||||
categories = [item['major_category_name'] for item in category_data]
|
||||
print(category_data)
|
||||
categories = [item['category'] for item in category_data]
|
||||
candidate_counts = [item['candidate_count'] for item in category_data]
|
||||
avg_scores = [round(item['avg_match_score'], 2) if item['avg_match_score'] is not None else 0 for item in category_data]
|
||||
# avg_scores = [round(item['avg_match_score'], 2) if item['avg_match_score'] is not None else 0 for item in category_data]
|
||||
|
||||
|
||||
context = {
|
||||
"job": job,
|
||||
"applicants": applicants,
|
||||
"total_applicants": total_applicant,
|
||||
"total_applicants": total_applicant, # This was total_candidates in the prompt, using total_applicant for consistency
|
||||
"applied_count": applied_count,
|
||||
'exam_count':exam_count,
|
||||
"interview_count": interview_count,
|
||||
@ -412,7 +456,13 @@ def job_detail(request, slug):
|
||||
'image_upload_form':image_upload_form,
|
||||
'categories': categories,
|
||||
'candidate_counts': candidate_counts,
|
||||
'avg_scores': avg_scores
|
||||
# 'avg_scores': avg_scores,
|
||||
# New statistics
|
||||
'avg_match_score': avg_match_score,
|
||||
'high_potential_count': high_potential_count,
|
||||
'high_potential_ratio': high_potential_ratio,
|
||||
'avg_t2i_days': avg_t2i_days,
|
||||
'avg_t_in_exam_days': avg_t_in_exam_days,
|
||||
}
|
||||
return render(request, "jobs/job_detail.html", context)
|
||||
|
||||
@ -762,7 +812,7 @@ def form_wizard_view(request, template_id):
|
||||
'Application limit reached: This job is no longer accepting new applications. Please explore other available positions.'
|
||||
)
|
||||
return redirect('job_detail_candidate',slug=job.slug)
|
||||
|
||||
|
||||
if job.is_expired:
|
||||
messages.error(
|
||||
request,
|
||||
@ -770,7 +820,6 @@ def form_wizard_view(request, template_id):
|
||||
)
|
||||
return redirect('job_detail_candidate',slug=job.slug)
|
||||
|
||||
|
||||
return render(
|
||||
request,
|
||||
"forms/form_wizard.html",
|
||||
@ -782,8 +831,6 @@ def form_wizard_view(request, template_id):
|
||||
def submit_form(request, template_id):
|
||||
"""Handle form submission"""
|
||||
template = get_object_or_404(FormTemplate, id=template_id)
|
||||
|
||||
|
||||
if request.method == "POST":
|
||||
try:
|
||||
with transaction.atomic():
|
||||
@ -791,6 +838,8 @@ def submit_form(request, template_id):
|
||||
|
||||
current_count = job_posting.candidates.count()
|
||||
if current_count >= job_posting.max_applications:
|
||||
template.is_active = False
|
||||
template.save()
|
||||
return JsonResponse(
|
||||
{"success": False, "message": "Application limit reached for this job."}
|
||||
)
|
||||
@ -1201,7 +1250,10 @@ def candidate_screening_view(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
candidates = job.screening_candidates
|
||||
|
||||
# Get filter parameters
|
||||
min_ai_score_str = request.GET.get('min_ai_score')
|
||||
min_experience_str = request.GET.get('min_experience')
|
||||
screening_rating = request.GET.get('screening_rating')
|
||||
tier1_count_str = request.GET.get('tier1_count')
|
||||
|
||||
try:
|
||||
@ -1211,6 +1263,11 @@ def candidate_screening_view(request, slug):
|
||||
else:
|
||||
min_ai_score = 0
|
||||
|
||||
if min_experience_str:
|
||||
min_experience = float(min_experience_str)
|
||||
else:
|
||||
min_experience = 0
|
||||
|
||||
if tier1_count_str:
|
||||
tier1_count = int(tier1_count_str)
|
||||
else:
|
||||
@ -1219,11 +1276,18 @@ def candidate_screening_view(request, slug):
|
||||
except ValueError:
|
||||
# This catches if the user enters non-numeric text (e.g., "abc")
|
||||
min_ai_score = 0
|
||||
min_experience = 0
|
||||
tier1_count = 0
|
||||
|
||||
# You can now safely use min_ai_score and tier1_count as integers (0 or greater)
|
||||
# Apply filters
|
||||
if min_ai_score > 0:
|
||||
candidates = candidates.filter(match_score__gte=min_ai_score)
|
||||
candidates = candidates.filter(ai_analysis_data__analysis_data__match_score__gte=min_ai_score)
|
||||
|
||||
if min_experience > 0:
|
||||
candidates = candidates.filter(ai_analysis_data__analysis_data__years_of_experience__gte=min_experience)
|
||||
|
||||
if screening_rating:
|
||||
candidates = candidates.filter(ai_analysis_data__analysis_data__screening_stage_rating=screening_rating)
|
||||
|
||||
if tier1_count > 0:
|
||||
candidates = candidates[:tier1_count]
|
||||
@ -1232,6 +1296,8 @@ def candidate_screening_view(request, slug):
|
||||
"job": job,
|
||||
"candidates": candidates,
|
||||
'min_ai_score':min_ai_score,
|
||||
'min_experience':min_experience,
|
||||
'screening_rating':screening_rating,
|
||||
'tier1_count':tier1_count,
|
||||
"current_stage" : "Applied"
|
||||
}
|
||||
@ -1950,34 +2016,24 @@ def schedule_meeting_for_candidate(request, slug, candidate_pk):
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
|
||||
def user_profile_image_update(request, pk):
|
||||
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
|
||||
# 2. Ensure Profile exists and get the instance
|
||||
try:
|
||||
|
||||
profile_instance = user.profile
|
||||
except ObjectDoesNotExist:
|
||||
|
||||
profile_instance = Profile.objects.create(user=user)
|
||||
|
||||
instance =user.profile
|
||||
|
||||
except ObjectDoesNotExist as e:
|
||||
Profile.objects.create(user=user)
|
||||
|
||||
if request.method == 'POST':
|
||||
|
||||
profile_form = ProfileImageUploadForm(
|
||||
request.POST,
|
||||
request.FILES,
|
||||
instance=profile_instance # <--- USE profile_instance HERE
|
||||
)
|
||||
|
||||
profile_form = ProfileImageUploadForm(request.POST, request.FILES, instance=user.profile)
|
||||
if profile_form.is_valid():
|
||||
profile_form.save()
|
||||
messages.success(request, 'Image uploaded successfully.')
|
||||
return redirect('user_detail', pk=user.pk)
|
||||
messages.success(request, 'Image uploaded successfully')
|
||||
return redirect('user_detail', pk=user.pk)
|
||||
else:
|
||||
messages.error(request, 'An error occurred while uploading the image. Please check the errors below.')
|
||||
else:
|
||||
#
|
||||
profile_form = ProfileImageUploadForm(instance=profile_instance)
|
||||
profile_form = ProfileImageUploadForm(instance=user.profile)
|
||||
|
||||
context = {
|
||||
'profile_form': profile_form,
|
||||
'user': user,
|
||||
@ -1986,10 +2042,9 @@ def user_profile_image_update(request, pk):
|
||||
|
||||
def user_detail(request, pk):
|
||||
user = get_object_or_404(User, pk=pk)
|
||||
|
||||
|
||||
|
||||
try:
|
||||
|
||||
profile_instance = user.profile
|
||||
profile_form = ProfileImageUploadForm(instance=profile_instance)
|
||||
except:
|
||||
@ -2122,7 +2177,6 @@ def set_staff_password(request,pk):
|
||||
return render(request,'user/staff_password_create.html',{'form':form,'user':user})
|
||||
|
||||
|
||||
|
||||
@user_passes_test(is_superuser_check)
|
||||
def account_toggle_status(request,pk):
|
||||
user=get_object_or_404(User,pk=pk)
|
||||
@ -2142,15 +2196,9 @@ def account_toggle_status(request,pk):
|
||||
return redirect('admin_settings')
|
||||
else:
|
||||
messages.error(f'Please correct the error below')
|
||||
|
||||
|
||||
|
||||
|
||||
# @login_required
|
||||
# def user_detail(requests,pk):
|
||||
# user=get_object_or_404(User,pk=pk)
|
||||
# return render(requests,'user/profile.html')
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
def zoom_webhook_view(request):
|
||||
@ -2235,9 +2283,70 @@ def edit_meeting_comment(request, slug, comment_id):
|
||||
form = MeetingCommentForm(instance=comment)
|
||||
|
||||
|
||||
def delete_meeting_comment(request):
|
||||
pass
|
||||
@login_required
|
||||
def delete_meeting_comment(request, slug, comment_id):
|
||||
"""Delete a meeting comment"""
|
||||
meeting = get_object_or_404(ZoomMeeting, slug=slug)
|
||||
comment = get_object_or_404(MeetingComment, id=comment_id, meeting=meeting)
|
||||
|
||||
# Check if user is the author
|
||||
if comment.author != request.user and not request.user.is_staff:
|
||||
messages.error(request, 'You can only delete your own comments.')
|
||||
return redirect('meeting_details', slug=slug)
|
||||
|
||||
if request.method == 'POST':
|
||||
comment.delete()
|
||||
messages.success(request, 'Comment deleted successfully!')
|
||||
|
||||
# HTMX response - return just the comment section
|
||||
if 'HX-Request' in request.headers:
|
||||
return render(request, 'includes/comment_list.html', {
|
||||
'comments': meeting.comments.all().order_by('-created_at'),
|
||||
'meeting': meeting
|
||||
})
|
||||
|
||||
return redirect('meeting_details', slug=slug)
|
||||
|
||||
# HTMX response - return the delete confirmation modal
|
||||
if 'HX-Request' in request.headers:
|
||||
return render(request, 'includes/delete_comment_form.html', {
|
||||
'meeting': meeting,
|
||||
'comment': comment,
|
||||
'delete_url': reverse('delete_meeting_comment', kwargs={'slug': slug, 'comment_id': comment_id})
|
||||
})
|
||||
|
||||
return redirect('meeting_details', slug=slug)
|
||||
|
||||
|
||||
def set_meeting_candidate(request):
|
||||
pass
|
||||
@login_required
|
||||
def set_meeting_candidate(request,slug):
|
||||
meeting = get_object_or_404(ZoomMeeting, slug=slug)
|
||||
if request.method == 'POST' and 'HX-Request' not in request.headers:
|
||||
form = InterviewForm(request.POST)
|
||||
if form.is_valid():
|
||||
candidate = form.save(commit=False)
|
||||
candidate.zoom_meeting = meeting
|
||||
candidate.interview_date = meeting.start_time.date()
|
||||
candidate.interview_time = meeting.start_time.time()
|
||||
candidate.save()
|
||||
messages.success(request, 'Candidate added successfully!')
|
||||
return redirect('list_meetings')
|
||||
job = request.GET.get("job")
|
||||
form = InterviewForm()
|
||||
|
||||
if job:
|
||||
form.fields['candidate'].queryset = Candidate.objects.filter(job=job)
|
||||
|
||||
else:
|
||||
form.fields['candidate'].queryset = Candidate.objects.none()
|
||||
form.fields['job'].widget.attrs.update({
|
||||
'hx-get': reverse('set_meeting_candidate', kwargs={'slug': slug}),
|
||||
'hx-target': '#div_id_candidate',
|
||||
'hx-select': '#div_id_candidate',
|
||||
'hx-swap': 'outerHTML'
|
||||
})
|
||||
context = {
|
||||
"form": form,
|
||||
"meeting": meeting
|
||||
}
|
||||
return render(request, 'meetings/set_candidate_form.html', context)
|
||||
|
||||
@ -2,8 +2,10 @@ import json
|
||||
from django.shortcuts import render, get_object_or_404,redirect
|
||||
from django.contrib import messages
|
||||
from django.http import JsonResponse
|
||||
|
||||
from django.db.models.fields.json import KeyTextTransform
|
||||
from recruitment.utils import json_to_markdown_table
|
||||
from django.db.models import Count, Avg, F, FloatField
|
||||
from django.db.models.functions import Cast
|
||||
from . import models
|
||||
from django.utils.translation import get_language
|
||||
from . import forms
|
||||
@ -247,6 +249,20 @@ def candidate_detail(request, slug):
|
||||
'stage_form': stage_form,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def candidate_resume_template_view(request, slug):
|
||||
"""Display formatted resume template for a candidate"""
|
||||
candidate = get_object_or_404(models.Candidate, slug=slug)
|
||||
|
||||
if not request.user.is_staff:
|
||||
messages.error(request, _("You don't have permission to view this page."))
|
||||
return redirect('candidate_list')
|
||||
|
||||
return render(request, 'recruitment/candidate_resume_template.html', {
|
||||
'candidate': candidate
|
||||
})
|
||||
|
||||
@login_required
|
||||
def candidate_update_stage(request, slug):
|
||||
"""Handle HTMX stage update requests"""
|
||||
@ -323,24 +339,104 @@ class TrainingDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||
|
||||
@login_required
|
||||
def dashboard_view(request):
|
||||
total_jobs = models.JobPosting.objects.count()
|
||||
total_candidates = models.Candidate.objects.count()
|
||||
jobs = models.JobPosting.objects.all()
|
||||
# --- Performance Optimization: Aggregate Data in ONE Query ---
|
||||
|
||||
# 1. Base Job Query: Get all jobs and annotate with candidate count
|
||||
jobs_with_counts = models.JobPosting.objects.annotate(
|
||||
candidate_count=Count('candidates')
|
||||
).order_by('-candidate_count')
|
||||
|
||||
total_jobs = jobs_with_counts.count()
|
||||
total_candidates = models.Candidate.objects.count()
|
||||
|
||||
job_titles = [job.title for job in jobs_with_counts]
|
||||
job_app_counts = [job.candidate_count for job in jobs_with_counts]
|
||||
|
||||
average_applications = round(jobs_with_counts.aggregate(
|
||||
avg_apps=Avg('candidate_count')
|
||||
)['avg_apps'] or 0, 2)
|
||||
|
||||
# 5. New: Candidate Quality & Funnel Metrics
|
||||
|
||||
# Assuming 'match_score' is a direct IntegerField/FloatField on the Candidate model
|
||||
# (based on the final, optimized version of handle_reume_parsing_and_scoring)
|
||||
|
||||
# Average Match Score (Overall Quality)
|
||||
candidates_with_score = models.Candidate.objects.filter(
|
||||
# Filter only candidates that have been parsed/scored
|
||||
is_resume_parsed=True
|
||||
).annotate(
|
||||
score_as_text=KeyTextTransform(
|
||||
'match_score',
|
||||
KeyTextTransform('scoring_data', F('ai_analysis_data'))
|
||||
)
|
||||
).annotate(
|
||||
# Cast the extracted text score to a FloatField so AVG() can operate on it.
|
||||
sortable_score=Cast('score_as_text', output_field=FloatField())
|
||||
)
|
||||
|
||||
# 2b. AGGREGATE using the newly created 'sortable_score' field
|
||||
avg_match_score_result = candidates_with_score.aggregate(
|
||||
avg_score=Avg('sortable_score')
|
||||
)['avg_score']
|
||||
|
||||
avg_match_score = round(avg_match_score_result or 0, 1)
|
||||
|
||||
# 2c. Use the annotated QuerySet for other metrics
|
||||
|
||||
# Scored Candidates Ratio (Now simpler, as we filtered the QuerySet)
|
||||
total_scored = candidates_with_score.count()
|
||||
scored_ratio = round((total_scored / total_candidates) * 100, 1) if total_candidates > 0 else 0
|
||||
|
||||
# High Potential Candidates (Filter the annotated QuerySet)
|
||||
high_potential_count = candidates_with_score.filter(
|
||||
sortable_score__gte=75
|
||||
).count()
|
||||
high_potential_ratio = round((high_potential_count / total_candidates) * 100, 1) if total_candidates > 0 else 0
|
||||
|
||||
#donut chart data
|
||||
jobs=models.JobPosting.objects.all()
|
||||
selected_job_id=request.GET.get('selected_job_id')
|
||||
print(jobs)
|
||||
print(selected_job_id)
|
||||
apply_counts,exam_counts,interview_counts,offer_counts=[0]*4
|
||||
if selected_job_id:
|
||||
job=jobs.filter(internal_job_id=selected_job_id)
|
||||
apply_counts=job.screening_candidates_count or 0
|
||||
exam_counts=job.exam_candidates_count or 0
|
||||
interview_counts=job.interview_candidates_count or 0
|
||||
offer_counts=job.offer_candidates_count or 0
|
||||
|
||||
|
||||
|
||||
|
||||
applicant_stages=['APPLIED','EXAM','INTERVIEW','OFFER']
|
||||
stage_counts=[apply_counts,exam_counts,interview_counts,offer_counts]
|
||||
|
||||
|
||||
|
||||
job_titles = [job.title for job in jobs]
|
||||
job_app_counts = [job.candidates.count() for job in jobs]
|
||||
average_applications = round(sum(job_app_counts) / total_jobs, 2) if total_jobs > 0 else 0
|
||||
|
||||
context = {
|
||||
'total_jobs': total_jobs,
|
||||
'total_candidates': total_candidates,
|
||||
'job_titles': job_titles,
|
||||
'job_app_counts': job_app_counts,
|
||||
'average_applications': average_applications,
|
||||
|
||||
# Chart Data
|
||||
'job_titles': json.dumps(job_titles),
|
||||
'job_app_counts': json.dumps(job_app_counts),
|
||||
|
||||
# New Analytical Metrics (FIXED)
|
||||
'avg_match_score': avg_match_score,
|
||||
'high_potential_count': high_potential_count,
|
||||
'high_potential_ratio': high_potential_ratio,
|
||||
'scored_ratio': scored_ratio,
|
||||
'applicant_stages':json.dumps(applicant_stages),
|
||||
'stage_counts':json.dumps(stage_counts),
|
||||
'jobs':'jobs',
|
||||
'selected_job_id':selected_job_id
|
||||
}
|
||||
return render(request, 'recruitment/dashboard.html', context)
|
||||
|
||||
|
||||
@login_required
|
||||
def candidate_offer_view(request, slug):
|
||||
"""View for candidates in the Offer stage"""
|
||||
|
||||
@ -5,9 +5,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ATS Form Builder - Vanilla JS</title>
|
||||
|
||||
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
|
||||
<style>
|
||||
/* Updated CSS styles with a new Teal/Aqua theme */
|
||||
:root {
|
||||
@ -678,8 +678,8 @@
|
||||
const djangoConfig = {
|
||||
csrfToken: "{{ csrf_token }}",
|
||||
saveUrl: "{% url 'save_form_template' %}",
|
||||
loadUrl: {% if template_id %}"{% url 'load_form_template' template_id %}"{% else %}null{% endif %},
|
||||
templateId: {% if template_id %}{{ template_id }}{% else %}null{% endif %},
|
||||
loadUrl: {% if template_slug %}"{% url 'load_form_template' template_slug %}"{% else %}null{% endif %},
|
||||
templateId: {% if template_slug %}'{{ template_slug }}'{% else %}null{% endif %},
|
||||
jobId: {% if job_id %}{{ job_id }}{% else %}null{% endif %} // Add this if you need it
|
||||
};
|
||||
</script>
|
||||
@ -779,7 +779,7 @@
|
||||
<a href="{% url 'dashboard' %}" style="color: #6c757d !important; text-decoration: none !important;">Home</a>
|
||||
/
|
||||
</span>
|
||||
|
||||
|
||||
<span class="me-2">
|
||||
<a href="{% url 'job_list' %}" style="color: #6c757d !important; text-decoration: none !important;">Jobs</a>
|
||||
/
|
||||
@ -793,7 +793,7 @@
|
||||
</nav>
|
||||
<div class="toolbar">
|
||||
<h1 id="formTitle">Resume Application Form</h1>
|
||||
|
||||
|
||||
<div>
|
||||
<button class="btn btn-outline" id="formSettingsBtn">
|
||||
<i class="fas fa-cog"></i> Settings
|
||||
@ -982,9 +982,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
|
||||
|
||||
// Application State
|
||||
const state = {
|
||||
draggedStageIndex: null,
|
||||
@ -1212,7 +1212,7 @@ const elements = {
|
||||
name: state.formName,
|
||||
description: state.formDescription,
|
||||
is_active: state.formActive,
|
||||
template_id: state.templateId,
|
||||
template_slug: state.templateId,
|
||||
stages: state.stages.map(stage => ({
|
||||
name: stage.name,
|
||||
predefined: stage.predefined,
|
||||
@ -1248,7 +1248,7 @@ const elements = {
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
state.templateId = result.template_id;
|
||||
state.templateId = result.template_slug;
|
||||
window.location.href = "{% url 'form_templates_list' %}";
|
||||
|
||||
} else {
|
||||
@ -1280,7 +1280,7 @@ const elements = {
|
||||
|
||||
// Set stages (this is where your actual stages come from)
|
||||
state.stages = templateData.stages;
|
||||
state.templateId = templateData.id;
|
||||
state.templateId = templateData.template_slug;
|
||||
|
||||
// Update next IDs to avoid conflicts
|
||||
let maxFieldId = 0;
|
||||
|
||||
@ -74,17 +74,17 @@
|
||||
<div class="card-footer bg-transparent">
|
||||
<div class="btn-group w-100" role="group">
|
||||
{% if form.created_by == user %}
|
||||
<a href="{% url 'edit_form' form.id %}" class="btn btn-sm btn-outline-warning">
|
||||
<a href="{% url 'edit_form' form.slug %}" class="btn btn-sm btn-outline-warning">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'form_preview' form.id %}" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<a href="{% url 'form_preview' form.slug %}" class="btn btn-sm btn-outline-primary" target="_blank">
|
||||
<i class="fas fa-eye"></i> Preview
|
||||
</a>
|
||||
<a href="{% url 'form_embed' form.id %}" class="btn btn-sm btn-outline-secondary" target="_blank">
|
||||
<a href="{% url 'form_embed' form.slug %}" class="btn btn-sm btn-outline-secondary" target="_blank">
|
||||
<i class="fas fa-code"></i> Embed
|
||||
</a>
|
||||
<a href="{% url 'form_submissions' form.id %}" class="btn btn-sm btn-outline-info">
|
||||
<a href="{% url 'form_submissions' form.slug %}" class="btn btn-sm btn-outline-info">
|
||||
<i class="fas fa-list"></i> Submissions
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -231,10 +231,10 @@
|
||||
<div class="mt-auto pt-2 border-top">
|
||||
<div class="d-flex gap-2 justify-content-end">
|
||||
|
||||
<a href="{% url 'form_wizard' template.id %}" class="btn btn-outline-primary btn-sm" title="{% trans 'Preview' %}">
|
||||
<a href="{% url 'form_wizard' template.slug %}" class="btn btn-outline-primary btn-sm" title="{% trans 'Preview' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'form_builder' template.id %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'Edit' %}">
|
||||
<a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'Submissions' %}">
|
||||
@ -286,10 +286,10 @@
|
||||
<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.id %}" class="btn btn-outline-primary" title="{% trans 'Preview' %}">
|
||||
<a href="{% url 'form_wizard' template.slug %}" class="btn btn-outline-primary" title="{% trans 'Preview' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'form_builder' template.id %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Submissions' %}">
|
||||
|
||||
@ -467,7 +467,7 @@
|
||||
/* The z-index is already 1030 in the inline style, which is correct */
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<nav
|
||||
id="bottomNavbar"
|
||||
class="navbar navbar-expand-lg sticky-top"
|
||||
@ -532,13 +532,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
// Application State
|
||||
const csrfToken = '{{ csrf_token }}';
|
||||
|
||||
const state = {
|
||||
templateId: {{ template_id }},
|
||||
templateId: '{{ template_slug }}',
|
||||
stages: [],
|
||||
currentStage: 0,
|
||||
formData: {},
|
||||
@ -838,20 +838,23 @@
|
||||
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
alert('Application submitted successfully! Thank you for your submission.');
|
||||
window.location.href = '/applications/'; // Redirect to applications list
|
||||
console.log('Application submitted successfully! Thank you for your submission.');
|
||||
const redirect_url = result['redirect_url']
|
||||
window.location.href = redirect_url; // Redirect to applications list
|
||||
} else {
|
||||
alert('Error submitting form: ' + (result.error || 'Unknown error'));
|
||||
console.log(result)
|
||||
console.log('Error submitting form: ' + (result.error || 'Unknown error'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Submission error:', error);
|
||||
console.log(error)
|
||||
//console.error('Submission error:', error);
|
||||
// Try to get response text for debugging
|
||||
try {
|
||||
const errorText = await response.text();
|
||||
console.error('Response text:', errorText);
|
||||
alert('Error submitting form. Server response: ' + errorText);
|
||||
console.log('Error submitting form. Server response: ' + errorText);
|
||||
} catch (e) {
|
||||
alert('Error submitting form: ' + error.message);
|
||||
console.log('Error submitting form: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,31 +1,123 @@
|
||||
{% load i18n %}
|
||||
<h5> {% trans "AI Score" %}: <span class="badge bg-success"><i class="fas fa-robot me-1"></i> {{ candidate.match_score }}</span> <span class="badge bg-success"><i class="fas fa-graduation-cap me-1"></i> {{ candidate.professional_category }} </span></h5>
|
||||
<h5> {% trans "AI Score" %}: <span class="badge bg-success"><i class="fas fa-robot me-1"></i> {{ candidate.match_score }}%</span> <span class="badge bg-success"><i class="fas fa-graduation-cap me-1"></i> {{ candidate.professional_category }} </span></h5>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-briefcase me-2 text-primary"></i>
|
||||
<small class="text-muted">{% trans "Job Fit" %}</small>
|
||||
</div>
|
||||
<p class="mb-1">{{ candidate.job_fit_narrative }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-star me-2 text-warning"></i>
|
||||
<small class="text-muted">{% trans "Top Keywords" %}</small>
|
||||
</div>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for keyword in candidate.top_3_keywords %}
|
||||
<span class="badge bg-info text-dark me-1">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-clock me-2 text-info"></i>
|
||||
<small class="text-muted">{% trans "Experience" %}</small>
|
||||
</div>
|
||||
<p class="mb-1"><strong>{{ candidate.years_of_experience }}</strong> {% trans "years" %}</p>
|
||||
<p class="mb-0"><strong>{% trans "Recent Role:" %}</strong> {{ candidate.most_recent_job_title }}</p>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-chart-line me-2 text-success"></i>
|
||||
<small class="text-muted">{% trans "Skills" %}</small>
|
||||
</div>
|
||||
<p class="mb-1"><strong>{% trans "Soft Skills:" %}</strong> {{ candidate.soft_skills_score }}%</p>
|
||||
<p class="mb-0"><strong>{% trans "Industry Match:" %}</strong>
|
||||
<span class="badge {% if candidate.industry_match_score >= 70 %}bg-success{% elif candidate.industry_match_score >= 40 %}bg-warning{% else %}bg-danger{% endif %}">
|
||||
{{ candidate.industry_match_score }}%
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-comment me-1 text-info"></i> {% trans "Recommendation" %}</label>
|
||||
<textarea class="form-control" rows="10" readonly>{{ candidate.recommendation }}</textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-thumbs-up me-1 text-success"></i> {% trans "Strengths" %}</label>
|
||||
<textarea class="form-control" rows="6" readonly>{{ candidate.strengths }}</textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-thumbs-down me-1 text-danger"></i> {% trans "Weaknesses" %}</label>
|
||||
<textarea class="form-control" rows="6" readonly>{{ candidate.weaknesses }}</textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-list-check me-1"></i> {% trans "Criteria Checklist" %}</label>
|
||||
<ul class="list-group">
|
||||
{% for key, value in candidate.criteria_checklist.items %}
|
||||
<li class="list-group-item d-flex justify-content-between">
|
||||
<span class="fw-" style="font-size: smaller;">{{ key }}</span>
|
||||
{% if value == 'Met' %}
|
||||
<span class="badge bg-success"><i class="fas fa-check me-1"></i> Yes</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger"><i class="fas fa-times me-1"></i> Not Mentioned</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<textarea class="form-control" rows="6" readonly>{{ candidate.recommendation }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-thumbs-up me-1 text-success"></i> {% trans "Strengths" %}</label>
|
||||
<textarea class="form-control" rows="4" readonly>{{ candidate.strengths }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-thumbs-down me-1 text-danger"></i> {% trans "Weaknesses" %}</label>
|
||||
<textarea class="form-control" rows="4" readonly>{{ candidate.weaknesses }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-list-check me-1"></i> {% trans "Criteria Assessment" %}</label>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{% trans "Criteria" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for criterion, status in candidate.criteria_checklist.items %}
|
||||
<tr>
|
||||
<td>{{ criterion }}</td>
|
||||
<td>
|
||||
{% if status == "Met" %}
|
||||
<span class="badge bg-success"><i class="fas fa-check me-1"></i> {% trans "Met" %}</span>
|
||||
{% elif status == "Not Met" %}
|
||||
<span class="badge bg-danger"><i class="fas fa-times me-1"></i> {% trans "Not Met" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-check-circle me-2 text-success"></i>
|
||||
<small class="text-muted">{% trans "Minimum Requirements" %}</small>
|
||||
</div>
|
||||
{% if candidate.min_requirements_met %}
|
||||
<span class="badge bg-success">{% trans "Met" %}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-danger">{% trans "Not Met" %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="d-flex align-items-center mb-2">
|
||||
<i class="fas fa-star me-2 text-warning"></i>
|
||||
<small class="text-muted">{% trans "Screening Rating" %}</small>
|
||||
</div>
|
||||
<span class="badge bg-secondary">{{ candidate.screening_stage_rating }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if candidate.language_fluency %}
|
||||
<div class="mb-3">
|
||||
<label class="form-label"><i class="fas fa-language me-1 text-info"></i> {% trans "Language Fluency" %}</label>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for language in candidate.language_fluency %}
|
||||
<span class="badge bg-light text-dark">{{ language }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
540
templates/includes/candidate_resume_template.html
Normal file
540
templates/includes/candidate_resume_template.html
Normal file
@ -0,0 +1,540 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Resume - Abdullah Sami Bakhsh</title>
|
||||
<style>
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-light-bg: #f9fbfd;
|
||||
--kaauh-border: #eaeff3;
|
||||
--text-primary: #2c3e50;
|
||||
--text-secondary: #6c757d;
|
||||
--white: #ffffff;
|
||||
--danger: #dc3545;
|
||||
--warning: #ffc107;
|
||||
--success: #28a745;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, var(--kaauh-light-bg) 0%, #e3f2fd 100%);
|
||||
color: var(--text-primary);
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.resume-container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: var(--white);
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0, 99, 110, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: linear-gradient(135deg, var(--kaauh-teal) 0%, var(--kaauh-teal-dark) 100%);
|
||||
color: var(--white);
|
||||
padding: 40px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
right: -10%;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.name {
|
||||
font-size: 2.5em;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.3em;
|
||||
opacity: 0.9;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.contact-info {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 350px;
|
||||
gap: 30px;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.left-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.right-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.section {
|
||||
background: var(--white);
|
||||
border-radius: 12px;
|
||||
padding: 25px;
|
||||
border: 1px solid var(--kaauh-border);
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
|
||||
.section:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(0, 99, 110, 0.1);
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.3em;
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal);
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid var(--kaauh-teal);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.summary {
|
||||
line-height: 1.8;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.experience-item {
|
||||
margin-bottom: 25px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
.experience-item:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.job-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.job-title {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.company {
|
||||
color: var(--text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.job-period {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9em;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.achievements {
|
||||
margin-top: 10px;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.achievements li {
|
||||
margin-bottom: 5px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.education-item {
|
||||
margin-bottom: 15px;
|
||||
padding-bottom: 15px;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
.education-item:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.degree {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
.institution {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.skills-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.skill-tag {
|
||||
background: linear-gradient(135deg, var(--kaauh-teal) 0%, var(--kaauh-teal-dark) 100%);
|
||||
color: var(--white);
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.language-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
.language-item:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.language-name {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.proficiency {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.score-card {
|
||||
background: linear-gradient(135deg, var(--kaauh-teal) 0%, var(--kaauh-teal-dark) 100%);
|
||||
color: var(--white);
|
||||
text-align: center;
|
||||
padding: 30px 20px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.match-score {
|
||||
font-size: 3em;
|
||||
font-weight: 700;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.score-label {
|
||||
font-size: 1.1em;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.recommendation {
|
||||
background: var(--kaauh-light-bg);
|
||||
border-left: 4px solid var(--danger);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.recommendation-title {
|
||||
font-weight: 600;
|
||||
color: var(--danger);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.recommendation-text {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.keyword-tag {
|
||||
background: var(--kaauh-light-bg);
|
||||
color: var(--kaauh-teal-dark);
|
||||
padding: 4px 10px;
|
||||
border-radius: 15px;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
.strengths-weaknesses {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.strength-box {
|
||||
background: rgba(40, 167, 69, 0.1);
|
||||
border: 1px solid var(--success);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.weakness-box {
|
||||
background: rgba(220, 53, 69, 0.1);
|
||||
border: 1px solid var(--danger);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.box-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.strength-box .box-title {
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.weakness-box .box-title {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.box-content {
|
||||
font-size: 0.9em;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
@media (max-width: 968px) {
|
||||
.main-content {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.strengths-weaknesses {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.name {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.job-header {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="resume-container">
|
||||
<div class="header">
|
||||
<div class="header-content">
|
||||
<h1 class="name">Abdullah Sami Bakhsh</h1>
|
||||
<div class="title">Head, Recruitment & Training</div>
|
||||
<div class="contact-info">
|
||||
<div class="contact-item">
|
||||
<span>📱</span>
|
||||
<span>+966 561 168 180</span>
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<span>✉️</span>
|
||||
<span>Bakhsh.Abdullah@Outlook.com</span>
|
||||
</div>
|
||||
<div class="contact-item">
|
||||
<span>📍</span>
|
||||
<span>Saudi Arabia</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-content">
|
||||
<div class="left-column">
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
<span>📋</span> Professional Summary
|
||||
</h2>
|
||||
<p class="summary">
|
||||
Strategic and results‑driven HR leader with over 11 years of experience in human resources, specializing in business partnering, workforce planning, talent acquisition, training & development, and organizational effectiveness.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
<span>💼</span> Work Experience
|
||||
</h2>
|
||||
<div class="experience-item">
|
||||
<div class="job-header">
|
||||
<div>
|
||||
<div class="job-title">Head, Recruitment & Training</div>
|
||||
<div class="company">TASNEE - DOWNSTREAM & METALLURGY SBUs</div>
|
||||
</div>
|
||||
<div class="job-period">Oct 2024 - Present</div>
|
||||
</div>
|
||||
<ul class="achievements">
|
||||
<li>Led recruitment and training initiatives across downstream and metallurgical divisions</li>
|
||||
<li>Developed workforce analytics program</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="experience-item">
|
||||
<div class="job-header">
|
||||
<div>
|
||||
<div class="job-title">Human Resources Business Partner</div>
|
||||
<div class="company">TASNEE – METALLURGY SBU</div>
|
||||
</div>
|
||||
<div class="job-period">Oct 2015 - Present</div>
|
||||
</div>
|
||||
<ul class="achievements">
|
||||
<li>Implemented HR strategies aligning with business goals</li>
|
||||
<li>Optimized recruitment processes</li>
|
||||
<li>Ensured regulatory compliance</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="experience-item">
|
||||
<div class="job-header">
|
||||
<div>
|
||||
<div class="job-title">Specialist, Recruitment</div>
|
||||
<div class="company">MARAFIQ</div>
|
||||
</div>
|
||||
<div class="job-period">Jul 2011 - Feb 2013</div>
|
||||
</div>
|
||||
<ul class="achievements">
|
||||
<li>Performed recruitment for various roles</li>
|
||||
<li>Improved selection processes</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
<span>🎓</span> Education
|
||||
</h2>
|
||||
<div class="education-item">
|
||||
<div class="degree">Associate Diploma in People Management Level 5</div>
|
||||
<div class="institution">Chartered Institute of Personnel and Development (CIPD)</div>
|
||||
</div>
|
||||
<div class="education-item">
|
||||
<div class="degree">Bachelor's Degree in Chemical Engineering Technology</div>
|
||||
<div class="institution">Yanbu Industrial College</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-column">
|
||||
<div class="score-card">
|
||||
<div class="match-score">10%</div>
|
||||
<div class="score-label">Match Score</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
<span>🔍</span> Assessment
|
||||
</h2>
|
||||
<div class="strengths-weaknesses">
|
||||
<div class="strength-box">
|
||||
<div class="box-title">Strengths</div>
|
||||
<div class="box-content">Extensive HR leadership and project management experience</div>
|
||||
</div>
|
||||
<div class="weakness-box">
|
||||
<div class="box-title">Weaknesses</div>
|
||||
<div class="box-content">Lack of IT infrastructure, cybersecurity, and relevant certifications</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="recommendation">
|
||||
<div class="recommendation-title">Recommendation</div>
|
||||
<div class="recommendation-text">Candidate does not meet the IT management requirements; not recommended for interview.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
<span>💡</span> Top Keywords
|
||||
</h2>
|
||||
<div class="skills-container">
|
||||
<span class="keyword-tag">HR</span>
|
||||
<span class="keyword-tag">Recruitment</span>
|
||||
<span class="keyword-tag">Training</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
<span>🛠️</span> Skills
|
||||
</h2>
|
||||
<div class="skills-container">
|
||||
<span class="skill-tag">Workforce Analytics</span>
|
||||
<span class="skill-tag">Succession Planning</span>
|
||||
<span class="skill-tag">Organizational Development</span>
|
||||
<span class="skill-tag">Recruitment & Selection</span>
|
||||
<span class="skill-tag">Employee Engagement</span>
|
||||
<span class="skill-tag">Training Needs Analysis</span>
|
||||
<span class="skill-tag">Performance Management</span>
|
||||
<span class="skill-tag">Time Management</span>
|
||||
<span class="skill-tag">Negotiation Skills</span>
|
||||
<span class="skill-tag">SAP & HRIS Systems</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2 class="section-title">
|
||||
<span>🌍</span> Languages
|
||||
</h2>
|
||||
<div class="language-item">
|
||||
<span class="language-name">Arabic</span>
|
||||
<span class="proficiency">Native</span>
|
||||
</div>
|
||||
<div class="language-item">
|
||||
<span class="language-name">English</span>
|
||||
<span class="proficiency">Fluent</span>
|
||||
</div>
|
||||
<div class="language-item">
|
||||
<span class="language-name">Japanese</span>
|
||||
<span class="proficiency">Intermediate</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -275,7 +275,7 @@
|
||||
{% elif active_tab == 'request' %}
|
||||
<tr>
|
||||
<td>{{ log.datetime|date:"Y-m-d H:i:s" }}</td>
|
||||
<td>{{ log.user.get_full_name|default:log.user.email|default:"Anonymous" }}</td>
|
||||
<td>{{ log.user.email|default:"Anonymous" }}</td>
|
||||
<td>
|
||||
<span class="badge rounded-pill badge-request-method">{{ log.method }}</span>
|
||||
</td>
|
||||
|
||||
@ -150,8 +150,113 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 2: JOB CONTENT (CKEDITOR 5 Fields) #}
|
||||
{# 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-md-6">
|
||||
<div>
|
||||
<label for="{{ form.open_positions.id_for_label }}" class="form-label">{% trans "Open Positions" %}</label>
|
||||
{{ form.open_positions }}
|
||||
{% if form.open_positions.errors %}<div class="text-danger small mt-1">{{ form.open_positions.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.max_applications.id_for_label }}" class="form-label">{% trans "Max Applications" %}</label>
|
||||
{{ form.max_applications }}
|
||||
{% 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) #}
|
||||
{# ================================================= #}
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header-themed">
|
||||
@ -179,21 +284,15 @@
|
||||
</div>
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 3: COMPENSATION AND APPLICATION #}
|
||||
{# SECTION 5: APPLICATION Instructions #}
|
||||
{# ================================================= #}
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header-themed">
|
||||
<h5><i class="fas fa-dollar-sign"></i> {% trans "Compensation & Application" %}</h5>
|
||||
<h5><i class="fas fa-dollar-sign"></i> {% trans "Benefits & Application Instructions" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<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>
|
||||
|
||||
{% comment %} (application_url comment removed for brevity) {% endcomment %}
|
||||
|
||||
<div class="col-12">
|
||||
@ -215,117 +314,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 4: 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, & Status" %}</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-4">
|
||||
<div>
|
||||
<label for="{{ form.application_deadline.id_for_label }}" class="form-label">{% trans "Application Deadline" %}</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-4">
|
||||
<div>
|
||||
<label for="{{ form.application_start_date.id_for_label }}" class="form-label">{% trans "Application Start Date" %}</label>
|
||||
{{ form.application_start_date }}
|
||||
{% if form.application_start_date.errors %}<div class="text-danger small mt-1">{{ form.application_start_date.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.joining_date.id_for_label }}" class="form-label">{% trans "Desired Joining Date" %}</label>
|
||||
{{ form.joining_date }}
|
||||
{% if form.joining_date.errors %}<div class="text-danger small mt-1">{{ form.joining_date.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 5: 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-md-4">
|
||||
<div>
|
||||
<label for="{{ form.open_positions.id_for_label }}" class="form-label">{% trans "Open Positions" %}</label>
|
||||
{{ form.open_positions }}
|
||||
{% if form.open_positions.errors %}<div class="text-danger small mt-1">{{ form.open_positions.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.max_applications.id_for_label }}" class="form-label">{% trans "Max Applications" %}</label>
|
||||
{{ form.max_applications }}
|
||||
{% if form.max_applications.errors %}<div class="text-danger small mt-1">{{ form.max_applications.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.created_by.id_for_label }}" class="form-label">{% trans "Created By" %}</label>
|
||||
{{ form.created_by }}
|
||||
{% if form.created_by.errors %}<div class="text-danger small mt-1">{{ form.created_by.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)" %}</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>
|
||||
|
||||
{# ================================================= #}
|
||||
{# ACTION BUTTONS #}
|
||||
|
||||
@ -150,8 +150,113 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 2: JOB CONTENT (CKEDITOR 5 Fields) #}
|
||||
{# 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-md-6">
|
||||
<div>
|
||||
<label for="{{ form.open_positions.id_for_label }}" class="form-label">{% trans "Open Positions" %}</label>
|
||||
{{ form.open_positions }}
|
||||
{% if form.open_positions.errors %}<div class="text-danger small mt-1">{{ form.open_positions.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div>
|
||||
<label for="{{ form.max_applications.id_for_label }}" class="form-label">{% trans "Max Applications" %}</label>
|
||||
{{ form.max_applications }}
|
||||
{% 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) #}
|
||||
{# ================================================= #}
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header-themed">
|
||||
@ -179,21 +284,15 @@
|
||||
</div>
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 3: COMPENSATION AND APPLICATION #}
|
||||
{# SECTION 5: APPLICATION Instructions #}
|
||||
{# ================================================= #}
|
||||
<div class="card mb-4 shadow-sm">
|
||||
<div class="card-header-themed">
|
||||
<h5><i class="fas fa-dollar-sign"></i> {% trans "Compensation & Application" %}</h5>
|
||||
<h5><i class="fas fa-dollar-sign"></i> {% trans "Benefits & Application Instructions" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row g-4">
|
||||
<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>
|
||||
|
||||
{% comment %} (application_url comment removed for brevity) {% endcomment %}
|
||||
|
||||
<div class="col-12">
|
||||
@ -215,117 +314,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 4: 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, & Status" %}</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-4">
|
||||
<div>
|
||||
<label for="{{ form.application_deadline.id_for_label }}" class="form-label">{% trans "Application Deadline" %}</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-4">
|
||||
<div>
|
||||
<label for="{{ form.application_start_date.id_for_label }}" class="form-label">{% trans "Application Start Date" %}</label>
|
||||
{{ form.application_start_date }}
|
||||
{% if form.application_start_date.errors %}<div class="text-danger small mt-1">{{ form.application_start_date.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.joining_date.id_for_label }}" class="form-label">{% trans "Desired Joining Date" %}</label>
|
||||
{{ form.joining_date }}
|
||||
{% if form.joining_date.errors %}<div class="text-danger small mt-1">{{ form.joining_date.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# ================================================= #}
|
||||
{# SECTION 5: 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-md-4">
|
||||
<div>
|
||||
<label for="{{ form.open_positions.id_for_label }}" class="form-label">{% trans "Open Positions" %}</label>
|
||||
{{ form.open_positions }}
|
||||
{% if form.open_positions.errors %}<div class="text-danger small mt-1">{{ form.open_positions.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.max_applications.id_for_label }}" class="form-label">{% trans "Max Applications" %}</label>
|
||||
{{ form.max_applications }}
|
||||
{% if form.max_applications.errors %}<div class="text-danger small mt-1">{{ form.max_applications.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div>
|
||||
<label for="{{ form.created_by.id_for_label }}" class="form-label">{% trans "Created By" %}</label>
|
||||
{{ form.created_by }}
|
||||
{% if form.created_by.errors %}<div class="text-danger small mt-1">{{ form.created_by.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)" %}</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>
|
||||
|
||||
{# ================================================= #}
|
||||
{# ACTION BUTTONS #}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
|
||||
{% extends "base.html" %}
|
||||
{% load i18n static %}
|
||||
|
||||
@ -171,8 +170,63 @@
|
||||
border: 1px solid;
|
||||
}
|
||||
|
||||
/* Enhanced styling for the Job Avg. Score card */
|
||||
.stats .card:first-child { /* Targets the first card in the .stats div (Job Avg. Score) */
|
||||
border-left: 5px solid var(--kaauh-teal); /* A distinctive left border */
|
||||
background: linear-gradient(to right bottom, rgba(0, 99, 110, 0.05), rgba(255, 255, 255, 0));
|
||||
}
|
||||
|
||||
.stats .card:first-child .card-header {
|
||||
background-color: rgba(0, 99, 110, 0.1); /* Subtle background for header */
|
||||
border-bottom: 2px solid var(--kaauh-teal); /* Emphasized border */
|
||||
border-left: none; /* Remove inherited border from parent if any */
|
||||
border-top: none;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.stats .card:first-child .card-header h3 {
|
||||
color: var(--kaauh-teal); /* Match the theme color */
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem; /* Slightly larger header */
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.stats .card:first-child .stat-icon {
|
||||
color: var(--kaauh-teal); /* Match the theme color */
|
||||
font-size: 1.2rem; /* Slightly larger icon */
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.stats .card:first-child .stat-value {
|
||||
font-size: 2.5rem; /* Larger, more prominent value */
|
||||
font-weight: 700;
|
||||
color: var(--kaauh-teal); /* Theme color for the value */
|
||||
text-align: center;
|
||||
margin: 1rem 0; /* Spacing around the value */
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.05); /* Subtle text shadow for depth */
|
||||
}
|
||||
|
||||
.stats .card:first-child .stat-caption {
|
||||
text-align: center;
|
||||
color: #6c757d; /* Muted color for caption */
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
margin-top: 0.5rem;
|
||||
border-top: 1px solid rgba(0, 99, 110, 0.2); /* Subtle top border for caption area */
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Custom CSS for simplified stat card (from previous answer) */
|
||||
.stats-grid .kpi-card {
|
||||
border-left: 4px solid var(--kaauh-teal);
|
||||
background-color: #f0faff;
|
||||
}
|
||||
.stats-grid .card-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
{% endblock %}
|
||||
@ -190,7 +244,7 @@
|
||||
<div class="row g-4">
|
||||
|
||||
{# LEFT COLUMN: JOB DETAILS WITH TABS #}
|
||||
<div class="col-lg-8">
|
||||
<div class="col-lg-7">
|
||||
<div class="card shadow-sm no-hover">
|
||||
|
||||
{# HEADER SECTION #}
|
||||
@ -202,6 +256,7 @@
|
||||
|
||||
<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" %}
|
||||
@ -216,6 +271,7 @@
|
||||
<span class="badge bg-secondary status-badge">
|
||||
{% endif %}
|
||||
{{ job.get_status_display }}
|
||||
</span>
|
||||
<button type="button" class="btn btn-outline-light btn-sm ms-2" data-bs-toggle="modal" data-bs-target="#editStatusModal">
|
||||
<i class="fas fa-edit text-primary"></i>
|
||||
</button>
|
||||
@ -235,13 +291,12 @@
|
||||
<i class="fas fa-file-alt me-1"></i> {% trans "Description & Requirements" %}
|
||||
</button>
|
||||
</li>
|
||||
{% if job.application_instructions %}
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="instructions-tab" data-bs-toggle="tab" data-bs-target="#instructions" type="button" role="tab" aria-controls="instructions" aria-selected="false">
|
||||
<i class="fas fa-paper-plane me-1"></i> {% trans "Application" %}
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="card-body">
|
||||
@ -277,21 +332,20 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-main-action btn-sm"
|
||||
id="copyJobLinkButton"
|
||||
data-url="{{ job.application_url }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6 heroicon">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 0 0 2.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 0 0-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 0 0 .75-.75 2.25 2.25 0 0 0-.1-.664m-5.8 0A2.251 2.251 0 0 1 13.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25ZM6.75 12h.008v.008H6.75V12Zm0 3h.008v.008H6.75V15Zm0 3h.008v.008H6.75V18Z" />
|
||||
</svg>
|
||||
{% trans "Share Public Link" %}
|
||||
</button>
|
||||
<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>
|
||||
<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>
|
||||
@ -346,19 +400,74 @@
|
||||
<div class="text-secondary">{{ job.benefits|safe}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# TAB 3 CONTENT: APPLICATION INSTRUCTIONS #}
|
||||
{% if job.application_instructions %}
|
||||
<div class="tab-pane fade" id="instructions" role="tabpanel" aria-labelledby="instructions-tab">
|
||||
<div class="mb-4">
|
||||
{% if job.application_instructions %}
|
||||
<div class="mb-4">
|
||||
<h5>{% trans "Application Instructions" %}</h5>
|
||||
<div class="text-secondary">{{ job.application_instructions|safe }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
</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>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{# FOOTER ACTIONS #}
|
||||
@ -377,7 +486,7 @@
|
||||
</div>
|
||||
|
||||
{# RIGHT COLUMN: TABBED CARDS #}
|
||||
<div class="col-lg-4 ">
|
||||
<div class="col-lg-5">
|
||||
|
||||
{# New Card for Candidate Category Chart #}
|
||||
<div class="card shadow-sm no-hover mb-4">
|
||||
@ -394,13 +503,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow-sm no-hover mb-4">
|
||||
<div class="card-body p-4">
|
||||
<h6 class="text-muted mb-4">{% trans "Applicant Tracking" %}</h6>
|
||||
{% include 'jobs/partials/applicant_tracking.html' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow-sm no-hover" style="height:350px;">
|
||||
{# 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">
|
||||
@ -409,14 +514,19 @@
|
||||
<i class="fas fa-users me-1 text-primary"></i> {% trans "Applicants" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
<button class="nav-link" id="tracking-tab" data-bs-toggle="tab" data-bs-target="#tracking-pane" type="button" role="tab" aria-controls="tracking-pane" aria-selected="false">
|
||||
<i class="fas fa-project-diagram me-1 text-primary"></i> {% trans "Tracking" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
<button class="nav-link" id="manage-tab" data-bs-toggle="tab" data-bs-target="#manage-pane" type="button" role="tab" aria-controls="manage-pane" aria-selected="false">
|
||||
<i class="fas fa-cogs me-1 text-secondary"></i> {% trans "Form Template" %}
|
||||
</button>
|
||||
</li>
|
||||
<li class="nav-item flex-fill" role="presentation">
|
||||
<button class="nav-link" id="internal-tab" data-bs-toggle="tab" data-bs-target="#internal-pane" type="button" role="tab" aria-controls="internal-pane" aria-selected="false">
|
||||
<i class="fas fa-info me-1 text-muted"></i> {% trans "Linkedin" %}
|
||||
<button class="nav-link" id="linkedin-tab" data-bs-toggle="tab" data-bs-target="#linkedin-pane" type="button" role="tab" aria-controls="linkedin-pane" aria-selected="false">
|
||||
<i class="fab fa-linkedin me-1 text-info"></i> {% trans "LinkedIn" %}
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
@ -426,35 +536,7 @@
|
||||
{# 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>
|
||||
{% comment %} {% if total_applicants > 0 %}
|
||||
<div class="row mb-4 applicant-stats">
|
||||
<div class="col-4">
|
||||
<div class="stat-item">
|
||||
<div class="text-primary">{{ applied_count }}</div>
|
||||
<small class="text-muted">{% trans "Applied" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="stat-item">
|
||||
<div class="text-info">{{ interview_count }}</div>
|
||||
<small class="text-muted">{% trans "Interview" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-4">
|
||||
<div class="stat-item">
|
||||
<div class="text-success">{{ offer_count }}</div>
|
||||
<small class="text-muted">{% trans "Offer" %}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-12 mb-2">
|
||||
<a href="{% url 'job_candidates_list' job.slug %}" class="btn btn-outline-secondary w-100">
|
||||
{% trans "View All Applicants" %} ({{ total_applicants }})
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{% endif %} {% endcomment %}
|
||||
|
||||
|
||||
<div class="d-grid gap-4">
|
||||
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus"></i> {% trans "Create Applicant" %}
|
||||
@ -465,13 +547,16 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# TAB 2: MANAGEMENT (LinkedIn & Forms) CONTENT #}
|
||||
{# NEW TAB 2: APPLICANT TRACKING CONTENT #}
|
||||
<div class="tab-pane fade" id="tracking-pane" role="tabpanel" aria-labelledby="tracking-tab">
|
||||
<h5 class="mb-3"><i class="fas fa-project-diagram me-2 text-primary"></i>{% trans "Pipeline Stages" %}</h5>
|
||||
{% include 'jobs/partials/applicant_tracking.html' %}
|
||||
<p class="text-muted small mt-3">{% trans "View the number of candidates currently in each stage of the hiring pipeline." %}</p>
|
||||
</div>
|
||||
|
||||
{# TAB 3: MANAGEMENT (Form Template) CONTENT #}
|
||||
<div class="tab-pane fade" id="manage-pane" role="tabpanel" aria-labelledby="manage-tab">
|
||||
|
||||
{# LinkedIn Integration (Content from old card) #}
|
||||
|
||||
|
||||
{# Applicant Form Management (Content from old card) #}
|
||||
<h5 class="mb-3"><i class="fas fa-clipboard-list me-2 text-primary"></i>{% trans "Form Management" %}</h5>
|
||||
<div class="d-grid gap-2">
|
||||
<p class="text-muted small mb-3">
|
||||
@ -492,8 +577,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# TAB 3: INTERNAL INFO CONTENT #}
|
||||
<div class="tab-pane fade" id="internal-pane" role="tabpanel" aria-labelledby="internal-tab">
|
||||
{# TAB 4: LINKEDIN INTEGRATION CONTENT #}
|
||||
<div class="tab-pane fade" id="linkedin-pane" role="tabpanel" aria-labelledby="linkedin-tab">
|
||||
<h5 class="mb-3"><i class="fab fa-linkedin me-2 text-info"></i>{% trans "LinkedIn Integration" %}</h5>
|
||||
<div class="mb-4">
|
||||
{% if job.posted_to_linkedin %}
|
||||
@ -534,48 +619,6 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% comment %} {# Applicant Form Management (Content from old card) #}
|
||||
<h5 class="mb-3"><i class="fas fa-clipboard-list me-2 text-primary"></i>{% trans "Form Management" %}</h5> {% endcomment %}
|
||||
{% comment %} <div class="d-grid gap-2">
|
||||
<p class="text-muted small mb-3">
|
||||
{% trans "Manage the custom application forms associated with this job posting." %}
|
||||
</p> {% endcomment %}
|
||||
|
||||
{% comment %} <a href="{% url 'create_form_template' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus-circle me-2"></i> {% trans "Create New Form" %}
|
||||
</a>
|
||||
|
||||
<a href="" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-list-alt me-1"></i> {% trans "View All Existing Forms" %}
|
||||
</a> {% endcomment %}
|
||||
|
||||
{% comment %} <a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-user-plus"></i> {% trans "Create Candidate" %}
|
||||
</a>
|
||||
<a href="{% url 'candidate_screening_view' job.slug %}" class="btn btn-main-action">
|
||||
<i class="fas fa-layer-group"></i> {% trans "Manage Tiers" %}
|
||||
</a> {% endcomment %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# TAB 3: INTERNAL INFO CONTENT #}
|
||||
<div class="tab-pane fade" id="internal-pane" role="tabpanel" aria-labelledby="internal-tab">
|
||||
<h5 class="mb-3"><i class="fas fa-info-circle me-2 text-secondary"></i>{% trans "Internal Information" %}</h5>
|
||||
<div class="small">
|
||||
<p class="mb-1"><strong>{% trans "Internal Job ID:" %}</strong> {{ job.internal_job_id }}</p>
|
||||
<p class="mb-1"><strong>{% trans "Created:" %}</strong> {{ job.created_at|date:"M d, Y" }}</p>
|
||||
<p class="mb-1"><strong>{% trans "Last Updated:" %}</strong> {{ job.updated_at|date:"M d, Y" }}</p>
|
||||
{% if job.reporting_to %}
|
||||
<p class="mb-0"><strong>{% trans "Reports To:" %}</strong> {{ job.reporting_to }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<a href="{% url 'job_list' %}" class="btn btn-outline-secondary w-100">
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to Jobs" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -583,10 +626,8 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--image modal class-->
|
||||
{% include "jobs/partials/image_upload.html" %}
|
||||
|
||||
<!-- JOB STATUS MODAL-->
|
||||
<div class="modal fade" id="editStatusModal" tabindex="-1" aria-labelledby="editStatusModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
@ -734,4 +775,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -128,7 +128,7 @@
|
||||
text-align: center;
|
||||
border-left: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
|
||||
/* Metrics Sub-Column Widths (7 total sub-columns, total 30%) */
|
||||
/* We have 5 main metrics: Applied, Screened, Exam, Interview, Offer.
|
||||
* Let's allocate the 30% evenly: 30% / 5 = 6% per metric column.
|
||||
@ -159,7 +159,7 @@
|
||||
border-left: 1px solid var(--kaauh-border);
|
||||
}
|
||||
/* Adds a distinctive vertical line before the metrics group (7th column) */
|
||||
.table tbody tr td:nth-child(7) {
|
||||
.table tbody tr td:nth-child(7) {
|
||||
border-left: 2px solid var(--kaauh-teal);
|
||||
}
|
||||
|
||||
@ -234,8 +234,7 @@
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="filter-buttons">
|
||||
|
||||
<button type="submit" class="btn btn-main-action btn-lg">
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-filter me-1"></i> {% trans "Apply Filters" %}
|
||||
</button>
|
||||
{% if job_filter or search_query %}
|
||||
@ -279,11 +278,11 @@
|
||||
</tr>
|
||||
|
||||
<tr class="nested-metrics-row">
|
||||
<th scope="col">{% trans "Applied" %}</th>
|
||||
<th scope="col">{% trans "Screened" %}</th>
|
||||
<th scope="col">{% trans "Exam" %}</th>
|
||||
<th scope="col">{% trans "Interview" %}</th>
|
||||
<th scope="col">{% trans "Offer" %}</th>
|
||||
<th style="width: calc(50% / 7);">{% trans "All" %}</th>
|
||||
<th style="width: calc(50% / 7);">{% trans "Screened" %}</th>
|
||||
<th style="width: calc(50% / 7 * 2);">{% trans "Exam" %}</th>
|
||||
<th style="width: calc(50% / 7 * 2);">{% trans "Interview" %}</th>
|
||||
<th style="width: calc(50% / 7);">{% trans "Offer" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@ -312,10 +311,10 @@
|
||||
<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.pk %}" class="btn btn-outline-secondary" title="{% trans 'Preview' %}">
|
||||
<a href="{% url 'form_wizard' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Preview' %}">
|
||||
<i class="fas fa-eye"></i>
|
||||
</a>
|
||||
<a href="{% url 'form_builder' job.form_template.id %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
<a href="{% url 'form_template_submissions_list' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Submissions' %}">
|
||||
@ -326,11 +325,11 @@
|
||||
</td>
|
||||
|
||||
{# CANDIDATE MANAGEMENT DATA - URLS NEUTRALIZED #}
|
||||
<td class="candidate-data-cell text-primary-theme"><a href="#" class="text-primary-theme">{% if job.all_candidates_count %}{{job.all_candidates_count}}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-info"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-info">{% if job.screening_candidates_count %}{{ job.screening_candidates_count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-success">{% if job.exam_candidates_count %}{{ job.exam_candidates_count}}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_exam_view' job.slug %}" class="text-success">{% if job.interview_candidates_count %}{{ job.interview_candidates_count}}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_interview_view' job.slug %}" class="text-success">{% if job.offer_candidates_count%}{{ job.offer_candidates_count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-primary-theme"><a href="#" class="text-primary-theme">{% if job.all_candidates.count %}{{ job.all_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-info"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-info">{% if job.screening_candidates.count %}{{ job.screening_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-success">{% if job.exam_candidates.count %}{{ job.exam_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_exam_view' job.slug %}" class="text-success">{% if job.interview_candidates.count %}{{ job.interview_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_interview_view' job.slug %}" class="text-success">{% if job.offer_candidates.count %}{{ job.offer_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@ -387,7 +386,7 @@
|
||||
{# --- END OF JOB LIST CONTAINER --- #}
|
||||
{% include "includes/paginator.html" %}
|
||||
{% if not jobs and not job_list_data and not page_obj %}
|
||||
<div class="text-center py-5 card shadow-sm">
|
||||
<div class="text-center py-5 card shadow-sm mt-4">
|
||||
<div class="card-body">
|
||||
<i class="fas fa-briefcase fa-3x mb-3" style="color: var(--kaauh-teal-dark);"></i>
|
||||
<h3>{% trans "No job postings found" %}</h3>
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
{% block customCSS %}
|
||||
<style>
|
||||
/* -------------------------------------------------------------------------- */
|
||||
/* KAAT-S Redesign CSS */
|
||||
/* KAAT-S Redesign CSS - Optimized Compact Detail View (Settings Removed) */
|
||||
/* -------------------------------------------------------------------------- */
|
||||
|
||||
:root {
|
||||
@ -20,93 +19,56 @@
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f0f2f5; /* Off-white page background */
|
||||
font-family: 'Inter', sans-serif; /* Use a modern font stack */
|
||||
background-color: #f0f2f5;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
/* ------------------ General Layout & Card Styles ------------------ */
|
||||
|
||||
.container {
|
||||
width:auto;
|
||||
padding: 3rem 1.5rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: none; /* Remove default border */
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.08), 0 4px 10px rgba(0,0,0,0.05); /* Deep, soft shadow */
|
||||
background-color: white;
|
||||
margin-bottom: 2.5rem;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 5px 15px rgba(0,0,0,0.05), 0 2px 5px rgba(0,0,0,0.03);
|
||||
margin-bottom: 1.5rem;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.card:not(.no-hover):hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 15px 40px rgba(0,0,0,0.1), 0 6px 15px rgba(0,0,0,0.08);
|
||||
}
|
||||
.card.no-hover:hover {
|
||||
transform: none;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.08), 0 4px 10px rgba(0,0,0,0.05);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(0,0,0,0.08);
|
||||
}
|
||||
.card-body {
|
||||
padding: 2rem;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* ------------------ Header & Title Styles ------------------ */
|
||||
/* ------------------ Main Header & Title Styles ------------------ */
|
||||
|
||||
.card-header {
|
||||
background-color: var(--kaauh-gray-light);
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
padding: 2rem;
|
||||
.main-title-card {
|
||||
padding: 1.5rem 2rem;
|
||||
background-color: white;
|
||||
border-bottom: 3px solid var(--kaauh-teal);
|
||||
border-radius: 12px 12px 0 0;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start; /* Align title group to the top */
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.card-header-title-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.card-header h1 {
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-weight: 800; /* Extra bold for prominence */
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
.card-header .heroicon {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
.card-header .btn-secondary-back {
|
||||
/* Subtle Back Button */
|
||||
align-self: flex-start;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: var(--kaauh-secondary-text);
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.card-header .btn-secondary-back:hover {
|
||||
color: var(--kaauh-teal);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ------------------ Status Badge Styles ------------------ */
|
||||
.main-title-card h1 {
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
font-size: 2rem;
|
||||
}
|
||||
.main-title-card .heroicon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.5em 1em;
|
||||
border-radius: 20px; /* Pill shape */
|
||||
font-size: 0.75rem;
|
||||
padding: 0.35em 0.8em;
|
||||
border-radius: 15px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
.bg-waiting { background-color: #ffc107 !important; color: var(--kaauh-primary-text) !important;}
|
||||
.bg-started { background-color: var(--kaauh-teal) !important; color: white !important;}
|
||||
@ -114,13 +76,13 @@ body {
|
||||
|
||||
/* ------------------ Detail Row & Content Styles ------------------ */
|
||||
|
||||
.card h2 {
|
||||
.detail-section h2 {
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-weight: 700;
|
||||
padding: 1.5rem 2rem 1rem;
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 2px solid var(--kaauh-teal-light);
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.detail-row-group {
|
||||
@ -128,10 +90,10 @@ body {
|
||||
}
|
||||
|
||||
.detail-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(150px, 40%) 1fr;
|
||||
padding: 1rem 2rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px dashed var(--kaauh-border);
|
||||
align-items: center;
|
||||
}
|
||||
.detail-row:last-child {
|
||||
@ -139,273 +101,194 @@ body {
|
||||
}
|
||||
.detail-label {
|
||||
font-weight: 600;
|
||||
color: var(--kaauh-teal-dark);
|
||||
text-align: left;
|
||||
font-size: 0.95rem;
|
||||
color: var(--kaauh-teal);
|
||||
font-size: 0.9rem;
|
||||
flex-basis: 45%;
|
||||
}
|
||||
.detail-value {
|
||||
text-align: right;
|
||||
color: var(--kaauh-primary-text);
|
||||
word-wrap: break-word;
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
font-size: 0.9rem;
|
||||
flex-basis: 55%;
|
||||
}
|
||||
|
||||
/* ------------------ Join Info & Copy Button ------------------ */
|
||||
|
||||
.join-info-card .card-body {
|
||||
padding-top: 2rem;
|
||||
.join-info-card {
|
||||
border-left: 5px solid var(--kaauh-teal); /* Highlight join info */
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: var(--kaauh-teal);
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
padding: 0.6rem 1.25rem;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
box-shadow: 0 4px 8px rgba(0, 99, 110, 0.3);
|
||||
}
|
||||
|
||||
.join-url-container {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
margin-top: 1.5rem;
|
||||
position: relative;
|
||||
padding: 1rem 0; /* Add padding for clear space around the copy area */
|
||||
margin-top: 1rem;
|
||||
}
|
||||
.join-url-display {
|
||||
flex-grow: 1;
|
||||
background-color: var(--kaauh-gray-light);
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem 1rem;
|
||||
word-break: break-all;
|
||||
font-size: 0.9rem;
|
||||
color: var(--kaauh-secondary-text);
|
||||
font-family: monospace; /* Monospace for links/code */
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.join-url-display strong {
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.btn-copy {
|
||||
flex-shrink: 0;
|
||||
background-color: var(--kaauh-teal-dark); /* Darker teal for a clean utility look */
|
||||
border: none;
|
||||
color: white;
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.btn-copy:hover {
|
||||
background-color: var(--kaauh-teal);
|
||||
}
|
||||
.btn-copy i {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
/* 🎯 Copy Message Pill Style */
|
||||
#copy-message {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
right: 0;
|
||||
background-color: var(--kaauh-success);
|
||||
color: white;
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 20px; /* Pill shape */
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease-in-out;
|
||||
z-index: 10;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||
padding: 0.5rem 0.75rem;
|
||||
background-color: var(--kaauh-teal-dark);
|
||||
}
|
||||
|
||||
/* ------------------ Footer & Actions ------------------ */
|
||||
|
||||
.card-footer {
|
||||
.action-bar-footer {
|
||||
border-top: 1px solid var(--kaauh-border);
|
||||
padding: 1.5rem 2rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
justify-content: flex-start;
|
||||
padding: 1rem 1.5rem;
|
||||
gap: 0.75rem;
|
||||
background-color: var(--kaauh-gray-light);
|
||||
border-radius: 0 0 12px 12px;
|
||||
}
|
||||
.btn-danger {
|
||||
background-color: var(--kaauh-danger);
|
||||
border-color: var(--kaauh-danger);
|
||||
color: white;
|
||||
.btn-footer-action {
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.btn-danger:hover {
|
||||
background-color: #c82333;
|
||||
border-color: #bd2130;
|
||||
box-shadow: 0 4px 8px rgba(220, 53, 69, 0.3);
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background-color: #6c757d;
|
||||
border-color: #6c757d;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 8px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background-color: #5a6268;
|
||||
border-color: #545b62;
|
||||
}
|
||||
|
||||
/* ------------------ API Response Styling ------------------ */
|
||||
#gateway-response-card {
|
||||
border-left: 5px solid var(--kaauh-teal); /* Prominent left border */
|
||||
}
|
||||
#gateway-response-card .card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
#gateway-response-card h3 {
|
||||
/* ------------------ Comments Section ------------------ */
|
||||
#comments-card .card-header {
|
||||
background-color: var(--kaauh-teal-light);
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-weight: 700;
|
||||
font-size: 1.35rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
#gateway-response-card pre {
|
||||
background-color: #fff;
|
||||
border: 1px solid var(--kaauh-border);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--kaauh-primary-text);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
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">
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="card no-hover">
|
||||
<div class="card-header">
|
||||
<div class="card-header-title-group">
|
||||
<h1>
|
||||
<svg class="heroicon" viewBox="0 0 24 24" fill="none" xmlns="[http://www.w3.org/2000/svg](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="col-auto">
|
||||
<span class="status-badge bg-{{ meeting.status }}">
|
||||
{{ meeting.status|title }}
|
||||
</span>
|
||||
{# --- TOP BAR / BACK BUTTON --- #}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<a href="{% url 'list_meetings' %}" class="btn btn-secondary-back">
|
||||
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Meetings" %}
|
||||
</a>
|
||||
</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>
|
||||
{% if meeting.interview %}
|
||||
<div class="col-auto">
|
||||
<span class="status-badge">
|
||||
Candidate Name : <a class="text-primary-theme" href="{% url 'candidate_detail' meeting.interview.candidate.slug %}">{{ meeting.interview.candidate.name }} </a>
|
||||
</span>
|
||||
|
||||
{# --- MAIN DETAIL BODY --- #}
|
||||
<div class="card-body detail-section">
|
||||
<h2>{% trans "Core Details" %}</h2>
|
||||
<div class="detail-row-group">
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Meeting ID" %}:</div><div class="detail-value">{{ meeting.meeting_id }}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Start Time" %}:</div><div class="detail-value">{{ meeting.start_time|date:"M d, Y H:i" }}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Duration" %}:</div><div class="detail-value">{{ meeting.duration }} {% trans "minutes" %}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Timezone" %}:</div><div class="detail-value">{{ meeting.timezone|default:"UTC" }}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Host Email" %}:</div><div class="detail-value">{{ meeting.host_email|default:"N/A" }}</div></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{% url 'list_meetings' %}" class="btn btn-secondary-back">
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to Meetings" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card no-hover">
|
||||
<h2>{% trans "Meeting Information" %}</h2>
|
||||
<div class="card-body 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 "Topic" %}:</div><div class="detail-value">{{ meeting.topic }}</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>
|
||||
{% if meeting.join_url %}
|
||||
<div class="card no-hover join-info-card">
|
||||
<h2>{% trans "Join Information" %}</h2>
|
||||
<div class="card-body">
|
||||
<a href="{{ meeting.join_url }}" class="btn btn-primary" target="_blank">
|
||||
<i class="fas fa-video"></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" id="join-url-display">
|
||||
<strong>{% trans "Join URL" %}:</strong> <span id="meeting-join-url">{{ meeting.join_url }}</span>
|
||||
</div>
|
||||
|
||||
<button class="btn-copy" onclick="copyLink()">
|
||||
<i class="fas fa-copy"></i>
|
||||
</div>
|
||||
|
||||
{# --- ACTION BAR AT THE BOTTOM OF THE MAIN CARD --- #}
|
||||
<div class="card-footer action-bar-footer d-flex justify-content-end">
|
||||
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary btn-footer-action">
|
||||
<i class="fas fa-edit me-1"></i> {% trans "Update" %}
|
||||
</a>
|
||||
{% if meeting.zoom_gateway_response %}
|
||||
<button type="button" class="btn btn-secondary btn-footer-action" onclick="toggleGateway()">
|
||||
<i class="fas fa-code me-1"></i> {% trans "API Response" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<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>
|
||||
|
||||
{% if meeting.password %}
|
||||
<div class="detail-row" style="border: none; padding: 1rem 0 0 0;">
|
||||
<div class="detail-label" style="font-size: 1rem;">{% trans "Password" %}:</div>
|
||||
<div class="detail-value" style="font-weight: 700; color: var(--kaauh-danger);">{{ meeting.password }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card no-hover">
|
||||
<h2>{% trans "Settings" %}</h2>
|
||||
<div class="card-body detail-row-group">
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Participant Video" %}:</div><div class="detail-value">{{ meeting.participant_video|yesno:"Yes,No" }}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Join Before Host" %}:</div><div class="detail-value">{{ meeting.join_before_host|yesno:"Yes,No" }}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Mute Upon Entry" %}:</div><div class="detail-value">{{ meeting.mute_upon_entry|yesno:"Yes,No" }}</div></div>
|
||||
<div class="detail-row"><div class="detail-label">{% trans "Waiting Room" %}:</div><div class="detail-value">{{ meeting.waiting_room|yesno:"Yes,No" }}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
{# --- RIGHT COLUMN (JOIN INFO) --- #}
|
||||
<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>
|
||||
|
||||
<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="card no-hover">
|
||||
<div class="card-footer">
|
||||
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary">
|
||||
<i class="fas fa-edit"></i> {% trans "Update Meeting" %}
|
||||
</a>
|
||||
<div class="join-url-container">
|
||||
<div id="copy-message" style="opacity: 0;">{% trans "Copied!" %}</div>
|
||||
|
||||
{% if meeting.zoom_gateway_response %}
|
||||
<button type="button" class="btn btn-secondary" onclick="toggleGateway()">
|
||||
<i class="fas fa-code"></i> {% trans "View API Response" %}
|
||||
</button>
|
||||
<div class="join-url-display d-flex justify-content-between align-items-center">
|
||||
<div class="text-truncate">
|
||||
<strong>{% trans "Join URL" %}:</strong>
|
||||
<span id="meeting-join-url">{{ meeting.join_url }}</span>
|
||||
</div>
|
||||
<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 %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-danger" 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"></i>
|
||||
Delete Meeting
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{# --- API RESPONSE CARD (Full width, hidden by default) --- #}
|
||||
{% if meeting.zoom_gateway_response %}
|
||||
<div id="gateway-response-card" class="card" style="display: none;">
|
||||
<div id="gateway-response-card" class="card mt-4" style="display: none;">
|
||||
<div class="card-body">
|
||||
<h3>{% trans "API Gateway Response" %}</h3>
|
||||
<pre>{{ meeting.zoom_gateway_response|safe }}</pre>
|
||||
@ -413,72 +296,68 @@ body {
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Comments Section -->
|
||||
<div class="card no-hover" id="comments-card">
|
||||
<div class="card-header text-primary-theme d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mb-0">
|
||||
|
||||
{# --- 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>
|
||||
Comments ({{ meeting.comments.count }})
|
||||
{% 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"></i> Add Comment
|
||||
<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 %}
|
||||
<div class="row">
|
||||
{% for comment in meeting.comments.all|dictsortreversed:"created_at" %}
|
||||
<div class="col-12 mb-3">
|
||||
<div class="card ">
|
||||
<div class="card-header d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
<strong>{{ comment.author.get_full_name|default:comment.author.username }}</strong>
|
||||
{% if comment.author != user %}
|
||||
<span class="badge bg-secondary ms-2">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="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="Delete Comment">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
</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">No comments yet. Be the first to comment!</p>
|
||||
<p class="text-muted">{% trans "No comments yet. Be the first to comment!" %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comment Modal (for Add/Edit) -->
|
||||
<div class="modal fade" id="commentModal" tabindex="-1" aria-labelledby="commentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
@ -487,8 +366,7 @@ body {
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body" id="commentModalBody">
|
||||
<!-- HTMX will load the form here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -505,41 +383,36 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
// CopyLink function remains the same (as provided in the original code)
|
||||
function copyLink() {
|
||||
const urlElement = document.getElementById('meeting-join-url');
|
||||
const messageElement = document.getElementById('copy-message');
|
||||
const textToCopy = urlElement.textContent || urlElement.innerText;
|
||||
|
||||
// Clear any existing message
|
||||
clearTimeout(window.copyMessageTimeout);
|
||||
|
||||
// Function to show the message
|
||||
function showMessage(success) {
|
||||
messageElement.textContent = success ? '{% trans "Copied!" %}' : '{% trans "Copy Failed." %}';
|
||||
messageElement.style.backgroundColor = success ? 'var(--kaauh-success)' : 'var(--kaauh-danger)';
|
||||
messageElement.style.opacity = '1';
|
||||
|
||||
// Hide the message after 2 seconds
|
||||
window.copyMessageTimeout = setTimeout(() => {
|
||||
messageElement.style.opacity = '0';
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Use the modern clipboard API
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
showMessage(true); // Show success message
|
||||
showMessage(true);
|
||||
}).catch(err => {
|
||||
console.error('Could not copy text: ', err);
|
||||
fallbackCopyTextToClipboard(textToCopy, showMessage); // Try fallback on failure
|
||||
fallbackCopyTextToClipboard(textToCopy, showMessage);
|
||||
});
|
||||
} else {
|
||||
// Fallback for older browsers
|
||||
fallbackCopyTextToClipboard(textToCopy, showMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback function for older browsers
|
||||
function fallbackCopyTextToClipboard(text, callback) {
|
||||
const textArea = document.createElement("textarea");
|
||||
textArea.value = text;
|
||||
@ -560,7 +433,7 @@ body {
|
||||
}
|
||||
|
||||
document.body.removeChild(textArea);
|
||||
callback(success); // Call the message function with the result
|
||||
callback(success);
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -294,14 +294,26 @@
|
||||
<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">
|
||||
<div>
|
||||
{% comment %} <div>
|
||||
<p class="mb-1"><strong>{{ candidate.resume.name }}</strong></p>
|
||||
<small class="text-muted">{{ candidate.resume.name|truncatechars:30 }}</small>
|
||||
</div> {% endcomment %}
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<div class="d-flex gap-2">
|
||||
<a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
|
||||
<i class="fas fa-eye me-1"></i>
|
||||
{% trans "View Actual Resume" %}
|
||||
</a>
|
||||
<a href="{{ candidate.resume.url }}" download class="btn btn-main-action">
|
||||
<i class="fas fa-download me-1"></i>
|
||||
{% trans "Download Resume" %}
|
||||
</a>
|
||||
</div>
|
||||
<a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
|
||||
<i class="fas fa-file-alt me-1"></i>
|
||||
{% trans "View Resume AI Overview" %}
|
||||
</a>
|
||||
</div>
|
||||
<a href="{{ candidate.resume.url }}" download class="btn btn-main-action">
|
||||
<i class="fas fa-download me-1"></i>
|
||||
{% trans "Download Resume" %}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@ -159,34 +159,31 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-md-6">
|
||||
{% url 'candidate_list' as candidate_list_url %}
|
||||
|
||||
|
||||
<form method="GET" class="row g-3 align-items-end h-100">
|
||||
{% if search_query %}<input type="hidden" name="q" value="{{ search_query }}">{% endif %}
|
||||
|
||||
{# Filter Group #}
|
||||
<div class="col-md-4 col-6">
|
||||
<label for="job_filter" class="form-label small text-muted">{% trans "Filter by Job" %}</label>
|
||||
<select name="job" id="job_filter" class="form-select form-select-sm">
|
||||
<option value="">{% trans "All Jobs" %}</option>
|
||||
{% for job in available_jobs %}
|
||||
<option value="{{ job.slug }}" {% if job_filter == job.slug %}selected{% endif %}>{{ job.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 col-6">
|
||||
<label for="stage_filter" class="form-label small text-muted">{% trans "Filter by Stage" %}</label>
|
||||
<select name="stage" id="stage_filter" class="form-select form-select-sm">
|
||||
<option value="">{% trans "All Stages" %}</option>
|
||||
<option value="Applied" {% if stage_filter == 'Applied' %}selected{% endif %}>{% trans "Applied" %}</option>
|
||||
<option value="Exam" {% if stage_filter == 'Exam' %}selected{% endif %}>{% trans "Exam" %}</option>
|
||||
<option value="Interview" {% if stage_filter == 'Interview' %}selected{% endif %}>{% trans "Interview" %}</option>
|
||||
<option value="Offer" {% if stage_filter == 'Offer' %}selected{% endif %}>{% trans "Offer" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label for="job_filter" class="form-label small text-muted">{% trans "Filter by Job" %}</label>
|
||||
<div class="d-flex gap-2">
|
||||
<select name="job" id="job_filter" class="form-select form-select-sm">
|
||||
<option value="">{% trans "All Jobs" %}</option>
|
||||
{% for job in available_jobs %}
|
||||
<option value="{{ job.slug }}" {% if job_filter == job.slug %}selected{% endif %}>{{ job.title }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<select name="stage" id="stage_filter" class="form-select form-select-sm">
|
||||
<option value="">{% trans "All Stages" %}</option>
|
||||
<option value="Applied" {% if stage_filter == 'Applied' %}selected{% endif %}>{% trans "Applied" %}</option>
|
||||
<option value="Exam" {% if stage_filter == 'Exam' %}selected{% endif %}>{% trans "Exam" %}</option>
|
||||
<option value="Interview" {% if stage_filter == 'Interview' %}selected{% endif %}>{% trans "Interview" %}</option>
|
||||
<option value="Offer" {% if stage_filter == 'Offer' %}selected{% endif %}>{% trans "Offer" %}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Buttons Group (pushed to the right/bottom) #}
|
||||
<div class="col-md-4 d-flex justify-content-end align-self-end">
|
||||
|
||||
959
templates/recruitment/candidate_resume_template.html
Normal file
959
templates/recruitment/candidate_resume_template.html
Normal file
@ -0,0 +1,959 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ candidate.resume_data.full_name|default:"Candidate" }} - Candidate Profile</title>
|
||||
<!-- Use a modern icon set -->
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
|
||||
<style>
|
||||
/* --- Color Variables --- */
|
||||
:root {
|
||||
--kaauh-teal: #00636e;
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-light-bg: #f9fbfd;
|
||||
--kaauh-border: #eaeff3;
|
||||
--score-green: #10b981;
|
||||
--score-yellow: #f59e0b;
|
||||
--score-red: #ef4444;
|
||||
--cert-gold: #d97706;
|
||||
|
||||
--color-white: #ffffff;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-200: #e5e7eb;
|
||||
--color-gray-500: #6b7280;
|
||||
--color-gray-600: #4b5563;
|
||||
--color-gray-700: #374151;
|
||||
--color-gray-800: #1f2937;
|
||||
--color-gray-900: #111827;
|
||||
}
|
||||
|
||||
/* --- General Layout and Typography --- */
|
||||
body {
|
||||
background-color: var(--kaauh-light-bg);
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1280px; /* max-w-7xl */
|
||||
margin: 0 auto;
|
||||
padding: 1rem; /* p-4 */
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem; /* gap-6 */
|
||||
}
|
||||
|
||||
/* Responsive Layout for large screens (lg:grid-cols-3) */
|
||||
@media (min-width: 1024px) {
|
||||
.content-grid {
|
||||
grid-template-columns: 2fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Card Styles --- */
|
||||
.card-section {
|
||||
background-color: var(--color-white);
|
||||
border-radius: 0.75rem; /* rounded-xl */
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1); /* shadow-md */
|
||||
padding: 1.5rem; /* p-6 */
|
||||
border: 1px solid var(--kaauh-border);
|
||||
margin-bottom: 1.5rem; /* space-y-6 */
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 1.5rem; /* text-2xl */
|
||||
font-weight: bold;
|
||||
color: var(--color-gray-800);
|
||||
margin-bottom: 1rem; /* mb-4 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.section-title i {
|
||||
color: var(--kaauh-teal);
|
||||
margin-right: 0.75rem; /* mr-3 */
|
||||
}
|
||||
|
||||
/* --- Header Section --- */
|
||||
.header-box {
|
||||
background: linear-gradient(145deg, var(--kaauh-teal), var(--kaauh-teal-dark));
|
||||
color: var(--color-white);
|
||||
padding: 2rem; /* p-8 */
|
||||
border-radius: 0.75rem; /* rounded-xl */
|
||||
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1); /* shadow-2xl */
|
||||
margin-bottom: 1.5rem; /* mb-6 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.header-box {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.header-info h1 {
|
||||
font-size: 2.25rem; /* text-4xl */
|
||||
font-weight: 800; /* font-extrabold */
|
||||
margin-bottom: 0.25rem; /* mb-1 */
|
||||
}
|
||||
|
||||
.header-info p {
|
||||
font-size: 1.25rem; /* text-xl */
|
||||
color: rgba(204, 251, 252, 0.9); /* text-teal-100 */
|
||||
margin-bottom: 1rem; /* mb-4 */
|
||||
}
|
||||
|
||||
.contact-details {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem; /* gap-6 */
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
}
|
||||
|
||||
.contact-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.contact-item i {
|
||||
margin-right: 0.5rem; /* mr-2 */
|
||||
}
|
||||
|
||||
.contact-item a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Match Score Box */
|
||||
.score-box {
|
||||
background-color: rgba(255, 255, 255, 0.2); /* bg-white bg-opacity-20 */
|
||||
border-radius: 0.75rem;
|
||||
padding: 1rem; /* p-4 */
|
||||
text-align: center;
|
||||
width: 8rem; /* w-32 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.06); /* shadow-inner */
|
||||
}
|
||||
|
||||
.score-value {
|
||||
font-size: 2.25rem; /* text-4xl */
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.score-text {
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
}
|
||||
|
||||
.assessment-rating {
|
||||
font-size: 0.75rem; /* text-xs */
|
||||
margin-top: 0.25rem; /* mt-1 */
|
||||
font-weight: 600; /* font-semibold */
|
||||
}
|
||||
|
||||
/* --- Dynamic Color Classes (Match Score) --- */
|
||||
.score-red { color: var(--score-red); }
|
||||
.score-yellow { color: var(--score-yellow); }
|
||||
.score-green { color: var(--score-green); }
|
||||
.text-green-check { color: var(--score-green); }
|
||||
.text-yellow-exclaim { color: var(--score-yellow); }
|
||||
.text-red-x { color: var(--score-red); }
|
||||
|
||||
|
||||
/* --- Summary Section --- */
|
||||
.summary-text {
|
||||
color: var(--color-gray-700);
|
||||
line-height: 1.625; /* leading-relaxed */
|
||||
border-left: 4px solid var(--kaauh-teal-dark);
|
||||
padding-left: 1rem; /* pl-4 */
|
||||
}
|
||||
|
||||
/* --- Experience Section --- */
|
||||
.experience-item {
|
||||
margin-bottom: 1.5rem; /* mb-6 */
|
||||
padding-bottom: 1.5rem; /* pb-6 */
|
||||
border-bottom: 1px solid var(--color-gray-100);
|
||||
}
|
||||
|
||||
.experience-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.experience-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.5rem; /* mb-2 */
|
||||
}
|
||||
|
||||
.experience-header h3 {
|
||||
font-size: 1.25rem; /* text-xl */
|
||||
font-weight: bold;
|
||||
color: var(--color-gray-800);
|
||||
}
|
||||
|
||||
.experience-header p {
|
||||
color: var(--kaauh-teal);
|
||||
font-weight: 600; /* font-semibold */
|
||||
}
|
||||
|
||||
.experience-tag {
|
||||
background-color: var(--kaauh-light-bg);
|
||||
color: var(--kaauh-teal);
|
||||
padding: 0.25rem 0.75rem; /* px-3 py-1 */
|
||||
border-radius: 9999px; /* rounded-full */
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
font-weight: 500; /* font-medium */
|
||||
border: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
.experience-meta {
|
||||
color: var(--color-gray-600);
|
||||
margin-bottom: 0.75rem; /* mb-3 */
|
||||
font-size: 0.875rem; /* text-sm */
|
||||
}
|
||||
|
||||
.experience-meta i {
|
||||
margin-right: 0.5rem; /* mr-2 */
|
||||
}
|
||||
|
||||
.experience-meta span {
|
||||
margin-left: 1rem; /* ml-4 */
|
||||
}
|
||||
|
||||
.achievement-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
color: var(--color-gray-700);
|
||||
}
|
||||
|
||||
.achievement-list li {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.achievement-list i {
|
||||
color: var(--kaauh-teal-dark);
|
||||
margin-top: 0.25rem; /* mt-1 */
|
||||
margin-right: 0.5rem; /* mr-2 */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* --- Education Section --- */
|
||||
.education-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid var(--color-gray-100);
|
||||
transition: background-color 0.15s ease-in-out; /* transition duration-150 */
|
||||
padding: 0.5rem; /* p-2 */
|
||||
margin: -0.5rem; /* -m-2 */
|
||||
border-radius: 0.5rem; /* rounded-lg */
|
||||
}
|
||||
|
||||
.education-item:hover {
|
||||
background-color: var(--kaauh-light-bg); /* hover:bg-kaauh-light-bg */
|
||||
}
|
||||
|
||||
.education-item:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.icon-badge {
|
||||
background-color: #e0f2f7; /* teal-100 */
|
||||
padding: 0.75rem; /* p-3 */
|
||||
border-radius: 9999px;
|
||||
margin-right: 1rem; /* mr-4 */
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-badge i {
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
.education-details {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.education-details h3 {
|
||||
font-size: 1.125rem; /* text-lg */
|
||||
font-weight: bold;
|
||||
color: var(--color-gray-800);
|
||||
}
|
||||
|
||||
.education-details p {
|
||||
color: var(--kaauh-teal);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.education-details .meta {
|
||||
color: var(--color-gray-600);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.education-details .meta i {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
/* --- Projects Section --- */
|
||||
.project-item {
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--color-gray-100);
|
||||
}
|
||||
|
||||
.project-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.project-item h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: bold;
|
||||
color: var(--color-gray-800);
|
||||
}
|
||||
|
||||
.project-item .description {
|
||||
color: var(--color-gray-600);
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.tag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem; /* gap-2 */
|
||||
}
|
||||
|
||||
.tag-item {
|
||||
background-color: #e0f2f7; /* bg-teal-100 */
|
||||
color: var(--kaauh-teal);
|
||||
padding: 0.25rem 0.75rem; /* px-3 py-1 */
|
||||
border-radius: 9999px;
|
||||
font-size: 0.75rem; /* text-xs */
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
/* --- Analysis Section --- */
|
||||
.analysis-summary {
|
||||
background-color: #fffbeb; /* bg-yellow-50 */
|
||||
border-left: 4px solid var(--score-yellow);
|
||||
padding: 1rem; /* p-4 */
|
||||
border-radius: 0 0.5rem 0.5rem 0; /* rounded-r-lg */
|
||||
}
|
||||
|
||||
.analysis-summary h3 {
|
||||
font-weight: 600;
|
||||
color: #b45309; /* text-yellow-800 */
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.analysis-summary p {
|
||||
color: #a16207; /* text-yellow-700 */
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.analysis-metric, .criteria-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-bottom: 0.25rem;
|
||||
border-bottom: 1px solid var(--color-gray-100);
|
||||
}
|
||||
|
||||
.analysis-metric:last-child, .criteria-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.metric-label {
|
||||
color: var(--color-gray-600);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metric-label i {
|
||||
margin-right: 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-weight: bold;
|
||||
color: var(--color-gray-900);
|
||||
}
|
||||
|
||||
.metric-title {
|
||||
font-weight: 500;
|
||||
color: var(--color-gray-700);
|
||||
}
|
||||
|
||||
/* Progress Bar */
|
||||
.progress-container {
|
||||
width: 100%;
|
||||
background-color: var(--color-gray-200);
|
||||
border-radius: 9999px;
|
||||
height: 0.75rem; /* h-3 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 0.75rem;
|
||||
border-radius: 9999px;
|
||||
transition: width 1s ease-in-out;
|
||||
}
|
||||
|
||||
/* Language fluency bar */
|
||||
.language-bar {
|
||||
height: 0.5rem;
|
||||
border-radius: 9999px;
|
||||
background-color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
/* --- Strength/Weakness/Flag Boxes --- */
|
||||
.narrative-box {
|
||||
padding-top: 1rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.red-flag-box {
|
||||
border-top: 1px solid #fee2e2; /* border-red-100 */
|
||||
}
|
||||
|
||||
.strength-box {
|
||||
border-top: 1px solid #d1fae5; /* border-green-100 */
|
||||
}
|
||||
|
||||
.flag-title {
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.flag-title i {
|
||||
margin-right: 0.5rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
.flag-title.red { color: #b91c1c; } /* text-red-700 */
|
||||
.flag-title.green { color: #065f46; } /* text-green-700 */
|
||||
|
||||
.flag-title.red i { color: #ef4444; } /* text-red-500 */
|
||||
|
||||
.narrative-text {
|
||||
color: var(--color-gray-700);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* --- Keywords Section --- */
|
||||
.keyword-tag {
|
||||
background-color: #e0f2f7; /* bg-teal-100 */
|
||||
color: var(--kaauh-teal-dark);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
border: 1px solid var(--kaauh-border);
|
||||
}
|
||||
|
||||
.cultural-tag {
|
||||
background-color: #f3e8ff; /* bg-purple-100 */
|
||||
color: #6b21a8; /* text-purple-800 */
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 9999px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.keyword-subheader {
|
||||
font-weight: 600;
|
||||
color: var(--color-gray-700);
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.keyword-subheader i {
|
||||
margin-right: 0.5rem;
|
||||
color: var(--kaauh-teal);
|
||||
}
|
||||
|
||||
/* Specific Fixes */
|
||||
.max-w-50-percent {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
/* Animation Re-implementation */
|
||||
@keyframes fade-in-grow {
|
||||
from { width: 0%; }
|
||||
}
|
||||
|
||||
.progress-bar-animated {
|
||||
animation: fade-in-grow 1s ease-in-out forwards;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-kaauh-light-bg font-sans">
|
||||
<div class="container">
|
||||
<!-- Header Section -->
|
||||
<header class="header-box">
|
||||
<div class="header-info">
|
||||
<h1>{{ candidate.resume_data.full_name|default:"Candidate Name" }}</h1>
|
||||
<p>{{ candidate.resume_data.current_title|default:"Professional Title" }}</p>
|
||||
<div class="contact-details">
|
||||
<div class="contact-item">
|
||||
<i class="fas fa-map-marker-alt"></i>
|
||||
<span>{{ candidate.resume_data.location|default:"Location" }}</span>
|
||||
</div>
|
||||
<!-- Displaying the raw contact string (which contains both phone and email in the example) -->
|
||||
<div class="contact-item">
|
||||
<i class="fas fa-id-card"></i>
|
||||
<span title="Contact Information: Phone and Email">{{ candidate.resume_data.contact|default:"Contact Information" }}</span>
|
||||
</div>
|
||||
<!-- 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>
|
||||
</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>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3" style="font-size: 0.9rem;">
|
||||
<span style="display: inline-flex; align-items: center;">
|
||||
<a href="{% url 'job_list' %}"
|
||||
style="color: white; text-decoration: none; padding-right: 8px;">
|
||||
JOBS
|
||||
</a>
|
||||
<span style="color: #6c757d; padding-right: 8px;">/</span>
|
||||
|
||||
<a href="{% url 'candidate_list' %}"
|
||||
style="color: white; text-decoration: none; padding-right: 8px;">
|
||||
CANDIDATES
|
||||
</a>
|
||||
<span style="color: #6c757d; padding-right: 8px;">/</span>
|
||||
|
||||
<a href="{% url 'candidate_detail' candidate.id %}"
|
||||
style="color: white; text-decoration: none; padding-right: 8px;">
|
||||
CANDIDATE
|
||||
</a>
|
||||
<span style="color: #6c757d; padding-right: 8px;">/</span>
|
||||
|
||||
<span style="color:gray; font-weight: 600;">
|
||||
RESUME Overview
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="score-box">
|
||||
<div class="score-value">{{ candidate.analysis_data.match_score|default:0 }}%</div>
|
||||
<div class="score-text">Match Score</div>
|
||||
<div class="assessment-rating
|
||||
{% if candidate.analysis_data.match_score|default:0 < 50 %}score-red{% elif candidate.analysis_data.match_score|default:0 < 75 %}score-yellow{% else %}score-green{% endif %}">
|
||||
<!-- scoring_data.screening_stage_rating -->
|
||||
{{ candidate.analysis_data.screening_stage_rating|default:"Assessment" }}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Main Content Grid -->
|
||||
<div class="content-grid">
|
||||
<!-- Left Column: Primary Content -->
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- Summary Section -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-user-circle"></i>
|
||||
Summary
|
||||
</h2>
|
||||
<p class="summary-text">
|
||||
<!-- candidate.resume_data.summary, falling back to scoring_data.job_fit_narrative -->
|
||||
{{ candidate.resume_data.summary|default:"Professional summary not available." }}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<!-- Experience Section -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title" style="margin-bottom: 1.5rem;">
|
||||
<i class="fas fa-briefcase"></i>
|
||||
Experience
|
||||
</h2>
|
||||
{% for experience in candidate.resume_data.experience %}
|
||||
<div class="experience-item">
|
||||
<div class="experience-header">
|
||||
<div>
|
||||
<h3>{{ experience.job_title }}</h3>
|
||||
<p>{{ experience.company }}</p>
|
||||
</div>
|
||||
<span class="experience-tag">
|
||||
{% if experience.end_date == "Present" %}Present{% else %}{{ experience.end_date|default:"Current" }}{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
<p class="experience-meta">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
{% if experience.start_date %}{{ experience.start_date }}{% endif %} -
|
||||
{% if experience.end_date and experience.end_date != "Present" %}{{ experience.end_date }}{% else %}Present{% endif %}
|
||||
<!-- candidate.resume_data.experience[].location -->
|
||||
{% if experience.location %}<span style="margin-left: 1rem;"><i class="fas fa-map-pin"></i>{{ experience.location }}</span>{% endif %}
|
||||
</p>
|
||||
{% if experience.key_achievements %}
|
||||
<ul class="achievement-list">
|
||||
{% for achievement in experience.key_achievements %}
|
||||
<li><i class="fas fa-caret-right"></i>{{ achievement }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<!-- Education Section -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title" style="margin-bottom: 1.5rem;">
|
||||
<i class="fas fa-graduation-cap"></i>
|
||||
Education
|
||||
</h2>
|
||||
{% for education in candidate.resume_data.education %}
|
||||
<div class="education-item">
|
||||
<div class="icon-badge">
|
||||
<i class="fas fa-certificate"></i>
|
||||
</div>
|
||||
<div class="education-details">
|
||||
<h3>{{ education.degree }}</h3>
|
||||
<p>{{ education.institution }}</p>
|
||||
{% if education.year %}
|
||||
<p class="meta"><i class="fas fa-calendar-alt"></i> {{ education.year }}</p>
|
||||
{% endif %}
|
||||
{% if education.gpa %}
|
||||
<p class="meta"><i class="fas fa-award"></i> GPA: {{ education.gpa }}</p>
|
||||
{% endif %}
|
||||
<!-- candidate.resume_data.education[].relevant_courses -->
|
||||
{% if education.relevant_courses %}
|
||||
<p class="meta" style="margin-top: 0.25rem;">Courses: {{ education.relevant_courses|join:", " }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
|
||||
<!-- Projects Section -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-project-diagram"></i>
|
||||
Projects
|
||||
</h2>
|
||||
{% for project in candidate.resume_data.projects %}
|
||||
<div class="project-item">
|
||||
<h3>{{ project.name }}</h3>
|
||||
<p class="description">{{ project.brief_description }}</p>
|
||||
{% if project.technologies_used %}
|
||||
<div class="tag-list">
|
||||
{% for tech in project.technologies_used %}
|
||||
<span class="tag-item">{{ tech }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not candidate.resume_data.projects %}
|
||||
<p style="color: var(--color-gray-500); font-size: 0.875rem;">No projects detailed in the resume.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- Keywords Section (NOW IN THE LEFT COLUMN) -->
|
||||
{% if candidate.analysis_data.top_3_keywords or candidate.analysis_data.cultural_fit_keywords %}
|
||||
<section class="card-section" style="margin-top: 0;">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-tags"></i>
|
||||
Keywords
|
||||
</h2>
|
||||
|
||||
{% if candidate.analysis_data.top_3_keywords %}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<h3 class="keyword-subheader"><i class="fas fa-key"></i>Top Keywords (Job Match)</h3>
|
||||
<div class="tag-list">
|
||||
<!-- scoring_data.top_3_keywords -->
|
||||
{% for keyword in candidate.analysis_data.top_3_keywords %}
|
||||
<span class="keyword-tag">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.cultural_fit_keywords %}
|
||||
<div>
|
||||
<h3 class="keyword-subheader"><i class="fas fa-users"></i>Cultural Fit Keywords</h3>
|
||||
<div class="tag-list">
|
||||
<!-- scoring_data.cultural_fit_keywords -->
|
||||
{% for keyword in candidate.analysis_data.cultural_fit_keywords %}
|
||||
<span class="cultural-tag">{{ keyword }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Skills & Analysis -->
|
||||
<div class="space-y-6">
|
||||
<!-- Analysis Section -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-chart-line"></i>
|
||||
Analysis
|
||||
</h2>
|
||||
|
||||
{% if candidate.analysis_data.category %}
|
||||
<div class="analysis-metric" style="margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--color-gray-100);">
|
||||
<span class="metric-title">Target Role Category:</span>
|
||||
<!-- scoring_data.category -->
|
||||
<span class="metric-value" style="color: var(--kaauh-teal);">{{ candidate.analysis_data.category }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<div class="analysis-metric" style="margin-bottom: 0.5rem; border-bottom: none;">
|
||||
<span class="metric-title">Match Score</span>
|
||||
<span class="metric-value">{{ candidate.analysis_data.match_score|default:0 }}/100</span>
|
||||
</div>
|
||||
<div class="progress-container">
|
||||
<div class="progress-bar progress-bar-animated"
|
||||
style="width: {{ candidate.analysis_data.match_score|default:0 }}%; background-color:
|
||||
{% if candidate.analysis_data.match_score|default:0 < 50 %}var(--score-red){% elif candidate.analysis_data.match_score|default:0 < 75 %}var(--score-yellow){% else %}var(--score-green){% endif %}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if candidate.analysis_data.red_flags %}
|
||||
<div class="narrative-box red-flag-box">
|
||||
<h3 class="flag-title red"><i class="fas fa-flag"></i>Red Flags</h3>
|
||||
<!-- scoring_data.red_flags -->
|
||||
<p class="narrative-text">{{ candidate.analysis_data.red_flags|join:". "|default:"None." }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.strengths %}
|
||||
<div class="narrative-box strength-box">
|
||||
<h3 class="flag-title green"><i class="fas fa-circle-check"></i>Strengths</h3>
|
||||
<!-- scoring_data.strengths -->
|
||||
<p class="narrative-text">{{ candidate.analysis_data.strengths }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.weaknesses %}
|
||||
<div class="narrative-box" style="margin-bottom: 1rem;">
|
||||
<h3 class="flag-title red"><i class="fas fa-triangle-exclamation"></i>Weaknesses</h3>
|
||||
<!-- scoring_data.weaknesses -->
|
||||
<p class="narrative-text">{{ candidate.analysis_data.weaknesses }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.recommendation %}
|
||||
<div class="analysis-summary">
|
||||
<h3 style="font-size: 0.875rem;">Recommendation</h3>
|
||||
<!-- scoring_data.recommendation -->
|
||||
<p style="font-size: 0.875rem;">{{ candidate.analysis_data.recommendation }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- Criteria Checklist Section -->
|
||||
{% if candidate.analysis_data.criteria_checklist %}
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-list-check"></i>
|
||||
Required Criteria Check
|
||||
</h2>
|
||||
<div style="margin-top: 0.75rem;">
|
||||
{% for criterion, status in candidate.analysis_data.criteria_checklist.items %}
|
||||
<div class="criteria-item">
|
||||
<span class="text-gray-700">{{ criterion }}</span>
|
||||
<span class="metric-value" style="font-size: 0.875rem;">
|
||||
{% if status == 'Met' %}<span class="text-green-check"><i class="fas fa-check-circle"></i> Met</span>
|
||||
{% elif status == 'Not Mentioned' %}<span class="text-yellow-exclaim"><i class="fas fa-exclamation-circle"></i> Not Mentioned</span>
|
||||
{% else %}<span class="text-red-x"><i class="fas fa-times-circle"></i> {{ status }}</span>
|
||||
{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- Skills Section -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-tools"></i>
|
||||
Skills
|
||||
</h2>
|
||||
{% if candidate.resume_data.skills %}
|
||||
{% for category, skills in candidate.resume_data.skills.items %}
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<!-- candidate.resume_data.skills -->
|
||||
<h3 class="keyword-subheader" style="color: var(--color-gray-700);"><i class="fas fa-list-alt" style="color: transparent;"></i>{{ category|cut:"_"|title }}</h3>
|
||||
<div class="tag-list">
|
||||
{% for skill in skills %}
|
||||
<span class="tag-item" style="color: var(--kaauh-teal-dark);">{{ skill }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="color: var(--color-gray-500); font-size: 0.875rem;">Skills information not available.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<!-- Languages Section -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-language"></i>
|
||||
Languages
|
||||
</h2>
|
||||
{% if candidate.analysis_data.language_fluency %}
|
||||
{% for language in candidate.analysis_data.language_fluency %}
|
||||
<div style="margin-bottom: 0.75rem;">
|
||||
<div class="analysis-metric" style="margin-bottom: 0.25rem; border-bottom: none;">
|
||||
<!-- scoring_data.language_fluency -->
|
||||
<span class="metric-title">{{ language }}</span>
|
||||
</div>
|
||||
<div class="progress-container" style="height: 0.5rem;">
|
||||
{% with fluency_check=language|lower %}
|
||||
<div class="language-bar"
|
||||
style="width: {% if 'native' in fluency_check %}100{% elif 'fluent' in fluency_check %}85{% elif 'intermediate' in fluency_check %}50{% elif 'basic' in fluency_check %}25{% else %}10{% endif %}%">
|
||||
</div>
|
||||
{% endwith %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p style="color: var(--color-gray-500); font-size: 0.875rem;">Language information not available.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
|
||||
<!-- Key Metrics -->
|
||||
<section class="card-section">
|
||||
<h2 class="section-title">
|
||||
<i class="fas fa-chart-pie"></i>
|
||||
Key Metrics
|
||||
</h2>
|
||||
<div style="margin-top: 0.75rem;">
|
||||
{% if candidate.analysis_data.min_req_met_bool is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-shield-halved"></i>Min Requirements Met:</span>
|
||||
<!-- scoring_data.min_req_met_bool -->
|
||||
<span class="metric-value {% if candidate.analysis_data.min_req_met_bool %}text-green-check{% else %}text-red-x{% endif %}">
|
||||
{% if candidate.analysis_data.min_req_met_bool %}<i class="fas fa-check-circle"></i> Yes{% else %}<i class="fas fa-times-circle"></i> No{% endif %}
|
||||
</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.years_of_experience is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-clock"></i>Total Experience:</span>
|
||||
<!-- scoring_data.years_of_experience -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.years_of_experience|floatformat:1 }} years</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.most_recent_job_title %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-id-badge"></i>Most Recent Title (Scoring):</span>
|
||||
<!-- scoring_data.most_recent_job_title (explicitly added) -->
|
||||
<span class="metric-value max-w-50-percent" style="text-align: right;">{{ candidate.analysis_data.most_recent_job_title }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.seniority_level_match is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-user-tie"></i>Seniority Match:</span>
|
||||
<!-- scoring_data.seniority_level_match -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.seniority_level_match|default:0 }}/100</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.soft_skills_score is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-handshake"></i>Soft Skills Score:</span>
|
||||
<!-- scoring_data.soft_skills_score -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.soft_skills_score|default:0 }}/100</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.employment_stability_score is not none %}
|
||||
<div class="analysis-metric">
|
||||
<span class="metric-label"><i class="fas fa-anchor"></i>Stability Score:</span>
|
||||
<!-- scoring_data.employment_stability_score -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.employment_stability_score|default:0 }}/100</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if candidate.analysis_data.experience_industry_match is not none %}
|
||||
<div class="analysis-metric" style="border-bottom: none; padding-bottom: 0;">
|
||||
<span class="metric-label"><i class="fas fa-industry"></i>Industry Match:</span>
|
||||
<!-- scoring_data.experience_industry_match -->
|
||||
<span class="metric-value">{{ candidate.analysis_data.experience_industry_match|default:0 }}/100</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if candidate.analysis_data.transferable_skills_narrative %}
|
||||
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-gray-100); font-size: 0.875rem; color: var(--color-gray-500);">
|
||||
<i class="fas fa-puzzle-piece" style="margin-right: 0.25rem;"></i> Transferable Skills:
|
||||
<!-- scoring_data.transferable_skills_narrative -->
|
||||
{{ candidate.analysis_data.transferable_skills_narrative }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Simple progress bar animation
|
||||
window.addEventListener('load', () => {
|
||||
const progressBars = document.querySelectorAll('.progress-bar');
|
||||
progressBars.forEach(bar => {
|
||||
// The width is already set in the style attribute via DTL
|
||||
const width = bar.style.width;
|
||||
// Temporarily set to 0 to prepare for animation
|
||||
bar.style.width = '0%';
|
||||
// Add class to trigger CSS animation defined in the style block
|
||||
bar.classList.add('progress-bar-animated');
|
||||
});
|
||||
|
||||
// Note: The progress-bar-animated class with the CSS keyframe will handle the width transition.
|
||||
// The original setTimeout logic is replaced by the CSS animation for smoother performance.
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -43,6 +43,36 @@
|
||||
placeholder="e.g., 75" style="width: 120px;">
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<label for="min_experience" class="form-label small text-muted mb-1">
|
||||
{% trans "Min Years Exp" %}
|
||||
</label>
|
||||
<input type="number" name="min_experience" id="min_experience" class="form-control form-control-sm"
|
||||
value="{{ min_experience }}" min="0" step="0.5" step="0.5"
|
||||
placeholder="e.g., 2" style="width: 120px;">
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<label for="screening_rating" class="form-label small text-muted mb-1">
|
||||
{% trans "Screening Rating" %}
|
||||
</label>
|
||||
<select name="screening_rating" id="screening_rating" class="form-select form-select-sm" style="width: 120px;">
|
||||
<option value="">{% trans "Any Rating" %}</option>
|
||||
<option value="Highly Qualified" {% if screening_rating == "Highly Qualified" %}selected{% endif %}>
|
||||
Highly Qualified
|
||||
</option>
|
||||
<option value="Qualified" {% if screening_rating == "Qualified" %}selected{% endif %}>
|
||||
Qualified
|
||||
</option>
|
||||
<option value="Partially Qualified" {% if screening_rating == "Partially Qualified" %}selected{% endif %}>
|
||||
Partially Qualified
|
||||
</option>
|
||||
<option value="Not Qualified" {% if screening_rating == "Not Qualified" %}selected{% endif %}>
|
||||
Not Qualified
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-auto">
|
||||
<label for="tier1_count" class="form-label small text-muted mb-1">
|
||||
{% trans "Top N Candidates" %}
|
||||
@ -279,4 +309,4 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@ -28,31 +28,31 @@
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
|
||||
.card-header {
|
||||
font-weight: 600;
|
||||
padding: 1.25rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
background-color: #f8f9fa;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Stats Grid Layout - Six columns for better detail display */
|
||||
.stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
/* Stat Card Specific Styling */
|
||||
.stat-value {
|
||||
font-size: 2.8rem;
|
||||
font-size: 2.8rem;
|
||||
text-align: center;
|
||||
color: var(--kaauh-teal-dark);
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-weight: 700;
|
||||
padding: 1rem 1rem 0.5rem;
|
||||
padding: 1rem 1rem 0.5rem;
|
||||
}
|
||||
|
||||
|
||||
.stat-caption {
|
||||
font-size: 0.9rem;
|
||||
text-align: center;
|
||||
@ -86,11 +86,11 @@
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
|
||||
<h1 class="mb-4" style="color: var(--kaauh-teal-dark); font-weight: 700;">{% trans "Recruitment Overview" %} 🚀</h1>
|
||||
|
||||
<h1 class="mb-4" style="color: var(--kaauh-teal-dark); font-weight: 700;">{% trans "Recruitment Intelligence" %} 🧠</h1>
|
||||
|
||||
<div class="stats">
|
||||
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-briefcase stat-icon"></i> {% trans "Total Jobs" %}</h3>
|
||||
@ -98,7 +98,7 @@
|
||||
<div class="stat-value">{{ total_jobs }}</div>
|
||||
<div class="stat-caption">{% trans "Active & Drafted Positions" %}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-users stat-icon"></i> {% trans "Total Candidates" %}</h3>
|
||||
@ -106,67 +106,90 @@
|
||||
<div class="stat-value">{{ total_candidates }}</div>
|
||||
<div class="stat-caption">{% trans "All Profiles in ATS" %}</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-chart-line stat-icon"></i> {% trans "Avg. Apps per Job" %}</h3>
|
||||
</div>
|
||||
<div class="stat-value">{{ average_applications|floatformat:1 }}</div>
|
||||
<div class="stat-caption">{% trans "Key Efficiency Metric" %}</div>
|
||||
<div class="stat-caption">{% trans "Efficiency Metric" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-arrow-alt-circle-up stat-icon" style="color: var(--color-success);"></i> {% trans "Active Listings" %}</h3>
|
||||
<h3><i class="fas fa-star stat-icon" style="color: var(--color-warning);"></i> {% trans "Avg. Match Score" %}</h3>
|
||||
</div>
|
||||
<div class="stat-value">22</div>
|
||||
<div class="stat-caption">{% trans "Jobs currently open for application" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-clock stat-icon" style="color: var(--color-info);"></i> {% trans "Time to Hire" %}</h3>
|
||||
</div>
|
||||
<div class="stat-value">35d</div>
|
||||
<div class="stat-caption">{% trans "Average days from apply to offer" %}</div>
|
||||
<div class="stat-value">{{ avg_match_score|floatformat:1 }}</div>
|
||||
<div class="stat-caption">{% trans "Average AI Score (0-100)" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-handshake stat-icon" style="color: var(--kaauh-teal-light);"></i> {% trans "Offer Acceptance" %}</h3>
|
||||
<h3><i class="fas fa-trophy stat-icon" style="color: var(--color-success);"></i> {% trans "High Potential" %}</h3>
|
||||
</div>
|
||||
<div class="stat-value">85%</div>
|
||||
<div class="stat-caption">{% trans "Successful offers vs. Total offers" %}</div>
|
||||
<div class="stat-value">{{ high_potential_count }}</div>
|
||||
<div class="stat-caption">{% trans "Candidates with Score ≥ 75 ({{ high_potential_ratio }}%)" %}</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-cogs stat-icon" style="color: var(--color-info);"></i> {% trans "Scored Profiles" %}</h3>
|
||||
</div>
|
||||
<div class="stat-value">{{ scored_ratio|floatformat:1 }}%</div>
|
||||
<div class="stat-caption">{% trans "Percent of profiles processed by AI" %}</div>
|
||||
</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 "Applications Volume by Job" %}
|
||||
{% trans "Top 5 Application Volume" %}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="chart-container">
|
||||
<canvas id="applicationsChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card shadow-lg">
|
||||
<div class="card-header">
|
||||
<h2 class="d-flex align-items-center mb-0">
|
||||
Select a job from the drop
|
||||
</h2>
|
||||
<form method="GET" action="{% url 'dashboard' %}">
|
||||
<select name="selected_job_id" onchange="this.form.submit()">
|
||||
<option value="">Show All</option>
|
||||
{% for job in jobs%}
|
||||
<option value="job" {% if request.GET.selected_job_id == selected_job_id %}selected{% endif %}>job</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
</div>
|
||||
<div style="width: 75%; margin: auto;">
|
||||
<canvas id="job-doughnut-chart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script>
|
||||
|
||||
// if the strings are visible to the user. 'Applications' is used for Chart.js here.
|
||||
|
||||
// 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, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: {{ job_titles|safe }},
|
||||
// Use the parsed and sliced data
|
||||
labels: jobTitles,
|
||||
datasets: [{
|
||||
|
||||
// For simplicity, we are leaving it as-is for now, assuming the context provides the translated labels.
|
||||
label: '{% trans "Applications" %}',
|
||||
data: {{ job_app_counts|safe }},
|
||||
label: '{% trans "Applications" %}',
|
||||
data: jobAppCounts,
|
||||
// Use the defined CSS variable for consistency
|
||||
backgroundColor: ' #00636e', // Green theme
|
||||
borderColor: ' #004a53',
|
||||
borderWidth: 1,
|
||||
@ -175,18 +198,30 @@
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
aspectRatio: 2.5, // Make the chart wider
|
||||
plugins: {
|
||||
legend: {
|
||||
labels: {
|
||||
color: '#333333'
|
||||
}
|
||||
display: false // Hide legend since there's only one dataset
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: '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'
|
||||
color: '#333333',
|
||||
precision: 0 // Ensure y-axis labels are integers
|
||||
},
|
||||
grid: {
|
||||
color: '#e0e0e0'
|
||||
@ -197,12 +232,60 @@
|
||||
color: '#333333'
|
||||
},
|
||||
grid: {
|
||||
color: '#e0e0e0'
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const applicant_stages=JSON.parse('{{applicant_stages|safe}}');
|
||||
const stage_counts=JSON.parse('{{stage_counts|safe}}');
|
||||
console.log(applicant_stages)
|
||||
console.log(stage_counts)
|
||||
|
||||
const job_data = {
|
||||
labels: applicant_stages,
|
||||
datasets: [{
|
||||
label: 'My Doughnut Dataset',
|
||||
data: stage_counts,
|
||||
backgroundColor: [ // Define colors for your slices
|
||||
'rgb(255, 99, 132)', // Red
|
||||
'rgb(54, 162, 235)', // Blue
|
||||
'rgb(255, 205, 86)', // Yellow
|
||||
'rgb(200,200,100)',
|
||||
'rgb(30,40,80)',
|
||||
],
|
||||
hoverOffset: 4
|
||||
}]
|
||||
};
|
||||
|
||||
const dnt_config = {
|
||||
// Set type to 'doughnut'
|
||||
type: 'doughnut',
|
||||
data: job_data,
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: 'top',
|
||||
},
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Django Data Doughnut Chart'
|
||||
}
|
||||
},
|
||||
// The 'cutout' option is what makes it a doughnut chart (defaults to '50%')
|
||||
// cutout: '50%',
|
||||
}
|
||||
};
|
||||
|
||||
// 4. Initialize and render the chart
|
||||
const ctx_dnt = document.getElementById('job-doughnut-chart').getContext('2d');
|
||||
new Chart(ctx_dnt, dnt_config);
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
@ -49,6 +49,7 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
jhashkdhkashkdhkash
|
||||
<div class="container mt-4">
|
||||
<div class="detail-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
|
||||
123
test_word_integration.py
Normal file
123
test_word_integration.py
Normal file
@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify Word document integration in recruitment/tasks.py
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
# Add the project directory to Python path
|
||||
sys.path.insert(0, '/home/ismail/projects/ats/kaauh_ats')
|
||||
|
||||
# Import the tasks module
|
||||
try:
|
||||
from recruitment.tasks import extract_text_from_document, extract_text_from_pdf, extract_text_from_word
|
||||
print("✓ Successfully imported text extraction functions")
|
||||
except ImportError as e:
|
||||
print(f"✗ Failed to import functions: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def test_pdf_extraction():
|
||||
"""Test PDF text extraction with a sample PDF"""
|
||||
print("\n--- Testing PDF Extraction ---")
|
||||
|
||||
# Create a temporary PDF file for testing
|
||||
with tempfile.NamedTemporaryFile(suffix='.pdf', delete=False) as tmp_pdf:
|
||||
try:
|
||||
# Create a simple PDF content (this would normally be a real PDF)
|
||||
tmp_pdf.write(b"%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n/Pages 2 0 R\n>>\nendobj\n")
|
||||
tmp_pdf_path = tmp_pdf.name
|
||||
|
||||
# Test the PDF extraction
|
||||
text = extract_text_from_pdf(tmp_pdf_path)
|
||||
print(f"✓ PDF extraction completed. Text length: {len(text)}")
|
||||
|
||||
# Clean up
|
||||
os.unlink(tmp_pdf_path)
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ PDF extraction failed: {e}")
|
||||
|
||||
def test_word_extraction():
|
||||
"""Test Word text extraction with a sample Word document"""
|
||||
print("\n--- Testing Word Extraction ---")
|
||||
|
||||
try:
|
||||
# Check if python-docx is available
|
||||
from recruitment.tasks import DOCX_AVAILABLE
|
||||
if not DOCX_AVAILABLE:
|
||||
print("⚠ python-docx not available. Skipping Word extraction test.")
|
||||
return
|
||||
|
||||
# Create a temporary Word file for testing
|
||||
with tempfile.NamedTemporaryFile(suffix='.docx', delete=False) as tmp_docx:
|
||||
try:
|
||||
# Create a simple Word document content
|
||||
tmp_docx.write(b'PK\x03\x04') # Basic DOCX header
|
||||
tmp_docx_path = tmp_docx.name
|
||||
|
||||
# Test the Word extraction
|
||||
text = extract_text_from_word(tmp_docx_path)
|
||||
print(f"✓ Word extraction completed. Text length: {len(text)}")
|
||||
|
||||
# Clean up
|
||||
os.unlink(tmp_docx_path)
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Word extraction failed: {e}")
|
||||
# Clean up on failure
|
||||
if os.path.exists(tmp_docx.name):
|
||||
os.unlink(tmp_docx.name)
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Word extraction setup failed: {e}")
|
||||
|
||||
def test_unified_document_parser():
|
||||
"""Test the unified document parser"""
|
||||
print("\n--- Testing Unified Document Parser ---")
|
||||
|
||||
# Test with non-existent file
|
||||
try:
|
||||
extract_text_from_document('/nonexistent/file.pdf')
|
||||
print("✗ Should have failed for non-existent file")
|
||||
except FileNotFoundError:
|
||||
print("✓ Correctly handled non-existent file")
|
||||
except Exception as e:
|
||||
print(f"✗ Unexpected error for non-existent file: {e}")
|
||||
|
||||
# Test with unsupported file type
|
||||
with tempfile.NamedTemporaryFile(suffix='.txt', delete=False) as tmp_txt:
|
||||
try:
|
||||
tmp_txt.write(b'This is a text file')
|
||||
tmp_txt_path = tmp_txt.name
|
||||
|
||||
try:
|
||||
extract_text_from_document(tmp_txt_path)
|
||||
print("✗ Should have failed for unsupported file type")
|
||||
except ValueError as e:
|
||||
print(f"✓ Correctly handled unsupported file type: {e}")
|
||||
except Exception as e:
|
||||
print(f"✗ Unexpected error for unsupported file type: {e}")
|
||||
|
||||
# Clean up
|
||||
os.unlink(tmp_txt_path)
|
||||
|
||||
except Exception as e:
|
||||
print(f"✗ Test setup failed: {e}")
|
||||
|
||||
def main():
|
||||
"""Run all tests"""
|
||||
print("Starting Word Document Integration Tests...")
|
||||
|
||||
test_pdf_extraction()
|
||||
test_word_extraction()
|
||||
test_unified_document_parser()
|
||||
|
||||
print("\n--- Test Summary ---")
|
||||
print("Integration tests completed. Check the output above for any errors.")
|
||||
print("\nNote: For full Word document processing, ensure python-docx is installed:")
|
||||
print("pip install python-docx")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
1
txt
Normal file
1
txt
Normal file
@ -0,0 +1 @@
|
||||
Please Indicate the area that you are interested in the CV (ICU, OR, Medical Ward, Surgical Ward, Women`s Health). Bachelor’s degree in Nursing with minimum GPA (4 out of 5). SCFHS Nurse license classified as (Specialist). Country Specialist Nurse license. BLS Certified (ACLS, PALS, NALS dependent on unit requirement). Home Country equivalent Board Certifications must be maintained when appropriate. 2 year or more experience. Experience in same area. Key Accountabilities & Responsibilities Clinical: Is accountable for utilizing the nursing process, appropriate tools, evidence based knowledge, empathy and compassion in the assessment, planning, implementation and evaluation of nursing care for individual patients, while considering the individuals holistic needs and in accordance with scope of practice, hospital policy and procedure. Demonstrates the knowledge and skills, including critical thinking, necessary to implement the nursing plan of care, nursing interventions and procedures, as necessary for the care of the individual patient taking into consideration the patient’s condition, culture and medical plan of care. Advocates for patient by guiding best practice and standards of care within multidisciplinary team. Reports any deterioration in patient’s condition to physician, escalating via chain of command as necessary. Works towards reducing length of stay by advocating commencing discharge planning on admission. Is an advocate for patient safety, minimizes patient risk by ensuring a safe environment and conditions. Follows all policies and procedures, reporting observed or perceived risks in a timely fashion Suggests opportunities for improvement in care and in care environment. Uses transcultural awareness during all interactions with patients, families and colleagues. Works effectively with multidisciplinary team towards safe, effective and efficient patient outcomes. Maintains patient confidentiality according to policy. Ensures appropriate delegation of any duties when necessary. Takes appropriate action in emergencies. Documents all care accurately, completely and in a timely fashion. Demonstrates the computer skills necessary for carrying out duties. Education: Educates and informs patients and families on the care and management of condition, to adapt a problem-solving approach to managing and reporting complications. Provides health promotion and disease prevention advice. Participates in own and others education, training and professional development as needed. Shares appropriate, relevant and up to date information and knowledge with colleagues. Leadership: Utilizes leadership skills as nurse in charge of shift. Communicates effectively to ensure patient safety. Demonstrates knowledge of and follows all hospital related policies and procedures Reports potential or observed safety hazards immediately to supervisor, removes hazard or provides solution when possible. Reports any work-related risks such as equipment failures or resource insufficiencies to supervisor immediately. Uses communication and conflict negotiation skills to prevent and resolve complaints, reports through the chain of command as per hospital policy and procedure. Contributes to improving quality outcomes including patient and staff satisfaction indicators. Utilizes multidisciplinary team to ensure optimal patient experience. Exercises care in utilizing resources to maintain a cost efficient service. Actively participates in unit based meetings and councils. Supports a just culture, speaks up and promotes civility in the workplace. Research: Identifies opportunities for improvement that are evidence based. Maintains and shares up to date knowledge related to evidence based practice. Is involved in relevant projects related to practice, improvement and safety. Is actively involved in unit journal club. Hospital-wide: Respects patients and their families and promotes a patient-centered care culture. Participates and supports quality improvement and patient safety activities as an individual or as part of multidisciplinary teams. Performs other job-related duties as assigned.
|
||||
Loading…
x
Reference in New Issue
Block a user