Compare commits

...

4 Commits

Author SHA1 Message Date
f1310189aa Merge pull request 'solved 10 issues' (#129) from frontend into main
Reviewed-on: #129
2025-12-18 22:55:37 +03:00
80791e5fa7 fix few ui issues 2025-12-18 19:59:27 +03:00
0cb21885f7 added the rate limit to the generate interview questions 2025-12-17 19:34:00 +03:00
5471046f46 solved 10 issues 2025-12-17 19:06:45 +03:00
18 changed files with 386 additions and 133 deletions

View File

@ -67,6 +67,7 @@ INSTALLED_APPS = [
"widget_tweaks", "widget_tweaks",
"easyaudit", "easyaudit",
"secured_fields", "secured_fields",
] ]
@ -319,7 +320,7 @@ Q_CLUSTER = {
"name": "KAAUH_CLUSTER", "name": "KAAUH_CLUSTER",
"workers": 2, "workers": 2,
"recycle": 500, "recycle": 500,
"timeout": 120, "timeout": 360,
"max_attempts": 1, "max_attempts": 1,
"compress": True, "compress": True,
"save_limit": 250, "save_limit": 250,
@ -551,3 +552,15 @@ LOGGING = {
SECURED_FIELDS_KEY="kvaCwxrIMtVRouBH5mzf9g-uelv7XUD840ncAiOXkt4=" SECURED_FIELDS_KEY="kvaCwxrIMtVRouBH5mzf9g-uelv7XUD840ncAiOXkt4="
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}

View File

@ -1889,12 +1889,16 @@ class RemoteInterviewForm(forms.Form):
duration = forms.IntegerField( duration = forms.IntegerField(
min_value=1, min_value=1,
required=False, required=True,
widget=forms.NumberInput(attrs={ widget=forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'placeholder': 'Duration in minutes' 'placeholder': 'Duration in minutes'
}), }),
label=_('Duration (minutes)') label=_('Duration (minutes)'),
error_messages={
'required': _('Please enter how long the interview will last.'),
'min_value': _('Duration must be at least 1 minute.')
}
) )
@ -1947,12 +1951,16 @@ class OnsiteInterviewForm(forms.Form):
) )
duration = forms.IntegerField( duration = forms.IntegerField(
min_value=1, min_value=1,
required=False, required=True,
widget=forms.NumberInput(attrs={ widget=forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'placeholder': 'Duration in minutes' 'placeholder': 'Duration in minutes'
}), }),
label=_('Duration (minutes)') label=_('Duration (minutes)'),
error_messages={
'required': _('Please enter how long the interview will last.'),
'min_value': _('Duration must be at least 1 minute.')
}
) )
class ScheduledInterviewForm(forms.Form): class ScheduledInterviewForm(forms.Form):
@ -1975,12 +1983,16 @@ class ScheduledInterviewForm(forms.Form):
) )
duration = forms.IntegerField( duration = forms.IntegerField(
min_value=1, min_value=1,
required=False, required=True,
widget=forms.NumberInput(attrs={ widget=forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'placeholder': 'Duration in minutes' 'placeholder': 'Duration in minutes'
}), }),
label=_('Duration (minutes)') label=_('Duration (minutes)'),
error_messages={
'required': _('Please enter how long the interview will last.'),
'min_value': _('Duration must be at least 1 minute.')
}
) )
def clean_start_time(self): def clean_start_time(self):
@ -2010,12 +2022,16 @@ class OnsiteScheduleInterviewUpdateForm(forms.Form):
) )
duration = forms.IntegerField( duration = forms.IntegerField(
min_value=1, min_value=1,
required=False, required=True,
widget=forms.NumberInput(attrs={ widget=forms.NumberInput(attrs={
'class': 'form-control', 'class': 'form-control',
'placeholder': 'Duration in minutes' 'placeholder': 'Duration in minutes'
}), }),
label=_('Duration (minutes)') label=_('Duration (minutes)'),
error_messages={
'required': _('Please enter how long the interview will last.'),
'min_value': _('Duration must be at least 1 minute.')
}
) )
physical_address = forms.CharField( physical_address = forms.CharField(
max_length=255, max_length=255,
@ -2204,7 +2220,10 @@ Job: {job.title}
if interview.location_type == 'Remote': if interview.location_type == 'Remote':
initial_message += f"Pease join using meeting link {interview.join_url} \n\n" initial_message += f"Pease join using meeting link {interview.join_url} \n\n"
else: else:
initial_message += "This is an onsite schedule. Please arrive 10 minutes early.\n\n" initial_message += f"""
Location: {interview.physical_address}
Room No: {interview.room_number}
This is an onsite schedule. Please arrive 10 minutes early.\n\n"""
@ -2212,20 +2231,48 @@ Job: {job.title}
class InterviewResultForm(forms.ModelForm): # class InterviewResultForm(forms.ModelForm):
class Meta: # class Meta:
model = Interview # model = Interview
fields = ['interview_result', 'result_comments'] # fields = ['interview_result', 'result_comments']
widgets = { # widgets = {
'interview_result': forms.Select(attrs={ # 'interview_result': forms.Select(attrs={
'class': 'form-select', # Standard Bootstrap class # 'class': 'form-select', # Standard Bootstrap class
'required': 'required' # 'required': True
}), # }),
'result_comments': forms.Textarea(attrs={ # 'result_comments': forms.Textarea(attrs={
# 'class': 'form-control',
# 'rows': 3,
# 'placeholder': 'Enter setting value',
# 'required': True
# }),
# }
RESULT_CHOICES = (
('passed', 'Passed'),
('failed', 'Failed'),
('on_hold', 'On Hold'),
)
class InterviewResultForm(forms.Form):
interview_result = forms.ChoiceField(
choices=RESULT_CHOICES,
widget=forms.Select(attrs={
'class': 'form-select',
})
)
result_comments = forms.CharField(
widget=forms.Textarea(attrs={
'class': 'form-control', 'class': 'form-control',
'rows': 3, 'rows': 3,
'placeholder': 'Enter setting value', 'placeholder': 'Enter result comment',
'required': True })
}), )
}

View File

@ -0,0 +1,2 @@

View File

@ -0,0 +1,44 @@
import ollama
import re
# def clean_json_response(raw_string):
# """
# Removes Markdown code blocks and extra whitespace from AI responses.
# """
# # Use regex to find content between ```json and ``` or just ```
# match = re.search(r'```(?:json)?\s*([\s\S]*?)\s*```', raw_string)
# if match:
# return match.group(1).strip()
# return raw_string.strip()
import json
import re
def robust_json_parser(raw_output):
# 1. Strip Markdown blocks
clean = re.sub(r'```(?:json)?|```', '', raw_output).strip()
# 2. Fix trailing commas before closing braces/brackets
clean = re.sub(r',\s*([\]}])', r'\1', clean)
try:
return json.loads(clean)
except json.JSONDecodeError:
# 3. Last resort: try to find the first '{' and last '}'
start_idx = clean.find('{')
end_idx = clean.rfind('}')
if start_idx != -1 and end_idx != -1:
try:
return json.loads(clean[start_idx:end_idx+1])
except:
pass
raise
def get_model_reponse(prompt):
response=ollama.chat(
model='alibayram/smollm3:latest',
messages=[{'role': 'user', 'content': prompt}],
stream=False # Set to True for real-time streaming
)
# print(response['message']['content'])
response=robust_json_parser(response['message']['content'])
return response

View File

@ -1655,8 +1655,12 @@ def generate_interview_questions(schedule_id: int) -> dict:
if not questions: if not questions:
return {"status": "error", "message": "No questions generated"} return {"status": "error", "message": "No questions generated"}
schedule.interview_questions.update(questions) if schedule.interview_questions is None:
schedule.interview_questions={}
schedule.interview_questions=questions
schedule.save(update_fields=["interview_questions"]) schedule.save(update_fields=["interview_questions"])
# schedule.save(update_fields=["interview_questions"])
logger.info(f"Successfully generated questions for schedule {schedule_id}") logger.info(f"Successfully generated questions for schedule {schedule_id}")

View File

@ -246,6 +246,33 @@ class PersonCreateView(CreateView, LoginRequiredMixin, StaffOrAgencyRequiredMixi
if view == "job": if view == "job":
return redirect("application_create") return redirect("application_create")
return super().form_valid(form) return super().form_valid(form)
def form_invalid(self, form):
"""
Re-renders the form with error messages while maintaining the UI state.
"""
messages.error(self.request, "There was an error saving the applicant. Please check the details below.")
# Optional: Add specific field errors as messages
for field, errors in form.errors.items():
for error in errors:
messages.error(self.request, f"{field.title()}: {error}")
view = self.request.POST.get("view")
agency_slug = self.request.POST.get("agency")
context = self.get_context_data(form=form)
context['view_type'] = view
context['agency_slug'] = agency_slug
if view == "portal":
return redirect('agency_portal_dashboard')
return self.render_to_response(context)
class PersonDetailView(DetailView, LoginRequiredMixin, StaffRequiredMixin): class PersonDetailView(DetailView, LoginRequiredMixin, StaffRequiredMixin):
@ -310,7 +337,7 @@ def create_job(request):
logger.error(f"Error creating job: {e}") logger.error(f"Error creating job: {e}")
messages.error(request, f"Error creating job: {e}") messages.error(request, f"Error creating job: {e}")
else: else:
messages.error(request, f"Please correct the errors below.{form.errors}") messages.error(request, f"Please correct the errors below.")
else: else:
form = JobPostingForm() form = JobPostingForm()
return render(request, "jobs/create_job.html", {"form": form}) return render(request, "jobs/create_job.html", {"form": form})
@ -4149,6 +4176,8 @@ def interview_create_onsite(request, application_slug):
form.initial["topic"] = ( form.initial["topic"] = (
f"Interview for {application.job.title} - {application.name}" f"Interview for {application.job.title} - {application.name}"
) )
messages.error(request, "Please fix the highlighted errors below.")
form = OnsiteInterviewForm() form = OnsiteInterviewForm()
form.initial["topic"] = ( form.initial["topic"] = (
@ -4253,14 +4282,15 @@ def cancel_interview_for_application(request, slug):
def update_interview_result(request,slug): def update_interview_result(request,slug):
interview = get_object_or_404(Interview,slug=slug) interview = get_object_or_404(Interview,slug=slug)
schedule=interview.scheduled_interview schedule=interview.scheduled_interview
form = InterviewResultForm(request.POST, instance=interview) form = InterviewResultForm(request.POST)
if form.is_valid(): if form.is_valid():
interview_result=form.cleaned_data.get("interview_result")
result_comments=form.cleaned_data.get("result_comments")
interview.interview_result=interview_result
interview.result_comments=result_comments
interview.save(update_fields=['interview_result', 'result_comments']) interview.save(update_fields=['interview_result', 'result_comments'])
form.save() # Saves form data
messages.success(request, _(f"Interview result updated successfully to {interview.interview_result}.")) messages.success(request, _(f"Interview result updated successfully to {interview.interview_result}."))
return redirect("interview_detail", slug=schedule.slug) return redirect("interview_detail", slug=schedule.slug)
else: else:
@ -4765,10 +4795,14 @@ def interview_list(request):
"search_query": search_query, "search_query": search_query,
"interviews": page_obj, "interviews": page_obj,
"jobs": jobs, "jobs": jobs,
"interview_type":interview_type,
} }
return render(request, "interviews/interview_list.html", context) return render(request, "interviews/interview_list.html", context)
from django_ratelimit.decorators import ratelimit
@ratelimit(key='user_or_ip', rate='1/m', block=True)
@login_required @login_required
@staff_user_required @staff_user_required
def generate_ai_questions(request, slug): def generate_ai_questions(request, slug):
@ -4776,15 +4810,18 @@ def generate_ai_questions(request, slug):
from django_q.tasks import async_task from django_q.tasks import async_task
schedule = get_object_or_404(ScheduledInterview, slug=slug) schedule = get_object_or_404(ScheduledInterview, slug=slug)
messages.info(request,_("Generating interview questions."))
if request.method == "POST": if request.method == "POST":
# Queue the AI question generation task # Queue the AI question generation task
task_id = async_task( task_id = async_task(
"recruitment.tasks.generate_interview_questions", "recruitment.tasks.generate_interview_questions",
schedule.id, schedule.id,
sync=True sync=False
) )
# if request.headers.get("X-Requested-With") == "XMLHttpRequest": # if request.headers.get("X-Requested-With") == "XMLHttpRequest":
# return JsonResponse({ # return JsonResponse({
# "status": "success", # "status": "success",
@ -4830,7 +4867,7 @@ def interview_detail(request, slug):
) )
schedule = get_object_or_404(ScheduledInterview, slug=slug) schedule = get_object_or_404(ScheduledInterview, slug=slug)
interview = schedule.interview interview = schedule.interview
interview_result_form=InterviewResultForm(instance=interview) interview_result_form=InterviewResultForm()
application = schedule.application application = schedule.application
job = schedule.job job = schedule.job
if interview.location_type == "Remote": if interview.location_type == "Remote":

View File

@ -47,12 +47,16 @@ django-ckeditor-5==0.2.18
django-cors-headers==4.9.0 django-cors-headers==4.9.0
django-countries==7.6.1 django-countries==7.6.1
django-crispy-forms==2.4 django-crispy-forms==2.4
django-easy-audit==1.3.7s django-easy-audit==1.3.7
django-extensions==4.1 django-extensions==4.1
django-fernet-encrypted-fields==0.3.1
django-filter==25.1 django-filter==25.1
django-js-asset==3.1.2 django-js-asset==3.1.2
django-mathfilters==1.0.0
django-picklefield==3.3 django-picklefield==3.3
django-q2==1.8.0 django-q2==1.8.0
django-ratelimit==4.1.0
django-secured-fields==0.4.4
django-template-partials==25.2 django-template-partials==25.2
django-unfold==0.67.0 django-unfold==0.67.0
django-widget-tweaks==1.5.0 django-widget-tweaks==1.5.0
@ -136,6 +140,7 @@ Pygments==2.19.2
PyJWT==2.10.1 PyJWT==2.10.1
PyMuPDF==1.26.4 PyMuPDF==1.26.4
pyparsing==3.2.5 pyparsing==3.2.5
pypdf==6.4.2
PyPDF2==3.0.1 PyPDF2==3.0.1
pypdfium2==4.30.0 pypdfium2==4.30.0
PyPrind==2.11.3 PyPrind==2.11.3
@ -206,9 +211,3 @@ wrapt==1.17.3
wurst==0.4 wurst==0.4
xlrd==2.0.2 xlrd==2.0.2
xlsxwriter==3.2.9 xlsxwriter==3.2.9
locust==2.32.0
psutil==6.1.0
matplotlib==3.9.2
pandas==2.3.2
faker==37.8.0
requests==2.32.3

View File

@ -107,7 +107,7 @@
} }
.dropdown-menu .dropdown-item:hover { .dropdown-menu .dropdown-item:hover {
background-color: var(--kaauh-teal); border-color: var(--kaauh-teal);
color: white; color: white;
transform: translateX(4px); transform: translateX(4px);
} }
@ -369,7 +369,7 @@
<span class="d-inline"></span> <span class="d-inline"></span>
{% if user.profile_image %} {% if user.profile_image %}
<img src="{{ user.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar" <img src="{{ user.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar"
style="width: 36px; height: 36px; object-fit: cover; background-color: var(--kaauh-teal); display: inline-block; vertical-align: middle;" style="width: 36px; height: 36px; object-fit: cover; display: inline-block; vertical-align: middle;"
title="{% trans 'Your account' %}"> title="{% trans 'Your account' %}">
{% else %} {% else %}
<div class="profile-avatar" title="{% trans 'Your account' %}"> <div class="profile-avatar" title="{% trans 'Your account' %}">
@ -388,11 +388,11 @@
<div class="me-3 d-flex align-items-center justify-content-center" style="min-width: 48px;"> <div class="me-3 d-flex align-items-center justify-content-center" style="min-width: 48px;">
{% if user.profile_image %} {% if user.profile_image %}
<img src="{{ user.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar shadow-sm border" <img src="{{ user.profile_image.url }}" alt="{{ user.username }}" class="profile-avatar shadow-sm border"
style="width: 44px; height: 44px; object-fit: cover; background-color: var(--kaauh-teal); display: block;" style="width: 44px; height: 44px; object-fit: cover; display: block;"
title="{% trans 'Your account' %}"> title="{% trans 'Your account' %}">
{% else %} {% else %}
<div class="profile-avatar shadow-sm border d-flex align-items-center justify-content-center text-primary-theme" <div class="profile-avatar shadow-sm border d-flex align-items-center justify-content-center text-primary-theme"
style="width: 44px; height: 44px; background-color: var(--kaauh-teal); font-size: 1.2rem;"> style="width: 44px; height: 44px; font-size: 1.2rem;">
{{ user.username|first|upper }} {{ user.username|first|upper }}
</div> </div>
{% endif %} {% endif %}

