before update the meeting

This commit is contained in:
ismail 2025-11-19 14:50:20 +03:00
parent 61cd47ccc9
commit d1a49717ee
8 changed files with 119 additions and 112 deletions

View File

@ -2490,12 +2490,11 @@ class StaffAssignmentForm(forms.ModelForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Filter users to only show staff members # Filter users to only show staff members
self.fields['assigned_to'].queryset = User.objects.filter( self.fields['assigned_to'].queryset = User.objects.filter(
user_type='staff' user_type='staff',is_superuser=False
).order_by('first_name', 'last_name') ).order_by('first_name', 'last_name')
# Add empty choice for unassigning # Add empty choice for unassigning
self.fields['assigned_to'].required = False self.fields['assigned_to'].required = False
self.fields['assigned_to'].empty_label = _('-- Unassign Staff --')
self.helper = FormHelper() self.helper = FormHelper()
self.helper.form_method = 'post' self.helper.form_method = 'post'
@ -2516,3 +2515,4 @@ class StaffAssignmentForm(forms.ModelForm):
if assigned_to and assigned_to.user_type != 'staff': if assigned_to and assigned_to.user_type != 'staff':
raise forms.ValidationError(_('Only staff members can be assigned to jobs.')) raise forms.ValidationError(_('Only staff members can be assigned to jobs.'))
return assigned_to return assigned_to

View File

@ -129,7 +129,7 @@ class JobPosting(Base):
blank=True, blank=True,
) )
application_deadline = models.DateField(db_index=True) # Added index application_deadline = models.DateField(db_index=True)
application_instructions = CKEditor5Field( application_instructions = CKEditor5Field(
blank=True, null=True, config_name="extends" blank=True, null=True, config_name="extends"
) )
@ -912,34 +912,34 @@ class Application(Base):
@property @property
def get_latest_meeting(self): def get_latest_meeting(self):
""" """
Retrieves the most specific location details (subclass instance) Retrieves the most specific location details (subclass instance)
of the latest ScheduledInterview for this application, or None. of the latest ScheduledInterview for this application, or None.
""" """
# 1. Get the latest ScheduledInterview # 1. Get the latest ScheduledInterview
schedule = self.scheduled_interviews.order_by("-created_at").first() schedule = self.scheduled_interviews.order_by("-created_at").first()
# Check if a schedule exists and if it has an interview location # Check if a schedule exists and if it has an interview location
if not schedule or not schedule.interview_location: if not schedule or not schedule.interview_location:
return None return None
# Get the base location instance # Get the base location instance
interview_location = schedule.interview_location interview_location = schedule.interview_location
# 2. Safely retrieve the specific subclass details # 2. Safely retrieve the specific subclass details
# Determine the expected subclass accessor name based on the location_type # Determine the expected subclass accessor name based on the location_type
if interview_location.location_type == 'Remote': if interview_location.location_type == 'Remote':
accessor_name = 'zoommeetingdetails' accessor_name = 'zoommeetingdetails'
else: # Assumes 'Onsite' or any other type defaults to Onsite else: # Assumes 'Onsite' or any other type defaults to Onsite
accessor_name = 'onsitelocationdetails' accessor_name = 'onsitelocationdetails'
# Use getattr to safely retrieve the specific meeting object (subclass instance). # Use getattr to safely retrieve the specific meeting object (subclass instance).
# If the accessor exists but points to None (because the subclass record was deleted), # If the accessor exists but points to None (because the subclass record was deleted),
# or if the accessor name is wrong for the object's true type, it will return None. # or if the accessor name is wrong for the object's true type, it will return None.
meeting_details = getattr(interview_location, accessor_name, None) meeting_details = getattr(interview_location, accessor_name, None)
return meeting_details return meeting_details
@property @property
def has_future_meeting(self): def has_future_meeting(self):
@ -1034,13 +1034,13 @@ class TrainingMaterial(Base):
class InterviewLocation(Base): class InterviewLocation(Base):
""" """
Base model for all interview location/meeting details (remote or onsite) Base model for all interview location/meeting details (remote or onsite)
using Multi-Table Inheritance. using Multi-Table Inheritance.
""" """
class LocationType(models.TextChoices): class LocationType(models.TextChoices):
REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)') REMOTE = 'Remote', _('Remote (e.g., Zoom, Google Meet)')
ONSITE = 'Onsite', _('In-Person (Physical Location)') ONSITE = 'Onsite', _('In-Person (Physical Location)')
class Status(models.TextChoices): class Status(models.TextChoices):
"""Defines the possible real-time statuses for any interview location/meeting.""" """Defines the possible real-time statuses for any interview location/meeting."""
WAITING = "waiting", _("Waiting") WAITING = "waiting", _("Waiting")
@ -1054,23 +1054,23 @@ class InterviewLocation(Base):
verbose_name=_("Location Type"), verbose_name=_("Location Type"),
db_index=True db_index=True
) )
details_url = models.URLField( details_url = models.URLField(
verbose_name=_("Meeting/Location URL"), verbose_name=_("Meeting/Location URL"),
max_length=2048, max_length=2048,
blank=True, blank=True,
null=True null=True
) )
topic = models.CharField( # Renamed from 'description' to 'topic' to match your input topic = models.CharField( # Renamed from 'description' to 'topic' to match your input
max_length=255, max_length=255,
verbose_name=_("Location/Meeting Topic"), verbose_name=_("Location/Meeting Topic"),
blank=True, blank=True,
help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room, 3rd Floor'") help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room, 3rd Floor'")
) )
timezone = models.CharField( timezone = models.CharField(
max_length=50, max_length=50,
verbose_name=_("Timezone"), verbose_name=_("Timezone"),
default='UTC' default='UTC'
) )
@ -1086,7 +1086,7 @@ class InterviewLocation(Base):
class ZoomMeetingDetails(InterviewLocation): class ZoomMeetingDetails(InterviewLocation):
"""Concrete model for remote interviews (Zoom specifics).""" """Concrete model for remote interviews (Zoom specifics)."""
status = models.CharField( status = models.CharField(
db_index=True, db_index=True,
max_length=20, max_length=20,
@ -1101,7 +1101,7 @@ class ZoomMeetingDetails(InterviewLocation):
) )
meeting_id = models.CharField( meeting_id = models.CharField(
db_index=True, db_index=True,
max_length=50, max_length=50,
unique=True, unique=True,
verbose_name=_("External Meeting ID") verbose_name=_("External Meeting ID")
) )
@ -1117,7 +1117,7 @@ class ZoomMeetingDetails(InterviewLocation):
join_before_host = models.BooleanField( join_before_host = models.BooleanField(
default=False, verbose_name=_("Join Before Host") default=False, verbose_name=_("Join Before Host")
) )
host_email=models.CharField(null=True,blank=True) host_email=models.CharField(null=True,blank=True)
mute_upon_entry = models.BooleanField( mute_upon_entry = models.BooleanField(
default=False, verbose_name=_("Mute Upon Entry") default=False, verbose_name=_("Mute Upon Entry")
@ -1137,17 +1137,17 @@ class ZoomMeetingDetails(InterviewLocation):
class OnsiteLocationDetails(InterviewLocation): class OnsiteLocationDetails(InterviewLocation):
"""Concrete model for onsite interviews (Room/Address specifics).""" """Concrete model for onsite interviews (Room/Address specifics)."""
physical_address = models.CharField( physical_address = models.CharField(
max_length=255, max_length=255,
verbose_name=_("Physical Address"), verbose_name=_("Physical Address"),
blank=True, blank=True,
null=True null=True
) )
room_number = models.CharField( room_number = models.CharField(
max_length=50, max_length=50,
verbose_name=_("Room Number/Name"), verbose_name=_("Room Number/Name"),
blank=True, blank=True,
null=True null=True
) )
start_time = models.DateTimeField( start_time = models.DateTimeField(
@ -1181,7 +1181,7 @@ class OnsiteLocationDetails(InterviewLocation):
class InterviewSchedule(Base): class InterviewSchedule(Base):
"""Stores the TEMPLATE criteria for BULK interview generation.""" """Stores the TEMPLATE criteria for BULK interview generation."""
# We need a field to store the template location details linked to this bulk schedule. # We need a field to store the template location details linked to this bulk schedule.
# This location object contains the generic Zoom/Onsite info to be cloned. # This location object contains the generic Zoom/Onsite info to be cloned.
template_location = models.ForeignKey( template_location = models.ForeignKey(
@ -1192,17 +1192,17 @@ class InterviewSchedule(Base):
blank=True, blank=True,
verbose_name=_("Location Template (Zoom/Onsite)") verbose_name=_("Location Template (Zoom/Onsite)")
) )
# NOTE: schedule_interview_type field is needed in the form, # NOTE: schedule_interview_type field is needed in the form,
# but not on the model itself if we use template_location. # but not on the model itself if we use template_location.
# If you want to keep it: # If you want to keep it:
schedule_interview_type = models.CharField( schedule_interview_type = models.CharField(
max_length=10, max_length=10,
choices=InterviewLocation.LocationType.choices, choices=InterviewLocation.LocationType.choices,
verbose_name=_("Interview Type"), verbose_name=_("Interview Type"),
default=InterviewLocation.LocationType.REMOTE default=InterviewLocation.LocationType.REMOTE
) )
job = models.ForeignKey( job = models.ForeignKey(
JobPosting, JobPosting,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -1212,14 +1212,14 @@ class InterviewSchedule(Base):
applications = models.ManyToManyField( applications = models.ManyToManyField(
Application, related_name="interview_schedules", blank=True Application, related_name="interview_schedules", blank=True
) )
start_date = models.DateField(db_index=True, verbose_name=_("Start Date")) start_date = models.DateField(db_index=True, verbose_name=_("Start Date"))
end_date = models.DateField(db_index=True, verbose_name=_("End Date")) end_date = models.DateField(db_index=True, verbose_name=_("End Date"))
working_days = models.JSONField( working_days = models.JSONField(
verbose_name=_("Working Days") verbose_name=_("Working Days")
) )
start_time = models.TimeField(verbose_name=_("Start Time")) start_time = models.TimeField(verbose_name=_("Start Time"))
end_time = models.TimeField(verbose_name=_("End Time")) end_time = models.TimeField(verbose_name=_("End Time"))
@ -1246,7 +1246,7 @@ class InterviewSchedule(Base):
class ScheduledInterview(Base): class ScheduledInterview(Base):
"""Stores individual scheduled interviews (whether bulk or individually created).""" """Stores individual scheduled interviews (whether bulk or individually created)."""
class InterviewStatus(models.TextChoices): class InterviewStatus(models.TextChoices):
SCHEDULED = "scheduled", _("Scheduled") SCHEDULED = "scheduled", _("Scheduled")
CONFIRMED = "confirmed", _("Confirmed") CONFIRMED = "confirmed", _("Confirmed")
@ -1265,13 +1265,13 @@ class ScheduledInterview(Base):
related_name="scheduled_interviews", related_name="scheduled_interviews",
db_index=True, db_index=True,
) )
# Links to the specific, individual location/meeting details for THIS interview # Links to the specific, individual location/meeting details for THIS interview
interview_location = models.OneToOneField( interview_location = models.OneToOneField(
InterviewLocation, InterviewLocation,
on_delete=models.CASCADE, on_delete=models.CASCADE,
related_name="scheduled_interview", related_name="scheduled_interview",
null=True, null=True,
blank=True, blank=True,
db_index=True, db_index=True,
verbose_name=_("Meeting/Location Details") verbose_name=_("Meeting/Location Details")
@ -1286,13 +1286,13 @@ class ScheduledInterview(Base):
blank=True, blank=True,
db_index=True, db_index=True,
) )
participants = models.ManyToManyField('Participants', blank=True) participants = models.ManyToManyField('Participants', blank=True)
system_users = models.ManyToManyField(User, related_name="attended_interviews", blank=True) system_users = models.ManyToManyField(User, related_name="attended_interviews", blank=True)
interview_date = models.DateField(db_index=True, verbose_name=_("Interview Date")) interview_date = models.DateField(db_index=True, verbose_name=_("Interview Date"))
interview_time = models.TimeField(verbose_name=_("Interview Time")) interview_time = models.TimeField(verbose_name=_("Interview Time"))
status = models.CharField( status = models.CharField(
db_index=True, db_index=True,
max_length=20, max_length=20,
@ -1320,7 +1320,7 @@ class InterviewNote(Base):
FEEDBACK = 'Feedback', _('Candidate Feedback') FEEDBACK = 'Feedback', _('Candidate Feedback')
LOGISTICS = 'Logistics', _('Logistical Note') LOGISTICS = 'Logistics', _('Logistical Note')
GENERAL = 'General', _('General Comment') GENERAL = 'General', _('General Comment')
1 1
interview = models.ForeignKey( interview = models.ForeignKey(
ScheduledInterview, ScheduledInterview,
@ -1329,7 +1329,7 @@ class InterviewNote(Base):
verbose_name=_("Scheduled Interview"), verbose_name=_("Scheduled Interview"),
db_index=True db_index=True
) )
author = models.ForeignKey( author = models.ForeignKey(
User, User,
on_delete=models.CASCADE, on_delete=models.CASCADE,
@ -1337,16 +1337,16 @@ class InterviewNote(Base):
verbose_name=_("Author"), verbose_name=_("Author"),
db_index=True db_index=True
) )
note_type = models.CharField( note_type = models.CharField(
max_length=50, max_length=50,
choices=NoteType.choices, choices=NoteType.choices,
default=NoteType.FEEDBACK, default=NoteType.FEEDBACK,
verbose_name=_("Note Type") verbose_name=_("Note Type")
) )
content = CKEditor5Field(verbose_name=_("Content/Feedback"), config_name="extends") content = CKEditor5Field(verbose_name=_("Content/Feedback"), config_name="extends")
class Meta: class Meta:
verbose_name = _("Interview Note") verbose_name = _("Interview Note")
verbose_name_plural = _("Interview Notes") verbose_name_plural = _("Interview Notes")
@ -1354,7 +1354,7 @@ class InterviewNote(Base):
def __str__(self): def __str__(self):
return f"{self.get_note_type_display()} by {self.author.get_username()} on {self.interview.id}" return f"{self.get_note_type_display()} by {self.author.get_username()} on {self.interview.id}"
class FormTemplate(Base): class FormTemplate(Base):
""" """

View File

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

View File

@ -162,11 +162,11 @@ class PersonListView(StaffRequiredMixin, ListView):
gender=self.request.GET.get('gender') gender=self.request.GET.get('gender')
if gender: if gender:
queryset=queryset.filter(gender=gender) queryset=queryset.filter(gender=gender)
nationality=self.request.GET.get('nationality') nationality=self.request.GET.get('nationality')
if nationality: if nationality:
queryset=queryset.filter(nationality=nationality) queryset=queryset.filter(nationality=nationality)
return queryset return queryset
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
context=super().get_context_data(**kwargs) context=super().get_context_data(**kwargs)
@ -174,7 +174,7 @@ class PersonListView(StaffRequiredMixin, ListView):
nationalities = self.model.objects.values_list('nationality', flat=True).filter( nationalities = self.model.objects.values_list('nationality', flat=True).filter(
nationality__isnull=False nationality__isnull=False
).distinct().order_by('nationality') ).distinct().order_by('nationality')
nationality=self.request.GET.get('nationality') nationality=self.request.GET.get('nationality')
context['nationality']=nationality context['nationality']=nationality
context['nationalities']=nationalities context['nationalities']=nationalities
@ -615,6 +615,7 @@ def job_detail(request, slug):
"avg_t2i_days": avg_t2i_days, "avg_t2i_days": avg_t2i_days,
"avg_t_in_exam_days": avg_t_in_exam_days, "avg_t_in_exam_days": avg_t_in_exam_days,
"linkedin_content_form": linkedin_content_form, "linkedin_content_form": linkedin_content_form,
"staff_form": StaffAssignmentForm(),
} }
return render(request, "jobs/job_detail.html", context) return render(request, "jobs/job_detail.html", context)
@ -625,7 +626,7 @@ def job_detail(request, slug):
# def job_cvs_download(request, slug): # def job_cvs_download(request, slug):
# job = get_object_or_404(JobPosting, slug=slug) # job = get_object_or_404(JobPosting, slug=slug)
# entries = Application.objects.filter(job=job) # entries = Application.objects.filter(job=job)
# # 2. Create an in-memory byte stream (BytesIO) # # 2. Create an in-memory byte stream (BytesIO)
# zip_buffer = io.BytesIO() # zip_buffer = io.BytesIO()
@ -681,7 +682,7 @@ def request_cvs_download(request, slug):
# Use async_task to run the function in the background # Use async_task to run the function in the background
# Pass only simple arguments (like the job ID) # Pass only simple arguments (like the job ID)
async_task('recruitment.tasks.generate_and_save_cv_zip', job.id) async_task('recruitment.tasks.generate_and_save_cv_zip', job.id)
# Provide user feedback and redirect # Provide user feedback and redirect
messages.info(request, "The CV compilation has started in the background. It may take a few moments. Refresh this page to check status.") messages.info(request, "The CV compilation has started in the background. It may take a few moments. Refresh this page to check status.")
return redirect('job_detail', slug=slug) # Redirect back to the job detail page return redirect('job_detail', slug=slug) # Redirect back to the job detail page
@ -701,7 +702,7 @@ def download_ready_cvs(request, slug):
# File is not ready or doesn't exist # File is not ready or doesn't exist
messages.warning(request, "The ZIP file is still being generated or an error occurred.") messages.warning(request, "The ZIP file is still being generated or an error occurred.")
return redirect('job_detail', slug=slug) return redirect('job_detail', slug=slug)
@login_required @login_required
@staff_user_required @staff_user_required
def job_image_upload(request, slug): def job_image_upload(request, slug):
@ -2991,10 +2992,11 @@ def staff_assignment_view(request, slug):
applications = job.applications.all() applications = job.applications.all()
if request.method == "POST": if request.method == "POST":
form = StaffAssignmentForm(request.POST, instance=job) form = StaffAssignmentForm(request.POST, instance=job)
if form.is_valid(): if form.is_valid():
assignment = form.save() job.assigned_to = form.cleaned_data["assigned_to"]
job.save(update_fields=["assigned_to"])
messages.success(request, f"Staff assigned to job '{job.title}' successfully!") messages.success(request, f"Staff assigned to job '{job.title}' successfully!")
return redirect("job_detail", slug=job.slug) return redirect("job_detail", slug=job.slug)
else: else:
@ -5268,7 +5270,7 @@ def compose_candidate_email(request, job_slug):
from_interview=False, from_interview=False,
job=job job=job
) )
if email_result["success"]: if email_result["success"]:
for candidate in candidates: for candidate in candidates:
if hasattr(candidate, 'person') and candidate.person: if hasattr(candidate, 'person') and candidate.person:
@ -5660,7 +5662,7 @@ def send_interview_email(request, slug):
meeting=meeting, meeting=meeting,
job=job, job=job,
) )
if form.is_valid(): if form.is_valid():
# 4. Extract cleaned data # 4. Extract cleaned data
subject = form.cleaned_data["subject"] subject = form.cleaned_data["subject"]
@ -5734,11 +5736,11 @@ def send_interview_email(request, slug):
) )
return redirect("list_meetings") return redirect("list_meetings")
else: else:
error_msg = "Failed to send email. Please check the form for errors." error_msg = "Failed to send email. Please check the form for errors."
print(form.errors) print(form.errors)
messages.error(request, error_msg) messages.error(request, error_msg)
return redirect("meeting_details", slug=meeting.slug) return redirect("meeting_details", slug=meeting.slug)
return redirect("meeting_details", slug=meeting.slug) return redirect("meeting_details", slug=meeting.slug)

