meetings detail

This commit is contained in:
Faheed 2025-11-19 12:30:44 +03:00
parent 88d1726721
commit 506766b6ca
8 changed files with 544 additions and 715 deletions

View File

@ -1698,6 +1698,7 @@ class CandidateEmailForm(forms.Form):
return message
class InterviewParticpantsForm(forms.ModelForm):
participants = forms.ModelMultipleChoiceField(
queryset=Participants.objects.all(),
@ -1706,7 +1707,7 @@ class InterviewParticpantsForm(forms.ModelForm):
)
system_users=forms.ModelMultipleChoiceField(
queryset=User.objects.all(),
queryset=User.objects.filter(user_type='staff'),
widget=forms.CheckboxSelectMultiple,
required=False,
label=_("Select Users"))
@ -1861,107 +1862,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.interview_location
# location = meeting
# --- 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):
@ -1986,6 +1987,125 @@ 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

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

View File

@ -129,7 +129,8 @@ from .models import (
Message,
Document,
OnsiteLocationDetails,
InterviewLocation
InterviewLocation,
InterviewNote
)
@ -249,123 +250,7 @@ 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
@ -3122,7 +3007,7 @@ def add_meeting_comment(request, slug):
meeting = get_object_or_404(ZoomMeetingDetails, slug=slug)
if request.method == "POST":
form = MeetingCommentForm(request.POST)
form = InterviewNoteForm(request.POST)
if form.is_valid():
comment = form.save(commit=False)
comment.meeting = meeting
@ -3143,7 +3028,7 @@ def add_meeting_comment(request, slug):
return redirect("meeting_details", slug=slug)
else:
form = MeetingCommentForm()
form = InterviewNoteForm()
context = {
"form": form,
@ -3169,7 +3054,7 @@ def edit_meeting_comment(request, slug, comment_id):
return redirect("meeting_details", slug=slug)
if request.method == "POST":
form = MeetingCommentForm(request.POST, instance=comment)
form = InterviewNoteForm(request.POST, instance=comment)
if form.is_valid():
comment = form.save()
messages.success(request, "Comment updated successfully!")
@ -3187,7 +3072,7 @@ def edit_meeting_comment(request, slug, comment_id):
return redirect("meeting_details", slug=slug)
else:
form = MeetingCommentForm(instance=comment)
form = InterviewNoteForm(instance=comment)
context = {"form": form, "meeting": meeting, "comment": comment}
return render(request, "includes/edit_comment_form.html", context)
@ -5773,7 +5658,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'),
@ -5947,3 +5832,60 @@ 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)

View File

