Compare commits

..

No commits in common. "a1d2dcebfb4cb92335ad25e377b09e05d8019ddd" and "4ee0406453872178dbffd6e204a901b0b7df7881" have entirely different histories.

25 changed files with 861 additions and 776 deletions

1
.gitignore vendored
View File

@ -54,7 +54,6 @@ htmlcov/
# Media and Static files (if served locally and not meant for version control)
media/
static/
# Deployment files
*.tar.gz

View File

@ -28,7 +28,7 @@ urlpatterns = [
path('application/<slug:slug>/apply/', views.application_detail, name='application_detail'),
path('application/<slug:slug>/signup/', views.candidate_signup, name='candidate_signup'),
path('application/<slug:slug>/success/', views.application_success, name='application_success'),
# path('application/applicant/profile', views.applicant_profile, name='applicant_profile'),
path('application/applicant/profile', views.applicant_profile, name='applicant_profile'),
path('api/templates/', views.list_form_templates, name='list_form_templates'),
path('api/templates/save/', views.save_form_template, name='save_form_template'),

View File

@ -1,24 +1,15 @@
"""
Email service for sending notifications related to agency messaging.
"""
from .models import Application
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__)
from django.core.mail import send_mail, EmailMultiAlternatives
from django.conf import settings
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.contrib.auth import get_user_model
import logging
from .models import Message
logger = logging.getLogger(__name__)
User=get_user_model()
class EmailService:
"""
@ -234,10 +225,17 @@ def send_interview_invitation_email(candidate, job, meeting_details=None, recipi
return {'success': False, 'error': error_msg}
from .models import Application
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,job=None):
def send_bulk_email(subject, message, recipient_list, request=None, attachments=None, async_task_=False, from_interview=False):
"""
Send bulk email to multiple recipients with HTML support and attachments,
supporting synchronous or asynchronous dispatch.
@ -303,8 +301,7 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
processed_attachments = attachments if attachments else []
task_ids = []
job_id=job.id
sender_user_id=request.user.id if request and hasattr(request, 'user') and request.user.is_authenticated else None
if not from_interview:
# Loop through ALL final customized sends
for recipient_email, custom_message in customized_sends:
@ -314,10 +311,7 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
custom_message, # Pass the custom message
[recipient_email], # Pass the specific recipient as a list of one
processed_attachments,
sender_user_id,
job_id,
hook='recruitment.tasks.email_success_hook',
hook='recruitment.tasks.email_success_hook'
)
task_ids.append(task_id)
@ -356,101 +350,80 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
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)}"}
else:
# --- 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.
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa')
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:
# 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.
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa')
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:
result=email_obj.send(fail_silently=False)
if result==1:
try:
user=get_object_or_404(User,email=recipient)
new_message = Message.objects.create(
sender=request.user,
recipient=user,
job=job,
subject=subject,
content=message, # Store the full HTML or plain content
message_type='DIRECT',
is_read=False, # It's just sent, not read yet
)
logger.info(f"Stored sent message ID {new_message.id} in DB.")
except Exception as e:
logger.error(f"Email sent to {recipient}, but failed to store in DB: {str(e)}")
else:
logger.error("fialed to send email")
successful_sends += 1
except Exception as e:
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
if not from_interview:
# Send Emails - Pure Candidates
for email in pure_candidate_emails:
candidate_name = Application.objects.filter(person__email=email).first().person.full_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 = Application.objects.filter(person__email=candidate_email).first().person.full_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.'
}
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).'
}
email_obj.send(fail_silently=False)
successful_sends += 1
except Exception as e:
error_msg = f"Failed to process bulk email send request: {str(e)}"
logger.error(error_msg, exc_info=True)
return {'success': False, 'error': error_msg}
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
if not from_interview:
# Send Emails - Pure Candidates
for email in pure_candidate_emails:
candidate_name = Application.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 = Application.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.'
}
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).'
}
except Exception as e:
error_msg = f"Failed to process bulk email send request: {str(e)}"
logger.error(error_msg, exc_info=True)
return {'success': False, 'error': error_msg}

View File

