littele ui fix
This commit is contained in:
parent
64e04a011d
commit
f1499f7be0
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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/",
|
||||
|
||||
@ -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)
|
||||
# =========================================================================
|
||||
|
||||
|
||||
@ -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 #}
|
||||
|
||||
@ -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 %}
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user