@ -1,12 +1,9 @@
{% 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;
@ -20,286 +17,189 @@
--kaauh-link: #007bff;
--kaauh-link-hover: #0056b3;
}
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 */
}
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; }
#comments-card .card-header {
background-color: white;
color: var(--kaauh-teal-dark);
padding: 0.75rem 1.25rem; /* Reduced header padding */
padding: 0.75rem 1.25rem;
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; /* Reduced size */
font-weight: 700;
font-size: 1.75rem; font-weight: 700;
}
.status-badge {
font-size: 0.7rem; /* Smaller badge */
padding: 0.3em 0.7em;
border-radius: 12px;
font-size: 0.7rem; 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;}
/* ------------------ Detail Row & Content Styles (Made Smaller) ------------------ */
.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; }
.detail-section h2, .card h2 {
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);
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);
}
.detail-row-simple {
display: flex;
padding: 0.4rem 0; /* Reduced vertical padding */
border-bottom: 1px dashed var(--kaauh-border);
font-size: 0.85rem; /* Smaller text */
display: flex; padding: 0.4rem 0; border-bottom: 1px dashed var(--kaauh-border); font-size: 0.85rem;
}
.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 ------------------ */
.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%; }
.btn-primary-teal {
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 */
background-color: var(--kaauh-teal); border-color: var(--kaauh-teal); padding: 0.6rem 1.2rem;
font-size: 0.95rem; border-radius: 6px; color: white;
}
.btn-primary-teal:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
}
/* Added Danger Button Style for main delete */
.btn-primary-teal:hover { background-color: var(--kaauh-teal-dark); }
.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;
}
.btn-danger-red:hover {
background-color: #c82333;
border-color: #bd2130;
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-secondary-back {
/* 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;
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; /* Reduced padding */
font-size: 0.85rem; /* Smaller text */
background-color: white; border: 1px solid var(--kaauh-border); padding: 0.5rem; font-size: 0.85rem;
}
.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);
}
/* ------------------ Simple Table Styles ------------------ */
.btn-copy-simple:hover { background-color: var(--kaauh-teal); }
.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; /* Reduced padding */
border: 1px solid var(--kaauh-border);
font-size: 0.8rem; /* Smaller table header text */
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;
}
.simple-table td {
padding: 8px 12px; /* Reduced padding */
border: 1px solid var(--kaauh-border);
background-color: white;
font-size: 0.85rem; /* Smaller table body text */
padding: 8px 12px; border: 1px solid var(--kaauh-border); background-color: white; font-size: 0.85rem;
}
/* ------------------ 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 */
.comment-item { border: 1px solid var(--kaauh-border); background-color: var(--kaauh-gray-light); border-radius: 6px; }
.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 / BACK BUTTON & ACTIONS (EDIT/DELETE) --- #}
{# --- TOP BAR --- #}
<div class="d-flex justify-content-between align-items-center mb-4">
{# 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 href="{% url 'list_meetings' %}" class="btn btn-secondary-back">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Meetings" %}
</a>
{# Edit and Delete Buttons #}
<div class="d-flex gap-2">
<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 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>
{# DELETE MEETING FORM #}
<form method="post" action="{% url 'delete_scheduled_interview' interview.slug %}" style="display: inline;">
<form method="post" action="{% url 'delete_meeting' meeting.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 interview? This action is permanent." %}')">
<i class="fas fa-trash-alt me-1"></i> {% trans "Delete Interview" %}
<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>
</form>
</div>
</div>
{# ========================================================= #}
{# --- MAIN TITLE AT TOP --- #}
{# ========================================================= #}
{% with zoom_details=interview.zoom_details.0 %}
{# --- MAIN TITLE --- #}
<div class="main-title-container mb-4">
<h1 class="text-start" style="color: var(--kaauh-teal-dark);">
{% 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 }})
<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" }}
</span>
</h1>
</div>
{# ========================================================= #}
{# --- SECTION 1: INTERVIEW & CONNECTION/LOCATION CARDS SIDE BY SIDE --- #}
{# ========================================================= #}
{# --- INTERVIEW & CONNECTION CARDS --- #}
<div class="row g-4 mb-5 align-items-stretch">
{# --- LEFT HALF: INTERVIEW DETAIL CARD --- #}
{# Interview Detail #}
<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 "Candidate & Job" %}</h2>
<h2 class="text-start"><i class="fas fa-briefcase me-2"></i> {% trans "Interview Detail" %}</h2>
<div class="detail-row-group flex-grow-1">
{# 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>
<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>
{% endif %}
</div>
</div>
</div>
{# --- RIGHT HALF: CONNECTION/LOCATION DETAILS CARD --- #}
{# Connection Details #}
<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-map-marker-alt me-2"></i> {% trans "Time & Location" %}</h2>
<h2 class="text-start"><i class="fas fa-info-circle me-2"></i> {% trans "Connection Details" %}</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|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>
<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>
{% 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">
{% 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">
<div class="text-truncate me-2">
<strong>{% trans "Join URL" %}:</strong>
<span id="meeting-join-url">{{ zoom_details.join_url }}</span>
<span id="meeting-join-url">{{ meeting.details_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>
@ -307,310 +207,230 @@ body {
</div>
</div>
{% endif %}
{% else %}
<p class="text-muted">{% trans "Location/Connection details are not available for this interview type." %}</p>
{% 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 %}
{% endif %}
</div>
</div>
</div>
</div>
{% endwith %}
{# ========================================================= #}
{# --- SECTION 2: PERSONNEL TABLES --- #}
{# ========================================================= #}
{# --- PARTICIPANTS --- #}
<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-item-center" >
<div class="d-flex justify-content-between align-items-center">
<h2 class="text-start"><i class="fas fa-users-cog me-2"></i> {% trans "Assigned Participants" %}</h2>
<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 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>
</div>
</div>
<table class="simple-table">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Role/Designation" %}</th>
<th>{% trans "Role" %}</th>
<th>{% trans "Email" %}</th>
<th>{% trans "Phone Number" %}</th>
<th>{% trans "Source Type" %}</th>
<th>{% trans "Phone" %}</th>
<th>{% trans "Type" %}</th>
</tr>
</thead>
<tbody>
{# External Participants #}
{% for participant in interview.participants.all %}
{% for participant in external_participants %}
<tr>
<td>{{participant.name}}</td>
<td>{{participant.designation}}</td>
<td>{{participant.email}}</td>
<td>{{participant.phone}}</td>
<td>{% trans "External Participants" %}</td>
<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>
</tr>
{% endfor %}
{# System Users (Internal Participants) #}
{% for user in interview.system_users.all %}
{% for user in system_participants %}
<tr>
<td>{{user.get_full_name}}</td>
<td>{% trans "System User" %}</td>
<td>{{user.email}}</td>
<td>{{user.phone}}</td>
<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>{% trans "System User" %}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{# ========================================================= #}
{# --- SECTION 3: COMMENTS (CORRECTED) --- #}
{# ========================================================= #}
{# --- COMMENTS --- #}
<div class="row g-4 mt-1">
<div class="col-lg-12">
<div class="card" id="comments-card" style="height: 100%;">
<div class="card" id="comments-card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
<h5 class="card-title mb-0">
<i class="fas fa-comments me-2"></i>
{% trans "Comments" %} ({% if interview.comments %}{{ interview.comments.count }}{% else %}0{% endif %})
{% trans "Comments" %} ({{ interview.notes.count }})
</h5>
</div>
<div class="card-body overflow-auto">
{# 1. COMMENT DISPLAY & IN-PAGE EDIT FORMS #}
<div class="card-body">
<div id="comment-section" class="mb-4">
{# 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>
{% 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>
</div>
{# 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 }}">
{% 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;">
{% csrf_token %}
<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 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>
</button>
</form>
</div>
{% endif %}
</div>
{% endfor %}
{% else %}
<p class="text-muted">{% trans "No comments yet. Be the first to comment!" %}</p>
{% endif %}
<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>
<hr>
{# 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 %}
<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>
</div>
</div>
</div>
</div>
</div>
{# --- 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">
{# MODALS #}
<!-- Participants Modal -->
<div class="modal fade" id="assignParticipants" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="jobAssignmentLabel">{% trans "Manage all participants" %}</h5>
<h5 class="modal-title">{% 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 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 method="post" action="{% url 'create_interview_participants' interview.pk %}">
{% 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>
</form>
</div>
</div>
</div>
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl modal-dialog-centered">
<!-- Email Modal -->
<div class="modal fade" id="emailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header bg-light">
<h5 class="modal-title" id="emailModalLabel">📧 {% trans "Compose Interview Invitation" %}</h5>
<h5 class="modal-title">📧 {% 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.slug %}">
<form method="post" action="{% url 'send_interview_email' interview.pk %}">
{% 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" }}
<label class="form-label fw-bold">{% trans "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 interview.candidate.belong_to_an_agency %}
<button class="nav-link active" id="candidate-tab" data-bs-toggle="tab" data-bs-target="#candidate-pane" type="button">
{% if candidate.belong_to_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" role="tab" aria-controls="participants-pane" aria-selected="false">
{% trans "Panel Message (Interviewers)" %}
<button class="nav-link" id="participants-tab" data-bs-toggle="tab" data-bs-target="#participants-pane" type="button">
{% trans "Panel Message" %}
</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">{% 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>
<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" }}
{% endif %}
</div>
{# --- 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 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" }}
</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>
@ -619,90 +439,36 @@ body {
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
// --- 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 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';
}
}
// --- 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;
function copyLink() {
const urlElement = document.getElementById('meeting-join-url');
const textToCopy = urlElement.textContent || urlElement.innerText;
const messageElement = document.getElementById('copy-message');
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);
}
navigator.clipboard.writeText(textToCopy).then(() => {
messageElement.style.opacity = '1';
setTimeout(() => {
messageElement.style.opacity = '0';
}, 2000);
}).catch(err => {
console.error('Failed to copy: ', err);
});
}
</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="{% url 'delete_meeting' meeting.slug %}"
hx-post=""
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="" class="btn btn-outline-primary" title="{% trans 'View' %}">
<i class="fas fa-eye"></i>
<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>
{# 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="{% url 'delete_meeting' meeting.slug %}"
hx-post=""
hx-target="#deleteModalBody"
hx-swap="outerHTML"
data-item-name="{{ meeting.topic }}">