@ -1621,7 +1621,7 @@ class CandidateEmailForm(forms.Form):
elif candidate and 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 you have cleared your exam!",
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",
@ -1698,7 +1698,6 @@ class CandidateEmailForm(forms.Form):
return message
class InterviewParticpantsForm(forms.ModelForm):
participants = forms.ModelMultipleChoiceField(
queryset=Participants.objects.all(),
@ -1707,7 +1706,7 @@ class InterviewParticpantsForm(forms.ModelForm):
)
system_users=forms.ModelMultipleChoiceField(
queryset=User.objects.filter(user_type='staff'),
queryset=User.objects.all(),
widget=forms.CheckboxSelectMultiple,
required=False,
label=_("Select Users"))
@ -1862,107 +1861,107 @@ class InterviewParticpantsForm(forms.ModelForm):
# self.initial['message_for_participants'] = participants_message.strip()
# class InterviewEmailForm(forms.Form):
# # ... (Field definitions)
class InterviewEmailForm(forms.Form):
# ... (Field definitions)
# def __init__(self, *args, candidate, external_participants, system_participants, meeting, job, **kwargs):
# super().__init__(*args, **kwargs)
def __init__(self, *args, candidate, external_participants, system_participants, meeting, job, **kwargs):
super().__init__(*args, **kwargs)
# location = meeting
location = meeting.interview_location
# # --- Data Preparation ---
# --- Data Preparation ---
# # Safely access details through the related InterviewLocation object
# if location and location.start_time:
# formatted_date = location.start_time.strftime('%Y-%m-%d')
# formatted_time = location.start_time.strftime('%I:%M %p')
# duration = location.duration
# meeting_link = location.details_url if location.details_url else "N/A (See Location Topic)"
# else:
# # Handle case where location or time is missing/None
# formatted_date = "TBD - Awaiting Scheduling"
# formatted_time = "TBD"
# duration = "N/A"
# meeting_link = "Not Available"
# Safely access details through the related InterviewLocation object
if location and location.start_time:
formatted_date = location.start_time.strftime('%Y-%m-%d')
formatted_time = location.start_time.strftime('%I:%M %p')
duration = location.duration
meeting_link = location.details_url if location.details_url else "N/A (See Location Topic)"
else:
# Handle case where location or time is missing/None
formatted_date = "TBD - Awaiting Scheduling"
formatted_time = "TBD"
duration = "N/A"
meeting_link = "Not Available"
# job_title = job.title
# agency_name = candidate.hiring_agency.name if candidate.belong_to_an_agency and candidate.hiring_agency else "Hiring Agency"
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 ])
# participant_names = ", ".join(filter(None, [external_participants_names, system_participants_names]))
# --- 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 ])
participant_names = ", ".join(filter(None, [external_participants_names, system_participants_names]))
# # --- 1. Candidate Message (Use meeting_link) ---
# candidate_message = f"""
# Dear {candidate.full_name},
# --- 1. Candidate Message (Use meeting_link) ---
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!
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:
The details of your virtual interview are as follows:
# - **Date:** {formatted_date}
# - **Time:** {formatted_time} (RIYADH TIME)
# - **Duration:** {duration}
# - **Meeting Link:** {meeting_link}
- **Date:** {formatted_date}
- **Time:** {formatted_time} (RIYADH TIME)
- **Duration:** {duration}
- **Meeting Link:** {meeting_link}
# Please click the link at the scheduled time to join the interview.
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.
Kindly reply to this email to **confirm your attendance** or to propose an alternative time if necessary.
# We look forward to meeting you.
We look forward to meeting you.
# Best regards,
# KAAUH Hiring Team
# """
# # ... (Messages for agency and participants remain the same, using the updated safe variables)
Best regards,
KAAUH Hiring Team
"""
# ... (Messages for agency and participants remain the same, using the updated safe variables)
# # --- 2. Agency Message (Professional and clear details) ---
# agency_message = f"""
# Dear {agency_name},
# ...
# **Interview Details:**
# ...
# - **Date:** {formatted_date}
# - **Time:** {formatted_time} (RIYADH TIME)
# - **Duration:** {duration}
# - **Meeting Link:** {meeting_link}
# ...
# """
# --- 2. Agency Message (Professional and clear details) ---
agency_message = f"""
Dear {agency_name},
...
**Interview Details:**
...
- **Date:** {formatted_date}
- **Time:** {formatted_time} (RIYADH TIME)
- **Duration:** {duration}
- **Meeting Link:** {meeting_link}
...
"""
# # --- 3. Participants Message (Action-oriented and informative) ---
# participants_message = f"""
# Hi Team,
# ...
# **Interview Summary:**
# --- 3. Participants Message (Action-oriented and informative) ---
participants_message = f"""
Hi Team,
...
**Interview Summary:**
# - **Candidate:** {candidate.full_name}
# - **Date:** {formatted_date}
# - **Time:** {formatted_time} (RIYADH TIME)
# - **Duration:** {duration}
# - **Your Fellow Interviewers:** {participant_names}
- **Candidate:** {candidate.full_name}
- **Date:** {formatted_date}
- **Time:** {formatted_time} (RIYADH TIME)
- **Duration:** {duration}
- **Your Fellow Interviewers:** {participant_names}
# **Action Items:**
**Action Items:**
# 1. Please review **{candidate.full_name}'s** resume and notes.
# 2. The official calendar invite contains the meeting link ({meeting_link}) and should be used to join.
# 3. Be ready to start promptly at the scheduled time.
# ...
# """
# # Set initial data
# self.initial['subject'] = f"Interview Invitation: {job_title} at KAAUH - {candidate.full_name}"
# self.initial['message_for_candidate'] = candidate_message.strip()
# self.initial['message_for_agency'] = agency_message.strip()
# self.initial['message_for_participants'] = participants_message.strip()
1. Please review **{candidate.full_name}'s** resume and notes.
2. The official calendar invite contains the meeting link ({meeting_link}) and should be used to join.
3. Be ready to start promptly at the scheduled time.
...
"""
# Set initial data
self.initial['subject'] = f"Interview Invitation: {job_title} at KAAUH - {candidate.full_name}"
self.initial['message_for_candidate'] = candidate_message.strip()
self.initial['message_for_agency'] = agency_message.strip()
self.initial['message_for_participants'] = participants_message.strip()
# # class OnsiteLocationForm(forms.ModelForm):
# # class Meta:
# # model=
# # fields=['location']
# # widgets={
# # 'location': forms.TextInput(attrs={'placeholder': 'Enter Interview Location'}),
# # }
# class OnsiteLocationForm(forms.ModelForm):
# class Meta:
# model=
# fields=['location']
# widgets={
# 'location': forms.TextInput(attrs={'placeholder': 'Enter Interview Location'}),
# }
#during bulk schedule
class OnsiteLocationForm(forms.ModelForm):
@ -1987,125 +1986,6 @@ class OnsiteLocationForm(forms.ModelForm):
}
class InterviewEmailForm(forms.Form):
subject = forms.CharField(max_length=255, widget=forms.TextInput(attrs={'class': 'form-control'}))
message_for_candidate = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6}))
message_for_agency = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6}))
message_for_participants = forms.CharField(widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 6}))
def __init__(self, *args, candidate, external_participants, system_participants, meeting, job, **kwargs):
"""
meeting: an InterviewLocation instance (e.g., ZoomMeetingDetails or OnsiteLocationDetails)
"""
super().__init__(*args, **kwargs)
# ✅ meeting is already the InterviewLocation — do NOT use .interview_location
location = meeting
# --- Determine concrete details (Zoom or Onsite) ---
if location.location_type == location.LocationType.REMOTE:
details = getattr(location, 'zoommeetingdetails', None)
elif location.location_type == location.LocationType.ONSITE:
details = getattr(location, 'onsitelocationdetails', None)
else:
details = None
# --- Extract meeting info safely ---
if details and details.start_time:
formatted_date = details.start_time.strftime('%Y-%m-%d')
formatted_time = details.start_time.strftime('%I:%M %p')
duration = details.duration
meeting_link = location.details_url or "N/A (See Location Topic)"
else:
formatted_date = "TBD - Awaiting Scheduling"
formatted_time = "TBD"
duration = "N/A"
meeting_link = "Not Available"
job_title = job.title
agency_name = (
candidate.hiring_agency.name
if candidate.belong_to_agency and candidate.hiring_agency
else "Hiring Agency"
)
# --- Participant names for internal email ---
external_names = ", ".join([p.name for p in external_participants])
system_names = ", ".join([u.get_full_name() or u.username for u in system_participants])
participant_names = ", ".join(filter(None, [external_names, system_names]))
# --- Candidate Message ---
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 interview are as follows:
- **Date:** {formatted_date}
- **Time:** {formatted_time} (RIYADH TIME)
- **Duration:** {duration} minutes
- **Meeting Link/Location:** {meeting_link}
Please be ready at the scheduled time.
Kindly reply to confirm your attendance or propose an alternative if needed.
We look forward to meeting you.
Best regards,
KAAUH Hiring Team
""".strip()
# --- Agency Message ---
agency_message = f"""
Dear {agency_name},
This is to inform you that your candidate, **{candidate.full_name}**, has been scheduled for an interview for the **{job_title}** position.
**Interview Details:**
- **Date:** {formatted_date}
- **Time:** {formatted_time} (RIYADH TIME)
- **Duration:** {duration} minutes
- **Meeting Link/Location:** {meeting_link}
Please ensure the candidate is informed and prepared.
Best regards,
KAAUH Hiring Team
""".strip()
# --- Participants (Interview Panel) Message ---
participants_message = f"""
Hi Team,
You are scheduled to interview **{candidate.full_name}** for the **{job_title}** role.
**Interview Summary:**
- **Candidate:** {candidate.full_name}
- **Date:** {formatted_date}
- **Time:** {formatted_time} (RIYADH TIME)
- **Duration:** {duration} minutes
- **Location/Link:** {meeting_link}
- **Fellow Interviewers:** {participant_names}
**Action Items:**
1. Review the candidates resume and application notes.
2. Join via the link above (or be at the physical location) on time.
3. Coordinate among yourselves for role coverage.
Thank you!
""".strip()
# --- Set initial values ---
self.initial.update({
'subject': f"Interview Invitation: {job_title} - {candidate.full_name}",
'message_for_candidate': candidate_message,
'message_for_agency': agency_message,
'message_for_participants': participants_message,
})
class OnsiteReshuduleForm(forms.ModelForm):
class Meta:
model = OnsiteLocationDetails

View File

