Compare commits

..

No commits in common. "bfd2ad935a6a36ec8f06c776c6b3ab5edc1ff391" and "6aa9ebd2794d227cbc39801a71bc607e7973ccde" have entirely different histories.

32 changed files with 983 additions and 2103 deletions

View File

@ -401,11 +401,3 @@ CKEDITOR_5_CONFIGS = {
# Define a constant in settings.py to specify file upload permissions
CKEDITOR_5_FILE_UPLOAD_PERMISSION = "staff" # Possible values: "staff", "authenticated", "any"
from django.contrib.messages import constants as messages
MESSAGE_TAGS = {
messages.ERROR: 'danger',
}

View File

@ -224,208 +224,138 @@ def send_interview_invitation_email(candidate, job, meeting_details=None, recipi
logger.error(error_msg, exc_info=True)
return {'success': False, 'error': error_msg}
from .models import Candidate
from django.shortcuts import get_object_or_404
# Assuming other necessary imports like logger, settings, EmailMultiAlternatives, strip_tags are present
from .models import Candidate
from django.shortcuts import get_object_or_404
import logging
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.utils.html import strip_tags
from django_q.tasks import async_task # Import needed at the top for clarity
logger = logging.getLogger(__name__)
def send_bulk_email(subject, message, recipient_list, request=None, attachments=None, async_task_=False, from_interview=False):
def send_bulk_email(subject, message, recipient_list, request=None, attachments=None, async_task_=False):
"""
Send bulk email to multiple recipients with HTML support and attachments,
supporting synchronous or asynchronous dispatch.
Send bulk email to multiple recipients with HTML support and attachments.
Args:
subject: Email subject
message: Email message (can be HTML)
recipient_list: List of email addresses
request: Django request object (optional)
attachments: List of file attachments (optional)
async_task: Whether to run as background task (default: False)
Returns:
dict: Result with success status and error message if failed
"""
# --- 1. Categorization and Custom Message Preparation (CORRECTED) ---
if not from_interview:
agency_emails = []
pure_candidate_emails = []
candidate_through_agency_emails = []
# Handle async task execution
if async_task_:
print("hereeeeeee")
from django_q.tasks import async_task
# Process attachments for background task serialization
# processed_attachments = []
# if attachments:
# for attachment in attachments:
# if hasattr(attachment, 'read'):
# # File-like object - save to temporary file
# filename = getattr(attachment, 'name', 'attachment')
# content_type = getattr(attachment, 'content_type', 'application/octet-stream')
# # Create temporary file
# with tempfile.NamedTemporaryFile(delete=False, suffix=f'_{filename}') as temp_file:
# content = attachment.read()
# temp_file.write(content)
# temp_file_path = temp_file.name
# # Store file info for background task
# processed_attachments.append({
# 'file_path': temp_file_path,
# 'filename': filename,
# 'content_type': content_type
# })
# elif isinstance(attachment, tuple) and len(attachment) == 3:
# # (filename, content, content_type) tuple - can be serialized directly
# processed_attachments.append(attachment)
# Queue the email sending as a background task
task_id = async_task(
'recruitment.tasks.send_bulk_email_task',
subject,
message,
recipient_list,
request,
)
logger.info(f"Bulk email queued as background task with ID: {task_id}")
return {
'success': True,
'async': True,
'task_id': task_id,
'message': f'Email queued for background sending to {len(recipient_list)} recipient(s)'
}
# Synchronous execution (default behavior)
try:
if not recipient_list:
return {'success': False, 'error': 'No recipients provided'}
# This must contain (final_recipient_email, customized_message) for ALL sends
customized_sends = []
# 1a. Classify Recipients and Prepare Custom Messages
for email in recipient_list:
email = email.strip().lower()
try:
candidate = get_object_or_404(Candidate, email=email)
except Exception:
logger.warning(f"Candidate not found for email: {email}")
continue
candidate_name = candidate.first_name
# --- Candidate belongs to an agency (Final Recipient: Agency) ---
if candidate.belong_to_an_agency and candidate.hiring_agency and candidate.hiring_agency.email:
agency_email = candidate.hiring_agency.email
agency_message = f"Hi, {candidate_name}" + "\n" + message
# Add Agency email as the recipient with the custom message
customized_sends.append((agency_email, agency_message))
agency_emails.append(agency_email)
candidate_through_agency_emails.append(candidate.email) # For sync block only
# --- Pure Candidate (Final Recipient: Candidate) ---
else:
candidate_message = f"Hi, {candidate_name}" + "\n" + message
# Add Candidate email as the recipient with the custom message
customized_sends.append((email, candidate_message))
pure_candidate_emails.append(email) # For sync block only
# Calculate total recipients based on the size of the final send list
total_recipients = len(customized_sends)
# Clean recipient list and remove duplicates
clean_recipients = []
seen_emails = set()
if total_recipients == 0:
return {'success': False, 'error': 'No valid recipients found for sending.'}
else:
# For interview flow
total_recipients = len(recipient_list)
# --- 2. Handle ASYNC Dispatch (FIXED: Single loop used) ---
if async_task_:
try:
processed_attachments = attachments if attachments else []
task_ids = []
if not from_interview:
# Loop through ALL final customized sends
for recipient_email, custom_message in customized_sends:
task_id = async_task(
'recruitment.tasks.send_bulk_email_task',
subject,
custom_message, # Pass the custom message
[recipient_email], # Pass the specific recipient as a list of one
processed_attachments,
hook='recruitment.tasks.email_success_hook'
)
task_ids.append(task_id)
logger.info(f"{len(task_ids)} tasks ({total_recipients} emails) queued.")
for recipient in recipient_list:
email = recipient.strip().lower()
if email and email not in seen_emails:
clean_recipients.append(email)
seen_emails.add(email)
return {
'success': True,
'async': True,
'task_ids': task_ids,
'message': f'Emails queued for background sending to {len(task_ids)} recipient(s).'
}
else: # from_interview is True (generic send to all participants)
task_id = async_task(
'recruitment.tasks.send_bulk_email_task',
subject,
message,
recipient_list, # Send the original message to the entire list
processed_attachments,
hook='recruitment.tasks.email_success_hook'
)
task_ids.append(task_id)
logger.info(f"Interview emails queued. ID: {task_id}")
return {
'success': True,
'async': True,
'task_ids': task_ids,
'message': f'Interview emails queued for background sending to {total_recipients} recipient(s)'
}
if not clean_recipients:
return {'success': False, 'error': 'No valid email addresses found'}
except ImportError:
logger.error("Async execution requested, but django_q or required modules not found. Defaulting to sync.")
async_task_ = False
except Exception as e:
logger.error(f"Failed to queue async tasks: {str(e)}", exc_info=True)
return {'success': False, 'error': f"Failed to queue async tasks: {str(e)}"}
# --- 3. Handle SYNCHRONOUS Send (No changes needed here, as it was fixed previously) ---
try:
# NOTE: The synchronous block below should also use the 'customized_sends'
# list for consistency instead of rebuilding messages from 'pure_candidate_emails'
# and 'agency_emails', but keeping your current logic structure to minimize changes.
# Prepare email content
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa')
# Check if message contains HTML tags
is_html = '<' in message and '>' in message
successful_sends = 0
# Helper Function for Sync Send (as provided)
def send_individual_email(recipient, body_message):
# ... (Existing helper function logic) ...
nonlocal successful_sends
if is_html:
plain_message = strip_tags(body_message)
email_obj = EmailMultiAlternatives(subject=subject, body=plain_message, from_email=from_email, to=[recipient])
email_obj.attach_alternative(body_message, "text/html")
else:
email_obj = EmailMultiAlternatives(subject=subject, body=body_message, from_email=from_email, to=[recipient])
if attachments:
for attachment in attachments:
if hasattr(attachment, 'read'):
filename = getattr(attachment, 'name', 'attachment')
content = attachment.read()
content_type = getattr(attachment, 'content_type', 'application/octet-stream')
email_obj.attach(filename, content, content_type)
elif isinstance(attachment, tuple) and len(attachment) == 3:
filename, content, content_type = attachment
email_obj.attach(filename, content, content_type)
try:
email_obj.send(fail_silently=False)
successful_sends += 1
except Exception as e:
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
if is_html:
# Create HTML email with plain text fallback
plain_message = strip_tags(message)
if not from_interview:
# Send Emails - Pure Candidates
for email in pure_candidate_emails:
candidate_name = Candidate.objects.filter(email=email).first().first_name
candidate_message = f"Hi, {candidate_name}" + "\n" + message
send_individual_email(email, candidate_message)
# Send Emails - Agencies
i = 0
for email in agency_emails:
candidate_email = candidate_through_agency_emails[i]
candidate_name = Candidate.objects.filter(email=candidate_email).first().first_name
agency_message = f"Hi, {candidate_name}" + "\n" + message
send_individual_email(email, agency_message)
i += 1
logger.info(f"Bulk email processing complete. Sent successfully to {successful_sends} out of {total_recipients} unique recipients.")
return {
'success': True,
'recipients_count': successful_sends,
'message': f'Email processing complete. {successful_sends} email(s) were sent successfully to {total_recipients} unique intended recipients.'
}
# Create email with both HTML and plain text versions
email = EmailMultiAlternatives(
subject=subject,
body=plain_message,
from_email=from_email,
to=clean_recipients,
)
email.attach_alternative(message, "text/html")
else:
for email in recipient_list:
send_individual_email(email, message)
logger.info(f"Interview email processing complete. Sent successfully to {successful_sends} out of {total_recipients} recipients.")
return {
'success': True,
'recipients_count': successful_sends,
'message': f'Interview emails sent successfully to {successful_sends} recipient(s).'
}
# Plain text email
email = EmailMultiAlternatives(
subject=subject,
body=message,
from_email=from_email,
to=clean_recipients,
)
# Add attachments if provided
# if attachments:
# for attachment in attachments:
# if hasattr(attachment, 'read'):
# # File-like object
# filename = getattr(attachment, 'name', 'attachment')
# content = attachment.read()
# content_type = getattr(attachment, 'content_type', 'application/octet-stream')
# email.attach(filename, content, content_type)
# elif isinstance(attachment, tuple) and len(attachment) == 3:
# # (filename, content, content_type) tuple
# filename, content, content_type = attachment
# email.attach(filename, content, content_type)
# Send email
email.send(fail_silently=False)
logger.info(f"Bulk email sent successfully to {len(clean_recipients)} recipients")
return {
'success': True,
'recipients_count': len(clean_recipients),
'message': f'Email sent successfully to {len(clean_recipients)} recipient(s)'
}
except Exception as e:
error_msg = f"Failed to process bulk email send request: {str(e)}"
error_msg = f"Failed to send bulk email: {str(e)}"
logger.error(error_msg, exc_info=True)
return {'success': False, 'error': error_msg}
return {'success': False, 'error': error_msg}

View File

@ -1243,24 +1243,24 @@ class ParticipantsForm(forms.ModelForm):
}
# class ParticipantsSelectForm(forms.ModelForm):
# """Form for selecting Participants"""
class ParticipantsSelectForm(forms.ModelForm):
"""Form for selecting Participants"""
# participants=forms.ModelMultipleChoiceField(
# queryset=Participants.objects.all(),
# widget=forms.CheckboxSelectMultiple,
# required=False,
# label=_("Select Participants"))
participants=forms.ModelMultipleChoiceField(
queryset=Participants.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False,
label=_("Select Participants"))
# users=forms.ModelMultipleChoiceField(
# queryset=User.objects.all(),
# widget=forms.CheckboxSelectMultiple,
# required=False,
# label=_("Select Users"))
users=forms.ModelMultipleChoiceField(
queryset=User.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False,
label=_("Select Users"))
# class Meta:
# model = JobPosting
# fields = ['participants','users'] # No direct fields from Participants model
class Meta:
model = JobPosting
fields = ['participants','users'] # No direct fields from Participants model
class CandidateEmailForm(forms.Form):
@ -1270,9 +1270,16 @@ class CandidateEmailForm(forms.Form):
'class': 'form-check'
}),
label=_('Select Candidates'), # Use a descriptive label
required=False
required=True
)
# to = forms.MultipleChoiceField(
# widget=forms.CheckboxSelectMultiple(attrs={
# 'class': 'form-check'
# }),
# label=_('candidates'),
# required=True
# )
subject = forms.CharField(
max_length=200,
@ -1296,7 +1303,31 @@ class CandidateEmailForm(forms.Form):
required=True
)
recipients = forms.MultipleChoiceField(
widget=forms.CheckboxSelectMultiple(attrs={
'class': 'form-check'
}),
label=_('Recipients'),
required=True
)
# include_candidate_info = forms.BooleanField(
# widget=forms.CheckboxInput(attrs={
# 'class': 'form-check-input'
# }),
# label=_('Include candidate information'),
# initial=True,
# required=False
# )
# include_meeting_details = forms.BooleanField(
# widget=forms.CheckboxInput(attrs={
# 'class': 'form-check-input'
# }),
# label=_('Include meeting details'),
# initial=True,
# required=False
# )
def __init__(self, job, candidates, *args, **kwargs):
@ -1304,6 +1335,24 @@ class CandidateEmailForm(forms.Form):
self.job = job
self.candidates=candidates
# Get all participants and users for this job
recipient_choices = []
# Add job participants
for participant in job.participants.all():
recipient_choices.append(
(f'participant_{participant.id}', f'{participant.name} - {participant.designation} (Participant)')
)
# Add job users
for user in job.users.all():
recipient_choices.append(
(f'user_{user.id}', f'{user.get_full_name() or user.username} - {user.email} (User)')
)
self.fields['recipients'].choices = recipient_choices
self.fields['recipients'].initial = [choice[0] for choice in recipient_choices] # Select all by default
candidate_choices=[]
for candidate in candidates:
candidate_choices.append(
@ -1314,70 +1363,17 @@ class CandidateEmailForm(forms.Form):
self.fields['to'].choices =candidate_choices
self.fields['to'].initial = [choice[0] for choice in candidate_choices]
# # Set initial subject
# self.fields['subject'].initial = f'Interview Update: {candidate.name} - {job.title}'
# Set initial message with candidate and meeting info
initial_message = self._get_initial_message()
if initial_message:
self.fields['message'].initial = initial_message
def _get_initial_message(self):
"""Generate initial message with candidate and meeting information"""
candidate=self.candidates.first()
message_parts=[]
if candidate.stage == 'Applied':
message_parts = [
f"Than you, for your interest in the {self.job.title} role.",
f"We regret to inform you that you were not selected to move forward to the exam round at this time.",
f"We encourage you to check our career page for further updates and future opportunities:",
f"https://kaauh/careers",
f"Wishing you the best in your job search,",
f"The KAAUH Hiring team"
]
elif candidate.stage == 'Exam':
message_parts = [
f"Than you,for your interest in the {self.job.title} role.",
f"We're pleased to inform you that your initial screening was successful!",
f"The next step is the mandatory online assessment exam.",
f"Please complete the assessment by using the following link:",
f"https://kaauh/hire/exam",
f"We look forward to reviewing your results.",
f"Best regards, The KAAUH Hiring team"
]
elif candidate.stage == 'Interview':
message_parts = [
f"Than you, for your interest in the {self.job.title} role.",
f"We're pleased to inform you that your initial screening was successful!",
f"The next step is the mandatory online assessment exam.",
f"Please complete the assessment by using the following link:",
f"https://kaauh/hire/exam",
f"We look forward to reviewing your results.",
f"Best regards, The KAAUH Hiring team"
]
elif candidate.stage == 'Offer':
message_parts = [
f"Congratulations, ! We are delighted to inform you that we are extending a formal offer of employment for the {self.job.title} role.",
f"This is an exciting moment, and we look forward to having you join the KAAUH team.",
f"A detailed offer letter and compensation package will be sent to you via email within 24 hours.",
f"In the meantime, please contact our HR department at [HR Contact] if you have immediate questions.",
f"Welcome to the team!",
f"Best regards, The KAAUH Hiring team"
]
elif candidate.stage == 'Hired':
message_parts = [
f"Welcome aboard,!",
f"We are thrilled to officially confirm your employment as our new {self.job.title}.",
f"You will receive a separate email shortly with details regarding your start date, first-day instructions, and onboarding documents.",
f"We look forward to seeing you at KAAUH.",
f"If you have any questions before your start date, please contact [Onboarding Contact].",
f"Best regards, The KAAUH Hiring team"
]
message_parts = ['hiiiiiiii']
# # Add candidate information
# if self.candidate:
@ -1398,207 +1394,78 @@ class CandidateEmailForm(forms.Form):
return '\n'.join(message_parts)
def clean_recipients(self):
"""Ensure at least one recipient is selected"""
recipients = self.cleaned_data.get('recipients')
if not recipients:
raise forms.ValidationError(_('Please select at least one recipient.'))
return recipients
# def clean_to(self):
# """Ensure at least one recipient is selected"""
# candidates = self.cleaned_data.get('to')
# print(candidates)
# if not candidates:
# raise forms.ValidationError(_('Please select at least one candidate.'))
# return candidates
def get_email_addresses(self):
"""Extract email addresses from selected recipients"""
email_addresses = []
recipients = self.cleaned_data.get('recipients', [])
candidates=self.cleaned_data.get('to',[])
if candidates:
for candidate in candidates:
if candidate.startswith('candidate_'):
print("candidadte: {candidate}")
candidate_id = candidate.split('_')[1]
try:
candidate = Candidate.objects.get(id=candidate_id)
email_addresses.append(candidate.email)
except Candidate.DoesNotExist:
continue
for recipient in recipients:
if recipient.startswith('participant_'):
participant_id = recipient.split('_')[1]
try:
participant = Participants.objects.get(id=participant_id)
email_addresses.append(participant.email)
except Participants.DoesNotExist:
continue
elif recipient.startswith('user_'):
user_id = recipient.split('_')[1]
try:
user = User.objects.get(id=user_id)
email_addresses.append(user.email)
except User.DoesNotExist:
continue
for candidate in candidates:
if candidate.startswith('candidate_'):
print("candidadte: {candidate}")
candidate_id = candidate.split('_')[1]
try:
candidate = Candidate.objects.get(id=candidate_id)
email_addresses.append(candidate.email)
except Candidate.DoesNotExist:
continue
return list(set(email_addresses)) # Remove duplicates
def get_formatted_message(self):
"""Get the formatted message with optional additional information"""
message = self.cleaned_data.get('message', '')
message = self.cleaned_data.get('message', 'mesaage from system user hiii')
# # Add candidate information if requested
# if self.cleaned_data.get('include_candidate_info') and self.candidate:
# candidate_info = f"\n\n--- Candidate Information ---\n"
# candidate_info += f"Name: {self.candidate.name}\n"
# candidate_info += f"Email: {self.candidate.email}\n"
# candidate_info += f"Phone: {self.candidate.phone}\n"
# message += candidate_info
# # Add meeting details if requested
# if self.cleaned_data.get('include_meeting_details') and self.candidate:
# latest_meeting = self.candidate.get_latest_meeting
# if latest_meeting:
# meeting_info = f"\n\n--- Meeting Details ---\n"
# meeting_info += f"Topic: {latest_meeting.topic}\n"
# meeting_info += f"Date & Time: {latest_meeting.start_time.strftime('%B %d, %Y at %I:%M %p')}\n"
# meeting_info += f"Duration: {latest_meeting.duration} minutes\n"
# if latest_meeting.join_url:
# meeting_info += f"Join URL: {latest_meeting.join_url}\n"
# message += meeting_info
return message
class InterviewParticpantsForm(forms.ModelForm):
participants = forms.ModelMultipleChoiceField(
queryset=Participants.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False ,
)
system_users=forms.ModelMultipleChoiceField(
queryset=User.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False,
label=_("Select Users"))
class Meta:
model = InterviewSchedule
fields = ['participants','system_users']
class InterviewEmailForm(forms.Form):
subject = forms.CharField(
max_length=200,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Enter email subject',
'required': True
}),
label=_('Subject'),
required=True
)
message_for_candidate= forms.CharField(
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 8,
'placeholder': 'Enter your message here...',
'required': True
}),
label=_('Message'),
required=False
)
message_for_agency= forms.CharField(
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 8,
'placeholder': 'Enter your message here...',
'required': True
}),
label=_('Message'),
required=False
)
message_for_participants= forms.CharField(
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 8,
'placeholder': 'Enter your message here...',
'required': True
}),
label=_('Message'),
required=False
)
def __init__(self, *args,candidate, external_participants, system_participants,meeting,job,**kwargs):
super().__init__(*args, **kwargs)
# --- Data Preparation ---
# Note: Added error handling for agency name if it's missing (though it shouldn't be based on your check)
formatted_date = meeting.start_time.strftime('%Y-%m-%d')
formatted_time = meeting.start_time.strftime('%I:%M %p')
zoom_link = meeting.join_url
duration = meeting.duration
job_title = job.title
agency_name = candidate.hiring_agency.name if candidate.belong_to_an_agency and candidate.hiring_agency else "Hiring Agency"
# --- Combined Participants List for Internal Email ---
external_participants_names = ", ".join([p.name for p in external_participants ])
system_participants_names = ", ".join([p.first_name for p in system_participants ])
# Combine and ensure no leading/trailing commas if one list is empty
participant_names = ", ".join(filter(None, [external_participants_names, system_participants_names]))
# --- 1. Candidate Message (More concise and structured) ---
candidate_message = f"""
Dear {candidate.full_name},
Thank you for your interest in the **{job_title}** position at KAAUH. We're pleased to invite you to an interview!
The details of your virtual interview are as follows:
- **Date:** {formatted_date}
- **Time:** {formatted_time} (RIYADH TIME)
- **Duration:** {duration}
- **Meeting Link:** {zoom_link}
Please click the link at the scheduled time to join the interview.
Kindly reply to this email to **confirm your attendance** or to propose an alternative time if necessary.
We look forward to meeting you.
Best regards,
KAAUH Hiring Team
"""
# --- 2. Agency Message (Professional and clear details) ---
agency_message = f"""
Dear {agency_name},
We have scheduled an interview for your candidate, **{candidate.full_name}**, for the **{job_title}** role.
Please forward the following details to the candidate and ensure they are fully prepared.
**Interview Details:**
- **Candidate:** {candidate.full_name}
- **Job Title:** {job_title}
- **Date:** {formatted_date}
- **Time:** {formatted_time} (RIYADH TIME)
- **Duration:** {duration}
- **Meeting Link:** {zoom_link}
Please let us know if you or the candidate have any questions.
Best regards,
KAAUH Hiring Team
"""
# --- 3. Participants Message (Action-oriented and informative) ---
participants_message = f"""
Hi Team,
This is a reminder of the upcoming interview you are scheduled to participate in for the **{job_title}** position.
**Interview Summary:**
- **Candidate:** {candidate.full_name}
- **Date:** {formatted_date}
- **Time:** {formatted_time} (RIYADH TIME)
- **Duration:** {duration}
- **Your Fellow Interviewers:** {participant_names}
**Action Items:**
1. Please review **{candidate.full_name}'s** resume and notes.
2. The official calendar invite contains the meeting link ({zoom_link}) and should be used to join.
3. Be ready to start promptly at the scheduled time.
Thank you for your participation.
Best regards,
KAAUH HIRING TEAM
"""
# Set initial data
self.initial['subject'] = f"Interview Invitation: {job_title} at KAAUH - {candidate.full_name}"
# .strip() removes the leading/trailing blank lines caused by the f""" format
self.initial['message_for_candidate'] = candidate_message.strip()
self.initial['message_for_agency'] = agency_message.strip()
self.initial['message_for_participants'] = participants_message.strip()

