littele ui fix

This commit is contained in:
Faheed 2025-11-18 13:13:16 +03:00
parent 64e04a011d
commit f1499f7be0
11 changed files with 299 additions and 364 deletions

View File

@ -1962,7 +1962,7 @@ Hi Team,
# }
#during bulk schedule
class OnsiteMeetingForm(forms.ModelForm):
class OnsiteLocationForm(forms.ModelForm):
class Meta:
model = OnsiteLocationDetails
# Include 'room_number' and update the field list

View File

@ -1241,7 +1241,7 @@ class ScheduledInterview(Base):
# Links to the specific, individual location/meeting details for THIS interview
interview_location = models.OneToOneField(
InterviewLocation,
on_delete=models.SET_NULL,
on_delete=models.CASCADE,
related_name="scheduled_interview",
null=True,
blank=True,

View File

@ -108,27 +108,8 @@ urlpatterns = [
name="training_delete",
),
# Meeting URLs
path("meetings/", views.ZoomMeetingListView.as_view(), name="list_meetings"),
path(
"meetings/create-meeting/",
views.ZoomMeetingCreateView.as_view(),
name="create_meeting",
),
path(
"meetings/meeting-details/<slug:slug>/",
views.ZoomMeetingDetailsView.as_view(),
name="meeting_details",
),
path(
"meetings/update-meeting/<slug:slug>/",
views.ZoomMeetingUpdateView.as_view(),
name="update_meeting",
),
path(
"meetings/delete-meeting/<slug:slug>/",
views.ZoomMeetingDeleteView,
name="delete_meeting",
),
# path("meetings/", views.ZoomMeetingListView.as_view(), name="list_meetings"),
# JobPosting functional views URLs (keeping for compatibility)
path("api/create/", views.create_job, name="create_job_api"),
path("api/<slug:slug>/edit/", views.edit_job, name="edit_job_api"),
@ -596,6 +577,26 @@ urlpatterns = [
name="confirm_schedule_interviews_view",
),
path(
"meetings/create-meeting/",
views.ZoomMeetingCreateView.as_view(),
name="create_meeting",
),
# path(
# "meetings/meeting-details/<slug:slug>/",
# views.ZoomMeetingDetailsView.as_view(),
# name="meeting_details",
# ),
path(
"meetings/update-meeting/<slug:slug>/",
views.ZoomMeetingUpdateView.as_view(),
name="update_meeting",
),
path(
"meetings/delete-meeting/<slug:slug>/",
views.ZoomMeetingDeleteView,
name="delete_meeting",
),
# Candidate Meeting Scheduling/Rescheduling URLs
path(
"jobs/<slug:job_slug>/candidates/<int:candidate_pk>/schedule-meeting/",

View File

@ -75,7 +75,7 @@ from .forms import (
PortalLoginForm,
MessageForm,
PersonForm,
OnsiteMeetingForm,
OnsiteLocationForm,
OnsiteReshuduleForm,
OnsiteScheduleForm,
InterviewEmailForm
@ -250,56 +250,56 @@ class ZoomMeetingCreateView(StaffRequiredMixin, CreateView):
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
# 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")
# def get_queryset(self):
# queryset = super().get_queryset().order_by("-start_time")
# Prefetch related interview data efficiently
# # 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
)
)
# 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 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 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)
)
# # 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
# 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
# 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
@ -857,13 +857,15 @@ def kaauh_career(request):
if selected_department and selected_department in department_type_keys:
active_jobs = active_jobs.filter(department=selected_department)
selected_workplace_type = request.GET.get("workplace_type", "")
print(selected_workplace_type)
selected_job_type = request.GET.get("employment_type", "")
job_type_keys = active_jobs.values_list("job_type", flat=True).distinct()
workplace_type_keys = active_jobs.values_list(
job_type_keys = active_jobs.order_by("job_type").distinct("job_type").values_list("job_type", flat=True)
print(job_type_keys)
workplace_type_keys = active_jobs.order_by("workplace_type").distinct("workplace_type").values_list(
"workplace_type", flat=True
).distinct()
print(workplace_type_keys)
if selected_job_type and selected_job_type in job_type_keys:
active_jobs = active_jobs.filter(job_type=selected_job_type)
if selected_workplace_type and selected_workplace_type in workplace_type_keys:
@ -1613,7 +1615,7 @@ def _handle_preview_submission(request, slug, job):
"interview_duration": interview_duration,
"buffer_time": buffer_time,
"schedule_interview_type":schedule_interview_type,
"form":OnsiteMeetingForm()
"form":OnsiteLocationForm()
},
)
else:
@ -1625,174 +1627,6 @@ def _handle_preview_submission(request, slug, job):
)
# def _handle_confirm_schedule(request, slug, job):
# """
# Handles the final POST request (Confirm Schedule).
# Creates the main schedule record and queues individual interviews asynchronously.
# """
# SESSION_DATA_KEY = "interview_schedule_data"
# SESSION_ID_KEY = f"schedule_candidate_ids_{slug}"
# # 1. Get schedule data from session
# schedule_data = request.session.get(SESSION_DATA_KEY)
# if not schedule_data:
# messages.error(request, "Session expired. Please try again.")
# return redirect("schedule_interviews", slug=slug)
# # 2. Create the Interview Schedule (Parent Record)
# # NOTE: You MUST convert the time strings back to Python time objects here.
# try:
# schedule = InterviewSchedule.objects.create(
# job=job,
# created_by=request.user,
# start_date=datetime.fromisoformat(schedule_data["start_date"]).date(),
# end_date=datetime.fromisoformat(schedule_data["end_date"]).date(),
# working_days=schedule_data["working_days"],
# start_time=time.fromisoformat(schedule_data["start_time"]),
# end_time=time.fromisoformat(schedule_data["end_time"]),
# interview_duration=schedule_data["interview_duration"],
# buffer_time=schedule_data["buffer_time"],
# # Use the simple break times saved in the session
# # If the value is None (because required=False in form), handle it gracefully
# break_start_time=schedule_data.get("break_start_time"),
# break_end_time=schedule_data.get("break_end_time"),
# schedule_interview_type=schedule_data.get("schedule_interview_type")
# )
# except Exception as e:
# # Handle database creation error
# messages.error(request, f"Error creating schedule: {e}")
# if SESSION_ID_KEY in request.session:
# del request.session[SESSION_ID_KEY]
# return redirect("schedule_interviews", slug=slug)
# # 3. Setup candidates and get slots
# candidates = Application.objects.filter(id__in=schedule_data["candidate_ids"])
# print(candidates)
# schedule.applications.set(candidates)
# available_slots = get_available_time_slots(
# schedule
# ) # This should still be synchronous and fast
# # 4. Queue scheduled interviews asynchronously (FAST RESPONSE)
# if schedule_data.get("schedule_interview_type")=='Remote':
# print('....remote..')
# queued_count = 0
# for i, candidate in enumerate(candidates):
# if i < len(available_slots):
# slot = available_slots[i]
# # Dispatch the individual creation task to the background queue
# async_task(
# "recruitment.tasks.create_interview_and_meeting",
# candidate.pk,
# job.pk,
# schedule.pk,
# slot["date"],
# slot["time"],
# schedule.interview_duration,
# )
# queued_count += 1
# messages.success(
# request,
# f"Schedule successfully created. Queued {queued_count} interviews to be booked asynchronously. Check back in a moment!",
# )
# # Clear both session data keys upon successful completion
# if SESSION_DATA_KEY in request.session:
# del request.session[SESSION_DATA_KEY]
# if SESSION_ID_KEY in request.session:
# del request.session[SESSION_ID_KEY]
# return redirect("job_detail", slug=slug)
# elif schedule_data.get("schedule_interview_type") == 'Onsite':
# # The form submission for Onsite details should happen here.
# # This block assumes the OnsiteMeetingForm is being submitted NOW.
# # NOTE: start_time and duration must be passed through the form
# # for OnsiteLocationDetails creation.
# if request.method == 'POST':
# if available_slots:
# first_slot = available_slots[0]
# # Combine the first slot's date and the schedule's start time
# location_start_dt = datetime.combine(first_slot['date'], schedule.start_time)
# else:
# # Fallback if no slots (should not happen if candidates > 0)
# location_start_dt = datetime.now()
# # Create a form using the submitted POST data
# form = OnsiteMeetingForm(request.POST)
# if form.is_valid():
# # 1. Extract location-specific data from the form
# topic = form.cleaned_data['topic']
# physical_address = form.cleaned_data['physical_address']
# room_number = form.cleaned_data['room_number']
# # 2. Create the OnsiteLocationDetails instance (The Location Template)
# # The duration comes from the parent InterviewSchedule
# try:
# onsite_location = OnsiteLocationDetails.create(
# start_time=location_start_dt, # Uses datetime derived from first slot date
# duration=schedule.interview_duration, # Uses duration from parent schedule
# physical_address=physical_address,
# room_number=room_number,
# )
# onsite_location.save()
# # 3. Create the ScheduledInterview entries, linking the location
# for i, candidate in enumerate(candidates):
# if i < len(available_slots):
# slot = available_slots[i]
# # Combine date and time from the slot for the ScheduledInterview creation
# ScheduledInterview.objects.create(
# application=candidate,
# job=job,
# schedule=schedule,
# interview_date=slot['date'],
# interview_time=slot['time'],
# # CRITICAL: Link the location object
# interview_location=onsite_location,
# # Assuming 'topic' is stored on the ScheduledInterview model
# # topic=topic
# )
# messages.success(
# request,
# f"Onsite schedule Interview Create succesfully"
# )
# # Clear session data keys upon successful completion
# if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY]
# if SESSION_ID_KEY in request.session: del request.session[SESSION_ID_KEY]
# # Redirect to a confirmation or job details page
# return redirect('job_detail', slug=job.slug)
# except Exception as e:
# # Handle database creation error
# messages.error(request, f"Error creating onsite location/interviews: {e}")
# # Keep the form data for re-submission if possible, or redirect
# return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule})
# else:
# # Form is invalid, re-render with errors
# return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule,'job':job})
# else:
# # For a GET request (First time after InterviewSchedule is created)
# # Render the form to collect location details
# form = OnsiteMeetingForm()
# print(f"job:{job}")
# return render(request,'interviews/onsite_location_form.html',{'form': form, 'schedule': schedule,'job':job})
def _handle_confirm_schedule(request, slug, job):
"""
@ -1871,7 +1705,7 @@ def _handle_confirm_schedule(request, slug, job):
print("inside...")
if request.method == 'POST':
form = OnsiteMeetingForm(request.POST)
form = OnsiteLocationForm(request.POST)
if form.is_valid():
@ -1882,6 +1716,8 @@ def _handle_confirm_schedule(request, slug, job):
# Extract common location data from the form
physical_address = form.cleaned_data['physical_address']
room_number = form.cleaned_data['room_number']
topic=form.cleaned_data['topic']
try:
# 1. Iterate over candidates and create a NEW Location object for EACH
@ -1898,7 +1734,8 @@ def _handle_confirm_schedule(request, slug, job):
duration=schedule.interview_duration,
physical_address=physical_address,
room_number=room_number,
location_type="Onsite"
location_type="Onsite",
topic=topic
)
@ -1935,7 +1772,7 @@ def _handle_confirm_schedule(request, slug, job):
else:
# For a GET request
form = OnsiteMeetingForm()
form = OnsiteLocationForm()
return render(request, 'interviews/onsite_location_form.html', {'form': form, 'schedule': schedule, 'job': job})
@ -3495,7 +3332,7 @@ def agency_detail(request, slug):
candidates = Application.objects.filter(hiring_agency=agency).order_by(
"-created_at"
)
# Statistics
total_candidates = candidates.count()
active_candidates = candidates.filter(
@ -6085,18 +5922,4 @@ def schedule_onsite_meeting_for_candidate(request, slug, candidate_pk):
}
return render(request, "meetings/schedule_onsite_meeting_form.html", context)
# def meeting_list_view(request):
# queryset = ScheduledInterview.filter(interview_location__isnull=False).select_related(
# 'interview_location',
# 'job',
# 'application__person',
# 'application',
# ).prefetch_related(
# 'interview_location__zoommeetingdetails',
# 'interview_location__onsitelocationdetails',
# )
# print(queryset)
# return render(request,)
# =========================================================================
# 2. Simple Meeting Creation Views (Placeholders)
# =========================================================================

View File

@ -143,12 +143,12 @@
{% trans "Type" %}:
{# Map the key back to its human-readable translation #}
<strong class="mx-1">
{% if selected_job_type == 'FULL_TIME' %}{% trans "Full-time" %}
{% elif selected_job_type == 'PART_TIME' %}{% trans "Part-time" %}
{% elif selected_job_type == 'CONTRACT' %}{% trans "Contract" %}
{% elif selected_job_type == 'INTERNSHIP' %}{% trans "Internship" %}
{% elif selected_job_type == 'FACULTY' %}{% trans "Faculty" %}
{% elif selected_job_type == 'TEMPORARY' %}{% trans "Temporary" %}
{% if selected_job_type == 'Full-time' %}{% trans "Full-time" %}
{% elif selected_job_type == 'Part-time' %}{% trans "Part-time" %}
{% elif selected_job_type == 'Contract' %}{% trans "Contract" %}
{% elif selected_job_type == 'Internship' %}{% trans "Internship" %}
{% elif selected_job_type == 'Faculty' %}{% trans "Faculty" %}
{% elif selected_job_type == 'Temporary' %}{% trans "Temporary" %}
{% endif %}
</strong>
{# Link to clear this specific filter: use current URL but remove `employment_type` parameter #}
@ -159,15 +159,15 @@
</span>
{% endif %}
{# --- Active Workplace Type Filter Chip --- #}
{# --- Active Workplace Type Filter Chip --- #}
{% if selected_workplace_type %}
<span class="filter-chip badge bg-primary-theme-subtle text-primary-theme fw-normal p-2 active-filter-chip">
{% trans "Workplace" %}:
{# Map the key back to its human-readable translation #}
<strong class="mx-1">
{% if selected_workplace_type == 'ON_SITE' %}{% trans "On-site" %}
{% elif selected_workplace_type == 'REMOTE' %}{% trans "Remote" %}
{% elif selected_workplace_type == 'HYBRID' %}{% trans "Hybrid" %}
{% if selected_workplace_type == 'On-site' %}{% trans "On-site" %}
{% elif selected_workplace_type == 'Remote' %}{% trans "Remote" %}
{% elif selected_workplace_type == 'Hybrid' %}{% trans "Hybrid" %}
{% endif %}
</strong>
{# Link to clear this specific filter: use current URL but remove `workplace_type` parameter #}

View File

@ -175,6 +175,38 @@
color: #6c757d;
}
/* Job List - Consistent with Candidate List */
.job-item {
background-color: white;
border: 1px solid var(--kaauh-border);
border-radius: 0.5rem;
padding: 1rem;
margin-bottom: 0.75rem;
transition: all 0.2s ease;
}
.job-item:hover {
background-color: #f8f9fa;
border-color: var(--kaauh-teal);
}
.job-title {
font-weight: 600;
color: var(--kaauh-primary-text);
margin-bottom: 0.25rem;
}
.job-details {
font-size: 0.875rem;
color: #6c757d;
}
.job-status-badge {
font-size: 0.75rem;
padding: 0.25rem 0.6rem;
border-radius: 0.3rem;
font-weight: 600;
display: inline-block;
background-color: #e9ecef;
color: #495057;
}
/* Stage Badge */
.stage-badge {
font-size: 0.75rem;
@ -200,7 +232,7 @@
}
.empty-state i {
font-size: 3rem;
font-size: 1.5rem;
margin-bottom: 1rem;
opacity: 0.5;
}
@ -235,12 +267,57 @@
.password-value:hover {
background-color: #f8f9fa;
}
/* --- TAB OVERRIDES FOR TEAL THEME CONSISTENCY AND VISIBILITY --- */
/* Ensure card-header-tabs sit correctly, use kaauh-border */
.card-header-tabs {
border-bottom: 1px solid var(--kaauh-border); /* Consistent thin bottom border for the entire row */
}
/* Default tab link styling */
.nav-tabs .nav-link {
color: var(--kaauh-primary-text); /* Default text color */
border: 1px solid var(--kaauh-border); /* Add border to all sides */
border-bottom: none; /* Remove tab's own bottom border */
border-radius: 0.5rem 0.5rem 0 0; /* Slightly smaller radius for tabs */
margin-right: 0.25rem;
padding: 0.75rem 1.25rem;
transition: all 0.2s ease-in-out;
background-color: #f8f9fa; /* Visible light background for inactive tabs */
}
/* Tab link hover state */
.nav-tabs .nav-link:hover:not(.active) {
color: var(--kaauh-teal);
background-color: #e9ecef; /* Slightly darker on hover */
border-color: var(--kaauh-teal); /* Use teal border on hover */
border-bottom: none; /* Keep the bottom flat */
}
/* Active tab link styling */
.nav-tabs .nav-link.active {
color: var(--kaauh-teal-dark);
background-color: white; /* White background for active */
border-color: var(--kaauh-border); /* Use border color for all three sides */
border-bottom: 2px solid white; /* Override the tab strip border with white to lift the tab */
margin-bottom: -1px; /* Overlap slightly with the card border for a cleaner look */
font-weight: 600;
}
/* Tab pane styling for border consistency */
.tab-content .tab-pane {
border: 1px solid var(--kaauh-border); /* Consistent border for the content */
border-top: none; /* The nav tabs handle the top border */
border-radius: 0 0 0.75rem 0.75rem;
background-color: white;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<!-- Header Section -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
@ -252,6 +329,9 @@
</p>
</div>
<div>
<a href="{% url 'agency_assignment_list' %}" class="btn btn-main-action me-2">
<i class="fas fa-tasks me-1"></i> {% trans "All Assignments" %}
</a>
<a href="{% url 'agency_assignment_create' agency.slug %}" class="btn btn-main-action me-2">
<i class="fas fa-edit me-1"></i> {% trans "Assign job" %}
</a>
@ -265,9 +345,7 @@
</div>
<div class="row">
<!-- Agency Information -->
<div class="col-lg-8">
<!-- Agency Header Card -->
<div class="card kaauh-card mb-4">
<div class="agency-header">
<div class="row align-items-center">
@ -302,7 +380,6 @@
</div>
<div class="card-body p-4">
<!-- Contact Information -->
<div class="row">
<div class="col-md-6">
<div class="info-section">
@ -310,7 +387,6 @@
<i class="fas fa-address-book me-2" style="color: var(--kaauh-teal);"></i>
{% trans "Contact Information" %}
</h5>
{% if agency.phone %}
<div class="info-item">
<div class="info-icon">
@ -322,7 +398,6 @@
</div>
</div>
{% endif %}
{% if agency.email %}
<div class="info-item">
<div class="info-icon">
@ -334,7 +409,6 @@
</div>
</div>
{% endif %}
{% if agency.website %}
<div class="info-item">
<div class="info-icon">
@ -360,7 +434,6 @@
<i class="fas fa-map-marker-alt me-2" style="color: var(--kaauh-teal);"></i>
{% trans "Location Information" %}
</h5>
{% if agency.address %}
<div class="info-item">
<div class="info-icon">
@ -372,7 +445,6 @@
</div>
</div>
{% endif %}
{% if agency.city %}
<div class="info-item">
<div class="info-icon">
@ -384,7 +456,6 @@
</div>
</div>
{% endif %}
{% if agency.country %}
<div class="info-item">
<div class="info-icon">
@ -400,7 +471,6 @@
</div>
</div>
<!-- Description -->
{% if agency.description %}
<div class="info-section mt-3">
<h5 class="mb-3">
@ -411,14 +481,12 @@
</div>
{% endif %}
<!-- Agency Login Information -->
{% if generated_password and request.user.is_staff %}
<div class="info-section mt-4">
<h5 class="mb-3">
<i class="fas fa-key me-2" style="color: var(--kaauh-teal);"></i>
{% trans "Agency Login Information" %}
</h5>
<div class="alert alert-info" role="alert">
<h6 class="alert-heading">
<i class="fas fa-info-circle me-2"></i>
@ -428,7 +496,6 @@
{% trans "This password provides access to the agency portal. Share it securely with the agency contact person." %}
</p>
</div>
<div class="password-display-section">
<div class="info-item">
<div class="info-icon">
@ -437,9 +504,8 @@
<div class="info-content">
<div class="info-label">{% trans "Username" %}</div>
<div class="info-value">{{ agency.user.username }}</div>
</div>
</div>
</div>
<div class="info-item">
<div class="info-icon">
<i class="fas fa-lock"></i>
@ -461,59 +527,138 @@
</div>
</div>
<!-- Recent Candidates -->
<div class="card kaauh-card">
<div class="card-header bg-white border-bottom">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-users me-2"></i>
{% trans "Recent Candidates" %}
</h5>
{% comment %} <a href="{% url 'agency_candidates' agency.slug %}" class="btn btn-main-action btn-sm">
{% trans "View All Candidates" %}
<i class="fas fa-arrow-right ms-1"></i>
</a> {% endcomment %}
</div>
<div class="card kaauh-card mb-4">
<div class="card-header p-0 bg-white">
<ul class="nav nav-tabs card-header-tabs" id="agencyTabs" role="tablist">
<li class="nav-item ms-2" role="presentation">
<button
class="nav-link active"
id="candidates-tab"
data-bs-toggle="tab"
data-bs-target="#candidates"
type="button"
role="tab"
aria-controls="candidates"
aria-selected="true"
>
<i class="fas fa-users me-1"></i>
{% trans "Recent Candidates" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
id="jobs-tab"
data-bs-toggle="tab"
data-bs-target="#jobs"
type="button"
role="tab"
aria-controls="jobs"
aria-selected="false"
>
<i class="fas fa-briefcase me-1"></i>
{% trans "Assigned Jobs" %}
</button>
</li>
</ul>
</div>
<div class="card-body">
{% if candidates %}
{% for candidate in candidates %}
<div class="candidate-item">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="candidate-name">{{ candidate.name }}</div>
<div class="candidate-details">
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}
{% if candidate.phone %}
<span class="ms-3"><i class="fas fa-phone me-1"></i> {{ candidate.phone }}</span>
{% endif %}
<div class="tab-content" id="agencyTabsContent">
<div
class="tab-pane fade show active p-4"
id="candidates"
role="tabpanel"
aria-labelledby="candidates-tab"
>
{% if candidates %}
{% for candidate in candidates %}
<div class="candidate-item">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="candidate-name">{{ candidate.name }}</div>
<div class="candidate-details">
<i class="fas fa-envelope me-1"></i> {{ candidate.email }}
{% if candidate.phone %}
<span class="ms-3"><i class="fas fa-phone me-1"></i> {{ candidate.phone }}</span>
{% endif %}
</div>
</div>
</div>
<div class="text-end">
<span class="stage-badge stage-{{ candidate.stage }}">
{{ candidate.get_stage_display }}
</span>
<div class="small text-muted mt-1">
{{ candidate.created_at|date:"M d, Y" }}
<div class="text-end">
<span class="stage-badge stage-{{ candidate.stage }}">
{{ candidate.get_stage_display }}
</span>
<div class="small text-muted mt-1">
{{ candidate.created_at|date:"M d, Y" }}
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<i class="fas fa-user-slash"></i>
<h6>{% trans "No candidates yet" %}</h6>
<p class="mb-0">{% trans "This agency hasn't submitted any candidates yet." %}</p>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<i class="fas fa-user-slash"></i>
<h6>{% trans "No candidates yet" %}</h6>
<p class="mb-0">{% trans "This agency hasn't submitted any candidates yet." %}</p>
</div>
{% endif %}
{% endif %}
</div>
<div
class="tab-pane fade p-4"
id="jobs"
role="tabpanel"
aria-labelledby="jobs-tab"
>
{% comment %}
NOTE: You will need to pass an 'assigned_jobs' list
from your Django view context to populate this section.
{% endcomment %}
{% if assigned_jobs %}
{% for assignment in assigned_jobs %}
<div class="job-item">
<div class="d-flex justify-content-between align-items-center">
<div>
<div class="job-title">
<a href="{% url 'job_details' assignment.job.slug %}" class="text-decoration-none text-primary-theme">
{{ assignment.job.title }}
</a>
</div>
<div class="job-details">
<i class="fas fa-map-pin me-1"></i> {{ assignment.job.location }}
<span class="ms-3"><i class="fas fa-user-tie me-1"></i> {{ assignment.job.department.name }}</span>
</div>
</div>
<div class="text-end">
<span class="job-status-badge">
{% trans "Assigned" %}
</span>
<div class="small text-muted mt-1">
{% trans "Assigned On:" %} {{ assignment.created_at|date:"M d, Y" }}
</div>
</div>
</div>
</div>
{% endfor %}
{% else %}
<div class="empty-state">
<i class="fas fa-briefcase-slash"></i>
<h6>{% trans "No jobs assigned" %}</h6>
<p class="mb-0">{% trans "There are no open job assignments for this agency." %}</p>
<a href="{% url 'agency_assignment_create' agency.slug %}" class="btn btn-main-action mt-3 btn-sm">
<i class="fas fa-plus me-1"></i> {% trans "Assign New Job" %}
</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="col-lg-4">
<!-- Statistics -->
<div class="card kaauh-card mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
@ -551,40 +696,6 @@
</div>
</div>
<!-- Quick Actions -->
{% comment %} <div class="card kaauh-card mb-4">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-bolt me-2"></i>
{% trans "Quick Actions" %}
</h5>
</div>
<div class="card-body">
<div class="d-grid gap-2">
<a href="{% url 'agency_update' agency.slug %}" class="btn btn-main-action">
<i class="fas fa-edit me-2"></i> {% trans "Edit Agency" %}
</a>
<a href="{% url 'agency_candidates' agency.slug %}" class="btn btn-outline-info">
<i class="fas fa-users me-2"></i> {% trans "View All Candidates" %}
</a>
<a href="#" class="btn btn-main-action">
<i class="fas fa-paper-plane me-2"></i> {% trans "Send Message" %}
</a>
{% if agency.website %}
<a href="{{ agency.website }}" target="_blank" class="btn btn-outline-secondary">
<i class="fas fa-external-link-alt me-2"></i> {% trans "Visit Website" %}
</a>
{% endif %}
{% if agency.email %}
<a href="mailto:{{ agency.email }}" class="btn btn-outline-success">
<i class="fas fa-envelope me-2"></i> {% trans "Send Email" %}
</a>
{% endif %}
</div>
</div>
</div> {% endcomment %}
<!-- Agency Information -->
<div class="card kaauh-card">
<div class="card-header bg-white border-bottom">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
@ -634,4 +745,4 @@ function copyPassword() {
}
</script>
{% endblock %}
{% endblock %}

View File

@ -328,8 +328,8 @@
</td>
<td>
{% with latest_meeting=candidate.get_latest_meeting %}
{% if latest_meeting and latest_meeting.join_url %}
<a href="{{ latest_meeting.join_url }}" target="_blank" class="btn btn-sm bg-primary-theme text-white" title="Join Interview"
{% if latest_meeting and latest_meeting.details_url %}
<a href="{{ latest_meeting.details_url }}" target="_blank" class="btn btn-sm bg-primary-theme text-white" title="Join Interview"
{% if latest_meeting.status == 'ended' %}disabled{% endif %}>
join
<i class="fas fa-video me-1"></i>