meetings detail
This commit is contained in:
parent
88d1726721
commit
506766b6ca
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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 candidate’s 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
|
||||
|
||||
@ -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"),
|
||||
|
||||
]
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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 %}
|
||||
@ -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 }}">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user