@ -12,9 +12,8 @@ from . linkedin_service import LinkedInService
from django.shortcuts import get_object_or_404
from . models import JobPosting
from django.utils import timezone
from . models import InterviewSchedule,ScheduledInterview,ZoomMeetingDetails,Message
from django.contrib.auth import get_user_model
User = get_user_model()
from . models import InterviewSchedule,ScheduledInterview,ZoomMeetingDetails
# Add python-docx import for Word document processing
try:
from docx import Document
@ -29,7 +28,7 @@ logger = logging.getLogger(__name__)
OPENROUTER_API_KEY ='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'
# OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free'
OPENROUTER_MODEL = 'openai/gpt-oss-20b'
OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct'
# OPENROUTER_MODEL = 'openai/gpt-oss-20b'
# OPENROUTER_MODEL = 'mistralai/mistral-small-3.2-24b-instruct:free'
@ -507,6 +506,7 @@ def handle_zoom_webhook_event(payload):
Background task to process a Zoom webhook event and update the local ZoomMeeting status.
It handles: created, updated, started, ended, and deleted events.
"""
print(payload)
event_type = payload.get('event')
object_data = payload['payload']['object']
@ -535,7 +535,9 @@ def handle_zoom_webhook_event(payload):
# elif event_type == 'meeting.updated':
# Only update time fields if they are in the payload
print(object_data)
meeting_instance.start_time = object_data.get('start_time', meeting_instance.start_time)
meeting_start_time = object_data.get('start_time', meeting_instance.start_time)
if meeting_start_time:
meeting_instance.start_time = datetime.fromisoformat(meeting_start_time)
meeting_instance.duration = object_data.get('duration', meeting_instance.duration)
meeting_instance.timezone = object_data.get('timezone', meeting_instance.timezone)
@ -756,7 +758,7 @@ 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,sender,job):
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')
@ -776,36 +778,16 @@ def _task_send_individual_email(subject, body_message, recipient, attachments,se
email_obj.attach(filename, content, content_type)
try:
result=email_obj.send(fail_silently=False)
if result==1:
try:
user=get_object_or_404(User,email=recipient)
new_message = Message.objects.create(
sender=sender,
recipient=user,
job=job,
subject=subject,
content=body_message, # Store the full HTML or plain content
message_type='DIRECT',
is_read=False, # It's just sent, not read yet
)
logger.info(f"Stored sent message ID {new_message.id} in DB.")
except Exception as e:
logger.error(f"Email sent to {recipient}, but failed to store in DB: {str(e)}")
else:
logger.error("fialed to send email")
email_obj.send(fail_silently=False)
return True
except Exception as e:
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
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,sender_user_id=None,job_id=None, hook='recruitment.tasks.email_success_hook'):
def send_bulk_email_task(subject, message, recipient_list, attachments=None, hook='recruitment.tasks.email_success_hook'):
"""
Django-Q background task to send pre-formatted email to a list of recipients.,
Django-Q background task to send pre-formatted email to a list of recipients.
Receives arguments directly from the async_task call.
"""
logger.info(f"Starting bulk email task for {len(recipient_list)} recipients")
@ -814,13 +796,11 @@ def send_bulk_email_task(subject, message, recipient_list,attachments=None,sende
if not recipient_list:
return {'success': False, 'error': 'No recipients provided to task.'}
sender=get_object_or_404(User,pk=sender_user_id)
job=get_object_or_404(JobPosting,pk=job_id)
# 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,sender,job):
if _task_send_individual_email(subject, message, recipient, attachments):
successful_sends += 1
if successful_sends > 0:

View File

@ -656,6 +656,5 @@ urlpatterns = [
# Detail View (assuming slug is on ScheduledInterview)
path("interviews/meetings/<slug:slug>/", views.meeting_details, name="meeting_details"),
# path("interviews/meetings/<slug:slug>/", views.MeetingDetailView.as_view(), name="meeting_details"),
]

View File

@ -129,8 +129,7 @@ from .models import (
Message,
Document,
OnsiteLocationDetails,
InterviewLocation,
InterviewNote
InterviewLocation
)
@ -250,7 +249,123 @@ class ZoomMeetingCreateView(StaffRequiredMixin, CreateView):
messages.error(self.request, f"Error creating meeting: {e}")
return redirect(reverse("create_meeting", kwargs={"slug": instance.slug}))
# class ZoomMeetingListView(StaffRequiredMixin, ListView):
# model = ZoomMeetingDetails
# template_name = "meetings/list_meetings.html"
# context_object_name = "meetings"
# paginate_by = 10
# def get_queryset(self):
# queryset = super().get_queryset().order_by("-start_time")
# # Prefetch related interview data efficiently
# queryset = queryset.prefetch_related(
# Prefetch(
# "interview", # related_name from ZoomMeeting to ScheduledInterview
# queryset=ScheduledInterview.objects.select_related("application", "job"),
# to_attr="interview_details", # Changed to not start with underscore
# )
# )
# # Handle search by topic or meeting_id
# search_query = self.request.GET.get(
# "q", ""
# ) # Renamed from 'search' to 'q' for consistency
# if search_query:
# queryset = queryset.filter(
# Q(topic__icontains=search_query) | Q(meeting_id__icontains=search_query)
# )
# # Handle filter by status
# status_filter = self.request.GET.get("status", "")
# if status_filter:
# queryset = queryset.filter(status=status_filter)
# # Handle search by candidate name
# candidate_name = self.request.GET.get("candidate_name", "")
# if candidate_name:
# # Filter based on the name of the candidate associated with the meeting's interview
# queryset = queryset.filter(
# Q(interview__application__first_name__icontains=candidate_name)
# | Q(interview__application__last_name__icontains=candidate_name)
# )
# return queryset
# def get_context_data(self, **kwargs):
# context = super().get_context_data(**kwargs)
# context["search_query"] = self.request.GET.get("q", "")
# context["status_filter"] = self.request.GET.get("status", "")
# context["candidate_name_filter"] = self.request.GET.get("candidate_name", "")
# return context
# @login_required
# def InterviewListView(request):
# # interview_type=request.GET.get('interview_type','Remote')
# # print(interview_type)
# interview_type='Onsite'
# meetings=ScheduledInterview.objects.filter(schedule__interview_type=interview_type)
# return render(request, "meetings/list_meetings.html",{
# 'meetings':meetings,
# })
# search_query = request.GET.get("q", "") # Renamed from 'search' to 'q' for consistency
# if search_query:
# interviews = interviews.filter(
# Q(topic__icontains=search_query) | Q(meeting_id__icontains=search_query)
# )
# # Handle filter by status
# status_filter = request.GET.get("status", "")
# if status_filter:
# queryset = queryset.filter(status=status_filter)
# # Handle search by candidate name
# candidate_name = request.GET.get("candidate_name", "")
# if candidate_name:
# # Filter based on the name of the candidate associated with the meeting's interview
# queryset = queryset.filter(
# Q(interview__candidate__first_name__icontains=candidate_name) |
# Q(interview__candidate__last_name__icontains=candidate_name)
# )
# @login_required
# def InterviewListView(request):
# # interview_type=request.GET.get('interview_type','Remote')
# # print(interview_type)
# interview_type='Onsite'
# meetings=ScheduledInterview.objects.filter(schedule__interview_type=interview_type)
# return render(request, "meetings/list_meetings.html",{
# 'meetings':meetings,
# })
# search_query = request.GET.get("q", "") # Renamed from 'search' to 'q' for consistency
# if search_query:
# interviews = interviews.filter(
# Q(topic__icontains=search_query) | Q(meeting_id__icontains=search_query)
# )
# # Handle filter by status
# status_filter = request.GET.get("status", "")
# if status_filter:
# queryset = queryset.filter(status=status_filter)
# # Handle search by candidate name
# candidate_name = request.GET.get("candidate_name", "")
# if candidate_name:
# # Filter based on the name of the candidate associated with the meeting's interview
# queryset = queryset.filter(
# Q(interview__candidate__first_name__icontains=candidate_name) |
# Q(interview__candidate__last_name__icontains=candidate_name)
# )
class ZoomMeetingDetailsView(StaffRequiredMixin, DetailView):
model = ZoomMeetingDetails
@ -1153,8 +1268,8 @@ def application_submit_form(request, template_slug):
)
# def applicant_profile(request):
# return render(request, "applicant/applicant_profile.html")
def applicant_profile(request):
return render(request, "applicant/applicant_profile.html")
@csrf_exempt
@ -3007,7 +3122,7 @@ def add_meeting_comment(request, slug):
meeting = get_object_or_404(ZoomMeetingDetails, slug=slug)
if request.method == "POST":
form = InterviewNoteForm(request.POST)
form = MeetingCommentForm(request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.meeting = meeting
@ -3028,7 +3143,7 @@ def add_meeting_comment(request, slug):
return redirect("meeting_details", slug=slug)
else:
form = InterviewNoteForm()
form = MeetingCommentForm()
context = {
"form": form,
@ -3054,7 +3169,7 @@ def edit_meeting_comment(request, slug, comment_id):
return redirect("meeting_details", slug=slug)
if request.method == "POST":
form = InterviewNoteForm(request.POST, instance=comment)
form = MeetingCommentForm(request.POST, instance=comment)
if form.is_valid():
comment = form.save()
messages.success(request, "Comment updated successfully!")
@ -3072,7 +3187,7 @@ def edit_meeting_comment(request, slug, comment_id):
return redirect("meeting_details", slug=slug)
else:
form = InterviewNoteForm(instance=comment)
form = MeetingCommentForm(instance=comment)
context = {"form": form, "meeting": meeting, "comment": comment}
return render(request, "includes/edit_comment_form.html", context)
@ -4577,8 +4692,7 @@ def message_detail(request, message_id):
@login_required
def message_create(request):
"""Create a new message"""
from .email_service import EmailService
"""Create a new message"""
if request.method == "POST":
form = MessageForm(request.user, request.POST)
@ -4586,25 +4700,8 @@ def message_create(request):
message = form.save(commit=False)
message.sender = request.user
message.save()
messages.success(request, "Message sent successfully!")
["recipient", "job", "subject", "content", "message_type"]
recipient_email = form.cleaned_data['recipient'].email # Assuming recipient is a User or Model with an 'email' field
subject = form.cleaned_data['subject']
custom_message = form.cleaned_data['content']
job_id = form.cleaned_data['job'].id if 'job' in form.cleaned_data and form.cleaned_data['job'] else None
sender_user_id = request.user.id
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
sender_user_id=sender_user_id,
job_id=job_id,
hook='recruitment.tasks.email_success_hook')
logger.info(f"{task_id} queued.")
return redirect("message_list")
else:
messages.error(request, "Please correct the errors below.")
@ -4617,8 +4714,6 @@ def message_create(request):
if request.user.user_type != "staff":
return render(request, "messages/candidate_message_form.html", context)
return render(request, "messages/message_form.html", context)
@login_required
def message_reply(request, message_id):
"""Reply to a message"""
@ -5111,7 +5206,7 @@ def compose_candidate_email(request, job_slug):
if request.method == 'POST':
print("........................................................inside candidate conpose.............")
candidate_ids = request.POST.getlist('candidate_ids')
candidates=Application.objects.filter(id__in=candidate_ids)
form = CandidateEmailForm(job, candidates, request.POST)
@ -5138,16 +5233,14 @@ def compose_candidate_email(request, job_slug):
# Send emails using email service (no attachments, synchronous to avoid pickle issues)
email_result = send_bulk_email( #
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,
job=job
from_interview=False
)
if email_result["success"]:
@ -5445,50 +5538,25 @@ def candidate_signup(request, slug):
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}
# )
def create_interview_participants(request, slug):
"""
Manage participants for a ScheduledInterview.
Uses interview_pk because ScheduledInterview has no slug.
"""
schedule_interview = get_object_or_404(ScheduledInterview, slug=slug)
# Get the slug from the related InterviewLocation (the "meeting")
meeting_slug = schedule_interview.interview_location.slug # ✅ Correct
interview_slug = schedule_interview.zoom_meeting.slug
if request.method == "POST":
form = InterviewParticpantsForm(request.POST, instance=schedule_interview)
if form.is_valid():
form.save() # No need for commit=False — it's not a create, just update
messages.success(request, "Participants updated successfully.")
return redirect("meeting_details", slug=meeting_slug)
# 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, "interview": schedule_interview}
request, "interviews/interview_participants_form.html", {"form": form}
)
@ -5683,7 +5751,7 @@ class MeetingListView(ListView):
'details': details,
'type': location.location_type,
'topic': location.topic,
# 'slug': interview.slug,
'slug': interview.slug,
'start_time': start_datetime, # Combined datetime object
# Duration should ideally be on ScheduledInterview or fetched from details
'duration': getattr(details, 'duration', 'N/A'),
@ -5857,62 +5925,3 @@ def schedule_onsite_meeting_for_candidate(request, slug, candidate_pk):
return render(request, "meetings/schedule_onsite_meeting_form.html", context)
from django.http import Http404
def meeting_details(request, slug):
# Fetch the meeting (InterviewLocation or subclass) by slug
meeting = get_object_or_404(
InterviewLocation.objects.select_related(
'scheduled_interview__application__person',
'scheduled_interview__job',
'zoommeetingdetails',
'onsitelocationdetails',
).prefetch_related(
'scheduled_interview__participants',
'scheduled_interview__system_users',
'scheduled_interview__notes',
),
slug=slug
)
try:
interview = meeting.scheduled_interview
except ScheduledInterview.DoesNotExist:
raise Http404("No interview is associated with this meeting.")
candidate = interview.application
job = interview.job
external_participants = interview.participants.all()
system_participants = interview.system_users.all()
total_participants = external_participants.count() + system_participants.count()
# Forms for modals
participant_form = InterviewParticpantsForm(instance=interview)
# email_form = InterviewEmailForm(
# candidate=candidate,
# external_participants=external_participants, # QuerySet of Participants
# system_participants=system_participants, # QuerySet of Users
# meeting=meeting, # ← This is InterviewLocation (e.g., ZoomMeetingDetails)
# job=job,
# )
context = {
'meeting': meeting,
'interview': interview,
'candidate': candidate,
'job': job,
'external_participants': external_participants,
'system_participants': system_participants,
'total_participants': total_participants,
'form': participant_form,
# 'email_form': email_form,
}
return render(request, 'interviews/detail_interview.html', context)

BIN
static/image/kaauh.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
static/image/kaauh.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

8
static/image/vision2.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -323,7 +323,7 @@
<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 'candidate_portal_dashboard' %}">{% translate "Profile" %}</a>
<a class="nav-link text-secondary" href="{% url 'applicant_profile' %}">{% translate "Profile" %}</a>
</li>
<li class="nav-item">
<a class="nav-link text-secondary" href="{% url 'kaauh_career' %}">{% translate "Careers" %}</a>

View File

@ -1,9 +1,12 @@
{% extends 'base.html' %}
{% load static i18n %}
{% load widget_tweaks %}
{% block customCSS %}
<style>
/* -------------------------------------------------------------------------- */
/* KAAT-S Redesign CSS - Compacted and Reordered Layout */
/* -------------------------------------------------------------------------- */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
@ -17,189 +20,286 @@
--kaauh-link: #007bff;
--kaauh-link-hover: #0056b3;
}
body { background-color: #f0f2f5; font-family: 'Inter', sans-serif; }
.card { border: none; border-radius: 8px; box-shadow: 0 3px 10px rgba(0,0,0,0.04); margin-bottom: 1rem; }
.card-body { padding: 1rem 1.25rem; }
body {
background-color: #f0f2f5;
font-family: 'Inter', sans-serif;
}
/* ------------------ Card & Header Styles ------------------ */
.card {
border: none;
border-radius: 8px; /* Slightly smaller radius */
box-shadow: 0 3px 10px rgba(0,0,0,0.04); /* Lighter shadow */
margin-bottom: 1rem;
}
.card-body {
padding: 1rem 1.25rem; /* Reduced padding */
}
#comments-card .card-header {
background-color: white;
color: var(--kaauh-teal-dark);
padding: 0.75rem 1.25rem;
padding: 0.75rem 1.25rem; /* Reduced header padding */
font-weight: 600;
border-radius: 8px 8px 0 0;
border-bottom: 1px solid var(--kaauh-border);
}
/* ------------------ Main Title & Status ------------------ */
.main-title-container {
padding: 0 0 1rem 0; /* Space below the main title */
}
.main-title-container h1 {
font-size: 1.75rem; font-weight: 700;
font-size: 1.75rem; /* Reduced size */
font-weight: 700;
}
.status-badge {
font-size: 0.7rem; padding: 0.3em 0.7em; border-radius: 12px;
font-size: 0.7rem; /* Smaller badge */
padding: 0.3em 0.7em;
border-radius: 12px;
}
.bg-scheduled { background-color: #00636e !important; color: white !important; }
.bg-completed { background-color: #198754 !important; color: white !important; }
.bg-waiting { background-color: #ffc107 !important; color: var(--kaauh-primary-text) !important; }
.bg-started { background-color: var(--kaauh-teal) !important; color: white !important; }
.bg-ended { background-color: var(--kaauh-danger) !important; color: white !important; }
.bg-cancelled { background-color: #6c757d !important; color: white !important; }
.bg-scheduled { background-color: #00636e !important; color: white !important;}
.bg-completed { background-color: #198754 !important; color: white !important;}
.bg-waiting { background-color: #ffc107 !important; color: var(--kaauh-primary-text) !important;}
.bg-started { background-color: var(--kaauh-teal) !important; color: white !important;}
.bg-ended { background-color: var(--kaauh-danger) !important; color: white !important;}
/* ------------------ Detail Row & Content Styles (Made Smaller) ------------------ */
.detail-section h2, .card h2 {
color: var(--kaauh-teal-dark); font-weight: 700; font-size: 1.25rem;
margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--kaauh-border);
color: var(--kaauh-teal-dark);
font-weight: 700;
font-size: 1.25rem; /* Reduced size */
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--kaauh-border);
}
.detail-row-simple {
display: flex; padding: 0.4rem 0; border-bottom: 1px dashed var(--kaauh-border); font-size: 0.85rem;
display: flex;
padding: 0.4rem 0; /* Reduced vertical padding */
border-bottom: 1px dashed var(--kaauh-border);
font-size: 0.85rem; /* Smaller text */
}
.detail-label-simple { font-weight: 600; color: var(--kaauh-teal-dark); flex-basis: 40%; }
.detail-value-simple { color: var(--kaauh-primary-text); font-weight: 500; flex-basis: 60%; }
.detail-label-simple {
font-weight: 600;
color: var(--kaauh-teal-dark);
flex-basis: 40%;
}
.detail-value-simple {
color: var(--kaauh-primary-text);
font-weight: 500;
flex-basis: 60%;
}
/* ------------------ Join Info & Copy Button ------------------ */
.btn-primary-teal {
background-color: var(--kaauh-teal); border-color: var(--kaauh-teal); padding: 0.6rem 1.2rem;
font-size: 0.95rem; border-radius: 6px; color: white;
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
padding: 0.6rem 1.2rem;
font-size: 0.95rem; /* Slightly smaller button */
border-radius: 6px;
color: white; /* Ensure text color is white for teal primary */
}
.btn-primary-teal:hover { background-color: var(--kaauh-teal-dark); }
.btn-primary-teal:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
}
/* Added Danger Button Style for main delete */
.btn-danger-red {
background-color: var(--kaauh-danger); border-color: var(--kaauh-danger); color: white;
padding: 0.6rem 1.2rem; font-size: 0.95rem; border-radius: 6px; font-weight: 600;
background-color: var(--kaauh-danger);
border-color: var(--kaauh-danger);
color: white;
padding: 0.6rem 1.2rem;
font-size: 0.95rem;
border-radius: 6px;
font-weight: 600;
}
.btn-danger-red:hover {
background-color: #c82333;
border-color: #bd2130;
}
.btn-danger-red:hover { background-color: #c82333; border-color: #bd2130; }
.btn-secondary-back {
background-color: transparent; border: none; color: var(--kaauh-secondary-text);
font-weight: 600; font-size: 1rem; padding: 0.5rem 0.75rem; transition: color 0.2s;
/* Subtle Back Button */
background-color: transparent;
border: none;
color: var(--kaauh-secondary-text);
font-weight: 600;
font-size: 1rem;
padding: 0.5rem 0.75rem;
transition: color 0.2s;
}
.btn-secondary-back:hover { color: var(--kaauh-teal); text-decoration: underline; }
.btn-secondary-back:hover {
color: var(--kaauh-teal);
text-decoration: underline;
}
.join-url-display {
background-color: white; border: 1px solid var(--kaauh-border); padding: 0.5rem; font-size: 0.85rem;
background-color: white;
border: 1px solid var(--kaauh-border);
padding: 0.5rem; /* Reduced padding */
font-size: 0.85rem; /* Smaller text */
}
.btn-copy-simple {
padding: 0.5rem 0.75rem; background-color: var(--kaauh-teal-dark); border: none; color: white; border-radius: 4px;
padding: 0.5rem 0.75rem;
background-color: var(--kaauh-teal-dark);
border: none;
color: white;
border-radius: 4px;
}
.btn-copy-simple:hover { background-color: var(--kaauh-teal); }
.btn-copy-simple:hover {
background-color: var(--kaauh-teal);
}
/* ------------------ Simple Table Styles ------------------ */
.simple-table {
width: 100%; margin-top: 0.5rem; border-collapse: collapse;
width: 100%;
margin-top: 0.5rem;
border-collapse: collapse;
}
.simple-table th {
background-color: var(--kaauh-teal-light); color: var(--kaauh-teal-dark); font-weight: 700;
padding: 8px 12px; border: 1px solid var(--kaauh-border); font-size: 0.8rem;
background-color: var(--kaauh-teal-light);
color: var(--kaauh-teal-dark);
font-weight: 700;
padding: 8px 12px; /* Reduced padding */
border: 1px solid var(--kaauh-border);
font-size: 0.8rem; /* Smaller table header text */
}
.simple-table td {
padding: 8px 12px; border: 1px solid var(--kaauh-border); background-color: white; font-size: 0.85rem;
padding: 8px 12px; /* Reduced padding */
border: 1px solid var(--kaauh-border);
background-color: white;
font-size: 0.85rem; /* Smaller table body text */
}
.comment-item { border: 1px solid var(--kaauh-border); background-color: var(--kaauh-gray-light); border-radius: 6px; }
/* ------------------ Comment Specific Styles ------------------ */
.comment-item {
border: 1px solid var(--kaauh-border);
background-color: var(--kaauh-gray-light);
border-radius: 6px;
}
/* Style for in-page edit button */
.btn-edit-comment {
background-color: transparent; border: 1px solid var(--kaauh-teal); color: var(--kaauh-teal);
padding: 0.25rem 0.5rem; font-size: 0.75rem; border-radius: 4px; font-weight: 500;
background-color: transparent;
border: 1px solid var(--kaauh-teal);
color: var(--kaauh-teal);
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
border-radius: 4px;
font-weight: 500;
}
.btn-edit-comment:hover { background-color: var(--kaauh-teal-light); }
.btn-edit-comment:hover {
background-color: var(--kaauh-teal-light);
}
</style>
{% endblock %}
{% block content %}
{% comment %}
NOTE: The variable 'meeting' has been renamed to 'interview' (ScheduledInterview)
NOTE: The variable 'meeting.slug' has been renamed to 'interview.slug'
NOTE: All 'meeting' URL names (update_meeting, delete_meeting, etc.) have been renamed
{% endcomment %}
<div class="container-fluid py-4">
{# --- TOP BAR --- #}
{# --- TOP BAR / BACK BUTTON & ACTIONS (EDIT/DELETE) --- #}
<div class="d-flex justify-content-between align-items-center mb-4">
<a href="{% url 'list_meetings' %}" class="btn btn-secondary-back">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Meetings" %}
{# Back Button #}
<a href="{% url 'interview_list' %}" class="btn btn-secondary-back">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Interviews" %}
</a>
{# Edit and Delete Buttons #}
<div class="d-flex gap-2">
<a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary-teal btn-sm">
<i class="fas fa-edit me-1"></i> {% trans "Edit Meeting" %}
<a href="{% url 'update_scheduled_interview' interview.slug %}" class="btn btn-primary-teal btn-sm">
<i class="fas fa-edit me-1"></i> {% trans "Edit Interview" %}
</a>
<form method="post" action="{% url 'delete_meeting' meeting.slug %}" style="display: inline;">
{# DELETE MEETING FORM #}
<form method="post" action="{% url 'delete_scheduled_interview' interview.slug %}" style="display: inline;">
{% csrf_token %}
<button type="submit" class="btn btn-danger-red btn-sm" onclick="return confirm('{% trans "Are you sure you want to delete this meeting? This action is permanent." %}')">
<i class="fas fa-trash-alt me-1"></i> {% trans "Delete Meeting" %}
<button type="submit" class="btn btn-danger-red btn-sm" onclick="return confirm('{% trans "Are you sure you want to delete this interview? This action is permanent." %}')">
<i class="fas fa-trash-alt me-1"></i> {% trans "Delete Interview" %}
</button>
</form>
</div>
</div>
{# --- MAIN TITLE --- #}
{# ========================================================= #}
{# --- MAIN TITLE AT TOP --- #}
{# ========================================================= #}
{% with zoom_details=interview.zoom_details.0 %}
<div class="main-title-container mb-4">
<h1 class="text-start" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-video me-2" style="color: var(--kaauh-teal);"></i>
{{ meeting.topic|default:"[Meeting Topic]" }}
<span class="status-badge bg-{{ interview.status|lower|default:'scheduled' }} ms-3">
{{ interview.get_status_display|default:"Scheduled" }}
{% if interview.schedule.interview_type == 'Remote' %}
<i class="fas fa-video me-2" style="color: var(--kaauh-teal);"></i>
{{ zoom_details.topic|default:"[Remote Interview]" }}
{% else %}
<i class="fas fa-building me-2" style="color: var(--kaauh-teal);"></i>
{{ interview.schedule.location|default:"[Onsite Interview]" }}
{% endif %}
<span class="status-badge bg-{{ interview.status|lower|default:'bg-secondary' }} ms-3">
{{ interview.status|title|default:'N/A' }} ({{ interview.schedule.interview_type }})
</span>
</h1>
</div>
{# --- INTERVIEW & CONNECTION CARDS --- #}
{# ========================================================= #}
{# --- SECTION 1: INTERVIEW & CONNECTION/LOCATION CARDS SIDE BY SIDE --- #}
{# ========================================================= #}
<div class="row g-4 mb-5 align-items-stretch">
{# Interview Detail #}
{# --- LEFT HALF: INTERVIEW DETAIL CARD --- #}
<div class="col-lg-6">
<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>
<h2 class="text-start"><i class="fas fa-briefcase me-2"></i> {% trans "Candidate & Job" %}</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">{{ 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">{{ candidate.full_name|default:"N/A" }}</div>
</div>
<div class="detail-row-simple">
<div class="detail-label-simple">{% trans "Candidate Email" %}:</div>
<div class="detail-value-simple">{{ candidate.email|default:"N/A" }}</div>
</div>
<div class="detail-row-simple">
<div class="detail-label-simple">{% trans "Job Type" %}:</div>
<div class="detail-value-simple">{{ job.job_type|default:"N/A" }}</div>
</div>
{% if candidate.belong_to_agency %}
<div class="detail-row-simple">
<div class="detail-label-simple">{% trans "Agency" %}:</div>
<div class="detail-value-simple">{{ candidate.hiring_agency.name|default:"N/A" }}</div>
</div>
{# NOTE: Assuming ScheduledInterview has direct relations to candidate and job #}
<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' interview.job.slug %}">{{ interview.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' interview.candidate.slug %}">{{ interview.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' interview.candidate.slug %}">{{ interview.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">{{ interview.job.job_type|default:"N/A" }}</div></div>
{% if interview.candidate.belong_to_agency %}
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Agency" %}:</div><div class="detail-value-simple"><a href="">{{ interview.candidate.hiring_agency.name|default:"N/A" }}</a></div></div>
{% endif %}
</div>
</div>
</div>
{# Connection Details #}
{# --- RIGHT HALF: CONNECTION/LOCATION DETAILS CARD --- #}
<div class="col-lg-6">
<div class="p-3 bg-white rounded shadow-sm h-100 d-flex flex-column">
<h2 class="text-start"><i class="fas fa-info-circle me-2"></i> {% trans "Connection Details" %}</h2>
<h2 class="text-start"><i class="fas fa-map-marker-alt me-2"></i> {% trans "Time & Location" %}</h2>
<div class="detail-row-group flex-grow-1">
<div class="detail-row-simple">
<div class="detail-label-simple">{% trans "Date & Time" %}:</div>
<div class="detail-value-simple">
{{ interview.interview_date }} {{ interview.interview_time }} ({{ meeting.timezone }})
</div>
</div>
<div class="detail-row-simple">
<div class="detail-label-simple">{% trans "Duration" %}:</div>
<div class="detail-value-simple">
{% if meeting.location_type == "Remote" %}
{{ meeting.zoommeetingdetails.duration|default:"N/A" }}
{% elif meeting.location_type == "Onsite" %}
{{ meeting.onsitelocationdetails.duration|default:"N/A" }}
{% else %}
N/A
{% endif %}
{% trans "minutes" %}
</div>
</div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Date & Time" %}:</div><div class="detail-value-simple">{{ interview.interview_date|date:"M d, Y"|default:"N/A" }} @ {{ interview.interview_time|time:"H:i"|default:"N/A" }}</div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Duration" %}:</div><div class="detail-value-simple">{{ interview.schedule.interview_duration|default:"N/A" }} {% trans "minutes" %}</div></div>
{% if interview.schedule.interview_type == 'Onsite' %}
{# --- Onsite Details --- #}
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Location" %}:</div><div class="detail-value-simple">{{ interview.schedule.location|default:"TBD" }}</div></div>
{% if meeting.location_type == "Remote" %}
{% with zoom=meeting.zoommeetingdetails %}
<div class="detail-row-simple">
<div class="detail-label-simple">{% trans "Meeting ID" %}:</div>
<div class="detail-value-simple">{{ zoom.meeting_id|default:"N/A" }}</div>
</div>
<div class="detail-row-simple">
<div class="detail-label-simple">{% trans "Host Email" %}:</div>
<div class="detail-value-simple">{{ zoom.host_email|default:"N/A" }}</div>
</div>
{% if meeting.details_url %}
<div class="join-url-container pt-3" style="position: relative;">
<div id="copy-message" class="text-white rounded px-2 py-1 small fw-bold mb-2 text-center" style="opacity: 0; transition: opacity 0.3s; position: absolute; right: 0; top: -35px; background-color: var(--kaauh-success); z-index: 10;">
{% trans "Copied!" %}
</div>
<div class="join-url-display d-flex justify-content-between align-items-center">
{% elif interview.schedule.interview_type == 'Remote' and zoom_details %}
{# --- Remote/Zoom Details --- #}
<h3 class="mt-3" style="font-size: 1.05rem; color: var(--kaauh-teal); font-weight: 600;">{% trans "Remote Details" %}</h3>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Meeting ID" %}:</div><div class="detail-value-simple">{{ zoom_details.meeting_id|default:"N/A" }}</div></div>
<div class="detail-row-simple"><div class="detail-label-simple">{% trans "Host Email" %}:</div><div class="detail-value-simple">{{ zoom_details.host_email|default:"N/A" }}</div></div>
{% if zoom_details.join_url %}
<div class="join-url-container pt-3">
<div id="copy-message" class="text-white rounded px-2 py-1 small fw-bold mb-2 text-center" style="opacity: 0; transition: opacity 0.3s; position: absolute; right: 0; top: 5px; background-color: var(--kaauh-success); z-index: 10;">{% trans "Copied!" %}</div>
<div class="join-url-display d-flex justify-content-between align-items-center position-relative">
<div class="text-truncate me-2">
<strong>{% trans "Join URL" %}:</strong>
<span id="meeting-join-url">{{ meeting.details_url }}</span>
<span id="meeting-join-url">{{ zoom_details.join_url }}</span>
</div>
<button class="btn-copy-simple ms-2 flex-shrink-0" onclick="copyLink()" title="{% trans 'Copy URL' %}">
<i class="fas fa-copy"></i>
@ -207,230 +307,310 @@ body { background-color: #f0f2f5; font-family: 'Inter', sans-serif; }
</div>
</div>
{% endif %}
{% endwith %}
{% elif meeting.location_type == "Onsite" %}
{% with onsite=meeting.onsitelocationdetails %}
<div class="detail-row-simple">
<div class="detail-label-simple">{% trans "Address" %}:</div>
<div class="detail-value-simple">{{ onsite.physical_address|default:"N/A" }}</div>
</div>
<div class="detail-row-simple">
<div class="detail-label-simple">{% trans "Room" %}:</div>
<div class="detail-value-simple">{{ onsite.room_number|default:"TBD" }}</div>
</div>
{% endwith %}
{% else %}
<p class="text-muted">{% trans "Location/Connection details are not available for this interview type." %}</p>
{% endif %}
</div>
</div>
</div>
</div>
{% endwith %}
{# --- PARTICIPANTS --- #}
{# ========================================================= #}
{# --- SECTION 2: PERSONNEL TABLES --- #}
{# ========================================================= #}
<div class="row g-4 mt-1 mb-5">
{# --- PARTICIPANTS TABLE --- #}
<div class="col-lg-12">
<div class="p-3 bg-white rounded shadow-sm">
<div class="d-flex justify-content-between align-items-center">
<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>
<div class="d-flex gap-2">
<button type="button" class="btn btn-primary-teal btn-sm"
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" data-bs-target="#emailModal">
<i class="fas fa-envelope"></i>
</button>
<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" %} ({{ interview.participants.count|add:interview.system_users.count }})
</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>
</div>
<table class="simple-table">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Role" %}</th>
<th>{% trans "Role/Designation" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Phone" %}</th>
<th>{% trans "Type" %}</th>
<th>{% trans "Phone Number" %}</th>
<th>{% trans "Source Type" %}</th>
</tr>
</thead>
<tbody>
{% for participant in external_participants %}
{# External Participants #}
{% for participant in interview.participants.all %}
<tr>
<td>{{ participant.name }}</td>
<td>{{ participant.designation|default:"Participant" }}</td>
<td>{{ participant.email|default:"N/A" }}</td>
<td>{{ participant.phone|default:"N/A" }}</td>
<td>{% trans "External" %}</td>
<td>{{participant.name}}</td>
<td>{{participant.designation}}</td>
<td>{{participant.email}}</td>
<td>{{participant.phone}}</td>
<td>{% trans "External Participants" %}</td>
</tr>
{% endfor %}
{% for user in system_participants %}
{# System Users (Internal Participants) #}
{% for user in interview.system_users.all %}
<tr>
<td>{{ user.get_full_name|default:user.username }}</td>
<td>Admin</td>
<td>{{ user.email|default:"N/A" }}</td>
<td>{{ user.phone|default:"N/A" }}</td>
<td>{{user.get_full_name}}</td>
<td>{% trans "System User" %}</td>
<td>{{user.email}}</td>
<td>{{user.phone}}</td>
<td>{% trans "System User" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{# --- COMMENTS --- #}
{# ========================================================= #}
{# --- SECTION 3: COMMENTS (CORRECTED) --- #}
{# ========================================================= #}
<div class="row g-4 mt-1">
<div class="col-lg-12">
<div class="card" id="comments-card">
<div class="card" id="comments-card" style="height: 100%;">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-comments me-2"></i>
{% trans "Comments" %} ({{ interview.notes.count }})
{% trans "Comments" %} ({% if interview.comments %}{{ interview.comments.count }}{% else %}0{% endif %})
</h5>
</div>
<div class="card-body">
<div class="card-body overflow-auto">
{# 1. COMMENT DISPLAY & IN-PAGE EDIT FORMS #}
<div id="comment-section" class="mb-4">
{% for note in interview.notes.all|dictsortreversed:"created_at" %}
<div class="comment-item mb-3 p-3">
<div id="comment-view-{{ note.pk }}">
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="comment-metadata" style="font-size: 0.9rem;">
<strong>{{ note.author.get_full_name|default:note.author.username }}</strong>
<span class="text-muted small ms-2">{{ note.created_at|date:"M d, Y H:i" }}</span>
{# NOTE: Assuming comment model has a ForeignKey to ScheduledInterview called 'interview' #}
{% if interview.comments.all %}
{% for comment in interview.comments.all|dictsortreversed:"created_at" %}
<div class="comment-item mb-3 p-3">
{# Read-Only Comment View #}
<div id="comment-view-{{ comment.pk }}">
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="comment-metadata" style="font-size: 0.9rem;">
<strong>{{ comment.author.get_full_name|default:comment.author.username }}</strong>
<span class="text-muted small ms-2">{{ comment.created_at|date:"M d, Y H:i" }}</span>
</div>
{% if comment.author == user or user.is_staff %}
<div class="comment-actions d-flex align-items-center gap-1">
{# Edit Button: Toggles the hidden form #}
<button type="button" class="btn btn-edit-comment py-0 px-1" onclick="toggleCommentEdit('{{ comment.pk }}')" id="edit-btn-{{ comment.pk }}" title="{% trans 'Edit Comment' %}">
<i class="fas fa-edit"></i>
</button>
{# Delete Form: Submits a POST request #}
<form method="post" action="{% url 'delete_meeting_comment' interview.slug comment.pk %}" style="display: inline;" id="delete-form-{{ comment.pk }}">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger py-0 px-1" title="{% trans 'Delete Comment' %}" onclick="return confirm('{% trans "Are you sure you want to delete this comment?" %}')">
<i class="fas fa-trash"></i>
</button>
</form>
</div>
{% endif %}
</div>
<p class="mb-0 comment-content" style="font-size: 0.85rem; white-space: pre-wrap;">{{ comment.content|linebreaksbr }}</p>
</div>
{% if note.author == user or user.is_staff %}
<div class="comment-actions d-flex align-items-center gap-1">
<button type="button" class="btn btn-edit-comment py-0 px-1" onclick="toggleCommentEdit('{{ note.pk }}')" id="edit-btn-{{ note.pk }}">
<i class="fas fa-edit"></i>
</button>
<form method="post" action="{% url 'delete_meeting_comment' meeting.slug note.pk %}" style="display: inline;">
{# Hidden Edit Form #}
<div id="comment-edit-form-{{ comment.pk }}" style="display: none; margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--kaauh-border);">
<form method="POST" action="{% url 'edit_meeting_comment' interview.slug comment.pk %}" id="form-{{ comment.pk }}">
{% csrf_token %}
<button type="submit" class="btn btn-outline-danger py-0 px-1" onclick="return confirm('{% trans "Are you sure you want to delete this comment?" %}')">
<i class="fas fa-trash"></i>
<div class="mb-2">
<label for="id_content_{{ comment.pk }}" class="form-label small">{% trans "Edit Comment" %}</label>
{# NOTE: The textarea name must match your Comment model field (usually 'content') #}
<textarea name="content" id="id_content_{{ comment.pk }}" rows="3" class="form-control" required>{{ comment.content }}</textarea>
</div>
<button type="submit" class="btn btn-sm btn-success me-2">
<i class="fas fa-save me-1"></i> {% trans "Save Changes" %}
</button>
<button type="button" class="btn btn-sm btn-secondary" onclick="toggleCommentEdit('{{ comment.pk }}')">
{% trans "Cancel" %}
</button>
</form>
</div>
{% endif %}
</div>
<p class="mb-0" style="font-size: 0.85rem; white-space: pre-wrap;">{{ note.content|linebreaksbr }}</p>
</div>
<div id="comment-edit-form-{{ note.pk }}" style="display: none; margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--kaauh-border);">
<form method="POST" action="{% url 'edit_meeting_comment' meeting.slug note.pk %}">
{% csrf_token %}
<div class="mb-2">
<label class="form-label small">{% trans "Edit Comment" %}</label>
<textarea name="content" class="form-control" rows="3" required>{{ note.content }}</textarea>
</div>
<button type="submit" class="btn btn-success btn-sm me-2">
<i class="fas fa-save me-1"></i> {% trans "Save Changes" %}
</button>
<button type="button" class="btn btn-secondary btn-sm" onclick="toggleCommentEdit('{{ note.pk }}')">
{% trans "Cancel" %}
</button>
</form>
</div>
</div>
{% empty %}
<p class="text-muted">{% trans "No comments yet. Be the first to comment!" %}</p>
{% endfor %}
</div>
{% endfor %}
{% else %}
<p class="text-muted">{% trans "No comments yet. Be the first to comment!" %}</p>
{% endif %}
</div>
<hr>
<h6 class="mb-3">{% trans "Add a New Comment" %}</h6>
<form method="POST" action="{% url 'add_meeting_comment' meeting.slug %}">
{% csrf_token %}
<div class="mb-3">
<label class="form-label small">{% trans "Comment" %}</label>
<textarea name="content" class="form-control" rows="3" required></textarea>
</div>
<button type="submit" class="btn btn-primary-teal btn-sm">
<i class="fas fa-paper-plane me-1"></i> {% trans "Submit Comment" %}
</button>
</form>
{# 2. NEW COMMENT SUBMISSION (Remains the same) #}
<h6 class="mb-3" style="color: var(--kaauh-teal-dark);">{% trans "Add a New Comment" %}</h6>
{% if user.is_authenticated %}
<form method="POST" action="{% url 'add_meeting_comment' interview.slug %}">
{% csrf_token %}
{% if comment_form %}
{{ comment_form.as_p }}
{% else %}
<div class="mb-3">
<label for="id_content" class="form-label small">{% trans "Comment" %}</label>
<textarea name="content" id="id_content" rows="3" class="form-control" required></textarea>
</div>
{% endif %}
<button type="submit" class="btn btn-primary-teal btn-sm mt-2">
<i class="fas fa-paper-plane me-1"></i> {% trans "Submit Comment" %}
</button>
</form>
{% else %}
<p class="text-muted small">{% trans "You must be logged in to add a comment." %}</p>
{% endif %}
</div>
</div>
</div>
</div>
</div>
{# MODALS #}
<!-- Participants Modal -->
<div class="modal fade" id="assignParticipants" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
{# --- MODALS (Updated to use interview.slug) --- #}
<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">{% trans "Manage all participants" %}</h5>
<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' interview.slug %}">
{% csrf_token %}
<div class="modal-body">
{{ form.participants.errors }}
{{ form.participants }}
{{ form.system_users.errors }}
{{ form.system_users }}
</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">{% trans "Save" %}</button>
</div>
{% csrf_token %}
<div class="modal-body table-responsive">
{{ interview.name }} {# This might need checking - ScheduledInterview usually doesn't have a 'name' field #}
<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 -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<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">📧 {% trans "Compose Interview Invitation" %}</h5>
<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' interview.pk %}">
<form method="post" action="{% url 'send_interview_email' interview.slug %}">
{% csrf_token %}
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-bold">{% trans "Subject" %}</label>
{{ email_form.subject|add_class:"form-control" }}
<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">
{% if candidate.belong_to_agency %}
<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 interview.candidate.belong_to_an_agency %}
{% trans "Agency Message" %}
{% else %}
{% trans "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">
{% trans "Panel Message" %}
<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">
{% trans "Panel Message (Interviewers)" %}
</button>
</li>
</ul>
<div class="tab-content border border-top-0 p-3 bg-light-subtle">
<div class="tab-pane fade show active" id="candidate-pane">
<p class="text-muted small">
{% if candidate.belong_to_agency %}
{% trans "This email will be sent to the hiring agency." %}
{% else %}
{% trans "This email will be sent to the candidate." %}
{% endif %}
</p>
{% if candidate.belong_to_agency %}
{{ email_form.message_for_agency|add_class:"form-control" }}
{% else %}
{{ email_form.message_for_candidate|add_class:"form-control" }}
{# --- Candidate/Agency Pane --- #}
<div class="tab-pane fade show active" id="candidate-pane" role="tabpanel" aria-labelledby="candidate-tab">
<p class="text-muted small">{% trans "This email will be sent to the candidate or their hiring agency." %}</p>
{% if not interview.candidate.belong_to_an_agency %}
<div class="form-group">
<label for="{{ email_form.message_for_candidate.id_for_label }}" class="form-label d-none">{% trans "Candidate Message" %}</label>
{{ email_form.message_for_candidate | add_class:"form-control" }}
</div>
{% endif %}
{% if interview.candidate.belong_to_an_agency %}
<div class="form-group">
<label for="{{ email_form.message_for_agency.id_for_label }}" class="form-label d-none">{% trans "Agency Message" %}</label>
{{ email_form.message_for_agency | add_class:"form-control" }}
</div>
{% endif %}
</div>
<div class="tab-pane fade" id="participants-pane">
<p class="text-muted small">{% trans "This email will be sent to all interview participants." %}</p>
{{ email_form.message_for_participants|add_class:"form-control" }}
{# --- Participants Pane --- #}
<div class="tab-pane fade" id="participants-pane" role="tabpanel" aria-labelledby="participants-tab">
<p class="text-muted small">{% trans "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">{% trans "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">{% trans "Close" %}</button>
<button type="submit" class="btn btn-primary-teal">{% trans "Send Invitation" %}</button>
@ -439,36 +619,90 @@ body { background-color: #f0f2f5; font-family: 'Inter', sans-serif; }
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
function toggleCommentEdit(commentPk) {
const viewDiv = document.getElementById(`comment-view-${commentPk}`);
const editFormDiv = document.getElementById(`comment-edit-form-${commentPk}`);
if (viewDiv.style.display === 'none') {
viewDiv.style.display = 'block';
editFormDiv.style.display = 'none';
} else {
viewDiv.style.display = 'none';
editFormDiv.style.display = 'block';
// --- COMMENT EDITING FUNCTION ---
function toggleCommentEdit(commentPk) {
const viewDiv = document.getElementById(`comment-view-${commentPk}`);
const editFormDiv = document.getElementById(`comment-edit-form-${commentPk}`);
const editButton = document.getElementById(`edit-btn-${commentPk}`);
const deleteForm = document.getElementById(`delete-form-${commentPk}`);
if (viewDiv.style.display !== 'none') {
// Switch to Edit Mode
viewDiv.style.display = 'none';
editFormDiv.style.display = 'block';
if (editButton) editButton.style.display = 'none'; // Hide edit button
if (deleteForm) deleteForm.style.display = 'none'; // Hide delete button
} else {
// Switch back to View Mode (Cancel)
viewDiv.style.display = 'block';
editFormDiv.style.display = 'none';
if (editButton) editButton.style.display = 'inline-block'; // Show edit button
if (deleteForm) deleteForm.style.display = 'inline'; // Show delete button
}
}
}
function copyLink() {
const urlElement = document.getElementById('meeting-join-url');
const textToCopy = urlElement.textContent || urlElement.innerText;
const messageElement = document.getElementById('copy-message');
// --- COPY LINK FUNCTION ---
// CopyLink function implementation (slightly improved for message placement)
function copyLink() {
const urlElement = document.getElementById('meeting-join-url');
const displayContainer = urlElement.closest('.join-url-display');
const messageElement = document.getElementById('copy-message');
const textToCopy = urlElement.textContent || urlElement.innerText;
navigator.clipboard.writeText(textToCopy).then(() => {
messageElement.style.opacity = '1';
setTimeout(() => {
messageElement.style.opacity = '0';
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
});
}
clearTimeout(window.copyMessageTimeout);
function showMessage(success) {
messageElement.textContent = success ? '{% trans "Copied!" %}' : '{% trans "Copy Failed." %}';
messageElement.style.backgroundColor = success ? 'var(--kaauh-success)' : 'var(--kaauh-danger)';
messageElement.style.opacity = '1';
// Position the message relative to the display container
const rect = displayContainer.getBoundingClientRect();
// Note: This positioning logic relies on the .join-url-container being position:relative or position:absolute
messageElement.style.left = (rect.width / 2) - (messageElement.offsetWidth / 2) + 'px';
messageElement.style.top = '-35px';
window.copyMessageTimeout = setTimeout(() => {
messageElement.style.opacity = '0';
}, 2000);
}
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(textToCopy).then(() => {
showMessage(true);
}).catch(err => {
console.error('Could not copy text: ', err);
fallbackCopyTextToClipboard(textToCopy, showMessage);
});
} else {
fallbackCopyTextToClipboard(textToCopy, showMessage);
}
}
function fallbackCopyTextToClipboard(text, callback) {
const textArea = document.createElement("textarea");
textArea.value = text;
textArea.style.top = "0";
textArea.style.left = "0";
textArea.style.position = "fixed";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
let success = false;
try {
success = document.execCommand('copy');
} catch (err) {
console.error('Fallback: Oops, unable to copy', err);
}
document.body.removeChild(textArea);
callback(success);
}
</script>
{% endblock %}

View File

@ -251,7 +251,7 @@
</a>
<button type="button" class="btn btn-outline-danger btn-sm" title="{% trans 'Delete' %}"
data-bs-toggle="modal" data-bs-target="#deleteModal"
hx-post=""
hx-post="{% url 'delete_meeting' meeting.slug %}"
hx-target="#deleteModalBody"
hx-swap="outerHTML"
data-item-name="{{ meeting.topic }}">
@ -310,8 +310,8 @@
<i class="fas fa-sign-in-alt"></i>
</a>
{% endif %}
<a href="{% url 'meeting_details' meetings.first.interview_location.slug%}" class="btn btn-outline-primary" title="{% trans 'View' %}">
<i class="fas fa-eye"></i>{{meetings.first.interview_location.slug}}
<a href="" class="btn btn-outline-primary" title="{% trans 'View' %}">
<i class="fas fa-eye"></i>
</a>
{# CORRECTED: Passing the slug to the update URL #}
<a href="" class="btn btn-outline-secondary" title="{% trans 'Update' %}">
@ -320,7 +320,7 @@
<button type="button" class="btn btn-outline-danger" title="{% trans 'Delete' %}"
data-bs-toggle="modal"
data-bs-target="#deleteModal"
hx-post=""
hx-post="{% url 'delete_meeting' meeting.slug %}"
hx-target="#deleteModalBody"
hx-swap="outerHTML"
data-item-name="{{ meeting.topic }}">

View File

@ -155,7 +155,7 @@
<i class="fas fa-user-friends me-2"></i> {% trans "Applicants List" %}
</h1>
<a href="{% url 'person_create' %}" class="btn btn-main-action">
<i class="fas fa-plus me-1"></i> {% trans "Add New Applicant" %}
<i class="fas fa-plus me-1"></i> {% trans "Add New" %}
</a>
</div>
@ -163,14 +163,18 @@
<div class="card mb-4 shadow-sm no-hover">
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label>
<div class="input-group input-group-lg">
<form method="get" action="" class="w-100">
{% include 'includes/search_form.html' %}
</form>
</div>
<form method="get" action="" class="w-100">
<div class="input-group input-group-lg">
<input type="text" name="q" class="form-control" id="search"
placeholder="{% trans 'Search applicant...' %}"
value="{{ request.GET.q }}">
<button class="btn btn-main-action" type="submit">
<i class="fas fa-search"></i>
</button>
</div>
</form>
</div>
<div class="col-md-6">
@ -196,10 +200,10 @@
</select>
</div>
<div class="col-md-4 d-flex">
<div class="col-md-4 d-flex justify-content-end align-self-end">
<div class="filter-buttons">
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-filter me-1"></i> {% trans "Apply Filter" %}
<i class="fas fa-filter me-1"></i> {% trans "Apply" %}
</button>
{% if request.GET.q or request.GET.nationality or request.GET.gender %}
<a href="{% url 'person_list' %}" class="btn btn-outline-secondary btn-sm">
@ -213,8 +217,7 @@
</div>
</div>
</div>
{% if people_list %}
<div id="person-list">
<!-- View Switcher -->

View File

@ -211,8 +211,8 @@
<option selected>
----------
</option>
<option value="Document Review">
{% trans "To Document Review" %}
<option value="Offer">
{% trans "To Offer" %}
</option>
<option value="Exam">
{% trans "To Exam" %}