View File

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load i18n static %} {% load i18n static crispy_forms_tags %}
{% block title %}{{ job.title }} - University ATS{% endblock %} {% block title %}{{ job.title }} - University ATS{% endblock %}
@ -303,7 +303,7 @@
</li> </li>
<li class="nav-item flex-fill" role="presentation"> <li class="nav-item flex-fill" role="presentation">
<button class="nav-link" id="staff-tab" data-bs-toggle="tab" data-bs-target="#staff-pane" type="button" role="tab" aria-controls="staff-pane" aria-selected="false"> <button class="nav-link" id="staff-tab" data-bs-toggle="tab" data-bs-target="#staff-pane" type="button" role="tab" aria-controls="staff-pane" aria-selected="false">
<i class="fas fa-user-tie me-1 text-primary"></i> {% trans "Staff" %} <i class="fas fa-user-tie me-1 text-primary"></i> {% trans "Assigned Staff" %}
</button> </button>
</li> </li>
<li class="nav-item flex-fill" role="presentation"> <li class="nav-item flex-fill" role="presentation">
@ -384,42 +384,47 @@
<div class="tab-pane fade" id="staff-pane" role="tabpanel" aria-labelledby="staff-tab"> <div class="tab-pane fade" id="staff-pane" role="tabpanel" aria-labelledby="staff-tab">
<h5 class="mb-3"><i class="fas fa-user-tie me-2 text-primary"></i>{% trans "Staff Assignment" %}</h5> <h5 class="mb-3"><i class="fas fa-user-tie me-2 text-primary"></i>{% trans "Staff Assignment" %}</h5>
<div class="d-grid gap-3"> <div class="d-grid gap-3">
<p class="text-muted small mb-3">
{% trans "Assign staff members to manage this job posting and track applications." %}
</p>
<a href="{% url 'staff_assignment_view' job.slug %}" class="btn btn-main-action">
<i class="fas fa-user-plus me-1"></i> {% trans "Assign Staff Member" %}
</a>
{% if job.assigned_to %} {% if job.assigned_to %}
<div class="mt-3"> <p class="text-muted small mb-3">
<h6 class="text-muted">{% trans "Current Assignments" %}</h6> <strong>{% trans "Assigned to:" %}</strong> {{ job.assigned_to }}
{% for assignment in job.staff_assignments.all %} </p>
<div class="card mb-2"> {% endif %}
<div class="card-body p-2"> {% if not job.assigned_to %}
<div class="d-flex justify-content-between align-items-center"> <button type="button" class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#staffAssignmentModal">
<div> <i class="fas fa-user-plus me-1"></i> {% trans "Assign Staff Member" %}
<strong>{{ assignment.staff.get_full_name|default:assignment.staff.username }}</strong> </button>
<br> {% elif job.assigned_to and job.assigned_to == request.user %}
<small class="text-muted">{{ assignment.staff.email }}</small> <button type="button" class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#staffAssignmentModal">
</div> <i class="fas fa-user-plus me-1"></i> {% trans "Assign Staff Member" %}
<div> </button>
{% if assignment.staff.is_active %} {% endif %}
<span class="badge bg-success">Active</span>
{% else %} <!-- Modal for Staff Assignment -->
<span class="badge bg-danger">Inactive</span> <div class="modal fade" id="staffAssignmentModal" tabindex="-1" aria-labelledby="staffAssignmentModalLabel" aria-hidden="true">
{% endif %} <div class="modal-dialog">
</div> <div class="modal-content">
</div> <div class="modal-header">
{% if assignment.notes %} <h5 class="modal-title" id="staffAssignmentModalLabel">
<small class="text-muted d-block mt-1">{{ assignment.notes }}</small> <i class="fas fa-user-plus me-2"></i> {% trans "Assign Staff Member" %}
{% endif %} </h5>
</div> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
{% endfor %} <div class="modal-body">
<form method="post" action="{% url 'staff_assignment_view' job.slug %}">
{% csrf_token %}
{{staff_form|crispy}}
<div class="d-flex justify-content-end mt-3">
<button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {% trans "Save Assignment" %}
</button>
</div>
</form>
</div>
</div>
</div> </div>
{% else %} </div>
{% if not job.assigned_to %}
<div class="alert alert-info p-2 small mb-0"> <div class="alert alert-info p-2 small mb-0">
<i class="fas fa-info-circle me-1"></i> {% trans "No staff members assigned to this job yet." %} <i class="fas fa-info-circle me-1"></i> {% trans "No staff members assigned to this job yet." %}
</div> </div>