View File

@ -1,4 +1,4 @@
# Generated by Django 5.2.7 on 2025-11-05 13:05
# Generated by Django 5.2.6 on 2025-10-30 10:22
import django.core.validators
import django.db.models.deletion

View File

@ -1,18 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-06 15:19
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='scheduledinterview',
name='participants',
field=models.ManyToManyField(blank=True, to='recruitment.participants'),
),
]

View File

@ -1,20 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-06 15:37
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0002_scheduledinterview_participants'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='scheduledinterview',
name='system_users',
field=models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL),
),
]

View File

@ -1,21 +0,0 @@
# Generated by Django 5.2.7 on 2025-11-06 15:44
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0003_scheduledinterview_system_users'),
]
operations = [
migrations.RemoveField(
model_name='jobposting',
name='participants',
),
migrations.RemoveField(
model_name='jobposting',
name='users',
),
]

View File

@ -54,18 +54,18 @@ class JobPosting(Base):
("HYBRID", "Hybrid"),
]
# users=models.ManyToManyField(
# User,
# blank=True,related_name="jobs_assigned",
# verbose_name=_("Internal Participant"),
# help_text=_("Internal staff involved in the recruitment process for this job"),
# )
users=models.ManyToManyField(
User,
blank=True,related_name="jobs_assigned",
verbose_name=_("Internal Participant"),
help_text=_("Internal staff involved in the recruitment process for this job"),
)
# participants=models.ManyToManyField('Participants',
# blank=True,related_name="jobs_participating",
# verbose_name=_("External Participant"),
# help_text=_("External participants involved in the recruitment process for this job"),
# )
participants=models.ManyToManyField('Participants',
blank=True,related_name="jobs_participating",
verbose_name=_("External Participant"),
help_text=_("External participants involved in the recruitment process for this job"),
)
# Core Fields
title = models.CharField(max_length=200)
@ -421,7 +421,6 @@ class Candidate(Base):
related_name="candidates",
verbose_name=_("Job"),
)
first_name = models.CharField(max_length=255, verbose_name=_("First Name"))
last_name = models.CharField(max_length=255, verbose_name=_("Last Name"))
email = models.EmailField(db_index=True, verbose_name=_("Email")) # Added index
@ -707,11 +706,6 @@ class Candidate(Base):
time_to_hire = self.hired_date - self.created_at.date()
return time_to_hire.days
return 0
@property
def belong_to_an_agency(self):
return self.hiring_source=='Agency'
class TrainingMaterial(Base):
title = models.CharField(max_length=255, verbose_name=_("Title"))
@ -778,7 +772,7 @@ class ZoomMeeting(Base):
# Timestamps
def __str__(self):
return self.topic
return self.topic\
@property
def get_job(self):
return self.interview.job
@ -787,10 +781,10 @@ class ZoomMeeting(Base):
return self.interview.candidate
@property
def get_participants(self):
return self.interview.participants.all()
return self.interview.job.participants.all()
@property
def get_users(self):
return self.interview.system_users.all()
return self.interview.job.users.all()
class MeetingComment(Base):
"""
@ -1645,9 +1639,6 @@ class ScheduledInterview(Base):
db_index=True
)
participants = models.ManyToManyField('Participants', blank=True)
system_users=models.ManyToManyField(User,blank=True)
job = models.ForeignKey(
"JobPosting", on_delete=models.CASCADE, related_name="scheduled_interviews", db_index=True
@ -1762,7 +1753,6 @@ class Notification(models.Model):
class Participants(Base):
"""Model to store Participants details"""
name = models.CharField(max_length=255, verbose_name=_("Participant Name"),null=True,blank=True)
email= models.EmailField(verbose_name=_("Email"))
phone = models.CharField(max_length=12,verbose_name=_("Phone Number"),null=True,blank=True)
@ -1773,5 +1763,3 @@ class Participants(Base):
def __str__(self):
return f"{self.name} - {self.email}"

View File

@ -746,74 +746,70 @@ def sync_candidate_to_source_task(candidate_id, source_id):
return {"success": False, "error": error_msg}
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.utils.html import strip_tags
def _task_send_individual_email(subject, body_message, recipient, attachments):
"""Internal helper to create and send a single email."""
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa')
is_html = '<' in body_message and '>' in body_message
if is_html:
plain_message = strip_tags(body_message)
email_obj = EmailMultiAlternatives(subject=subject, body=plain_message, from_email=from_email, to=[recipient])
email_obj.attach_alternative(body_message, "text/html")
else:
email_obj = EmailMultiAlternatives(subject=subject, body=body_message, from_email=from_email, to=[recipient])
if attachments:
for attachment in attachments:
if isinstance(attachment, tuple) and len(attachment) == 3:
filename, content, content_type = attachment
email_obj.attach(filename, content, content_type)
try:
email_obj.send(fail_silently=False)
return True
except Exception as e:
logger.error(f"Task failed to send email to {recipient}: {str(e)}", exc_info=True)
return False
def send_bulk_email_task(subject, message, recipient_list, attachments=None, hook='recruitment.tasks.email_success_hook'):
def send_bulk_email_task(subject, message, recipient_list, request=None, attachments=None):
"""
Django-Q background task to send pre-formatted email to a list of recipients.
Receives arguments directly from the async_task call.
Django-Q background task to send bulk email to multiple recipients.
Args:
subject: Email subject
message: Email message (can be HTML)
recipient_list: List of email addresses
request: Django request object (optional)
attachments: List of file attachment data (optional)
Returns:
dict: Result with success status and error message if failed
"""
from .email_service import send_bulk_email
import os
logger.info(f"Starting bulk email task for {len(recipient_list)} recipients")
successful_sends = 0
total_recipients = len(recipient_list)
if not recipient_list:
return {'success': False, 'error': 'No recipients provided to task.'}
try:
# Process attachments - convert file data back to file objects if needed
# processed_attachments = []
# if attachments:
# for attachment in attachments:
# if isinstance(attachment, dict) and 'file_path' in attachment:
# # This is a serialized file from background task
# file_path = attachment['file_path']
# filename = attachment.get('filename', os.path.basename(file_path))
# content_type = attachment.get('content_type', 'application/octet-stream')
# Since the async caller sends one task per recipient, total_recipients should be 1.
for recipient in recipient_list:
# The 'message' is the custom message specific to this recipient.
if _task_send_individual_email(subject, message, recipient, attachments):
successful_sends += 1
if successful_sends > 0:
logger.info(f"Bulk email task completed successfully. Sent to {successful_sends}/{total_recipients} recipients.")
return {
'success': True,
'recipients_count': successful_sends,
'message': f"Sent successfully to {successful_sends} recipient(s)."
}
else:
logger.error(f"Bulk email task failed: No emails were sent successfully.")
return {'success': False, 'error': "No emails were sent successfully in the background task."}
# try:
# with open(file_path, 'rb') as f:
# content = f.read()
# processed_attachments.append((filename, content, content_type))
# # Clean up temporary file
# try:
# os.unlink(file_path)
# except OSError:
# pass # File might already be deleted
def email_success_hook(task):
"""
The success hook must accept the Task object as the first and only required positional argument.
"""
if task.success:
logger.info(f"Task ID {task.id} succeeded. Result: {task.result}")
else:
logger.error(f"Task ID {task.id} failed. Error: {task.result}")
# except FileNotFoundError:
# logger.warning(f"Attachment file not found: {file_path}")
# continue
# else:
# # Direct attachment (file object or tuple)
# processed_attachments.append(attachment)
# Call the existing send_bulk_email function synchronously within the task
result = send_bulk_email(
subject=subject,
message=message,
recipient_list=recipient_list,
request=request,
)
if result['success']:
logger.info(f"Bulk email task completed successfully for {result.get('recipients_count', len(recipient_list))} recipients")
else:
logger.error(f"Bulk email task failed: {result.get('error', 'Unknown error')}")
return result
except Exception as e:
error_msg = f"Critical error in bulk email task: {str(e)}"
logger.error(error_msg, exc_info=True)
return {'success': False, 'error': error_msg}

View File

@ -232,7 +232,4 @@ urlpatterns = [
# Email composition URLs
path('jobs/<slug:job_slug>/candidates/compose_email/', views.compose_candidate_email, name='compose_candidate_email'),
path('interview/partcipants/<slug:slug>/',views.create_interview_participants,name='create_interview_participants'),
path('interview/email/<slug:slug>/',views.send_interview_email,name='send_interview_email'),
]

View File

@ -41,9 +41,9 @@ from .forms import (
AgencyAccessLinkForm,
AgencyJobAssignmentForm,
LinkedPostContentForm,
ParticipantsSelectForm,
CandidateEmailForm,
SourceForm,
InterviewEmailForm
SourceForm
)
from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent
from rest_framework import viewsets
@ -198,30 +198,6 @@ class ZoomMeetingDetailsView(LoginRequiredMixin, DetailView):
model = ZoomMeeting
template_name = "meetings/meeting_details.html"
context_object_name = "meeting"
def get_context_data(self, **kwargs):
context=super().get_context_data(**kwargs)
meeting = self.object
interview=meeting.interview
candidate = interview.candidate
job=meeting.get_job
# Assuming interview.participants and interview.system_users hold the people:
participants = list(interview.participants.all()) + list(interview.system_users.all())
external_participants=list(interview.participants.all())
system_participants= list(interview.system_users.all())
total_participants=len(participants)
form = InterviewParticpantsForm(instance=interview)
context['form']=form
context['email_form'] = InterviewEmailForm(
candidate=candidate,
external_participants=external_participants,
system_participants=system_participants,
meeting=meeting,
job=job
)
context['total_participants']=total_participants
return context
class ZoomMeetingUpdateView(LoginRequiredMixin, UpdateView):
@ -584,7 +560,7 @@ def kaauh_career(request):
form_template__is_active=True
)
return render(request,'applicant/career.html',{'active_jobs':active_jobs})
return render(request,'jobs/career.html',{'active_jobs':active_jobs})
# job detail facing the candidate:
def application_detail(request, slug):
@ -1481,12 +1457,40 @@ def candidate_update_status(request, slug):
def candidate_interview_view(request,slug):
job = get_object_or_404(JobPosting,slug=slug)
if request.method == "POST":
form = ParticipantsSelectForm(request.POST, instance=job)
print(form.errors)
if form.is_valid():
# Save the main instance (JobPosting)
job_instance = form.save(commit=False)
job_instance.save()
# MANUALLY set the M2M relationships based on submitted data
job_instance.participants.set(form.cleaned_data['participants'])
job_instance.users.set(form.cleaned_data['users'])
messages.success(request, "Interview participants updated successfully.")
return redirect("candidate_interview_view", slug=job.slug)
else:
initial_data = {
'participants': job.participants.all(),
'users': job.users.all(),
}
form = ParticipantsSelectForm(instance=job, initial=initial_data)
else:
form = ParticipantsSelectForm(instance=job)
context = {
"job":job,
"candidates":job.interview_candidates,
'current_stage':'Interview',
'form':form,
'participants_count': job.participants.count() + job.users.count(),
}
return render(request,"recruitment/candidate_interview_view.html",context)
@ -3695,7 +3699,6 @@ def api_candidate_detail(request, candidate_id):
return JsonResponse({'success': False, 'error': str(e)})
@login_required
def compose_candidate_email(request, job_slug):
"""Compose email to participants about a candidate"""
@ -3705,64 +3708,112 @@ def compose_candidate_email(request, job_slug):
candidate_ids=request.GET.getlist('candidate_ids')
candidates=Candidate.objects.filter(id__in=candidate_ids)
print(candidates)
# candidate = get_object_or_404(Candidate, slug=candidate_slug, job=job)
if request.method == 'POST':
print("........................................................inside candidate conpose.............")
candidate_ids = request.POST.getlist('candidate_ids')
print(f"inside the POST {candidate_ids}" )
candidates=Candidate.objects.filter(id__in=candidate_ids)
form = CandidateEmailForm(job, candidates, request.POST)
print(form)
if form.is_valid():
print("form is valid ...")
# Get email addresses
email_addresses = form.get_email_addresses()
print(email_addresses)
print(f"hiii {email_addresses} ")
if not email_addresses:
messages.error(request, 'No email selected')
referer = request.META.get('HTTP_REFERER')
if referer:
# Redirect back to the referring page
return redirect(referer)
else:
return redirect('dashboard')
message = form.get_formatted_message()
subject = form.cleaned_data.get('subject')
# Send emails using email service (no attachments, synchronous to avoid pickle issues)
email_result = send_bulk_email(
subject=subject,
message=message,
recipient_list=email_addresses,
request=request,
attachments=None,
async_task_=True, # Changed to False to avoid pickle issues
from_interview=False
)
if email_result['success']:
messages.success(request, f'Email sent successfully to {len(email_addresses)} recipient(s).')
return redirect('candidate_interview_view', slug=job.slug)
else:
messages.error(request, f'Failed to send email: {email_result.get("message", "Unknown error")}')
messages.error(request, 'No valid email addresses found for selected recipients.')
return render(request, 'includes/email_compose_form.html', {
'form': form,
'job': job,
'candidate': candidates
})
# Check if this is an interview invitation
subject = form.cleaned_data.get('subject', '').lower()
is_interview_invitation = 'interview' in subject or 'meeting' in subject
if is_interview_invitation:
# Use HTML template for interview invitations
meeting_details = None
# if form.cleaned_data.get('include_meeting_details'):
# # Try to get meeting details from candidate
# meeting_details = {
# 'topic': f'Interview for {job.title}',
# 'date_time': getattr(candidate, 'interview_date', 'To be scheduled'),
# 'duration': '60 minutes',
# 'join_url': getattr(candidate, 'meeting_url', ''),
# }
from .email_service import send_interview_invitation_email
email_result = send_interview_invitation_email(
candidate=candidates,
job=job,
meeting_details=meeting_details,
recipient_list=email_addresses
)
else:
# Get formatted message for regular emails
message = form.get_formatted_message()
subject = form.cleaned_data.get('subject')
# Send emails using email service (no attachments, synchronous to avoid pickle issues)
email_result = send_bulk_email(
subject=subject,
message=message,
recipient_list=email_addresses,
request=request,
async_task_=False # Changed to False to avoid pickle issues
)
if email_result['success']:
messages.success(request, f'Email sent successfully to {len(email_addresses)} recipient(s).')
# # For HTMX requests, return success response
# if 'HX-Request' in request.headers:
# return JsonResponse({
# 'success': True,
# 'message': f'Email sent successfully to {len(email_addresses)} recipient(s).'
# })
return redirect('candidate_interview_view', slug=job.slug)
else:
messages.error(request, f'Failed to send email: {email_result.get("message", "Unknown error")}')
# For HTMX requests, return error response
# if 'HX-Request' in request.headers:
# return JsonResponse({
# 'success': False,
# 'error': email_result.get("message", "Failed to send email")
# })
return render(request, 'includes/email_compose_form.html', {
'form': form,
'job': job,
'candidate': candidates
})
# except Exception as e:
# logger.error(f"Error sending candidate email: {e}")
# messages.error(request, f'An error occurred while sending the email: {str(e)}')
# # For HTMX requests, return error response
# if 'HX-Request' in request.headers:
# return JsonResponse({
# 'success': False,
# 'error': f'An error occurred while sending the email: {str(e)}'
# })
# return render(request, 'includes/email_compose_form.html', {
# 'form': form,
# 'job': job,
# 'candidate': candidate
# })
else:
# Form validation errors
print('form is not valid')
@ -3775,7 +3826,7 @@ def compose_candidate_email(request, job_slug):
'success': False,
'error': 'Please correct the form errors and try again.'
})
return render(request, 'includes/email_compose_form.html', {
'form': form,
'job': job,
@ -3786,6 +3837,14 @@ def compose_candidate_email(request, job_slug):
# GET request - show the form
form = CandidateEmailForm(job, candidates)
# try:
# l = [x.split("_")[1] for x in candidates]
# print(l)
# candidates_qs = Candidate.objects.filter(pk__in=l)
# print(candidates_qs)
# form.initial["to"]. = candidates_qs
# except:
# pass
print("GET request made for candidate email form")
@ -3963,103 +4022,3 @@ def source_toggle_status(request, slug):
# For GET requests, return error
return JsonResponse({'success': False, 'error': 'Method not allowed'})
from .forms import InterviewParticpantsForm
def create_interview_participants(request,slug):
schedule_interview=get_object_or_404(ScheduledInterview,slug=slug)
interview_slug=schedule_interview.zoom_meeting.slug
if request.method == 'POST':
form = InterviewParticpantsForm(request.POST,instance=schedule_interview)
if form.is_valid():
# Save the main Candidate object, but don't commit to DB yet
candidate = form.save(commit=False)
candidate.save()
# This is important for ManyToMany fields: save the many-to-many data
form.save_m2m()
return redirect('meeting_details',slug=interview_slug) # Redirect to a success page
else:
form = InterviewParticpantsForm(instance=schedule_interview)
return render(request, 'interviews/interview_participants_form.html', {'form': form})
from django.core.mail import send_mail
def send_interview_email(request, slug):
from .email_service import send_bulk_email
interview = get_object_or_404(ScheduledInterview, slug=slug)
# 2. Retrieve the required data for the form's constructor
candidate = interview.candidate
job=interview.job
meeting=interview.zoom_meeting
participants = list(interview.participants.all()) + list(interview.system_users.all())
external_participants=list(interview.participants.all())
system_participants=list(interview.system_users.all())
participant_emails = [p.email for p in participants if hasattr(p, 'email')]
print(participant_emails)
total_recipients=1+len(participant_emails)
# --- POST REQUEST HANDLING ---
if request.method == 'POST':
form = InterviewEmailForm(
request.POST,
candidate=candidate,
external_participants=external_participants,
system_participants=system_participants,
meeting=meeting,
job=job
)
if form.is_valid():
# 4. Extract cleaned data
subject = form.cleaned_data['subject']
msg_candidate = form.cleaned_data['message_for_candidate']
msg_agency = form.cleaned_data['message_for_agency']
msg_participants = form.cleaned_data['message_for_participants']
# --- SEND EMAILS Candidate or agency---
if candidate.belong_to_an_agency:
send_mail(
subject,
msg_agency,
settings.DEFAULT_FROM_EMAIL,
[candidate.hiring_agency.email],
fail_silently=False,
)
else:
send_mail(
subject,
msg_candidate,
settings.DEFAULT_FROM_EMAIL,
[candidate.email],
fail_silently=False,
)
email_result = send_bulk_email(
subject=subject,
message=msg_participants,
recipient_list=participant_emails,
request=request,
attachments=None,
async_task_=True, # Changed to False to avoid pickle issues,
from_interview=True
)
if email_result['success']:
messages.success(request, f'Email sent successfully to {total_recipients} recipient(s).')
return redirect('list_meetings')
else:
messages.error(request, f'Failed to send email: {email_result.get("message", "Unknown error")}')
return redirect('list_meetings')

View File

@ -2,421 +2,161 @@
{% load static i18n %}
{% block title %}{% trans "My Dashboard" %} - {{ block.super }}{% endblock %}
{% block title %}{% trans "My Profile" %} - {{ block.super }}{% endblock %}
{% block customCSS %}
<style>
/* ---------------------------------------------------------------------- */
/* THEME VARIABLES (Refined for Premium Look) */
/* ---------------------------------------------------------------------- */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-teal-accent: #008c9c; /* A slightly brighter, more vibrant teal for key links */
--kaauh-teal-light: #e6f7f8;
--kaauh-bg-subtle: #f9fbfb; /* Lighter, cleaner background */
--kaauh-border: #e0e5eb; /* Very subtle border color */
--kaauh-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05); /* Lightest shadow */
--kaauh-shadow-lg: 0 10px 30px rgba(0, 0, 0, 0.08); /* Modern, lifted shadow */
--gray-text: #5e6c84; /* Deeper, more readable gray */
--success-subtle: #d4edda;
--danger-subtle: #f8d7da;
}
body {
background-color: var(--kaauh-bg-subtle);
}
.text-primary-theme { color: var(--kaauh-teal-accent) !important; }
.text-gray-subtle { color: var(--gray-text) !important; }
/* ---------------------------------------------------------------------- */
/* 1. PRIMARY BUTTONS & ACTIONS */
/* ---------------------------------------------------------------------- */
.btn-main-action {
background-color: var(--kaauh-teal);
color: white;
border: none;
transition: all 0.2s ease;
box-shadow: 0 4px 15px rgba(0, 99, 110, 0.4);
font-weight: 600;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
transform: translateY(-1px);
box-shadow: 0 6px 20px rgba(0, 99, 110, 0.5);
}
.btn-outline-secondary {
border-color: var(--kaauh-border);
color: var(--kaauh-teal-dark);
font-weight: 500;
transition: all 0.2s ease;
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
}
/* ---------------------------------------------------------------------- */
/* 2. CARDS & CONTAINERS (Increased Padding & Smoother Shadow) */
/* ---------------------------------------------------------------------- */
.kaauh-card {
border: 1px solid var(--kaauh-border);
border-radius: 12px; /* Smoother corner radius */
box-shadow: var(--kaauh-shadow-sm);
transition: box-shadow 0.3s ease;
}
.kaauh-card:hover {
box-shadow: var(--kaauh-shadow-lg); /* Subtle lift on hover */
}
.profile-data-list li {
padding: 1rem 0; /* More vertical space */
border-bottom: 1px dashed var(--kaauh-border);
font-size: 0.95rem;
font-weight: 500;
}
.profile-data-list li strong {
font-weight: 700;
color: var(--kaauh-teal-dark);
min-width: 120px;
display: inline-block;
}
/* ---------------------------------------------------------------------- */
/* 3. TABS (Minimalist & Clean) */
/* ---------------------------------------------------------------------- */
.nav-tabs {
border-bottom: 1px solid var(--kaauh-border); /* Thinner border */
}
.nav-tabs .nav-link {
color: var(--gray-text); /* Use subdued text color for inactive tabs */
border: none;
border-bottom: 3px solid transparent; /* Thinner accent line */
padding: 1rem 1.75rem;
font-weight: 600;
}
.nav-tabs .nav-link:hover {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-light);
}
.nav-tabs .nav-link.active {
color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
background-color: transparent;
}
.nav-tabs .nav-link i {
color: var(--kaauh-teal-accent) !important;
}
.nav-scroll {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
white-space: nowrap;
}
.nav-scroll .nav-tabs { flex-wrap: nowrap; border-bottom: none; }
.nav-scroll .nav-tabs .nav-item { flex-shrink: 0; }
/* ---------------------------------------------------------------------- */
/* 4. APPLICATION TABLE (Refined Aesthetics) */
/* ---------------------------------------------------------------------- */
.application-table thead th {
background-color: var(--kaauh-teal-light); /* Light, subtle header */
color: var(--kaauh-teal-dark);
font-weight: 700;
border-bottom: 1px solid var(--kaauh-border);
padding: 1rem 1.5rem;
}
.application-table tbody tr {
transition: background-color 0.2s ease;
}
.application-table tbody tr:hover {
background-color: var(--kaauh-teal-light);
}
.badge-stage {
font-weight: 600;
padding: 0.4em 0.8em;
border-radius: 50rem;
}
.bg-success { background-color: #38a169 !important; }
.bg-warning { background-color: #f6ad55 !important; }
/* Responsive Table for Mobile (High contrast labels) */
@media (max-width: 767.98px) {
.application-table thead { display: none; }
.application-table tr {
margin-bottom: 1rem;
border: 1px solid var(--kaauh-border);
border-radius: 8px;
box-shadow: var(--kaauh-shadow-sm);
}
.application-table td {
text-align: right !important;
padding: 0.75rem 1rem;
padding-left: 50%;
position: relative;
}
.application-table td::before {
content: attr(data-label);
position: absolute;
left: 1rem;
width: 45%;
font-weight: 700;
color: var(--gray-text); /* Use muted gray for labels */
}
}
/* Document Management List */
.list-group-item {
border-radius: 8px;
margin-bottom: 0.5rem;
border: 1px solid var(--kaauh-border);
transition: all 0.2s ease;
}
.list-group-item:hover {
background-color: var(--kaauh-teal-light);
border-color: var(--kaauh-teal-accent);
}
</style>
{% endblock %}
{% block content %}
<div class="container py-4 py-md-5">
<div class="container-fluid py-5">
{# Header: Larger, more dynamic on large screens. Stacks cleanly on mobile. #}
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5">
<h1 class="display-6 display-md-5 fw-extrabold mb-3 mb-md-0" style="color: var(--kaauh-teal-dark);">
{% trans "Your Candidate Dashboard" %}
{# Profile Header #}
<div class="d-flex justify-content-between align-items-center mb-5 border-bottom pb-4">
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-user-circle me-3 text-primary-theme"></i> {% trans "My Candidate Profile" %}
</h1>
<a href="#profile-details" data-bs-toggle="tab" class="btn btn-main-action btn-sm btn-md-lg px-4 py-2 rounded-pill shadow-sm shadow-md-lg">
<i class="fas fa-edit me-2"></i> {% trans "Update Profile" %}
<a href="#" class="btn btn-main-action btn-lg">
<i class="fas fa-edit me-2"></i> {% trans "Edit Profile" %}
</a>
</div>
{# Candidate Quick Overview Card: Use a softer background color #}
<div class="card kaauh-card mb-5 p-4 bg-white">
<div class="d-flex align-items-center flex-column flex-sm-row text-center text-sm-start">
<img src="{% if candidate.profile_picture %}{{ candidate.profile_picture.url }}{% else %}{% static 'image/default_avatar.png' %}{% endif %}"
alt="{% trans 'Profile Picture' %}"
class="rounded-circle me-sm-4 mb-3 mb-sm-0 shadow-lg"
style="width: 80px; height: 80px; object-fit: cover; border: 4px solid var(--kaauh-teal-accent);">
<div>
<h3 class="card-title mb-1 fw-bold text-dark">{{ candidate.name|default:"Candidate Name" }}</h3>
<p class="text-gray-subtle mb-0">{{ candidate.email }}</p>
</div>
</div>
</div>
{# ================================================= #}
{# MAIN TABBED INTERFACE #}
{# ================================================= #}
<div class="card kaauh-card p-0 bg-white">
{# Profile and Account Management Row #}
<div class="row g-5 mb-5">
{# Tab Navigation: Used nav-scroll for responsiveness #}
<div class="nav-scroll px-4 pt-3">
<ul class="nav nav-tabs" id="candidateTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="profile-tab" data-bs-toggle="tab" data-bs-target="#profile-details" type="button" role="tab" aria-controls="profile-details" aria-selected="true">
<i class="fas fa-user-circle me-2"></i> {% trans "Profile Details" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="applications-tab" data-bs-toggle="tab" data-bs-target="#applications-history" type="button" role="tab" aria-controls="applications-history" aria-selected="false">
<i class="fas fa-list-alt me-2"></i> {% trans "My Applications" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="documents-tab" data-bs-toggle="tab" data-bs-target="#document-management" type="button" role="tab" aria-controls="document-management" aria-selected="false">
<i class="fas fa-file-upload me-2"></i> {% trans "Documents" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="settings-tab" data-bs-toggle="tab" data-bs-target="#account-settings" type="button" role="tab" aria-controls="account-settings" aria-selected="false">
<i class="fas fa-cogs me-2"></i> {% trans "Settings" %}
</button>
</li>
</ul>
</div>
{# Tab Content #}
<div class="tab-content p-4 p-md-5" id="candidateTabsContent">
<div class="tab-pane fade show active" id="profile-details" role="tabpanel" aria-labelledby="profile-tab">
<h4 class="mb-4 fw-bold text-gray-subtle">{% trans "Personal Information" %}</h4>
<ul class="list-unstyled profile-data-list p-0">
<li class="d-flex justify-content-between align-items-center">
<div><i class="fas fa-phone-alt me-2 text-primary-theme"></i> <strong>{% trans "Phone" %}</strong></div>
<span class="text-end">{{ candidate.phone|default:"N/A" }}</span>
</li>
<li class="d-flex justify-content-between align-items-center">
<div><i class="fas fa-globe me-2 text-primary-theme"></i> <strong>{% trans "Nationality" %}</strong></div>
<span class="text-end">{{ candidate.get_nationality_display|default:"N/A" }}</span>
</li>
<li class="d-flex justify-content-between align-items-center">
<div><i class="fas fa-calendar-alt me-2 text-primary-theme"></i> <strong>{% trans "Date of Birth" %}</strong></div>
<span class="text-end">{{ candidate.date_of_birth|date:"M d, Y"|default:"N/A" }}</span>
</li>
<li class="small pt-3 text-muted border-bottom-0">{% trans "Use the 'Update Profile' button above to edit these details." %}</li>
</ul>
<hr class="my-5">
<h4 class="mb-4 fw-bold text-gray-subtle">{% trans "Quick Actions" %}</h4>
<div class="row g-3 g-md-4">
<div class="col-6 col-sm-4 col-md-4">
<a href="#applications-history" data-bs-toggle="tab" class="btn btn-action-tile w-100 d-grid text-center text-dark text-decoration-none">
<span class="action-tile-icon mb-2"><i class="fas fa-list-check"></i></span>
<span class="fw-bold">{% trans "Track Jobs" %}</span>
<span class="small text-muted d-none d-sm-block">{% trans "View stages" %}</span>
</a>
</div>
<div class="col-6 col-sm-4 col-md-4">
<a href="#document-management" data-bs-toggle="tab" class="btn btn-action-tile w-100 d-grid text-center text-dark text-decoration-none">
<span class="action-tile-icon mb-2"><i class="fas fa-cloud-upload-alt"></i></span>
<span class="fw-bold">{% trans "Manage Documents" %}</span>
<span class="small text-muted d-none d-sm-block">{% trans "Upload/View files" %}</span>
</a>
</div>
<div class="col-12 col-sm-4 col-md-4">
<a href="{% url 'kaauh_career' %}" class="btn btn-action-tile w-100 d-grid text-center text-dark text-decoration-none">
<span class="action-tile-icon mb-2"><i class="fas fa-search"></i></span>
<span class="fw-bold">{% trans "Find New Careers" %}</span>
<span class="small text-muted d-none d-sm-block">{% trans "Explore open roles" %}</span>
</a>
</div>
</div>
</div>
<div class="tab-pane fade" id="applications-history" role="tabpanel" aria-labelledby="applications-tab">
<h4 class="mb-4 fw-bold text-gray-subtle">{% trans "Application Tracking" %}</h4>
{% if applications %}
<div class="kaauh-card shadow-lg p-0">
<table class="table table-borderless align-middle mb-0 application-table">
<thead>
<tr>
<th scope="col" style="min-width: 250px;">{% trans "Job Title" %}</th>
<th scope="col">{% trans "Applied On" %}</th>
<th scope="col">{% trans "Current Stage" %}</th>
<th scope="col">{% trans "Status" %}</th>
<th scope="col" class="text-end" style="min-width: 140px;">{% trans "Action" %}</th>
</tr>
</thead>
<tbody>
{% for application in applications %}
<tr>
<td class="fw-medium" data-label="{% trans 'Job Title' %}">
<a href="{% url 'job_detail' application.job.slug %}" class="text-decoration-none text-primary-theme">
{{ application.job.title }}
</a>
</td>
<td data-label="{% trans 'Applied On' %}" class="text-gray-subtle">{{ application.applied_date|date:"d M Y" }}</td>
<td data-label="{% trans 'Current Stage' %}">
<span class="badge badge-stage bg-info text-white">
{{ application.stage }}
</span>
</td>
<td data-label="{% trans 'Status' %}">
{% if application.is_active %}
<span class="badge badge-stage bg-success">{% trans "Active" %}</span>
{% else %}
<span class="badge badge-stage bg-warning text-dark">{% trans "Closed" %}</span>
{% endif %}
</td>
<td class="text-end" data-label="{% trans 'Action' %}">
<a href="#" class="btn btn-outline-secondary btn-sm rounded-pill px-3">
<i class="fas fa-eye me-1"></i> {% trans "Details" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{# Candidate Details Card #}
<div class="col-lg-5">
<div class="card kaauh-card h-100 p-4">
<div class="card-body p-0">
<div class="d-flex align-items-center mb-4 pb-4 border-bottom">
<img src="{% if candidate.profile_picture %}{{ candidate.profile_picture.url }}{% else %}{% static 'image/default_avatar.png' %}{% endif %}"
alt="{% trans 'Profile Picture' %}"
class="rounded-circle me-4 shadow-sm"
style="width: 100px; height: 100px; object-fit: cover; border: 4px solid var(--kaauh-teal-light);">
<div>
<h3 class="card-title mb-1" style="color: var(--kaauh-teal-dark); font-weight: 600;">{{ candidate.name|default:"N/A" }}</h3>
<p class="text-muted mb-0">{{ candidate.email }}</p>
</div>
</div>
{% else %}
<div class="alert alert-info text-center p-5 rounded-3" style="border: 1px dashed var(--kaauh-border); background-color: var(--kaauh-teal-light);">
<i class="fas fa-info-circle fa-2x mb-3 text-primary-theme"></i>
<h5 class="mb-3 fw-bold text-primary-theme">{% trans "You haven't submitted any applications yet." %}</h5>
<a href="{% url 'kaauh_career' %}" class="ms-3 btn btn-main-action mt-2 rounded-pill px-4">
{% trans "View Available Jobs" %} <i class="fas fa-arrow-right ms-2"></i>
</a>
</div>
{% endif %}
<ul class="list-unstyled profile-data-list pt-2">
<li>
<i class="fas fa-phone-alt me-2 text-primary-theme"></i>
<strong>{% trans "Phone" %}</strong> {{ candidate.phone|default:"N/A" }}
</li>
<li>
<i class="fas fa-globe me-2 text-primary-theme"></i>
<strong>{% trans "Nationality" %}</strong> {{ candidate.get_nationality_display|default:"N/A" }}
</li>
<li>
<i class="fas fa-calendar-alt me-2 text-primary-theme"></i>
<strong>{% trans "Date of Birth" %}</strong> {{ candidate.date_of_birth|date:"M d, Y"|default:"N/A" }}
</li>
<li>
<i class="fas fa-file-alt me-2 text-primary-theme"></i>
<strong>{% trans "Resume" %}</strong>
{% if candidate.resume %}
<a href="#" target="_blank" class="text-primary-theme text-decoration-none fw-medium">
{% trans "View/Download" %} <i class="fas fa-external-link-alt small ms-1"></i>
</a>
{% else %}
<span class="text-muted">{% trans "Not uploaded" %}</span>
{% endif %}
</li>
</ul>
</div>
</div>
<div class="tab-pane fade" id="document-management" role="tabpanel" aria-labelledby="documents-tab">
<h4 class="mb-4 fw-bold text-gray-subtle">{% trans "My Uploaded Documents" %}</h4>
<p class="text-gray-subtle">{% trans "You can upload and manage your resume, certificates, and professional documents here. These documents will be attached to your applications." %}</p>
<a href="#" class="btn btn-main-action rounded-pill px-4 me-3 d-block d-sm-inline-block w-100 w-sm-auto mb-4">
<i class="fas fa-cloud-upload-alt me-2"></i> {% trans "Upload New Document" %}
</a>
<hr class="my-5">
{# Example Document List (Refined structure) #}
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center bg-white p-3">
<div class="mb-2 mb-sm-0 fw-medium">
<i class="fas fa-file-pdf me-2 text-primary-theme"></i> **{% trans "Resume" %}** <span class="text-muted small">(CV\_John\_Doe\_2024.pdf)</span>
</div>
<div class="d-flex align-items-center">
<span class="text-muted small me-3">{% trans "Uploaded: 10 Jan 2024" %}</span>
<a href="#" class="btn btn-sm btn-outline-secondary me-2"><i class="fas fa-eye"></i></a>
<a href="#" class="btn btn-sm btn-outline-danger"><i class="fas fa-trash-alt"></i></a>
</div>
</li>
<li class="list-group-item d-flex flex-column flex-sm-row justify-content-between align-items-start align-items-sm-center bg-white p-3">
<div class="mb-2 mb-sm-0 fw-medium">
<i class="fas fa-file-alt me-2 text-primary-theme"></i> **{% trans "Medical Certificate" %}** <span class="text-muted small">(Cert\_KSA\_MED.jpg)</span>
</div>
<div class="d-flex align-items-center">
<span class="text-muted small me-3">{% trans "Uploaded: 22 Feb 2023" %}</span>
<a href="#" class="btn btn-sm btn-outline-secondary me-2"><i class="fas fa-eye"></i></a>
<a href="#" class="btn btn-sm btn-outline-danger"><i class="fas fa-trash-alt"></i></a>
</div>
</li>
</ul>
</div>
<div class="tab-pane fade" id="account-settings" role="tabpanel" aria-labelledby="settings-tab">
<h4 class="mb-4 fw-bold text-gray-subtle">{% trans "Security & Preferences" %}</h4>
<div class="row g-4">
<div class="col-12 col-md-6">
<div class="card kaauh-card p-4 h-100 bg-white">
<h5 class="fw-bold"><i class="fas fa-key me-2 text-primary-theme"></i> {% trans "Password Security" %}</h5>
<p class="text-muted small">{% trans "Update your password regularly to keep your account secure." %}</p>
<a href="#" class="btn btn-outline-secondary mt-auto w-100 py-2 fw-medium">
{% trans "Change Password" %}
</div>
{# Account Management / Quick Actions Card #}
<div class="col-lg-7">
<div class="card kaauh-card h-100 p-4">
<div class="card-header bg-white border-0 p-0 mb-4 profile-header">
<h4 class="mb-0 py-2" style="color: var(--kaauh-teal-dark); font-weight: 600;"><i class="fas fa-cogs me-2"></i> {% trans "Account Settings" %}</h4>
</div>
<div class="card-body p-0">
<div class="row g-4">
<div class="col-md-6">
<a href="#" class="btn btn-outline-secondary w-100 py-3 d-flex align-items-center justify-content-center">
<i class="fas fa-key me-2"></i> {% trans "Change Password" %}
</a>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card kaauh-card p-4 h-100 bg-white">
<h5 class="fw-bold"><i class="fas fa-envelope me-2 text-primary-theme"></i> {% trans "Email Preferences" %}</h5>
<p class="text-muted small">{% trans "Manage subscriptions and job alert settings." %}</p>
<a href="#" class="btn btn-outline-secondary mt-auto w-100 py-2 fw-medium">
{% trans "Manage Alerts" %}
<div class="col-md-6">
<a href="#" class="btn btn-outline-secondary w-100 py-3 d-flex align-items-center justify-content-center">
<i class="fas fa-id-card me-2"></i> {% trans "Update Contact Info" %}
</a>
</div>
<div class="col-12">
<blockquote class="blockquote small text-muted mt-3 p-3" style="border-left: 3px solid var(--kaauh-teal-light); background-color: var(--kaauh-bg-subtle); border-radius: 0.5rem;">
<p class="mb-0">{% trans "Your profile is essential for the application process. Keep your resume and contact information up-to-date for timely communication." %}</p>
</blockquote>
</div>
</div>
</div>
<div class="alert mt-5 py-3" style="background-color: var(--danger-subtle); color: #842029; border: 1px solid #f5c2c7; border-radius: 8px;">
<i class="fas fa-exclamation-triangle me-2"></i> {% trans "To delete your profile, please contact HR support." %}
</div>
</div>
</div>
</div>
{# ================================================= #}
{# Application Tracking Section #}
<h2 class="h4 mb-4" style="color: var(--kaauh-teal-dark); border-bottom: 1px solid var(--kaauh-border); padding-bottom: 0.5rem;">
<i class="fas fa-list-alt me-2 text-primary-theme"></i> {% trans "My Applications History" %}
</h2>
{% if applications %}
<div class="table-responsive kaauh-card shadow-sm">
<table class="table table-hover align-middle mb-0 application-table">
<thead>
<tr>
<th scope="col" style="min-width: 250px;">{% trans "Job Title" %}</th>
<th scope="col">{% trans "Applied On" %}</th>
<th scope="col">{% trans "Current Stage" %}</th>
<th scope="col">{% trans "Status" %}</th>
<th scope="col" class="text-end" style="min-width: 120px;">{% trans "Action" %}</th>
</tr>
</thead>
<tbody>
{% for application in applications %}
<tr>
<td class="fw-medium">
<a href="{% url 'job_detail' application.job.slug %}" class="text-decoration-none text-primary-theme">
{{ application.job.title }}
</a>
</td>
<td>{{ application.applied_date|date:"d M Y" }}</td>
<td>
<span class="badge badge-stage">
{{ application.stage }}
</span>
</td>
<td>
{% if application.is_active %}
<span class="badge badge-success">{% trans "Active" %}</span>
{% else %}
<span class="badge badge-warning">{% trans "Closed" %}</span>
{% endif %}
</td>
<td class="text-end">
<a href="#" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-eye"></i> {% trans "Details" %}
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{# Placeholder for Pagination #}
{% comment %} {% include "includes/paginator.html" with page_obj=applications_page %} {% endcomment %}
{% else %}
<div class="alert alert-info text-center kaauh-card p-5" style="border: 1px dashed var(--kaauh-border);">
<i class="fas fa-info-circle fa-2x mb-3 text-primary-theme"></i>
<h5 class="mb-3">{% trans "You have no active applications." %}</h5>
<a href="{% url 'job_list' %}" class="ms-3 btn btn-main-action mt-2">
{% trans "View Available Jobs" %} <i class="fas fa-arrow-right ms-2"></i>
</a>
</div>
{% endif %}
</div>
{% endblock %}

View File

@ -1,6 +1,5 @@
{% extends 'applicant/partials/candidate_facing_base.html'%}
{% load static i18n %}
{% block content %}
<nav id="bottomNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: var(--kaauh-teal); z-index: 1030;">
@ -36,8 +35,8 @@
<div class="col-lg-4 order-lg-2 order-1 d-none d-lg-block">
<div class="card shadow-sm sticky-top">
<div class="card-header bg-kaauh-teal-dark bg-white p-3">
<h5 class="mb-0 fw-bold"><i class="fas fa-file-signature me-2" style="color: var(--kaauh-teal);"></i>{% trans "Ready to Apply?" %}</h5>
<div class="card-header bg-kaauh-teal-dark text-white">
<h5 class="mb-0"><i class="fas fa-file-signature me-2"></i>{% trans "Ready to Apply?" %}</h5>
</div>
<div class="card-body text-center">
<p class="text-muted">{% trans "Review the job details, then apply below." %}</p>
@ -53,15 +52,14 @@
<div class="col-lg-8 order-lg-1 order-2">
<div class="card shadow-sm">
<div class="card-header bg-white border-bottom p-4">
<h1 class="h3 mb-0 fw-bold" style="color: var(--kaauh-teal);">{{ job.title }}</h1>
<div class="card-header bg-kaauh-teal-dark text-white d-flex justify-content-between align-items-center">
<h2 class="h3 mb-0 fw-bold">{{ job.title }}</h2>
</div>
<div class="card-body p-4">
<div class="card-body">
<h4 class="mb-3 fw-bold" style="color: var(--kaauh-teal-dark);">{% trans "Job Overview" %}</h4>
<div class="row row-cols-1 row-cols-md-2 g-3 mb-5 small text-secondary border p-3 rounded">
<h4 class="mb-3" style="color: var(--kaauh-teal-dark);">{% trans "Job Overview" %}</h4>
<div class="row row-cols-1 row-cols-md-2 g-3 mb-4 small text-secondary">
{% if job.salary_range %}
<div class="col">
<i class="fas fa-money-bill-wave text-success me-2"></i>
@ -90,79 +88,21 @@
<div class="col"> <i class="fas fa-desktop text-muted me-2"></i> <strong>{% trans "Workplace:" %}</strong> {{ job.get_workplace_type_display }} </div>
</div>
{% if job.has_description_content %}<hr class="my-4"><div class="mb-4"><h5 class="fw-bold" style="color: var(--kaauh-teal-dark);"><i class="fas fa-info-circle me-2"></i>{% trans "Job Description" %}</h5><div class="text-secondary">{{ job.description|safe }}</div></div>{% endif %}
{% if job.has_qualifications_content %}<hr class="my-4"><div class="mb-4"><h5 class="fw-bold" style="color: var(--kaauh-teal-dark);"><i class="fas fa-graduation-cap me-2"></i>{% trans "Qualifications" %}</h5><div class="text-secondary">{{ job.qualifications|safe }}</div></div>{% endif %}
{% if job.has_benefits_content %}<hr class="my-4"><div class="mb-4"><h5 class="fw-bold" style="color: var(--kaauh-teal-dark);"><i class="fas fa-hand-holding-usd me-2"></i>{% trans "Benefits" %}</h5><div class="text-secondary">{{ job.benefits|safe }}</div></div>{% endif %}
{% if job.has_application_instructions_content %}<hr class="my-4"><div class="mb-4"><h5 class="fw-bold" style="color: var(--kaauh-teal-dark);"><i class="fas fa-file-alt me-2"></i>{% trans "Application Instructions" %}</h5><div class="text-secondary">{{ job.application_instructions|safe }}</div></div>{% endif %}
<div class="accordion" id="jobDetailAccordion">
{% if job.has_description_content %}
<div class="accordion-item">
<h2 class="accordion-header" id="headingOne">
<button class="accordion-button fw-bold fs-5 text-primary-theme" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
<i class="fas fa-info-circle me-3"></i> {% trans "Job Description" %}
</button>
</h2>
<div id="collapseOne" class="accordion-collapse collapse show" aria-labelledby="headingOne" data-bs-parent="#jobDetailAccordion">
<div class="accordion-body text-secondary p-4">
{{ job.description|safe }}
</div>
</div>
</div>
{% endif %}
{% if job.has_qualifications_content %}
<div class="accordion-item">
<h2 class="accordion-header" id="headingTwo">
<button class="accordion-button collapsed fw-bold fs-5 text-primary-theme" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo" aria-expanded="false" aria-controls="collapseTwo">
<i class="fas fa-graduation-cap me-3"></i> {% trans "Qualifications" %}
</button>
</h2>
<div id="collapseTwo" class="accordion-collapse collapse" aria-labelledby="headingTwo" data-bs-parent="#jobDetailAccordion">
<div class="accordion-body text-secondary p-4">
{{ job.qualifications|safe }}
</div>
</div>
</div>
{% endif %}
{% if job.has_benefits_content %}
<div class="accordion-item">
<h2 class="accordion-header" id="headingThree">
<button class="accordion-button collapsed fw-bold fs-5 text-primary-theme" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree" aria-expanded="false" aria-controls="collapseThree">
<i class="fas fa-hand-holding-usd me-3"></i> {% trans "Benefits" %}
</button>
</h2>
<div id="collapseThree" class="accordion-collapse collapse" aria-labelledby="headingThree" data-bs-parent="#jobDetailAccordion">
<div class="accordion-body text-secondary p-4">
{{ job.benefits|safe }}
</div>
</div>
</div>
{% endif %}
{% if job.has_application_instructions_content %}
<div class="accordion-item">
<h2 class="accordion-header" id="headingFour">
<button class="accordion-button collapsed fw-bold fs-5 text-primary-theme" type="button" data-bs-toggle="collapse" data-bs-target="#collapseFour" aria-expanded="false" aria-controls="collapseFour">
<i class="fas fa-file-alt me-3"></i> {% trans "Application Instructions" %}
</button>
</h2>
<div id="collapseFour" class="accordion-collapse collapse" aria-labelledby="headingFour" data-bs-parent="#jobDetailAccordion">
<div class="accordion-body text-secondary p-4">
{{ job.application_instructions|safe }}
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="mobile-fixed-apply-bar d-lg-none text-center">
<div class="mobile-fixed-apply-bar d-lg-none">
{% if job.form_template %}
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100">
<a href="{% url 'application_submit_form' job.form_template.pk %}" class="btn btn-main-action btn-lg w-100">
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
</a>
{% endif %}

View File

@ -1,156 +0,0 @@
{% extends 'applicant/partials/candidate_facing_base.html'%}
{% load static i18n %}
{% block title %}{% trans "My Profile" %} - {{ block.super }}{% endblock %}
{% block content %}
<div class="main-content-area">
<header class="hero-section">
<div class="container">
<div class="row">
<div class="col-lg-12 col-xl-10">
<h1 class="hero-title mb-4">
{% translate "Your Career in Health & Academia starts here." %}
</h1>
<p class="lead mb-5">
{% translate "Join KAAUH, a national leader in patient care, research, and education. We are building the future of healthcare." %}
</p>
<a href="#filterSidebar" class="btn btn-hero-action me-3 mb-3 mb-lg-0">
<i class="fas fa-compass me-2"></i> {% translate "Find Your Path" %}
</a>
<a href="https://kaauh.edu.sa/about-us" class="btn btn-outline-light rounded-pill px-4 btn-lg">
{% translate "About US" %}
</a>
</div>
</div>
</div>
</header>
<section class="py-5 job-listing-section">
<div class="container">
<div class="row g-5">
<div class="col-lg-3 col-xl-3">
<button class="btn btn-outline-dark filter-toggle-button d-lg-none" type="button"
data-bs-toggle="collapse" data-bs-target="#filterSidebar" aria-expanded="false" aria-controls="filterSidebar">
<i class="fas fa-filter me-2"></i> {% translate "Filter Jobs" %}
</button>
<div class="collapse d-lg-block filter-sidebar-collapse" id="filterSidebar">
<div class="card sticky-top-filters p-4 bg-white">
<h4 class="fw-bold mb-4 text-primary-theme">
{% translate "Refine Your Search" %}
</h4>
<div class="d-grid gap-3">
<select class="form-select" aria-label="Department filter">
<option selected>{% translate "Department (Faculty/Admin)" %}</option>
<option value="1">{% translate "Clinical Services" %}</option>
<option value="2">{% translate "Research Labs" %}</option>
<option value="3">{% translate "Training & Education" %}</option>
</select>
<select class="form-select" aria-label="Employment Type filter">
<option selected>{% translate "Employment Type" %}</option>
<option value="1">{% translate "Full-Time" %}</option>
<option value="2">{% translate "Part-Time" %}</option>
</select>
<select class="form-select" aria-label="Specialty filter">
<option selected>{% translate "Specialty / Focus" %}</option>
<option value="1">{% translate "Women's Health" %}</option>
<option value="2">{% translate "Child Growth & Dev" %}</option>
</select>
<button class="btn btn-main-action rounded-pill mt-3">{% translate "Apply Filters" %}</button>
</div>
</div>
</div>
</div>
<div class="col-lg-9 col-xl-9">
<div class="sticky-filter-bar">
<div class="d-flex flex-wrap justify-content-between align-items-center">
<h3 class="fw-bold mb-0 text-dark fs-5">
{% comment %}
Assuming a 'job_count' context variable exists, otherwise
this remains static as in the original template.
{% endcomment %}
{% translate "Showing 37 Open Roles" %}
</h3>
<div class="d-flex flex-wrap gap-2">
<span class="filter-chip">
{% translate "Specialty: Women's Health" %}
<i class="fas fa-times text-xs ms-2 cursor-pointer" role="button" aria-label="Remove filter: Women's Health"></i>
</span>
</div>
</div>
</div>
<div class="mt-4 d-grid gap-3">
{% for job in active_jobs %}
{# The original card structure, now dynamically filled with job data #}
<a href="{% url 'application_detail' job.slug %}" class="card d-block text-decoration-none text-dark job-listing-card bg-white">
<div class="d-flex justify-content-between align-items-start">
<h4 class="h5 fw-bold mb-1 text-primary-theme-hover">
{{ job.title }}
</h4>
{# NOTE: You will need to define how job.category or job.tag is determined for the badge logic #}
<span class="badge bg-primary-theme job-tag">
{% comment %} Placeholder: Use job.tag or implement conditional logic {% endcomment %}
{% if job.tag_slug == 'clinical' %}{% translate "Clinical" %}
{% elif job.tag_slug == 'research' %}{% translate "Research/Contract" %}
{% else %}{% translate "General" %}{% endif %}
</span>
</div>
{# NOTE: Assuming job.department and job.location exist in your context #}
<p class="text-muted small mb-3">{{ job.department }}</p>
<div class="d-flex flex-wrap gap-4 small text-muted">
<span class="d-flex align-items-center">
<i class="fas fa-map-marker-alt me-2 job-detail-icon"></i>
{{ job.location|default:"Riyadh, KSA" }}
</span>
<span class="d-flex align-items-center">
<i class="fas fa-user-md me-2 job-detail-icon"></i>
{{ job.focus|default:"High Reliability Focus" }}
</span>
<span class="d-flex align-items-center">
<i class="fas fa-calendar-alt me-2 job-detail-icon"></i>
{{ job.employment_type|default:"Full-Time" }}
</span>
</div>
</a>
{% empty %}
<div class="alert alert-info" role="alert">
{% translate "We currently have no open roles that match your search. Please check back soon!" %}
</div>
{% endfor %}
<div class="text-center mt-5 mb-3">
<button class="btn btn-main-action btn-lg rounded-pill px-5 shadow-sm">
{% translate "Load More Jobs" %}
</button>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
{% endblock content %}

View File

@ -8,7 +8,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans "Careers" %} - {% block title %}{% translate "Application Form" %}{% endblock %}</title>
<title>{% translate "Application Form" %}</title>
{% comment %} Load the correct Bootstrap CSS file for RTL/LTR {% endcomment %}
{% if LANGUAGE_CODE == 'ar' %}
@ -25,17 +25,13 @@
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-teal-light: #e6f7f8; /* Very light teal for hover/background */
--success: #198754;
--danger: #dc3545;
--light-bg: #f8f9fa;
--gray-text: #6c757d;
--kaauh-border: #d0d7de; /* Cleaner border color */
--kaauh-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); /* Deeper shadow for premium look */
--kaauh-dark-bg: #0d0d0d;
--kaauh-dark-contrast: #1c1c1c;
--kaauh-border: #eaeff3; /* Added for dropdown styling */
/* CALCULATED STICKY HEIGHTS (As provided in base) */
/* CALCULATED STICKY HEIGHTS */
--navbar-height: 56px;
--navbar-gap: 16px;
--sticky-navbar-total-height: 128px;
@ -43,142 +39,208 @@
body {
min-height: 100vh;
background-color: #f0f0f5;
background-color: #f0f0f5; /* Light gray background for contrast */
padding-top: 0;
}
.text-primary-theme { color: var(--kaauh-teal) !important; }
.text-primary-theme-hover:hover { color: var(--kaauh-teal-dark) !important; }
.btn-main-action {
background-color: var(--kaauh-teal);
color: white;
border: none;
transition: background-color 0.3s ease, transform 0.2s ease, box-shadow 0.3s ease;
box-shadow: 0 6px 15px rgba(0, 99, 110, 0.4); /* Stronger shadow */
font-weight: 600;
letter-spacing: 0.02em;
transition: background-color 0.3s ease, transform 0.2s ease;
box-shadow: 0 4px 12px rgba(0, 99, 110, 0.3);
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
color: white;
transform: translateY(-2px); /* More pronounced lift */
box-shadow: 0 10px 20px rgba(0, 99, 110, 0.5);
transform: translateY(-1px);
}
/* ---------------------------------------------------------------------- */
/* 1. DARK HERO STYLING (High Contrast) */
/* ---------------------------------------------------------------------- */
.hero-section {
background: linear-gradient(135deg, var(--kaauh-dark-contrast) 0%, var(--kaauh-dark-bg) 100%);
padding: 4rem 0; /* Reduced from 8rem to 4rem */
margin-top: -1px;
color: white;
position: relative;
overflow: hidden;
}
.hero-title {
font-size: 2.5rem; /* Reduced from 3.5rem to 2.5rem */
font-weight: 800; /* Extra bold */
line-height: 1.1;
letter-spacing: -0.05em;
max-width: 900px;
}
.hero-section .lead {
font-size: 1.35rem; /* Larger lead text */
}
.btn-hero-action {
/* Primary CTA: Retained strong teal look */
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
font-weight: 600;
padding: 0.8rem 2.2rem;
border-radius: 50rem;
box-shadow: 0 4px 10px rgba(0, 99, 110, 0.4);
}
@media (min-width: 992px) {
.hero-section {
padding: 10rem 0;
}
.hero-title {
font-size: 5.5rem;
}
.bg-kaauh-teal-dark {
background-color: var(--kaauh-teal-dark) !important;
}
/* ---------------------------------------------------------------------- */
/* 2. PATH CARDS SECTION (New Segmented Entry) */
/* NEW: MESSAGES STYLING */
/* ---------------------------------------------------------------------- */
.path-card-section {
background-color: white;
margin-top: -80px; /* Pulls the section up over the dark hero for a modern layered look */
position: relative;
z-index: 50;
padding-top: 5rem;
padding-bottom: 5rem;
border-top-left-radius: 2rem;
border-top-right-radius: 2rem;
box-shadow: var(--kaauh-shadow); /* Defines the separation */
.message-container {
/* Position right below the sticky navbar (56px) with a small top margin */
margin-top: calc(var(--navbar-height) + 10px);
}
.path-card {
padding: 2rem;
border-radius: 1rem;
border: 1px solid var(--kaauh-border);
transition: all 0.3s ease;
height: 100%;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
.alert {
padding: 0.75rem 1.25rem;
border-radius: 0.5rem;
font-weight: 500;
box-shadow: 0 4px 8px rgba(0,0,0,0.08);
margin-bottom: 0; /* Handled by container margin */
border-left: 5px solid; /* Feature highlight */
}
.path-card:hover {
border-color: var(--kaauh-teal);
transform: translateY(-5px);
box-shadow: 0 15px 30px rgba(0, 99, 110, 0.1);
.alert-success {
color: var(--success);
background-color: #d1e7dd;
border-color: var(--success);
}
.path-card-icon {
color: var(--kaauh-teal);
font-size: 2.5rem;
margin-bottom: 1rem;
.alert-error, .alert-danger {
color: var(--danger);
background-color: #f8d7da;
border-color: var(--danger);
}
.path-card h5 {
font-weight: 700;
.alert-info {
color: var(--kaauh-teal-dark);
margin-bottom: 0.5rem;
background-color: #cff4fc;
border-color: var(--kaauh-teal);
}
/* ---------------------------------------------------------------------- */
/* 3. JOB LISTING & FILTER (Refined) */
/* LANGUAGE TOGGLE STYLES (COPIED FROM MAIN LAYOUT) */
/* ---------------------------------------------------------------------- */
.job-listing-section {
background-color: #f0f0f5; /* Separates the job list from the white path section */
padding-top: 3rem;
.language-toggle-btn {
color: var(--gray-text) !important; /* Use secondary color */
background: none !important;
border: none !important;
display: flex;
align-items: center;
gap: 0.3rem;
padding: 0.5rem 0.75rem !important;
font-weight: 500;
transition: all 0.2s ease;
}
.job-listing-card {
border: 1px solid var(--kaauh-border);
border-left: 6px solid var(--kaauh-teal);
border-radius: 0.75rem;
padding: 2rem !important;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); /* Lighter default shadow */
}
.job-listing-card:hover {
transform: translateY(-3px); /* Increased lift */
box-shadow: 0 12px 25px rgba(0, 99, 110, 0.15); /* Stronger hover shadow */
background-color: var(--kaauh-teal-light);
}
.card.sticky-top-filters {
box-shadow: var(--kaauh-shadow); /* Uses the deeper card shadow */
.language-toggle-btn:hover {
background: var(--light-bg) !important;
border-radius: 4px;
color: var(--kaauh-teal) !important;
}
/* RTL Corrections (retained) */
html[dir="rtl"] .alert { border-right: 5px solid; border-left: none; }
html[dir="rtl"] .job-listing-card { border-right: 6px solid var(--kaauh-teal); border-left: 1px solid var(--kaauh-border); }
</style>
/* Dropdown Menu styling for language */
.dropdown-menu {
backdrop-filter: blur(4px);
background-color: rgba(255, 255, 255, 0.98);
border: 1px solid var(--kaauh-border);
box-shadow: 0 6px 20px rgba(0,0,0,0.12);
border-radius: 8px;
padding: 0.5rem 0;
min-width: 150px;
}
.dropdown-item {
padding: 0.5rem 1.25rem;
transition: background-color 0.15s;
text-align: inherit; /* Ensure text alignment is controlled by dir="rtl" */
}
/* Use button as dropdown-item inside form for full click area */
.dropdown-item[type="submit"] {
width: 100%;
text-align: inherit;
border: none;
}
/* ---------------------------------------------------------------------- */
/* LAYOUT & STICKY POSITIONING FIXES (Desktop/Tablet) */
/* ---------------------------------------------------------------------- */
#topNavbar {
z-index: 1040; /* Higher than the bottom bar */
}
/* 1. Position the dark navbar below the white navbar + gap */
#bottomNavbar {
/* 56px (white nav) + 16px (mb-3) = 72px */
top: calc(var(--navbar-height) + var(--navbar-gap));
z-index: 1030;
}
/* 2. Pushes the main content down so it's not hidden under the navbars */
.main-content-area {
/* Total Sticky Height (128px) + Extra Margin (12px) = 140px */
margin-top: calc(var(--sticky-navbar-total-height) + 12px);
}
/* 3. Positions the sticky sidebar correctly */
.card.sticky-top {
/* Start scrolling the sidebar just below the two navbars + a small gap */
top: calc(var(--sticky-navbar-total-height) + 15px);
}
/* ---------------------------------------------------------------------- */
/* RTL / ARABIC SUPPORT - Optimized */
/* ---------------------------------------------------------------------- */
html[dir="rtl"] {
text-align: right;
}
/* Flip Margin Utilities (m-end and m-start) */
html[dir="rtl"] .ms-auto { margin-right: auto !important; margin-left: 0 !important; }
html[dir="rtl"] .me-auto { margin-left: auto !important; margin-right: 0 !important; }
html[dir="rtl"] .ms-2 { margin-right: 0.5rem !important; margin-left: 0 !important; }
html[dir="rtl"] .me-2 { margin-left: 0.5rem !important; margin-right: 0 !important; }
html[dir="rtl"] .me-1 { margin-left: 0.25rem !important; margin-right: 0 !important; } /* For the globe icon */
/* Flip alignment for text-end/text-start */
html[dir="rtl"] .text-end { text-align: left !important; }
html[dir="rtl"] .text-start { text-align: right !important; }
/* Flip border-left for RTL alerts */
html[dir="rtl"] .alert {
border-right: 5px solid;
border-left: none;
}
/* ---------------------------------------------------------------------- */
/* MOBILE RESPONSIVE STYLES (Below 992px) */
/* ---------------------------------------------------------------------- */
@media (max-width: 991.98px) {
/* Ensures dropdown items in mobile menu align correctly */
html[dir="rtl"] .navbar-collapse .dropdown-menu {
text-align: right;
left: auto;
right: 0;
}
/* On mobile, the top navbar is generally only 56px tall when collapsed. */
#bottomNavbar {
top: calc(var(--navbar-height) + var(--navbar-gap));
}
.main-content-area {
/* Reduced margin-top for smaller screens */
margin-top: calc(var(--sticky-navbar-total-height) / 2);
}
/* Mobile Fixed Footer Bar for Application */
.mobile-fixed-apply-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100%;
padding: 15px;
background-color: var(--light-bg);
border-top: 1px solid #ddd;
z-index: 1000;
box-shadow: 0 -4px 10px rgba(0, 0, 0, 0.08);
}
/* Add padding to the bottom of the body content to prevent it from hiding under the fixed bar */
body {
padding-bottom: 90px;
}
/* Fix job overview grid responsiveness (ensures 1 column) */
.row-cols-md-2 > .col {
flex: 0 0 100%;
max-width: 100%;
}
}
</style>
</head>
<body>
<nav id="topNavbar" class="navbar navbar-expand-lg sticky-top bg-white border-bottom" style="z-index: 1040;">
<nav id="topNavbar" class="navbar navbar-expand-lg sticky-top" style="background-color: white; z-index: 1040;">
<div class="container-fluid">
<a class="navbar-brand text-dark fw-bold" href="{% url 'kaauh_career' %}">
<a class="navbar-brand text-white fw-bold" href="{% url 'kaauh_career' %}">
<img src="{% static 'image/kaauh.jpeg' %}" alt="{% translate 'KAAUH IMAGE' %}" style="height: 50px; margin-right: 10px;">
KAAUH
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav"
aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
@ -187,11 +249,11 @@
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
{% comment %} <li class="nav-item">
<a class="nav-link text-secondary" href="{% url 'applicant_profile' %}">{% translate "Applications" %}</a>
</li> {% endcomment %}
<li class="nav-item">
<a class="nav-link text-secondary" href="{% url 'applicant_profile' %}">{% translate "Profile" %}</a>
<a class="nav-link text-secondary" href="{% url 'applicant_profile' %}">{% translate "Applications" %}</a>
</li>
<li class="nav-item">
<a class="nav-link text-secondary" href="/profile/">{% translate "Profile" %}</a>
</li>
<li class="nav-item">
<a class="nav-link text-secondary" href="{% url 'kaauh_career' %}">{% translate "Careers" %}</a>
@ -207,6 +269,7 @@
<ul class="dropdown-menu {% if LANGUAGE_CODE == 'ar' %}dropdown-menu-start{% else %}dropdown-menu-end{% endif %}" aria-labelledby="navbarLanguageDropdown">
{% comment %} English Button {% endcomment %}
<li>
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
@ -216,6 +279,7 @@
</form>
</li>
{% comment %} Arabic Button {% endcomment %}
<li>
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
@ -232,13 +296,9 @@
</nav>
{% block content %}
{% endblock content %}
{% block content %}
{% endblock content %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
{% block customJS %}
{% endblock %}
</body>
</html>

View File

@ -290,15 +290,14 @@
<main class="container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show mt-2" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button>
</div>
{% endfor %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show mt-2" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="{% trans 'Close' %}"></button>
</div>
{% endfor %}
{% endif %}
{% block content %}
{% endblock %}
</main>

View File

@ -1,19 +1,8 @@
{% load i18n %}
{{ form.media }}
<div class="row">
<div class="container-fluid">
<div class="row">
<div class="col-12">
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>
{{ message }}
</li>
{% endfor %}
</ul>
{% endif %}
<div class="card">
<div class="card-body">
@ -41,7 +30,25 @@
{% endif %}
</div>
<div class="mb-3">
<label class="form-label fw-bold">
{% trans "Recipients" %}
</label>
<div class="border rounded p-3 bg-light" style="max-height: 200px; overflow-y: auto;">
{% for choice in form.recipients %}
<div class="form-check mb-2">
{{ choice }}
</div>
{% endfor %}
</div>
{% if form.recipients.errors %}
<div class="text-danger small mt-1">
{% for error in form.recipients.errors %}
<span>{{ error }}</span>
{% endfor %}
</div>
{% endif %}
</div>
<!-- Subject Field -->
<div class="mb-3">
<label for="{{ form.subject.id_for_label }}" class="form-label fw-bold">
@ -73,6 +80,37 @@
</div>
{% endif %}
</div>
{% comment %}
<!-- Options Checkboxes -->
<div class="mb-4">
<div class="row">
<div class="col-md-6">
<div class="form-check">
<input type="checkbox"
class="form-check-input"
name="{{ form.include_candidate_info.name }}"
id="{{ form.include_candidate_info.id_for_label }}"
{% if form.include_candidate_info.value %}checked{% endif %}>
<label class="form-check-label" for="{{ form.include_candidate_info.id_for_label }}">
{{ form.include_candidate_info.label }}
</label>
</div>
</div>
<div class="col-md-6">
<div class="form-check">
<input type="checkbox"
class="form-check-input"
name="{{ form.include_meeting_details.name }}"
id="{{ form.include_meeting_details.id_for_label }}"
{% if form.include_meeting_details.value %}checked{% endif %}>
<label class="form-check-label" for="{{ form.include_meeting_details.id_for_label }}">
{{ form.include_meeting_details.label }}
</label>
</div>
</div>
</div>
</div> {% endcomment %}
<!-- Form Actions -->
<div class="d-flex justify-content-between align-items-center">

View File

@ -1,5 +0,0 @@
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<button type="submit">Save Participants</button>
</form>

View File

@ -1,167 +1,82 @@
{% extends "base.html" %}
{% load static %}
{%load i18n %}
{% block customCSS %}
<style>
/* Custom Teal Theme Variables (Adapt these if defined globally) */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-light: #e0f2f4; /* Very light background accent */
--kaauh-teal-dark: #004a53;
--kaauh-border-color: #e3e8ed;
--kaauh-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
}
/* Primary Theme Utilities */
.text-primary-theme { color: var(--kaauh-teal) !important; }
.bg-primary-theme-light { background-color: var(--kaauh-teal-light) !important; }
.border-primary-theme { border-color: var(--kaauh-teal) !important; }
/* Custom Button Style */
.btn-teal-primary {
background-color: var(--kaauh-teal);
color: white;
border-color: var(--kaauh-teal);
transition: all 0.2s ease;
}
.btn-teal-primary:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
color: white;
}
/* Layout and Typography */
.page-header {
font-weight: 700;
color: var(--kaauh-teal-dark);
border-left: 5px solid var(--kaauh-teal);
padding-left: 15px;
}
/* Card Styling */
.schedule-card {
border: 1px solid var(--kaauh-border-color);
border-radius: 1rem;
box-shadow: var(--kaauh-shadow);
transition: box-shadow 0.3s ease;
padding: 0; /* Control padding inside body */
}
.schedule-card:hover {
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1);
}
.card-title-border {
font-weight: 600;
padding-bottom: 0.75rem;
border-bottom: 2px solid var(--kaauh-teal);
margin-bottom: 1.5rem;
color: var(--kaauh-teal);
}
/* Break Times Display */
.break-time-container {
border-left: 3px solid var(--kaauh-teal);
border-radius: 0.5rem;
padding: 10px 15px;
background-color: var(--kaauh-teal-light);
}
/* FullCalendar Customization */
#calendar {
font-size: 0.9em;
}
.fc-event-main-frame {
color: var(--kaauh-teal-dark);
font-weight: 500;
}
/* Event bar color (Candidates) */
.fc-event-title-container {
background-color: var(--kaauh-teal-light);
border-left: 3px solid var(--kaauh-teal);
padding: 2px 5px;
border-radius: 3px;
}
/* Break background color is set in JS events */
</style>
{% endblock %}
{% block content %}
<div class="container py-5">
<div class="d-flex justify-content-between align-items-center mb-5">
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 page-header">
<i class="fas fa-calendar-alt me-2 text-primary-theme"></i> Interview Schedule Preview: **{{ job.title }}**
<i class="fas fa-calendar-check me-2"></i> Interview Schedule Preview for {{ job.title }}
</h1>
</div>
<div class="card schedule-card mb-5">
<div class="card-body p-4 p-lg-5">
<h4 class="card-title-border">{% trans "Schedule Parameters" %}</h4>
<div class="row g-4">
<div class="card mt-4 shadow-sm">
<div class="card-body">
<h5 class="card-title pb-2 border-bottom">Schedule Details</h5>
<div class="row">
<div class="col-md-6">
<p class="mb-2"><strong><i class="fas fa-clock me-2 text-primary-theme"></i> Working Hours:</strong> {{ start_time|time:"g:i A" }} to {{ end_time|time:"g:i A" }}</p>
<p class="mb-2"><strong><i class="fas fa-hourglass-half me-2 text-primary-theme"></i> Interview Duration:</strong> {{ interview_duration }} minutes</p>
<p class="mb-2"><strong><i class="fas fa-shield-alt me-2 text-primary-theme"></i> Buffer Time:</strong> {{ buffer_time }} minutes</p>
</div>
<div class="col-md-6">
<p class="mb-2"><strong><i class="fas fa-calendar-day me-2 text-primary-theme"></i> Interview Period:</strong> {{ start_date|date:"F j, Y" }} &mdash; {{ end_date|date:"F j, Y" }}</p>
<p class="mb-2"><strong><i class="fas fa-list-check me-2 text-primary-theme"></i> Active Days:</strong>
<p><strong>Period:</strong> {{ start_date|date:"F j, Y" }} to {{ end_date|date:"F j, Y" }}</p>
<p>
<strong>Working Days:</strong>
{% for day_id in working_days %}
{% if day_id == 0 %}Mon{% endif %}
{% if day_id == 1 %}Tue{% endif %}
{% if day_id == 2 %}Wed{% endif %}
{% if day_id == 3 %}Thu{% endif %}
{% if day_id == 4 %}Fri{% endif %}
{% if day_id == 5 %}Sat{% endif %}
{% if day_id == 6 %}Sun{% endif %}
{% if day_id == 0 %}Monday{% endif %}
{% if day_id == 1 %}Tuesday{% endif %}
{% if day_id == 2 %}Wednesday{% endif %}
{% if day_id == 3 %}Thursday{% endif %}
{% if day_id == 4 %}Friday{% endif %}
{% if day_id == 5 %}Saturday{% endif %}
{% if day_id == 6 %}Sunday{% endif %}
{% if not forloop.last %}, {% endif %}
{% endfor %}
</p>
<p><strong>Working Hours:</strong> {{ start_time|time:"g:i A" }} to {{ end_time|time:"g:i A" }}</p>
<p><strong>Interview Duration:</strong> {{ interview_duration }} minutes</p>
<p><strong>Buffer Time:</strong> {{ buffer_time }} minutes</p>
</div>
<div class="col-md-6">
<p class="mb-2"><strong>Daily Break Times:</strong></p>
{% if breaks %}
<!-- New structured display for breaks -->
<div class="d-flex flex-column gap-1 mb-3 p-3 border rounded bg-light">
{% for break in breaks %}
<small class="text-dark">
<i class="far fa-clock me-1 text-muted"></i>
{{ break.start_time|time:"g:i A" }} &mdash; {{ break.end_time|time:"g:i A" }}
</small>
{% endfor %}
</div>
{% else %}
<p class="mb-3"><small class="text-muted">No daily breaks scheduled.</small></p>
{% endif %}
</div>
</div>
<h5 class="mt-4 pt-3 border-top">{% trans "Daily Break Times" %}</h5>
{% if breaks %}
<div class="d-flex flex-wrap gap-3 mt-3">
{% for break in breaks %}
<span class="badge rounded-pill bg-primary-theme-light text-primary-theme p-2 px-3 fw-normal shadow-sm">
<i class="far fa-mug-hot me-1"></i>
{{ break.start_time|time:"g:i A" }} &mdash; {{ break.end_time|time:"g:i A" }}
</span>
{% endfor %}
</div>
{% else %}
<p class="mt-3"><small class="text-muted"><i class="fas fa-exclamation-circle me-1"></i> No daily breaks scheduled.</small></p>
{% endif %}
</div>
</div>
<div class="card schedule-card">
<div class="card-body p-4 p-lg-5">
<h4 class="card-title-border">{% trans "Scheduled Interviews Overview" %}</h4>
<div class="card mt-4 shadow-sm">
<div class="card-body">
<h5 class="card-title pb-2 border-bottom">Scheduled Interviews</h5>
<div id="calendar-container" class="mb-5 p-3 border rounded bg-light">
<!-- Calendar View -->
<div id="calendar-container">
<div id="calendar"></div>
</div>
<h5 class="pb-2 border-bottom mb-3 text-primary-theme">{% trans "Detailed List" %}</h5>
<div class="table-responsive">
<table class="table table-hover table-striped">
<thead class="bg-primary-theme-light">
<!-- List View -->
<div class="table-responsive mt-4">
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Date</th>
<th scope="col">Time</th>
<th scope="col">Candidate</th>
<th scope="col">Email</th>
<th>Date</th>
<th>Time</th>
<th>Candidate</th>
<th>Email</th>
</tr>
</thead>
<tbody>
{% for item in schedule %}
<tr>
<td>{{ item.date|date:"F j, Y" }}</td>
<td class="fw-bold text-primary-theme">{{ item.time|time:"g:i A" }}</td>
<td>{{ item.time|time:"g:i A" }}</td>
<td>{{ item.candidate.name }}</td>
<td>{{ item.candidate.email }}</td>
</tr>
@ -170,19 +85,20 @@
</table>
</div>
<form method="post" action="{% url 'confirm_schedule_interviews_view' slug=job.slug %}" class="mt-4 d-flex justify-content-end gap-3">
<form method="post" action="{% url 'confirm_schedule_interviews_view' slug=job.slug %}" class="mt-4">
{% csrf_token %}
<a href="{% url 'schedule_interviews' slug=job.slug %}" class="btn btn-secondary px-4">
<i class="fas fa-arrow-left me-2"></i> {% trans "Back to Edit" %}
</a>
<button type="submit" name="confirm_schedule" class="btn btn-teal-primary px-4">
<i class="fas fa-check me-2"></i> {% trans "Confirm Schedule" %}
<button type="submit" name="confirm_schedule" class="btn btn-success">
<i class="fas fa-check"></i> Confirm Schedule
</button>
<a href="{% url 'schedule_interviews' slug=job.slug %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Edit
</a>
</form>
</div>
</div>
</div>
<!-- Include FullCalendar CSS and JS -->
<link href="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.10.1/main.min.js"></script>
@ -202,8 +118,6 @@ document.addEventListener('DOMContentLoaded', function() {
title: '{{ item.candidate.name }}',
start: '{{ item.date|date:"Y-m-d" }}T{{ item.time|time:"H:i:s" }}',
url: '#',
// Use the theme color for candidate events
color: 'var(--kaauh-teal-dark)',
extendedProps: {
email: '{{ item.candidate.email }}',
time: '{{ item.time|time:"g:i A" }}'
@ -213,36 +127,27 @@ document.addEventListener('DOMContentLoaded', function() {
{% for break in breaks %}
{
title: 'Break',
// FullCalendar requires a specific date for breaks, using start_date as a placeholder for daily breaks.
// Note: Breaks displayed on the monthly grid will only show on start_date, but weekly/daily view should reflect it daily if implemented correctly in the backend or using recurring events.
start: '{{ start_date|date:"Y-m-d" }}T{{ break.start_time|time:"H:i:s" }}',
end: '{{ start_date|date:"Y-m-d" }}T{{ break.end_time|time:"H:i:s" }}',
color: '#ff9f89', // A nice soft orange/salmon color for breaks
color: '#ff9f89',
display: 'background'
},
{% endfor %}
],
eventClick: function(info) {
// Log details to console instead of using alert()
// Show candidate details in a modal or alert
if (info.event.title !== 'Break') {
console.log('--- Candidate Interview Details ---');
console.log('Candidate: ' + info.event.title);
console.log('Date: ' + info.event.start.toLocaleDateString());
console.log('Time: ' + info.event.extendedProps.time);
console.log('Email: ' + info.event.extendedProps.email);
console.log('-----------------------------------');
// You would typically open a Bootstrap modal here instead of using console.log
// IMPORTANT: Since alert() is forbidden, using console log as a fallback.
// In a production environment, this would be a custom modal dialog.
console.log('Candidate: ' + info.event.title +
'\nDate: ' + info.event.start.toLocaleDateString() +
'\nTime: ' + info.event.extendedProps.time +
'\nEmail: ' + info.event.extendedProps.email);
}
info.jsEvent.preventDefault();
},
eventDidMount: function(info) {
// Darken the text for background events (breaks) for better contrast
if (info.event.display === 'background') {
info.el.style.backgroundColor = '#ff9f89';
}
}
});
calendar.render();
});
</script>
{% endblock %}
{% endblock %}

View File

@ -1,6 +1,5 @@
{% extends 'base.html' %}
{% load static i18n %}
{% load widget_tweaks %}
{% block customCSS %}
<style>
/* -------------------------------------------------------------------------- */
@ -249,14 +248,10 @@ body {
<div class="p-3 bg-white rounded shadow-sm h-100 d-flex flex-column">
<h2 class="text-start"><i class="fas fa-briefcase me-2"></i> {% trans "Interview Detail" %}</h2>
<div class="detail-row-group flex-grow-1">
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Job Title" %}:</div><div class="detail-value-simple"><a class="text-decoration-none text-dark" href="{% url 'job_detail' meeting.get_job.slug %}">{{ meeting.get_job.title|default:"N/A" }}</a></div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Candidate Name" %}:</div><div class="detail-value-simple"><a class="text-decoration-none text-dark" href="{% url 'candidate_detail' meeting.get_candidate.slug %}">{{ meeting.get_candidate.name|default:"N/A" }}</a></div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Candidate Email" %}:</div><div class="detail-value-simple"><a class="text-decoration-none text-dark" href="{% url 'candidate_detail' meeting.get_candidate.slug %}">{{ meeting.get_candidate.email|default:"N/A" }}</a></div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Job Title" %}:</div><div class="detail-value-simple">{{ meeting.get_job.title|default:"N/A" }}</div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Candidate Name" %}:</div><div class="detail-value-simple"><a href="">{{ meeting.get_candidate.name|default:"N/A" }}</a></div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Candidate Email" %}:</div><div class="detail-value-simple"><a href="">{{ meeting.get_candidate.email|default:"N/A" }}</a></div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Job Type" %}:</div><div class="detail-value-simple">{{ meeting.get_job.job_type|default:"N/A" }}</div></div>
{% if meeting.get_candidate.belong_to_agency %}
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Agency" %}:</div><div class="detail-value-simple"><a href="">{{ meeting.get_candidate.hiring_agency.name|default:"N/A" }}</a></div></div>
{% endif %}
</div>
</div>
</div>
@ -300,24 +295,7 @@ body {
{# --- PARTICIPANTS TABLE --- #}
<div class="col-lg-12">
<div class="p-3 bg-white rounded shadow-sm">
<div class="d-flex justify-content-between align-item-center" >
<h2 class="text-start"><i class="fas fa-users-cog me-2"></i> {% trans "Assigned Participants" %}</h2>
<!--manage participants for interview-->
<div class="d-flex justify-content-center align-item-center">
<button type="button" class="btn btn-primary-teal btn-sm me-2"
data-bs-toggle="modal"
data-bs-target="#assignParticipants">
<i class="fas fa-users-cog me-1"></i> {% trans "Manage Participants" %} ({{total_participants}})
</button>
<button type="button" class="btn btn-outline-info"
data-bs-toggle="modal"
title="Send Interview Emails"
data-bs-target="#emailModal">
<i class="fas fa-envelope"></i>
</button>
</div>
</div>
<h2 class="text-start"><i class="fas fa-users-cog me-2"></i> {% trans "Assigned Participants" %}</h2>
<table class="simple-table">
<thead>
@ -458,139 +436,6 @@ body {
</div>
</div>
<div class="modal fade" id="assignParticipants" tabindex="-1" aria-labelledby="assignParticipantsLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="jobAssignmentLabel">{% trans "Manage all participants" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{% url 'create_interview_participants' meeting.interview.slug %}">
{% csrf_token %}
<div class="modal-body table-responsive">
{{ meeting.name }}
<hr>
<table class="table tab table-bordered mt-3">
<thead>
<th class="col">👥 {% trans "Participants" %}</th>
<th class="col">🧑‍💼 {% trans "Users" %}</th>
</thead>
<tbody>
<tr>
<td>
{{ form.participants.errors }}
{{ form.participants }}
</td>
<td> {{ form.system_users.errors }}
{{ form.system_users }}
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger-red" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-primary-teal btn-sm">{% trans "Save" %}</button>
</div>
</form>
</div>
</div>
</div>
<!--email modal class-->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-light">
<h5 class="modal-title" id="emailModalLabel">📧 {% trans "Compose Interview Invitation" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{% url 'send_interview_email' meeting.interview.slug %}">
{% csrf_token %}
<div class="modal-body">
<div class="mb-3">
<label for="{{ email_form.subject.id_for_label }}" class="form-label fw-bold">Subject</label>
{{ email_form.subject | add_class:"form-control" }}
</div>
<ul class="nav nav-tabs" id="messageTabs" role="tablist">
{# Candidate/Agency Tab - Active by default #}
<li class="nav-item" role="presentation">
<button class="nav-link active" id="candidate-tab" data-bs-toggle="tab" data-bs-target="#candidate-pane" type="button" role="tab" aria-controls="candidate-pane" aria-selected="true">
{% if candidate.belong_to_an_agency %}
Agency Message
{% else %}
Candidate Message
{% endif %}
</button>
</li>
{# Participants Tab #}
<li class="nav-item" role="presentation">
<button class="nav-link" id="participants-tab" data-bs-toggle="tab" data-bs-target="#participants-pane" type="button" role="tab" aria-controls="participants-pane" aria-selected="false">
Panel Message (Interviewers)
</button>
</li>
</ul>
<div class="tab-content border border-top-0 p-3 bg-light-subtle">
{# --- Candidate/Agency Pane --- #}
<div class="tab-pane fade show active" id="candidate-pane" role="tabpanel" aria-labelledby="candidate-tab">
<p class="text-muted small">This email will be sent to the candidate or their hiring agency.</p>
{% if not candidate.belong_to_an_agency %}
<div class="form-group">
<label for="{{ email_form.message_for_candidate.id_for_label }}" class="form-label d-none">Candidate Message</label>
{{ email_form.message_for_candidate | add_class:"form-control" }}
</div>
{% endif %}
{% if candidate.belong_to_an_agency %}
<div class="form-group">
<label for="{{ email_form.message_for_agency.id_for_label }}" class="form-label d-none">Agency Message</label>
{{ email_form.message_for_agency | add_class:"form-control" }}
</div>
{% endif %}
</div>
{# --- Participants Pane --- #}
<div class="tab-pane fade" id="participants-pane" role="tabpanel" aria-labelledby="participants-tab">
<p class="text-muted small">This email will be sent to the internal and external interview participants.</p>
<div class="form-group">
<label for="{{ email_form.message_for_participants.id_for_label }}" class="form-label d-none">Participants Message</label>
{{ email_form.message_for_participants | add_class:"form-control" }}
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger-red" data-bs-dismiss="modal">Close</button>
<button type="submit" class="btn btn-primary-teal">Send Invitation</button>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>

View File

@ -229,17 +229,6 @@
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
</button>
<button type="button" class="btn btn-outline-info btn-sm"
data-bs-toggle="modal"
hx-boost='true'
data-bs-target="#emailModal"
hx-get="{% url 'compose_candidate_email' job.slug %}"
hx-target="#emailModalBody"
hx-include="#candidate-form"
title="Email Participants">
<i class="fas fa-envelope"></i>
</button>
</div>
</form>
</div>
@ -369,28 +358,6 @@
</div>
</div>
</div>
<!-- Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="emailModalLabel" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-envelope me-2"></i>{% trans "Compose Email" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div id="emailModalBody" class="modal-body">
<div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
{% trans "Loading email form..." %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -225,63 +225,10 @@
</div>
<div class="kaauh-card shadow-sm p-3">
{% if candidates %}
<div class="bulk-action-bar p-3 bg-light border-bottom">
<form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post" class="action-group">
{% csrf_token %}
{# MODIFIED: Using d-flex for horizontal alignment and align-items-end to align everything based on the baseline of the button/select #}
<div class="d-flex align-items-end gap-3">
{# Select Input Group #}
<div>
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="min-width: 150px;">
<option selected>
----------
</option>
<option value="Offer">
{% trans "Offer Stage" %}
</option>
{# Include other options here, such as Interview, Offer, Rejected, etc. #}
</select>
</div>
{# Button #}
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
</button>
{# email button#}
<button type="button" class="btn btn-outline-info btn-sm"
data-bs-toggle="modal"
hx-boost='true'
data-bs-target="#emailModal"
hx-get="{% url 'compose_candidate_email' job.slug %}"
hx-target="#emailModalBody"
hx-include="#candidate-form"
title="Email Participants">
<i class="fas fa-envelope"></i>
</button>
</div>
</form>
</div>
{% endif %}
<div class="table-responsive">
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get">
{% csrf_token %}
<table class="table candidate-table align-middle">
<thead>
<tr>
<th style="width: 2%">
{% if candidates %}
<div class="form-check">
<input
type="checkbox" class="form-check-input" id="selectAllCheckbox">
</div>
{% endif %}
</th>
<th style="width: 15%"><i class="fas fa-user me-1"></i> {% trans "Name" %}</th>
<th style="width: 15%"><i class="fas fa-phone me-1"></i> {% trans "Contact" %}</th>
<th style="width: 15%"><i class="fas fa-briefcase me-1"></i> {% trans "Applied Position" %}</th>
@ -293,12 +240,6 @@
<tbody>
{% for candidate in candidates %}
<tr>
<td>
<div class="form-check">
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
</div>
</td>
<td>
<div class="candidate-name">
{{ candidate.name }}
@ -347,15 +288,12 @@
title="View Resume Template">
<i class="fas fa-file-alt"></i>
</a>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</form>
{% if not candidates %}
<div class="alert alert-info text-center mt-3 mb-0" role="alert">
<i class="fas fa-info-circle me-1"></i>
@ -409,76 +347,13 @@
</div>
</div>
<!-- Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="emailModalLabel" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-envelope me-2"></i>{% trans "Compose Email" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div id="emailModalBody" class="modal-body">
<div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
{% trans "Loading email form..." %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function () {
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
if (selectAllCheckbox) {
// Function to safely update the header checkbox state
function updateSelectAllState() {
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
const totalCount = rowCheckboxes.length;
if (checkedCount === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (checkedCount === totalCount) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
}
}
// 1. Logic for the 'Select All' checkbox (Clicking it updates all rows)
selectAllCheckbox.addEventListener('change', function () {
const isChecked = selectAllCheckbox.checked;
rowCheckboxes.forEach(checkbox => checkbox.removeEventListener('change', updateSelectAllState));
rowCheckboxes.forEach(function (checkbox) {
checkbox.checked = isChecked;
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
});
rowCheckboxes.forEach(checkbox => checkbox.addEventListener('change', updateSelectAllState));
updateSelectAllState();
});
// 2. Logic to update 'Select All' state based on row checkboxes
rowCheckboxes.forEach(function (checkbox) {
checkbox.addEventListener('change', updateSelectAllState);
});
// Initial check to set the correct state on load (in case items are pre-checked)
updateSelectAllState();
}
document.addEventListener('DOMContentLoaded', function () {
// Add any specific JavaScript for hired candidates view if needed
console.log('Hired candidates view loaded');
});
function syncHiredCandidates() {

View File

@ -233,7 +233,12 @@
</button>
</form>
<div class="vr" style="height: 28px;"></div>
<!--manage participants for interview-->
<button type="button" class="btn btn-main-action btn-sm"
data-bs-toggle="modal"
data-bs-target="#jobAssignmentModal">
<i class="fas fa-users-cog me-1"></i> {% trans "Manage Participants" %} ({{participants_count}})
</button>
<button type="button" class="btn btn-outline-info btn-sm"
data-bs-toggle="modal"
@ -244,7 +249,7 @@
hx-include="#candidate-form"
title="Email Participants">
<i class="fas fa-envelope"></i>
</button>
</button>
</div>
</div>
{% endif %}
@ -409,7 +414,7 @@
data-modal-title="{% trans 'Schedule Interview' %}"
title="Schedule Interview">
<i class="fas fa-calendar-plus"></i>
</button>
</button>
{% endif %}
</td>
@ -448,7 +453,59 @@
</div>
<div class="modal fade" id="jobAssignmentModal" tabindex="-1" aria-labelledby="jobAssignmentLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="jobAssignmentLabel">{% trans "Manage all participants" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<form method="post" action="{% url 'candidate_interview_view' job.slug %}">
{% csrf_token %}
<div class="modal-body table-responsive">
{{ job.internal_job_id }} {{ job.title}}
<hr>
<table class="table tab table-bordered mt-3">
<thead>
<th class="col">👥 {% trans "Participants" %}</th>
<th class="col">🧑‍💼 {% trans "Users" %}</th>
</thead>
<tbody>
<tr>
<td>
{{ form.participants.errors }}
{{ form.participants }}
</td>
<td> {{ form.users.errors }}
{{ form.users }}
</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-main-action">{% trans "Save" %}</button>
</div>
</form>
</div>
</div>
</div>
<!-- Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">

View File

@ -221,24 +221,10 @@
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
</button>
</form>
{# Separator (Vertical Rule) #}
<div class="vr" style="height: 28px;"></div>
<button type="button" class="btn btn-outline-info btn-sm"
data-bs-toggle="modal"
hx-boost='true'
data-bs-target="#emailModal"
hx-get="{% url 'compose_candidate_email' job.slug %}"
hx-target="#emailModalBody"
hx-include="#candidate-form"
title="Email Participants">
<i class="fas fa-envelope"></i>
</button>
</div>
</div>
@ -352,28 +338,6 @@
</div>
</div>
<!-- Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="emailModalLabel" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-envelope me-2"></i>{% trans "Compose Email" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div id="emailModalBody" class="modal-body">
<div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
{% trans "Loading email form..." %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}

View File

@ -162,6 +162,18 @@
font-size: 0.8rem !important; /* Slightly smaller font */
}
<<<<<<< HEAD
.kaats-spinner {
animation: kaats-spinner-rotate 1.5s linear infinite; /* Faster rotation */
width: 40px; /* Standard size */
height: 40px;
display: inline-block; /* Useful for table cells */
vertical-align: middle;
}
=======
>>>>>>> f71a202ed3606d299f9ac6515247662b6d3370b4
.kaats-spinner .path {
stroke: var(--kaauh-teal, #00636e); /* Use Teal color, fallback to dark teal */
@ -328,17 +340,6 @@
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}
</button>
{# email button#}
<button type="button" class="btn btn-outline-info btn-sm"
data-bs-toggle="modal"
hx-boost='true'
data-bs-target="#emailModal"
hx-get="{% url 'compose_candidate_email' job.slug %}"
hx-target="#emailModalBody"
hx-include="#candidate-form"
title="Email Participants">
<i class="fas fa-envelope"></i>
</button>
</div>
</form>
@ -497,26 +498,6 @@
</div>
</div>
</div>
<!-- Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content kaauh-card">
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
<h5 class="modal-title" id="emailModalLabel" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-envelope me-2"></i>{% trans "Compose Email" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div id="emailModalBody" class="modal-body">
<div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
{% trans "Loading email form..." %}
</div>
</div>
</div>
</div>
</div>
{% endblock %}