View File

@ -455,7 +455,7 @@
}); });
} }
form_loader(); //form_loader();
try{ try{
document.body.addEventListener('htmx:afterRequest', function(evt) { document.body.addEventListener('htmx:afterRequest', function(evt) {

View File

@ -1,5 +1,5 @@
{% load i18n %} {% load i18n %}
<form hx-boost="true" action="{% url 'document_upload' slug %}" method="POST" enctype="multipart/form-data"> <form hx-boost="true" action="{% url 'document_upload' slug %}" method="POST" enctype="multipart/form-data" hx-on::after-request="this.reset()">
{% csrf_token %} {% csrf_token %}
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">

View File

@ -1,18 +1,16 @@
{% load i18n %} {% load i18n %}
<style>
</style>
<form method="get" class="d-flex gap-2 align-items-center"> <form method="get" class="d-flex gap-2 align-items-center">
<div class="input-group flex-grow-1" style="max-width: 300px;"> <div class="input-group flex-grow-1" style="max-width: 300px;">
<span class="input-group-text bg-white border-end-0">
<svg class="heroicon icon-sm" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path>
</svg>
</span>
<input type="text" name="search" <input type="text" name="search"
class="form-control border-start-0" class="form-control border-start-0"
placeholder="{% trans 'Search...' %}" placeholder="{% trans 'Search...' %}"
value="{{ search_query }}" value="{{ search_query }}"
aria-label="{% trans 'Search' %}"> aria-label="{% trans 'Search' %}">
<button class="btn btn-outline-primary" type="submit">
<i class="fas fa-search"></i>
</button>
</div> </div>
</form> </form>

View File

@ -486,10 +486,14 @@
<div class="mt-3"> <div class="mt-3">
<a href="{{ interview.join_url }}" <a href="{{ interview.join_url }}"
target="_blank" target="_blank"
class="btn btn-main-action btn-sm w-100"> class="btn btn-main-action btn-sm w-100" id="joinMeetingLink">
<i class="fas fa-video me-1"></i> {% trans "Join Meeting" %} <i class="fas fa-video me-1"></i> {% trans "Join Meeting" %}
</a> </a>
<button onclick="copyJoinUrl()" class="btn btn-outline-secondary btn-sm mt-2 w-100">
{% trans "Copy & share Join URL" %}
</button>
</div> </div>
<p id="copyMessage" style="color: green;"></p>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>
@ -763,26 +767,73 @@
</button> </button>
{% if schedule.status == 'completed' %} {% if schedule.status == 'completed' %}
<button type="button" class="btn btn-outline-success btn-sm w-100" <button type="button" class="btn btn-outline-primary btn-sm w-100"
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#resultModal"> data-bs-target="#resultModal">
<i class="fas fa-check-circle me-1"></i> {% trans "Update Result" %} <i class="fas fa-check-circle me-1"></i> {% trans "Update Result" %}
</button> </button>
<div class="w-100 text-center"> <div class="w-100 text-center">
{% if interview.interview_result %} {% if interview.interview_result %}
{% trans 'Interview Result : ' %} <div class="my-2">{% trans 'Interview Result : ' %}</div>
{% if interview.interview_result == 'passed' %} {% if interview.interview_result == 'passed' %}
<span class="badge bg-success text-white p-1"> <div class="card mb-3" style="max-width: 30rem;">
<i class="fas fa-check-circle me-1"></i> {{ interview.interview_result }} <div class="card-header text-white">
<!-- Status Badge as a Pill -->
<span class="badge rounded-pill bg-white text-primary-theme p-2">
<i class="fas fa-check-circle me-1"></i>
<strong>{{ interview.interview_result|upper }}</strong>
</span> </span>
</div>
{% if interview.result_comments %}
<div class="card-body text-primary-theme">
<h5 class="card-title">{% trans 'Result Comment:' %}</h5>
<!-- The comment text with slightly adjusted margins -->
<p class="card-text text-dark">
{{ interview.result_comments }}
</p>
</div>
{% endif %}
</div>
{% elif interview.interview_result == 'failed' %} {% elif interview.interview_result == 'failed' %}
<span class="badge bg-danger text-white p-1 fs-5">
<i class="fas fa-times-circle me-1"></i> {{ interview.interview_result }} <div class="card mb-3" style="max-width: 30rem;">
<div class="card-header text-white">
<!-- Status Badge as a Pill -->
<span class="badge rounded-pill bg-white text-danger p-2">
<i class="fas fa-check-circle me-1"></i>
<strong>{{ interview.interview_result|upper }}</strong>
</span> </span>
</div>
{% if interview.result_comments %}
<div class="card-body text-danger">
<h5 class="card-title">{% trans 'Result Comment:' %}</h5>
<!-- The comment text with slightly adjusted margins -->
<p class="card-text text-dark">
{{ interview.result_comments }}
</p>
</div>
{% endif %}
</div>
{% else %} {% else %}
<span class="badge bg-info text-dark p-1">
<i class="fas fa-info-circle me-1"></i> {{ interview.interview_result }} <div class="card mb-3" style="max-width: 30rem;">
<div class="card-header text-white">
<!-- Status Badge as a Pill -->
<span class="badge rounded-pill bg-white text-primary-theme p-2">
<i class="fas fa-check-circle me-1"></i>
<strong>{{ interview.interview_result|upper }}</strong>
</span> </span>
</div>
{% if interview.result_comments %}
<div class="card-body text-primary-theme">
<h5 class="card-title">{% trans 'Result Comment:' %}</h5>
<!-- The comment text with slightly adjusted margins -->
<p class="card-text text-dark">
{{ interview.result_comments }}
</p>
</div>
{% endif %}
</div>
{% endif %} {% endif %}
{% else %} {% else %}
<span class="badge rounded-pill bg-secondary text-white"> <span class="badge rounded-pill bg-secondary text-white">
@ -998,5 +1049,30 @@ document.addEventListener('DOMContentLoaded', function () {
} }
}); });
}); });
//COPY TO Clipboard ZOOM LINK
async function copyJoinUrl(){
const joinUrlElement=document.getElementById('joinMeetingLink');
const urlToCopy=joinUrlElement.href;
const messageElement=document.getElementById('copyMessage');
console.log(urlToCopy)
try{
await navigator.clipboard.writeText(urlToCopy);
messageElement.textContent='Join URL copied successfully!';
setTimeout(()=>{
messageElement.textContent='';
},
3000
);
console.log('URL copied:', urlToCopy);
}
catch(e){
messageElement.textContent='Failed to copy Join URL.';
console.error('could not copy the URL',e);
}
}
</script> </script>
{% endblock %} {% endblock %}

