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 return message
class InterviewParticpantsForm(forms.ModelForm): class InterviewParticpantsForm(forms.ModelForm):
participants = forms.ModelMultipleChoiceField( participants = forms.ModelMultipleChoiceField(
queryset=Participants.objects.all(), queryset=Participants.objects.all(),
@ -1706,7 +1707,7 @@ class InterviewParticpantsForm(forms.ModelForm):
) )
system_users=forms.ModelMultipleChoiceField( system_users=forms.ModelMultipleChoiceField(
queryset=User.objects.all(), queryset=User.objects.filter(user_type='staff'),
widget=forms.CheckboxSelectMultiple, widget=forms.CheckboxSelectMultiple,
required=False, required=False,
label=_("Select Users")) label=_("Select Users"))
@ -1861,107 +1862,107 @@ class InterviewParticpantsForm(forms.ModelForm):
# self.initial['message_for_participants'] = participants_message.strip() # self.initial['message_for_participants'] = participants_message.strip()
class InterviewEmailForm(forms.Form): # class InterviewEmailForm(forms.Form):
# ... (Field definitions) # # ... (Field definitions)
def __init__(self, *args, candidate, external_participants, system_participants, meeting, job, **kwargs): # def __init__(self, *args, candidate, external_participants, system_participants, meeting, job, **kwargs):
super().__init__(*args, **kwargs) # super().__init__(*args, **kwargs)
location = meeting.interview_location # location = meeting
# --- Data Preparation --- # # --- Data Preparation ---
# Safely access details through the related InterviewLocation object # # Safely access details through the related InterviewLocation object
if location and location.start_time: # if location and location.start_time:
formatted_date = location.start_time.strftime('%Y-%m-%d') # formatted_date = location.start_time.strftime('%Y-%m-%d')
formatted_time = location.start_time.strftime('%I:%M %p') # formatted_time = location.start_time.strftime('%I:%M %p')
duration = location.duration # duration = location.duration
meeting_link = location.details_url if location.details_url else "N/A (See Location Topic)" # meeting_link = location.details_url if location.details_url else "N/A (See Location Topic)"
else: # else:
# Handle case where location or time is missing/None # # Handle case where location or time is missing/None
formatted_date = "TBD - Awaiting Scheduling" # formatted_date = "TBD - Awaiting Scheduling"
formatted_time = "TBD" # formatted_time = "TBD"
duration = "N/A" # duration = "N/A"
meeting_link = "Not Available" # meeting_link = "Not Available"
job_title = job.title # job_title = job.title
agency_name = candidate.hiring_agency.name if candidate.belong_to_an_agency and candidate.hiring_agency else "Hiring Agency" # 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 --- # # --- Combined Participants List for Internal Email ---
external_participants_names = ", ".join([p.name for p in external_participants ]) # external_participants_names = ", ".join([p.name for p in external_participants ])
system_participants_names = ", ".join([p.first_name for p in system_participants ]) # system_participants_names = ", ".join([p.first_name for p in system_participants ])
participant_names = ", ".join(filter(None, [external_participants_names, system_participants_names])) # participant_names = ", ".join(filter(None, [external_participants_names, system_participants_names]))
# --- 1. Candidate Message (Use meeting_link) --- # # --- 1. Candidate Message (Use meeting_link) ---
candidate_message = f""" # candidate_message = f"""
Dear {candidate.full_name}, # 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} # - **Date:** {formatted_date}
- **Time:** {formatted_time} (RIYADH TIME) # - **Time:** {formatted_time} (RIYADH TIME)
- **Duration:** {duration} # - **Duration:** {duration}
- **Meeting Link:** {meeting_link} # - **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, # Best regards,
KAAUH Hiring Team # KAAUH Hiring Team
""" # """
# ... (Messages for agency and participants remain the same, using the updated safe variables) # # ... (Messages for agency and participants remain the same, using the updated safe variables)
# --- 2. Agency Message (Professional and clear details) --- # # --- 2. Agency Message (Professional and clear details) ---
agency_message = f""" # agency_message = f"""
Dear {agency_name}, # Dear {agency_name},
... # ...
**Interview Details:** # **Interview Details:**
... # ...
- **Date:** {formatted_date} # - **Date:** {formatted_date}
- **Time:** {formatted_time} (RIYADH TIME) # - **Time:** {formatted_time} (RIYADH TIME)
- **Duration:** {duration} # - **Duration:** {duration}
- **Meeting Link:** {meeting_link} # - **Meeting Link:** {meeting_link}
... # ...
""" # """
# --- 3. Participants Message (Action-oriented and informative) --- # # --- 3. Participants Message (Action-oriented and informative) ---
participants_message = f""" # participants_message = f"""
Hi Team, # Hi Team,
... # ...
**Interview Summary:** # **Interview Summary:**
- **Candidate:** {candidate.full_name} # - **Candidate:** {candidate.full_name}
- **Date:** {formatted_date} # - **Date:** {formatted_date}
- **Time:** {formatted_time} (RIYADH TIME) # - **Time:** {formatted_time} (RIYADH TIME)
- **Duration:** {duration} # - **Duration:** {duration}
- **Your Fellow Interviewers:** {participant_names} # - **Your Fellow Interviewers:** {participant_names}
**Action Items:** # **Action Items:**
1. Please review **{candidate.full_name}'s** resume and notes. # 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. # 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. # 3. Be ready to start promptly at the scheduled time.
... # ...
""" # """
# Set initial data # # Set initial data
self.initial['subject'] = f"Interview Invitation: {job_title} at KAAUH - {candidate.full_name}" # 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_candidate'] = candidate_message.strip()
self.initial['message_for_agency'] = agency_message.strip() # self.initial['message_for_agency'] = agency_message.strip()
self.initial['message_for_participants'] = participants_message.strip() # self.initial['message_for_participants'] = participants_message.strip()
# class OnsiteLocationForm(forms.ModelForm): # # class OnsiteLocationForm(forms.ModelForm):
# class Meta: # # class Meta:
# model= # # model=
# fields=['location'] # # fields=['location']
# widgets={ # # widgets={
# 'location': forms.TextInput(attrs={'placeholder': 'Enter Interview Location'}), # # 'location': forms.TextInput(attrs={'placeholder': 'Enter Interview Location'}),
# } # # }
#during bulk schedule #during bulk schedule
class OnsiteLocationForm(forms.ModelForm): 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 OnsiteReshuduleForm(forms.ModelForm):
class Meta: class Meta:
model = OnsiteLocationDetails model = OnsiteLocationDetails