View File

@ -12,7 +12,7 @@
--kaauh-border: #eaeff3; --kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40; --kaauh-primary-text: #343a40;
--kaauh-gray-light: #f8f9fa; --kaauh-gray-light: #f8f9fa;
--kaauh-warning: #ffc107; --kaauh-warning: #ffc107;
--kaauh-danger: #dc3545; --kaauh-danger: #dc3545;
--kaauh-success: #28a745; --kaauh-success: #28a745;
} }
@ -158,7 +158,7 @@
<option value="Onsite" {% if type_filter == 'Onsite' %}selected{% endif %}>{% trans "Onsite" %}</option> <option value="Onsite" {% if type_filter == 'Onsite' %}selected{% endif %}>{% trans "Onsite" %}</option>
</select> </select>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label for="status" class="form-label small text-muted">{% trans "Filter by Status" %}</label> <label for="status" class="form-label small text-muted">{% trans "Filter by Status" %}</label>
<select name="status" id="status" class="form-select form-select-sm"> <select name="status" id="status" class="form-select form-select-sm">
@ -190,7 +190,7 @@
</div> </div>
</div> </div>
</div> </div>
{% if meetings_data %} {% if meetings_data %}
<div id="meetings-list"> <div id="meetings-list">
{# View Switcher (not provided, assuming standard include) #} {# View Switcher (not provided, assuming standard include) #}
@ -213,7 +213,7 @@
<p class="card-text text-muted small mb-3"> <p class="card-text text-muted small mb-3">
<i class="fas fa-user"></i> {% trans "Candidate" %}: {{ meeting.interview.application.person.full_name|default:"N/A" }}<br> <i class="fas fa-user"></i> {% trans "Candidate" %}: {{ meeting.interview.application.person.full_name|default:"N/A" }}<br>
<i class="fas fa-briefcase"></i> {% trans "Job" %}: {{ meeting.interview.job.title|default:"N/A" }}<br> <i class="fas fa-briefcase"></i> {% trans "Job" %}: {{ meeting.interview.job.title|default:"N/A" }}<br>
{# Dynamic location/type details #} {# Dynamic location/type details #}
{% if meeting.type == 'Remote' %} {% if meeting.type == 'Remote' %}
<i class="fas fa-link"></i> {% trans "Remote ID" %}: {{ meeting.meeting_id|default:meeting.location.id }}<br> <i class="fas fa-link"></i> {% trans "Remote ID" %}: {{ meeting.meeting_id|default:meeting.location.id }}<br>
@ -224,7 +224,7 @@
<i class="fas fa-clock"></i> {% trans "Start" %}: {{ meeting.start_time|date:"M d, Y H:i" }}<br> <i class="fas fa-clock"></i> {% trans "Start" %}: {{ meeting.start_time|date:"M d, Y H:i" }}<br>
<i class="fas fa-stopwatch"></i> {% trans "Duration" %}: {{ meeting.duration }} minutes <i class="fas fa-stopwatch"></i> {% trans "Duration" %}: {{ meeting.duration }} minutes
</p> </p>
<span class="status-badge bg-{{ meeting.status }}"> <span class="status-badge bg-{{ meeting.status }}">
{{ meeting.interview.get_status_display }} {{ meeting.interview.get_status_display }}
</span> </span>
@ -244,7 +244,7 @@
<i class="fas fa-check"></i> {% trans "Physical Event" %} <i class="fas fa-check"></i> {% trans "Physical Event" %}
</button> </button>
{% endif %} {% endif %}
{# CORRECTED: Passing the slug to the update URL #} {# CORRECTED: Passing the slug to the update URL #}
<a href="" class="btn btn-sm btn-outline-secondary"> <a href="" class="btn btn-sm btn-outline-secondary">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>

View File

@ -273,7 +273,7 @@ body {
{% if meeting.join_url %} {% if meeting.join_url %}
<div class="join-url-container pt-3"> <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 id="copy-message" class="text-white rounded px-2 py-1 small fw-bold mb-2 text-center" style="opacity: 0; transition: opacity 0.3s; position: absolute; right: 0; top: 5px; background-color: var(--kaauh-success); z-index: 10;">{% trans "Copied!" %}</div>
<div class="join-url-display d-flex justify-content-between align-items-center position-relative"> <div class="join-url-display d-flex justify-content-between align-items-center position-relative">
<div class="text-truncate me-2"> <div class="text-truncate me-2">
<strong>{% trans "Join URL" %}:</strong> <strong>{% trans "Join URL" %}:</strong>

View File

@ -206,7 +206,7 @@
{% csrf_token %} {% csrf_token %}
{# Select Input Group - No label needed for this one, so we just flex the select and button #} {# Select Input Group - No label needed for this one, so we just flex the select and button #}
<select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;"> <select name="mark_as" id="update_status" class="form-select form-select-sm" style="width: 120px;">
<option selected> <option selected>
---------- ----------
@ -233,7 +233,7 @@
</button> </button>
</form> </form>
<div class="vr" style="height: 28px;"></div> <div class="vr" style="height: 28px;"></div>
<button type="button" class="btn btn-outline-info btn-sm" <button type="button" class="btn btn-outline-info btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
@ -248,7 +248,7 @@
</div> </div>
</div> </div>
{% endif %} {% endif %}
</div> </div>
<div class="table-responsive"> <div class="table-responsive">
<form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get"> <form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="get">
@ -291,7 +291,7 @@
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}" hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
hx-target="#candidateviewModalBody" hx-target="#candidateviewModalBody"
title="View Profile"> title="View Profile">
{{ candidate.name }}<i class="fas fa-eye ms-1"></i> {{ candidate.name }} <i class="fas fa-eye ms-1"></i>
</button> </button>
{% comment %} <div class="candidate-name"> {% comment %} <div class="candidate-name">
{{ candidate.name }} {{ candidate.name }}
@ -380,7 +380,7 @@
{% endif %} {% endif %}
</td> </td>
<td> <td>
{% if candidate.get_latest_meeting %} {% if candidate.get_latest_meeting %}
{% if candidate.get_latest_meeting.location_type == 'Remote'%} {% if candidate.get_latest_meeting.location_type == 'Remote'%}
@ -402,7 +402,7 @@
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
{% else%} {% else%}
<button type="button" class="btn btn-outline-secondary btn-sm" <button type="button" class="btn btn-outline-secondary btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#candidateviewModal" data-bs-target="#candidateviewModal"
@ -422,7 +422,7 @@
</button> </button>
{% endif %} {% endif %}
{% else %} {% else %}
<button type="button" class="btn btn-main-action btn-sm" <button type="button" class="btn btn-main-action btn-sm"
data-bs-toggle="modal" data-bs-toggle="modal"
@ -497,7 +497,7 @@
<div class="text-center py-5 text-muted"> <div class="text-center py-5 text-muted">
<i class="fas fa-spinner fa-spin fa-2x"></i><br> <i class="fas fa-spinner fa-spin fa-2x"></i><br>
{% trans "Loading email form..." %} {% trans "Loading email form..." %}
</div> </div>
</div> </div>
</div> </div>