View File

@ -174,51 +174,70 @@
</div> </div>
</div> </div>
<div class="filter-controls"> <div class="card mb-4 shadow-sm no-hover">
<form method="get" class="row g-3"> <div class="card-body">
<div class="col-md-3"> <div class="row g-4">
<label for="job_filter" class="form-label small text-muted">{% trans "Filter by Job" %}</label> <div class="col-md-4">
<label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label>
<div class="input-group input-group-lg mb-3">
<form method="get" action="" class="w-100">
{% include 'includes/search_form.html' %}
</form>
</div>
</div>
<div class="col-md-8">
<form method="GET" class="row g-2 align-items-end h-100">
{# Keep search query context when filtering #}
{% if request.GET.search %}<input type="hidden" name="search" value="{{ request.GET.search }}">{% endif %}
<div class="col-md-4">
<label for="job_filter" class="form-label small text-muted">{% trans "Filter by Job" %}</label>
<select name="job" id="job_filter" class="form-select form-select-sm"> <select name="job" id="job_filter" class="form-select form-select-sm">
<option value="">{% trans "All Jobs" %}</option> <option value="">{% trans "All Jobs" %}</option>
{% for job in jobs %} {% for job in jobs %}
<option value="{{ job.slug }}" {% if job_filter == job.slug %}selected{% endif %}>{{ job.title }}</option> <option value="{{ job.slug }}" {% if request.GET.job == job.slug %}selected{% endif %}>{{ job.title }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div class="col-md-2">
<label for="status_filter" class="form-label form-label-sm">{% trans "Status" %}</label> <div class="col-md-3">
<label for="status_filter" class="form-label small text-muted">{% trans "Status" %}</label>
<select name="status" id="status_filter" class="form-select form-select-sm"> <select name="status" id="status_filter" class="form-select form-select-sm">
<option value="">{% trans "All Status" %}</option> <option value="">{% trans "All" %}</option>
<option value="scheduled" {% if request.GET.status == "scheduled" %}selected{% endif %}>{% trans "Scheduled" %}</option> <option value="scheduled" {% if request.GET.status == "scheduled" %}selected{% endif %}>{% trans "Scheduled" %}</option>
<option value="confirmed" {% if request.GET.status == "confirmed" %}selected{% endif %}>{% trans "Confirmed" %}</option> <option value="confirmed" {% if request.GET.status == "confirmed" %}selected{% endif %}>{% trans "Confirmed" %}</option>
<option value="cancelled" {% if request.GET.status == "cancelled" %}selected{% endif %}>{% trans "Cancelled" %}</option> <option value="cancelled" {% if request.GET.status == "cancelled" %}selected{% endif %}>{% trans "Cancelled" %}</option>
<option value="completed" {% if request.GET.status == "completed" %}selected{% endif %}>{% trans "Completed" %}</option> <option value="completed" {% if request.GET.status == "completed" %}selected{% endif %}>{% trans "Completed" %}</option>
</select> </select>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label for="type_filter" class="form-label form-label-sm">{% trans "Type" %}</label> <label for="type_filter" class="form-label small text-muted">{% trans "Type" %}</label>
<select name="type" id="type_filter" class="form-select form-select-sm"> <select name="type" id="type_filter" class="form-select form-select-sm">
<option value="">{% trans "All Types" %}</option> <option value="">{% trans "All" %}</option>
<option value="Remote" {% if request.GET.type == "Remote" %}selected{% endif %}>{% trans "Remote" %}</option> <option value="Remote" {% if request.GET.type == "Remote" %}selected{% endif %}>{% trans "Remote" %}</option>
<option value="Onsite" {% if request.GET.type == "Onsite" %}selected{% endif %}>{% trans "Onsite" %}</option> <option value="Onsite" {% if request.GET.type == "Onsite" %}selected{% endif %}>{% trans "Onsite" %}</option>
</select> </select>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label for="search_filter" class="form-label form-label-sm">{% trans "Search Candidate" %}</label> <div class="d-flex gap-1">
<input type="text" name="search" id="search_filter" class="form-control form-control-sm" <button type="submit" class="btn btn-main-action btn-sm flex-grow-1 text-nowrap">
value="{{ request.GET.search }}" placeholder="{% trans 'Name or Email' %}"> <i class="fas fa-filter me-1"></i> {% trans "Apply Filters" %}
</div>
<div class="col-md-2 d-flex align-items-end">
<button type="submit" class="btn btn-main-action btn-sm me-2">
<i class="fas fa-filter me-1"></i> {% trans "Filter" %}
</button> </button>
<a href="{% url 'interview_list' %}" class="btn btn-outline-secondary btn-sm"> {% if request.GET.search or request.GET.job or request.GET.status or request.GET.type %}
<i class="fas fa-times me-1"></i> {% trans "Clear" %} <a href="{% url 'interview_list' %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'Clear Filter' %}">
<i class="fas fa-times me-1"></i>{% trans 'Clear Filter' %}
</a> </a>
{% endif %}
</div>
</div> </div>
</form> </form>
</div> </div>
</div>
</div>
</div>
{% if interviews %} {% if interviews %}
<div id="interview-list"> <div id="interview-list">

View File

@ -201,9 +201,9 @@
<button type="submit" class="btn btn-main-action btn-sm"> <button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-filter me-1"></i> {% trans "Apply Filters" %} <i class="fas fa-filter me-1"></i> {% trans "Apply Filters" %}
</button> </button>
{% if request.GET.q or request.GET.nationality or request.GET.gender %} {% if search_query or nationality %}
<a href="{% url 'person_list' %}" class="btn btn-outline-secondary btn-sm"> <a href="{% url 'person_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-times me-1"></i> {% trans "Clear" %} <i class="fas fa-times me-1"></i> {% trans "Clear Filter" %}
</a> </a>
{% endif %} {% endif %}
</div> </div>

View File

@ -71,18 +71,24 @@
<!-- Search and Filters --> <!-- Search and Filters -->
<div class="kaauh-card p-3 mb-4"> <div class="kaauh-card p-3 mb-4">
<form method="get" class="row g-3"> <form method="get" class="row g-3 align-items-end">
<div class="col-md-6"> <div class="col-md-4 me-5">
<div class="form-group"> <label for="search" class="form-label">{% trans "Search by name and job" %}</label>
<label for="search" class="form-label">{% trans "Search" %}</label> <div class="input-group flex-grow-1">
<input type="text" class="form-control" id="search" name="q"
value="{{ search_query }}" placeholder="{% trans 'Search by agency or job title...' %}"> <input type="text" class="form-control border-start-0 w-50" id="search" name="q"
value="{{ search_query }}" placeholder="{% trans 'Search....' %}">
<button class="btn btn-outline-primary" type="submit">
<i class="fas fa-search"></i>
</button>
</div> </div>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<div class="form-group"> <div class="form-group-sm">
<label for="status" class="form-label">{% trans "Status" %}</label> <label for="status" class="form-label">{% trans "Status" %}</label>
<select class="form-select" id="status" name="status"> <select class="form-select form-select-sm" id="status" name="status">
<option value="">{% trans "All Statuses" %}</option> <option value="">{% trans "All Statuses" %}</option>
<option value="ACTIVE" {% if status_filter == 'ACTIVE' %}selected{% endif %}>{% trans "Active" %}</option> <option value="ACTIVE" {% if status_filter == 'ACTIVE' %}selected{% endif %}>{% trans "Active" %}</option>
<option value="EXPIRED" {% if status_filter == 'EXPIRED' %}selected{% endif %}>{% trans "Expired" %}</option> <option value="EXPIRED" {% if status_filter == 'EXPIRED' %}selected{% endif %}>{% trans "Expired" %}</option>
@ -91,15 +97,21 @@
</select> </select>
</div> </div>
</div> </div>
<div class="col-md-2"> <div class="col-md-3">
<div class="form-group"> <div class="filter-buttons">
<label class="form-label">&nbsp;</label> <button type="submit" class="btn btn-main-action btn-sm">
<button type="submit" class="btn btn-outline-secondary w-100"> <i class="fas fa-filter me-1"></i> {% trans "Apply Filters" %}
<i class="fas fa-search me-1"></i> {% trans "Search" %}
</button> </button>
{% if status_filter or search_query %}
<a href="{% url 'agency_assignment_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-times me-1"></i> {% trans "Clear Filter" %}
</a>
{% endif %}
</div> </div>
</div> </div>
</form> </form>
</div> </div>
<!-- Assignments List --> <!-- Assignments List -->

View File

@ -584,6 +584,7 @@
{% if application.phone %} {% if application.phone %}
<span class="ms-3"><i class="fas fa-phone me-1"></i> {{ application.phone }}</span> <span class="ms-3"><i class="fas fa-phone me-1"></i> {{ application.phone }}</span>
{% endif %} {% endif %}
<span class="ms-3"><i class="fas fa-briefcase me-1"></i> {{ application.job.title}}</span>
</div> </div>
</div> </div>
<div class="text-end"> <div class="text-end">

View File

@ -271,12 +271,13 @@
<option selected> <option selected>
---------- ----------
</option> </option>
<option value="Interview">
{% trans "To Interview" %}
</option>
<option value="Offer"> <option value="Offer">
{% trans "To Offer" %} {% trans "To Offer" %}
</option> </option>
<option value="Interview">
{% trans "To Interview" %}
</option>
</select> </select>
<button id="changeStage" type="submit" class="btn btn-main-action btn-sm"> <button id="changeStage" type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %} <i class="fas fa-arrow-right me-1"></i> {% trans "Change Stage" %}

View File

@ -251,7 +251,7 @@
</button> </button>
{% if job_filter or stage_filter or search_query %} {% if job_filter or stage_filter or search_query %}
<a href="{% url 'application_list' %}" class="btn btn-outline-secondary btn-sm"> <a href="{% url 'application_list' %}" class="btn btn-outline-secondary btn-sm">
<i class="fas fa-times me-1"></i> {% trans "Clear" %} <i class="fas fa-times me-1"></i> {% trans "Clear Filter" %}
</a> </a>
{% endif %} {% endif %}
</div> </div>