View File

@ -656,5 +656,6 @@ urlpatterns = [
# Detail View (assuming slug is on ScheduledInterview) # 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, Message,
Document, Document,
OnsiteLocationDetails, OnsiteLocationDetails,
InterviewLocation InterviewLocation,
InterviewNote
) )
@ -249,123 +250,7 @@ class ZoomMeetingCreateView(StaffRequiredMixin, CreateView):
messages.error(self.request, f"Error creating meeting: {e}") messages.error(self.request, f"Error creating meeting: {e}")
return redirect(reverse("create_meeting", kwargs={"slug": instance.slug})) 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): class ZoomMeetingDetailsView(StaffRequiredMixin, DetailView):
model = ZoomMeetingDetails model = ZoomMeetingDetails
@ -3122,7 +3007,7 @@ def add_meeting_comment(request, slug):
meeting = get_object_or_404(ZoomMeetingDetails, slug=slug) meeting = get_object_or_404(ZoomMeetingDetails, slug=slug)
if request.method == "POST": if request.method == "POST":
form = MeetingCommentForm(request.POST) form = InterviewNoteForm(request.POST)
if form.is_valid(): if form.is_valid():
comment = form.save(commit=False) comment = form.save(commit=False)
comment.meeting = meeting comment.meeting = meeting
@ -3143,7 +3028,7 @@ def add_meeting_comment(request, slug):
return redirect("meeting_details", slug=slug) return redirect("meeting_details", slug=slug)
else: else:
form = MeetingCommentForm() form = InterviewNoteForm()
context = { context = {
"form": form, "form": form,
@ -3169,7 +3054,7 @@ def edit_meeting_comment(request, slug, comment_id):
return redirect("meeting_details", slug=slug) return redirect("meeting_details", slug=slug)
if request.method == "POST": if request.method == "POST":
form = MeetingCommentForm(request.POST, instance=comment) form = InterviewNoteForm(request.POST, instance=comment)
if form.is_valid(): if form.is_valid():
comment = form.save() comment = form.save()
messages.success(request, "Comment updated successfully!") messages.success(request, "Comment updated successfully!")
@ -3187,7 +3072,7 @@ def edit_meeting_comment(request, slug, comment_id):
return redirect("meeting_details", slug=slug) return redirect("meeting_details", slug=slug)
else: else:
form = MeetingCommentForm(instance=comment) form = InterviewNoteForm(instance=comment)
context = {"form": form, "meeting": meeting, "comment": comment} context = {"form": form, "meeting": meeting, "comment": comment}
return render(request, "includes/edit_comment_form.html", context) return render(request, "includes/edit_comment_form.html", context)
@ -5773,7 +5658,7 @@ class MeetingListView(ListView):
'details': details, 'details': details,
'type': location.location_type, 'type': location.location_type,
'topic': location.topic, 'topic': location.topic,
'slug': interview.slug, # 'slug': interview.slug,
'start_time': start_datetime, # Combined datetime object 'start_time': start_datetime, # Combined datetime object
# Duration should ideally be on ScheduledInterview or fetched from details # Duration should ideally be on ScheduledInterview or fetched from details
'duration': getattr(details, 'duration', 'N/A'), '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) 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' %} {% extends 'base.html' %}
{% load static i18n %} {% load static i18n %}
{% load widget_tweaks %} {% load widget_tweaks %}
{% block customCSS %} {% block customCSS %}
<style> <style>
/* -------------------------------------------------------------------------- */
/* KAAT-S Redesign CSS - Compacted and Reordered Layout */
/* -------------------------------------------------------------------------- */
:root { :root {
--kaauh-teal: #00636e; --kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53; --kaauh-teal-dark: #004a53;
@ -20,286 +17,189 @@
--kaauh-link: #007bff; --kaauh-link: #007bff;
--kaauh-link-hover: #0056b3; --kaauh-link-hover: #0056b3;
} }
body { background-color: #f0f2f5; font-family: 'Inter', sans-serif; }
body { .card { border: none; border-radius: 8px; box-shadow: 0 3px 10px rgba(0,0,0,0.04); margin-bottom: 1rem; }
background-color: #f0f2f5; .card-body { padding: 1rem 1.25rem; }
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 { #comments-card .card-header {
background-color: white; background-color: white;
color: var(--kaauh-teal-dark); color: var(--kaauh-teal-dark);
padding: 0.75rem 1.25rem; /* Reduced header padding */ padding: 0.75rem 1.25rem;
font-weight: 600; font-weight: 600;
border-radius: 8px 8px 0 0; border-radius: 8px 8px 0 0;
border-bottom: 1px solid var(--kaauh-border); 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 { .main-title-container h1 {
font-size: 1.75rem; /* Reduced size */ font-size: 1.75rem; font-weight: 700;
font-weight: 700;
} }
.status-badge { .status-badge {
font-size: 0.7rem; /* Smaller badge */ font-size: 0.7rem; padding: 0.3em 0.7em; border-radius: 12px;
padding: 0.3em 0.7em;
border-radius: 12px;
} }
.bg-scheduled { background-color: #00636e !important; color: white !important;} .bg-scheduled { background-color: #00636e !important; color: white !important; }
.bg-completed { background-color: #198754 !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-waiting { background-color: #ffc107 !important; color: var(--kaauh-primary-text) !important; }
.bg-started { background-color: var(--kaauh-teal) !important; color: white !important;} .bg-started { background-color: var(--kaauh-teal) !important; color: white !important; }
.bg-ended { background-color: var(--kaauh-danger) !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 Row & Content Styles (Made Smaller) ------------------ */
.detail-section h2, .card h2 { .detail-section h2, .card h2 {
color: var(--kaauh-teal-dark); color: var(--kaauh-teal-dark); font-weight: 700; font-size: 1.25rem;
font-weight: 700; margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--kaauh-border);
font-size: 1.25rem; /* Reduced size */
margin-bottom: 0.75rem;
padding-bottom: 0.5rem;
border-bottom: 1px solid var(--kaauh-border);
} }
.detail-row-simple { .detail-row-simple {
display: flex; display: flex; padding: 0.4rem 0; border-bottom: 1px dashed var(--kaauh-border); font-size: 0.85rem;
padding: 0.4rem 0; /* Reduced vertical padding */
border-bottom: 1px dashed var(--kaauh-border);
font-size: 0.85rem; /* Smaller text */
} }
.detail-label-simple { .detail-label-simple { font-weight: 600; color: var(--kaauh-teal-dark); flex-basis: 40%; }
font-weight: 600; .detail-value-simple { color: var(--kaauh-primary-text); font-weight: 500; flex-basis: 60%; }
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 { .btn-primary-teal {
background-color: var(--kaauh-teal); background-color: var(--kaauh-teal); border-color: var(--kaauh-teal); padding: 0.6rem 1.2rem;
border-color: var(--kaauh-teal); font-size: 0.95rem; border-radius: 6px; color: white;
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 { .btn-primary-teal:hover { background-color: var(--kaauh-teal-dark); }
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
}
/* Added Danger Button Style for main delete */
.btn-danger-red { .btn-danger-red {
background-color: var(--kaauh-danger); background-color: var(--kaauh-danger); border-color: var(--kaauh-danger); color: white;
border-color: var(--kaauh-danger); padding: 0.6rem 1.2rem; font-size: 0.95rem; border-radius: 6px; font-weight: 600;
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 { .btn-secondary-back {
/* Subtle Back Button */ background-color: transparent; border: none; color: var(--kaauh-secondary-text);
background-color: transparent; font-weight: 600; font-size: 1rem; padding: 0.5rem 0.75rem; transition: color 0.2s;
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 { .btn-secondary-back:hover { color: var(--kaauh-teal); text-decoration: underline; }
color: var(--kaauh-teal);
text-decoration: underline;
}
.join-url-display { .join-url-display {
background-color: white; background-color: white; border: 1px solid var(--kaauh-border); padding: 0.5rem; font-size: 0.85rem;
border: 1px solid var(--kaauh-border);
padding: 0.5rem; /* Reduced padding */
font-size: 0.85rem; /* Smaller text */
} }
.btn-copy-simple { .btn-copy-simple {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem; background-color: var(--kaauh-teal-dark); border: none; color: white; border-radius: 4px;
background-color: var(--kaauh-teal-dark);
border: none;
color: white;
border-radius: 4px;
} }
.btn-copy-simple:hover { .btn-copy-simple:hover { background-color: var(--kaauh-teal); }
background-color: var(--kaauh-teal);
}
/* ------------------ Simple Table Styles ------------------ */
.simple-table { .simple-table {
width: 100%; width: 100%; margin-top: 0.5rem; border-collapse: collapse;
margin-top: 0.5rem;
border-collapse: collapse;
} }
.simple-table th { .simple-table th {
background-color: var(--kaauh-teal-light); background-color: var(--kaauh-teal-light); color: var(--kaauh-teal-dark); font-weight: 700;
color: var(--kaauh-teal-dark); padding: 8px 12px; border: 1px solid var(--kaauh-border); font-size: 0.8rem;
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 { .simple-table td {
padding: 8px 12px; /* Reduced padding */ padding: 8px 12px; border: 1px solid var(--kaauh-border); background-color: white; font-size: 0.85rem;
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 { .btn-edit-comment {
background-color: transparent; background-color: transparent; border: 1px solid var(--kaauh-teal); color: var(--kaauh-teal);
border: 1px solid var(--kaauh-teal); padding: 0.25rem 0.5rem; font-size: 0.75rem; border-radius: 4px; font-weight: 500;
color: var(--kaauh-teal);
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
border-radius: 4px;
font-weight: 500;
} }
.btn-edit-comment:hover { .btn-edit-comment:hover { background-color: var(--kaauh-teal-light); }
background-color: var(--kaauh-teal-light);
}
</style> </style>
{% endblock %} {% endblock %}
{% block content %} {% 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"> <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"> <div class="d-flex justify-content-between align-items-center mb-4">
{# Back Button #} <a href="{% url 'list_meetings' %}" class="btn btn-secondary-back">
<a href="{% url 'interview_list' %}" class="btn btn-secondary-back"> <i class="fas fa-arrow-left me-1"></i> {% trans "Back to Meetings" %}
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Interviews" %}
</a> </a>
{# Edit and Delete Buttons #}
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<a href="{% url 'update_scheduled_interview' interview.slug %}" class="btn btn-primary-teal btn-sm"> <a href="{% url 'update_meeting' meeting.slug %}" class="btn btn-primary-teal btn-sm">
<i class="fas fa-edit me-1"></i> {% trans "Edit Interview" %} <i class="fas fa-edit me-1"></i> {% trans "Edit Meeting" %}
</a> </a>
{# DELETE MEETING FORM #} <form method="post" action="{% url 'delete_meeting' meeting.slug %}" style="display: inline;">
<form method="post" action="{% url 'delete_scheduled_interview' interview.slug %}" style="display: inline;">
{% csrf_token %} {% 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." %}')"> <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 Interview" %} <i class="fas fa-trash-alt me-1"></i> {% trans "Delete Meeting" %}
</button> </button>
</form> </form>
</div> </div>
</div> </div>
{# ========================================================= #} {# --- MAIN TITLE --- #}
{# --- MAIN TITLE AT TOP --- #}
{# ========================================================= #}
{% with zoom_details=interview.zoom_details.0 %}
<div class="main-title-container mb-4"> <div class="main-title-container mb-4">
<h1 class="text-start" style="color: var(--kaauh-teal-dark);"> <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>
<i class="fas fa-video me-2" style="color: var(--kaauh-teal);"></i> {{ meeting.topic|default:"[Meeting Topic]" }}
{{ zoom_details.topic|default:"[Remote Interview]" }} <span class="status-badge bg-{{ interview.status|lower|default:'scheduled' }} ms-3">
{% else %} {{ interview.get_status_display|default:"Scheduled" }}
<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> </span>
</h1> </h1>
</div> </div>
{# ========================================================= #} {# --- INTERVIEW & CONNECTION CARDS --- #}
{# --- SECTION 1: INTERVIEW & CONNECTION/LOCATION CARDS SIDE BY SIDE --- #}
{# ========================================================= #}
<div class="row g-4 mb-5 align-items-stretch"> <div class="row g-4 mb-5 align-items-stretch">
{# Interview Detail #}
{# --- LEFT HALF: INTERVIEW DETAIL CARD --- #}
<div class="col-lg-6"> <div class="col-lg-6">
<div class="p-3 bg-white rounded shadow-sm h-100 d-flex flex-column"> <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"> <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-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-label-simple">{% trans "Job Title" %}:</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-value-simple">{{ job.title|default:"N/A" }}</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>
<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> <div class="detail-row-simple">
{% if interview.candidate.belong_to_agency %} <div class="detail-label-simple">{% trans "Candidate Name" %}:</div>
<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-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 %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
{# --- RIGHT HALF: CONNECTION/LOCATION DETAILS CARD --- #} {# Connection Details #}
<div class="col-lg-6"> <div class="col-lg-6">
<div class="p-3 bg-white rounded shadow-sm h-100 d-flex flex-column"> <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-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-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> <div class="detail-label-simple">{% trans "Date & Time" %}:</div>
<div class="detail-value-simple">
{% if interview.schedule.interview_type == 'Onsite' %} {{ interview.interview_date }} {{ interview.interview_time }} ({{ meeting.timezone }})
{# --- Onsite Details --- #} </div>
<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>
<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 %} {% if meeting.location_type == "Remote" %}
{# --- Remote/Zoom Details --- #} {% with zoom=meeting.zoommeetingdetails %}
<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-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-label-simple">{% trans "Meeting ID" %}:</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> <div class="detail-value-simple">{{ zoom.meeting_id|default:"N/A" }}</div>
</div>
{% if zoom_details.join_url %} <div class="detail-row-simple">
<div class="join-url-container pt-3"> <div class="detail-label-simple">{% trans "Host Email" %}:</div>
<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="detail-value-simple">{{ zoom.host_email|default:"N/A" }}</div>
</div>
<div class="join-url-display d-flex justify-content-between align-items-center position-relative"> {% 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"> <div class="text-truncate me-2">
<strong>{% trans "Join URL" %}:</strong> <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> </div>
<button class="btn-copy-simple ms-2 flex-shrink-0" onclick="copyLink()" title="{% trans 'Copy URL' %}"> <button class="btn-copy-simple ms-2 flex-shrink-0" onclick="copyLink()" title="{% trans 'Copy URL' %}">
<i class="fas fa-copy"></i> <i class="fas fa-copy"></i>
@ -307,310 +207,230 @@ body {
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% else %} {% endwith %}
<p class="text-muted">{% trans "Location/Connection details are not available for this interview type." %}</p> {% 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 %} {% endif %}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{% endwith %}
{# ========================================================= #} {# --- PARTICIPANTS --- #}
{# --- SECTION 2: PERSONNEL TABLES --- #}
{# ========================================================= #}
<div class="row g-4 mt-1 mb-5"> <div class="row g-4 mt-1 mb-5">
{# --- PARTICIPANTS TABLE --- #}
<div class="col-lg-12"> <div class="col-lg-12">
<div class="p-3 bg-white rounded shadow-sm"> <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> <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"> <div class="d-flex gap-2">
<button type="button" class="btn btn-primary-teal btn-sm me-2" <button type="button" class="btn btn-primary-teal btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal" data-bs-target="#assignParticipants">
data-bs-target="#assignParticipants"> <i class="fas fa-users-cog me-1"></i> {% trans "Manage Participants" %} ({{ total_participants }})
<i class="fas fa-users-cog me-1"></i> {% trans "Manage Participants" %} ({{ interview.participants.count|add:interview.system_users.count }}) </button>
</button> <button type="button" class="btn btn-outline-info" data-bs-toggle="modal" data-bs-target="#emailModal">
<i class="fas fa-envelope"></i>
<button type="button" class="btn btn-outline-info" </button>
data-bs-toggle="modal"
title="Send Interview Emails"
data-bs-target="#emailModal">
<i class="fas fa-envelope"></i>
</button>
</div> </div>
</div> </div>
<table class="simple-table"> <table class="simple-table">
<thead> <thead>
<tr> <tr>
<th>{% trans "Name" %}</th> <th>{% trans "Name" %}</th>
<th>{% trans "Role/Designation" %}</th> <th>{% trans "Role" %}</th>
<th>{% trans "Email" %}</th> <th>{% trans "Email" %}</th>
<th>{% trans "Phone Number" %}</th> <th>{% trans "Phone" %}</th>
<th>{% trans "Source Type" %}</th> <th>{% trans "Type" %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{# External Participants #} {% for participant in external_participants %}
{% for participant in interview.participants.all %}
<tr> <tr>
<td>{{participant.name}}</td> <td>{{ participant.name }}</td>
<td>{{participant.designation}}</td> <td>{{ participant.designation|default:"Participant" }}</td>
<td>{{participant.email}}</td> <td>{{ participant.email|default:"N/A" }}</td>
<td>{{participant.phone}}</td> <td>{{ participant.phone|default:"N/A" }}</td>
<td>{% trans "External Participants" %}</td> <td>{% trans "External" %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
{# System Users (Internal Participants) #} {% for user in system_participants %}
{% for user in interview.system_users.all %}
<tr> <tr>
<td>{{user.get_full_name}}</td> <td>{{ user.get_full_name|default:user.username }}</td>
<td>{% trans "System User" %}</td> <td>Admin</td>
<td>{{user.email}}</td> <td>{{ user.email|default:"N/A" }}</td>
<td>{{user.phone}}</td> <td>{{ user.phone|default:"N/A" }}</td>
<td>{% trans "System User" %}</td> <td>{% trans "System User" %}</td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
</div> </div>
{# ========================================================= #} {# --- COMMENTS --- #}
{# --- SECTION 3: COMMENTS (CORRECTED) --- #}
{# ========================================================= #}
<div class="row g-4 mt-1"> <div class="row g-4 mt-1">
<div class="col-lg-12"> <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"> <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> <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> </h5>
</div> </div>
<div class="card-body overflow-auto"> <div class="card-body">
{# 1. COMMENT DISPLAY & IN-PAGE EDIT FORMS #}
<div id="comment-section" class="mb-4"> <div id="comment-section" class="mb-4">
{# NOTE: Assuming comment model has a ForeignKey to ScheduledInterview called 'interview' #} {% for note in interview.notes.all|dictsortreversed:"created_at" %}
{% if interview.comments.all %} <div class="comment-item mb-3 p-3">
{% for comment in interview.comments.all|dictsortreversed:"created_at" %} <div id="comment-view-{{ note.pk }}">
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="comment-item mb-3 p-3"> <div class="comment-metadata" style="font-size: 0.9rem;">
<strong>{{ note.author.get_full_name|default:note.author.username }}</strong>
{# Read-Only Comment View #} <span class="text-muted small ms-2">{{ note.created_at|date:"M d, Y H:i" }}</span>
<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> </div>
{% if note.author == user or user.is_staff %}
{# Hidden Edit Form #} <div class="comment-actions d-flex align-items-center gap-1">
<div id="comment-edit-form-{{ comment.pk }}" style="display: none; margin-top: 10px; padding-top: 10px; border-top: 1px dashed var(--kaauh-border);"> <button type="button" class="btn btn-edit-comment py-0 px-1" onclick="toggleCommentEdit('{{ note.pk }}')" id="edit-btn-{{ note.pk }}">
<form method="POST" action="{% url 'edit_meeting_comment' interview.slug comment.pk %}" id="form-{{ comment.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 %} {% csrf_token %}
<div class="mb-2"> <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?" %}')">
<label for="id_content_{{ comment.pk }}" class="form-label small">{% trans "Edit Comment" %}</label> <i class="fas fa-trash"></i>
{# 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> </button>
</form> </form>
</div> </div>
{% endif %}
</div> </div>
{% endfor %} <p class="mb-0" style="font-size: 0.85rem; white-space: pre-wrap;">{{ note.content|linebreaksbr }}</p>
{% else %} </div>
<p class="text-muted">{% trans "No comments yet. Be the first to comment!" %}</p>
{% endif %} <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> </div>
<hr> <hr>
<h6 class="mb-3">{% trans "Add a New Comment" %}</h6>
{# 2. NEW COMMENT SUBMISSION (Remains the same) #} <form method="POST" action="{% url 'add_meeting_comment' meeting.slug %}">
<h6 class="mb-3" style="color: var(--kaauh-teal-dark);">{% trans "Add a New Comment" %}</h6> {% csrf_token %}
{% if user.is_authenticated %} <div class="mb-3">
<form method="POST" action="{% url 'add_meeting_comment' interview.slug %}"> <label class="form-label small">{% trans "Comment" %}</label>
{% csrf_token %} <textarea name="content" class="form-control" rows="3" required></textarea>
{% if comment_form %} </div>
{{ comment_form.as_p }} <button type="submit" class="btn btn-primary-teal btn-sm">
{% else %} <i class="fas fa-paper-plane me-1"></i> {% trans "Submit Comment" %}
<div class="mb-3"> </button>
<label for="id_content" class="form-label small">{% trans "Comment" %}</label> </form>
<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> </div>
</div> </div>
</div> </div>
{# MODALS #}
{# --- MODALS (Updated to use interview.slug) --- #} <!-- Participants Modal -->
<div class="modal fade" id="assignParticipants" tabindex="-1" aria-hidden="true">
<div class="modal fade" id="assignParticipants" tabindex="-1" aria-labelledby="assignParticipantsLabel" aria-hidden="true"> <div class="modal-dialog modal-lg">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <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> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form method="post" action="{% url 'create_interview_participants' interview.pk %}">
<form method="post" action="{% url 'create_interview_participants' interview.slug %}"> {% csrf_token %}
{% csrf_token %} <div class="modal-body">
{{ form.participants.errors }}
{{ form.participants }}
<div class="modal-body table-responsive"> {{ form.system_users.errors }}
{{ form.system_users }}
{{ interview.name }} {# This might need checking - ScheduledInterview usually doesn't have a 'name' field #} </div>
<div class="modal-footer">
<hr> <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>
<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> </form>
</div> </div>
</div> </div>
</div> </div>
<div class="modal fade" id="emailModal" tabindex="-1" aria-labelledby="emailModalLabel" aria-hidden="true"> <!-- Email Modal -->
<div class="modal-dialog modal-xl modal-dialog-centered"> <div class="modal fade" id="emailModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header bg-light"> <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> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form method="post" action="{% url 'send_interview_email' interview.pk %}">
<form method="post" action="{% url 'send_interview_email' interview.slug %}">
{% csrf_token %} {% csrf_token %}
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<label for="{{ email_form.subject.id_for_label }}" class="form-label fw-bold">Subject</label> <label class="form-label fw-bold">{% trans "Subject" %}</label>
{{ email_form.subject | add_class:"form-control" }} {{ email_form.subject|add_class:"form-control" }}
</div> </div>
<ul class="nav nav-tabs" id="messageTabs" role="tablist"> <ul class="nav nav-tabs" id="messageTabs" role="tablist">
{# Candidate/Agency Tab - Active by default #}
<li class="nav-item" role="presentation"> <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"> <button class="nav-link active" id="candidate-tab" data-bs-toggle="tab" data-bs-target="#candidate-pane" type="button">
{% if interview.candidate.belong_to_an_agency %} {% if candidate.belong_to_agency %}
{% trans "Agency Message" %} {% trans "Agency Message" %}
{% else %} {% else %}
{% trans "Candidate Message" %} {% trans "Candidate Message" %}
{% endif %} {% endif %}
</button> </button>
</li> </li>
{# Participants Tab #}
<li class="nav-item" role="presentation"> <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"> <button class="nav-link" id="participants-tab" data-bs-toggle="tab" data-bs-target="#participants-pane" type="button">
{% trans "Panel Message (Interviewers)" %} {% trans "Panel Message" %}
</button> </button>
</li> </li>
</ul> </ul>
<div class="tab-content border border-top-0 p-3 bg-light-subtle"> <div class="tab-content border border-top-0 p-3 bg-light-subtle">
<div class="tab-pane fade show active" id="candidate-pane">
{# --- Candidate/Agency Pane --- #} <p class="text-muted small">
<div class="tab-pane fade show active" id="candidate-pane" role="tabpanel" aria-labelledby="candidate-tab"> {% if candidate.belong_to_agency %}
<p class="text-muted small">{% trans "This email will be sent to the candidate or their hiring agency." %}</p> {% trans "This email will be sent to the hiring agency." %}
{% else %}
{% if not interview.candidate.belong_to_an_agency %} {% trans "This email will be sent to the candidate." %}
<div class="form-group"> {% endif %}
<label for="{{ email_form.message_for_candidate.id_for_label }}" class="form-label d-none">{% trans "Candidate Message" %}</label> </p>
{{ email_form.message_for_candidate | add_class:"form-control" }} {% if candidate.belong_to_agency %}
</div> {{ email_form.message_for_agency|add_class:"form-control" }}
{% endif %} {% else %}
{{ email_form.message_for_candidate|add_class:"form-control" }}
{% 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 %} {% endif %}
</div> </div>
<div class="tab-pane fade" id="participants-pane">
{# --- Participants Pane --- #} <p class="text-muted small">{% trans "This email will be sent to all interview participants." %}</p>
<div class="tab-pane fade" id="participants-pane" role="tabpanel" aria-labelledby="participants-tab"> {{ email_form.message_for_participants|add_class:"form-control" }}
<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>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-danger-red" data-bs-dismiss="modal">{% trans "Close" %}</button> <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> <button type="submit" class="btn btn-primary-teal">{% trans "Send Invitation" %}</button>
@ -619,90 +439,36 @@ body {
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block customJS %} {% block customJS %}
<script> <script>
// --- COMMENT EDITING FUNCTION --- function toggleCommentEdit(commentPk) {
function toggleCommentEdit(commentPk) { const viewDiv = document.getElementById(`comment-view-${commentPk}`);
const viewDiv = document.getElementById(`comment-view-${commentPk}`); const editFormDiv = document.getElementById(`comment-edit-form-${commentPk}`);
const editFormDiv = document.getElementById(`comment-edit-form-${commentPk}`); if (viewDiv.style.display === 'none') {
const editButton = document.getElementById(`edit-btn-${commentPk}`); viewDiv.style.display = 'block';
const deleteForm = document.getElementById(`delete-form-${commentPk}`); editFormDiv.style.display = 'none';
} else {
if (viewDiv.style.display !== 'none') { viewDiv.style.display = 'none';
// Switch to Edit Mode editFormDiv.style.display = 'block';
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
}
} }
}
// --- COPY LINK FUNCTION --- function copyLink() {
// CopyLink function implementation (slightly improved for message placement) const urlElement = document.getElementById('meeting-join-url');
function copyLink() { const textToCopy = urlElement.textContent || urlElement.innerText;
const urlElement = document.getElementById('meeting-join-url'); const messageElement = document.getElementById('copy-message');
const displayContainer = urlElement.closest('.join-url-display');
const messageElement = document.getElementById('copy-message');
const textToCopy = urlElement.textContent || urlElement.innerText;
clearTimeout(window.copyMessageTimeout); navigator.clipboard.writeText(textToCopy).then(() => {
messageElement.style.opacity = '1';
function showMessage(success) { setTimeout(() => {
messageElement.textContent = success ? '{% trans "Copied!" %}' : '{% trans "Copy Failed." %}'; messageElement.style.opacity = '0';
messageElement.style.backgroundColor = success ? 'var(--kaauh-success)' : 'var(--kaauh-danger)'; }, 2000);
messageElement.style.opacity = '1'; }).catch(err => {
console.error('Failed to copy: ', err);
// 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> </script>
{% endblock %} {% endblock %}

View File

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