Merge pull request 'frontend' (#28) from frontend into main

Reviewed-on: #28
This commit is contained in:
ismail 2025-11-03 12:58:32 +03:00
commit 15f8cb2650
25 changed files with 252 additions and 217 deletions

4
.gitignore vendored
View File

@ -27,7 +27,7 @@ var/
*.log
*.pot
*.sqlite3
local_settings.py
settings.py
db.sqlite3
# Virtual environment
@ -95,7 +95,7 @@ coverage.xml
# Django stuff:
# Local settings
local_settings.py
settings.py
# Database sqlite files:
# The base directory for relative paths in .gitignore

View File

@ -66,7 +66,7 @@ INSTALLED_APPS = [
SITE_ID = 1
LOGIN_REDIRECT_URL = '/'
LOGIN_REDIRECT_URL = 'dashboard'
ACCOUNT_LOGOUT_REDIRECT_URL = '/'
@ -135,9 +135,9 @@ WSGI_APPLICATION = 'NorahUniversity.wsgi.application'
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'norahuniversity',
'USER': 'norahuniversity',
'PASSWORD': 'norahuniversity',
'NAME': 'haikal_db',
'USER': 'faheed',
'PASSWORD': 'Faheed@215',
'HOST': '127.0.0.1',
'PORT': '5432',
}
@ -183,32 +183,19 @@ ACCOUNT_LOGIN_METHODS = ['email']
ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*']
ACCOUNT_UNIQUE_EMAIL = True
ACCOUNT_EMAIL_VERIFICATION = 'none'
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
ACCOUNT_FORMS = {'signup': 'recruitment.forms.StaffSignupForm'}
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = '10.10.1.110' #'smtp.gmail.com'
EMAIL_PORT = 2225 #587
EMAIL_USE_TLS = False
EMAIL_USE_SSL = False
EMAIL_TIMEOUT = 10
DEFAULT_FROM_EMAIL = 'norahuniversity@example.com'
# Gmail SMTP credentials
# Remove the comment below if you want to use Gmail SMTP server
# EMAIL_HOST_USER = 'your_email@gmail.com'
# EMAIL_HOST_PASSWORD = 'your_password'
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
# Crispy Forms Configuration
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrapconsole5"
# Bootstrap 5 Configuration
CRISPY_BS5 = {

View File

@ -162,10 +162,17 @@ def send_interview_invitation_email(candidate, job, meeting_details=None, recipi
try:
# Prepare recipient list
recipients = []
if candidate.email:
if candidate.hiring_source == "Agency":
try:
recipients.append(candidate.hiring_agency.email)
except :
pass
else:
recipients.append(candidate.email)
if recipient_list:
recipients.extend(recipient_list)
if not recipients:
return {'success': False, 'error': 'No recipient email addresses provided'}

View File

@ -682,7 +682,30 @@ class Candidate(Base):
@property
def scoring_timeout(self):
return timezone.now() <= (self.created_at + timezone.timedelta(minutes=5))
@property
def get_interview_date(self):
if hasattr(self, 'scheduled_interview') and self.scheduled_interview:
return self.scheduled_interviews.first().interview_date
return None
@property
def get_interview_time(self):
if hasattr(self, 'scheduled_interview') and self.scheduled_interview:
return self.scheduled_interviews.first().interview_time
return None
@property
def time_to_hire_days(self):
if self.hired_date and self.created_at:
time_to_hire = self.hired_date - self.created_at.date()
return time_to_hire.days
return 0
class TrainingMaterial(Base):
title = models.CharField(max_length=255, verbose_name=_("Title"))

View File

@ -5,7 +5,7 @@ from . import views_integration
from . import views_source
urlpatterns = [
path('', views_frontend.dashboard_view, name='dashboard'),
path('dashboard/', views_frontend.dashboard_view, name='dashboard'),
# Job URLs (using JobPosting model)
path('jobs/', views_frontend.JobListView.as_view(), name='job_list'),
@ -62,6 +62,7 @@ urlpatterns = [
# Form Preview URLs
# path('forms/', views.form_list, name='form_list'),
path('forms/builder/', views.form_builder, name='form_builder'),
path('forms/builder/<slug:template_slug>/', views.form_builder, name='form_builder'),
path('forms/', views.form_templates_list, name='form_templates_list'),

View File

@ -449,7 +449,7 @@ def schedule_interviews(schedule):
interview_date=slot['date'],
interview_time=slot['time']
)
candidate.interview_date=interview_datetime
# Send email to candidate
send_interview_email(scheduled_interview)

View File

@ -860,13 +860,13 @@ def application_submit_form(request, template_slug):
if is_limit_exceeded:
messages.error(
request,
'Application limit reached: This job is no longer accepting new applications. Please explore other available positions.'
_('Application limit reached: This job is no longer accepting new applications.')
)
return redirect('application_detail',slug=job.slug)
if job.is_expired:
messages.error(
request,
'Application deadline passed: This job is no longer accepting new applications. Please explore other available positions.'
_('Application deadline passed: This job is no longer accepting new applications.')
)
return redirect('application_detail',slug=job.slug)
@ -1424,10 +1424,26 @@ def candidate_set_exam_date(request, slug):
def candidate_update_status(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
mark_as = request.POST.get('mark_as')
if mark_as != '----------':
candidate_ids = request.POST.getlist("candidate_ids")
print(candidate_ids)
if c := Candidate.objects.filter(pk__in = candidate_ids):
c.update(stage=mark_as,exam_date=timezone.now(),applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
if mark_as=='Exam':
c.update(exam_date=timezone.now(),interview_date=None,offer_date=None,hired_date=None,stage=mark_as,applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
elif mark_as=='Interview':
# interview_date update when scheduling the interview
c.update(stage=mark_as,offer_date=None,hired_date=None,applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
elif mark_as=='Offer':
c.update(stage=mark_as,offer_date=timezone.now(),hired_date=None,applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
elif mark_as=='Hired':
print('hired')
c.update(stage=mark_as,hired_date=timezone.now(),applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
else:
c.update(stage=mark_as,exam_date=None,interview_date=None,offer_date=None,hired_date=None,applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
messages.success(request, f"Candidates Updated")
response = HttpResponse(redirect("candidate_screening_view", slug=job.slug))
@ -2973,11 +2989,12 @@ def agency_assignment_create(request,slug=None):
messages.success(request, f'Assignment created for {assignment.agency.name} - {assignment.job.title}!')
return redirect('agency_assignment_detail', slug=assignment.slug)
else:
messages.error(request, 'Please correct the errors below.')
messages.error(request, f'Please correct the errors below.{form.errors.as_text()}')
print(form.errors.as_json())
else:
form = AgencyJobAssignmentForm()
try:
from django.forms import HiddenInput
# from django.forms import HiddenInput
form.initial['agency'] = agency
# form.fields['agency'].widget = HiddenInput()
except HiringAgency.DoesNotExist:
@ -3086,6 +3103,7 @@ def agency_access_link_detail(request, slug):
AgencyAccessLink.objects.select_related('assignment__agency', 'assignment__job'),
slug=slug
)
context = {
'access_link': access_link,

View File

@ -257,6 +257,7 @@ def candidate_detail(request, slug):
if request.user.is_staff:
stage_form = forms.CandidateStageForm()
# parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False)
# parsed = json_to_markdown_table([parsed])
return render(request, 'recruitment/candidate_detail.html', {
@ -458,19 +459,25 @@ def dashboard_view(request):
# B. Efficiency & Conversion Metrics (Scoped)
hired_candidates = candidate_queryset.filter(
Q(offer_status="Accepted") | Q(stage='HIRED'),
join_date__isnull=False
stage='Hired'
)
print(hired_candidates)
lst=[c.time_to_hire_days for c in hired_candidates]
print(lst)
time_to_hire_query = hired_candidates.annotate(
time_diff=ExpressionWrapper(
F('join_date') - F('created_at__date'),
F('hired_date') - F('created_at__date'),
output_field=fields.DurationField()
)
).aggregate(avg_time_to_hire=Avg('time_diff'))
print(time_to_hire_query)
avg_time_to_hire_days = (
time_to_hire_query.get('avg_time_to_hire').days
if time_to_hire_query.get('avg_time_to_hire') else 0
)
print(avg_time_to_hire_days)
applied_count = candidate_queryset.filter(stage='Applied').count()
advanced_count = candidate_queryset.filter(stage__in=['Exam', 'Interview', 'Offer']).count()

View File

@ -4,7 +4,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% translate "Application Submitted - Thank You" %}</title>
<title>{% trans "Application Submitted - Thank You" %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@ -166,25 +166,25 @@
</div>
<h1 class="text-success-header">{% translate "Thank You!" %}</h1>
<h2 class="h4" style="color: #333;">{% translate "Your application has been submitted successfully" %}</h2>
<h2 class="h4" style="color: #333;">{% trans "Your application has been submitted successfully" %}</h2>
{% comment %} {# JOB INFO BLOCK #}
<div class="job-info-block">
<p class="mb-2"><strong>{% translate "Position" %}:</strong> <span class="fw-bold">{{ job.title }}</span></p>
<p class="mb-2"><strong>{% translate "Job ID" %}:</strong> {{ job.internal_job_id }}</p>
<p class="mb-2"><strong>{% translate "Department" %}:</strong> {{ job.department|default:"Not specified" }}</p>
<p class="mb-2"><strong>{% trans "Position" %}:</strong> <span class="fw-bold">{{ job.title }}</span></p>
<p class="mb-2"><strong>{% trans "Job ID" %}:</strong> {{ job.internal_job_id }}</p>
<p class="mb-2"><strong>{% trans "Department" %}:</strong> {{ job.department|default:"Not specified" }}</p>
{% if job.application_deadline %}
<p><strong>{% translate "Application Deadline" %}:</strong> {{ job.application_deadline|date:"F j, Y" }}</p>
<p><strong>{% trans "Application Deadline" %}:</strong> {{ job.application_deadline|date:"F j, Y" }}</p>
{% endif %}
</div> {% endcomment %}
<p style="font-size: 1rem; line-height: 1.6; color: #555;">
{% translate "We appreciate your interest in joining our team. Our hiring team will review your application and contact you if there's a potential match for this position." %}
{% trans "We appreciate your interest in joining our team. Our hiring team will review your application and contact you if there's a potential match for this position." %}
</p>
<div style="margin-top: 30px;">
<a href="https://kaauh.edu.sa/career" class="btn btn-main-action btn-lg">
<i class="fas fa-arrow-left me-2"></i> {% translate "Return to Job Listings" %}
<a href="{% url 'kaauh_career' %}" class="btn btn-main-action btn-lg">
<i class="fas fa-arrow-left me-2"></i> {% trans "Return to Job Listings" %}
</a>
{# You can add a link to view the saved application here if applicable #}
{% comment %} <a href="{% url 'jobs:job_detail' job.internal_job_id %}" class="btn btn-secondary">View Job Details</a> {% endcomment %}

View File

@ -251,25 +251,25 @@
</div>
{# Description Blocks (Main Content) #}
{% if job.description %}
{% if job.has_description_content %}
<div class="mb-4">
<h5>{% trans "Job Description" %}</h5>
<div class="text-secondary">{{ job.description|safe }}</div>
</div>
{% endif %}
{% if job.qualifications %}
{% if job.has_qualifications_content %}
<div class="mb-4">
<h5>{% trans "Required Qualifications" %}</h5>
<div class="text-secondary">{{ job.qualifications|safe }}</div>
</div>
{% endif %}
{% if job.benefits %}
{% if job.has_benefits_content %}
<div class="mb-4">
<h5>{% trans "Benefits" %}</h5>
<div class="text-secondary">{{ job.benefits|safe}}</div>
</div>
{% endif %}
{% if job.application_instructions %}
{% if job.has_application_instructions_content %}
<div class="mb-4">
<h5>{% trans "Application Instructions" %}</h5>
<div class="text-secondary">{{ job.application_instructions|safe }}</div>
@ -347,11 +347,14 @@
<i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %}
</a>
{% else %}
{% if job.form_template.is_active %}
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
<i class="fas fa-list-alt me-1"></i> {% trans "View Form Template" %}
</a>
{% else %}
{% if job.form_template.is_active %}
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
<i class="fas fa-list-alt me-1"></i> {% trans "View Form Template" %}
</a>
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
<i class="fas fa-list-alt me-1"></i> {% trans "Manage Form Template" %}
</a>
{% else %}
<p>{% trans "This job status is not active, the form will appear once the job is made active"%}</p>
{% endif %}
@ -446,7 +449,7 @@
<div class="row g-3 stats-grid">
{# 1. Job Avg. Score #}
<div class="col-6">
<div class="col-4">
<div class="card text-center h-100 kpi-card">
<div class="card-body p-2">
<i class="fas fa-star text-primary mb-1 d-block" style="font-size: 1.2rem;"></i>
@ -457,7 +460,7 @@
</div>
{# 2. High Potential Count #}
<div class="col-6">
<div class="col-4">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-trophy text-success mb-1 d-block" style="font-size: 1.2rem;"></i>
@ -468,7 +471,7 @@
</div>
{# 3. Avg. Time to Interview #}
<div class="col-6">
{% comment %} <div class="col-6">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-calendar-alt text-info mb-1 d-block" style="font-size: 1.2rem;"></i>
@ -487,9 +490,9 @@
<small class="text-muted d-block">{% trans "Avg. Exam Review" %}</small>
</div>
</div>
</div>
</div> {% endcomment %}
<!--Vacancy fill rate-->
<div class="col-6">
<div class="col-4">
<div class="card text-center h-100">
<div class="card-body p-2">
<i class="fas fa-trophy text-secondary mb-1 d-block" style="font-size: 1.2rem;"></i>

View File

@ -325,7 +325,7 @@
</td>
{# CANDIDATE MANAGEMENT DATA - URLS NEUTRALIZED #}
<td class="candidate-data-cell text-primary-theme"><a href="#" class="text-primary-theme">{% if job.all_candidates.count %}{{ job.all_candidates.count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-primary-theme"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-primary-theme">{% if job.all_candidates.count %}{{ job.all_candidates.count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-info"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-info">{% if job.screening_candidates.count %}{{ job.screening_candidates.count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_exam_view' job.slug %}" class="text-success">{% if job.exam_candidates.count %}{{ job.exam_candidates.count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_interview_view' job.slug %}" class="text-success">{% if job.interview_candidates.count %}{{ job.interview_candidates.count }}{% else %}-{% endif %}</a></td>

View File

@ -5,7 +5,7 @@
{% block content %}
<div class="container-fluid py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div class="d-flex justify-content-between align-items-center mb-4 px-3 py-3">
<div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-link me-2"></i>
@ -20,7 +20,7 @@
<div class="row">
<div class="col-md-8">
<div class="kaauh-card shadow-sm mb-4">
<div class="kaauh-card shadow-sm mb-4 px-3 py-3">
<div class="card-body">
<div class="d-flex justify-content-between align-items-start mb-3">
<h5 class="card-title mb-0">
@ -77,7 +77,7 @@
</div>
<div class="kaauh-card shadow-sm">
<div class="card-body">
<div class="card-body px-3 py-3">
<h5 class="card-title mb-3">
<i class="fas fa-key me-2 text-warning"></i>
{% trans "Access Credentials" %}
@ -125,7 +125,7 @@
</div>
<div class="col-md-4">
<div class="kaauh-card shadow-sm mb-4">
<div class="kaauh-card shadow-sm mb-4 px-3 py-3">
<div class="card-body">
<h5 class="card-title mb-3">
<i class="fas fa-chart-line me-2 text-info"></i>
@ -161,7 +161,7 @@
</div>
</div>
<div class="kaauh-card shadow-sm">
<div class="kaauh-card shadow-sm px-3 py-3">
<div class="card-body">
<h5 class="card-title mb-3">
<i class="fas fa-cog me-2 text-secondary"></i>

View File

@ -473,7 +473,7 @@
</div>
<!-- Quick Actions -->
<div class="card kaauh-card mb-4">
{% 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>
@ -503,7 +503,7 @@
{% endif %}
</div>
</div>
</div>
</div> {% endcomment %}
<!-- Agency Information -->
<div class="card kaauh-card">

View File

@ -417,15 +417,16 @@
</div>
{% endif %}
{% if candidate.interview_date %}
{% if candidate.get_interview_date %}
<div class="timeline-item">
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-comments"></i></div>
<div class="timeline-content">
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Interview" %}</p>
<small class="text-muted">
<i class="far fa-calendar-alt me-1"></i> {{ candidate.interview_date|date:"M d, Y" }}
<i class="far fa-calendar-alt me-1"></i> {{ candidate.get_interview_date}}
<span class="ms-2">|</span>
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.interview_date|date:"h:i A" }}
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.get_interview_time}}
</small>
</div>
@ -439,8 +440,21 @@
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
<small class="text-muted">
<i class="far fa-calendar-alt me-1"></i> {{ candidate.offer_date|date:"M d, Y" }}
<span class="ms-2">|</span>
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.offer_date|date:"h:i A" }}
</small>
</div>
</div>
{% endif %}
{% if candidate.hired_date %}
<div class="timeline-item">
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div>
<div class="timeline-content">
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
<small class="text-muted">
<i class="far fa-calendar-alt me-1"></i> {{ candidate.hired_date|date:"M d, Y" }}
</small>
</div>
@ -651,7 +665,16 @@
</div>
<div class="card shadow-sm mb-4 p-2">
<h5 class="text-muted mb-3"><i class="fas fa-clock me-2"></i>{% trans "Time to Hire: " %}{{candidate.time_to_hire|default:100}}&nbsp;days</h5>
<h5 class="text-muted mb-3"><i class="fas fa-clock me-2"></i>{% trans "Time to Hire:" %}
{% with days=candidate.time_to_hire_days %}
{% if days > 0 %}
{{ days }} day{{ days|pluralize }}
{% else %}
N/A
{% endif %}
{% endwith %}
</h5>
</div>

View File

@ -265,7 +265,7 @@
<th style="width: 10%"><i class="fas fa-calendar me-1"></i> {% trans "Meeting Date" %}</th>
<th style="width: 7%"><i class="fas fa-video me-1"></i> {% trans "Link" %}</th>
<th style="width: 8%"><i class="fas fa-check-circle me-1"></i> {% trans "Meeting Status" %}</th>
<th style="width: 5%"><i class="fas fa-check-circle me-1"></i> {% trans "Interview Result" %}</th>
<th style="width: 5%"><i class="fas fa-check-circle me-1"></i> {% trans "Interview Result"%}</th>
<th style="width: 10%"><i class="fas fa-cog me-1"></i> {% trans "Actions" %}</th>
</tr>
</thead>
@ -276,11 +276,20 @@
<div class="form-check">
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
</div>
</td>
<td>
<div class="candidate-name">
<button type="button" class="btn btn-outline-secondary btn-sm"
data-bs-toggle="modal"
data-bs-target="#candidateviewModal"
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
hx-target="#candidateviewModalBody"
title="View Profile">
{{ candidate.name }}<i class="fas fa-eye ms-1"></i>
</button>
{% comment %} <div class="candidate-name">
{{ candidate.name }}
</div>
</div> {% endcomment %}
</td>
<td>
<div class="candidate-details">
@ -365,14 +374,14 @@
{% endif %}
</td>
<td>
<button type="button" class="btn btn-outline-secondary btn-sm"
{% comment %} <button type="button" class="btn btn-outline-secondary btn-sm"
data-bs-toggle="modal"
data-bs-target="#candidateviewModal"
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
hx-target="#candidateviewModalBody"
title="View Profile">
<i class="fas fa-eye"></i>
</button>
</button> {% endcomment %}
<button type="button" class="btn btn-outline-info btn-sm"
data-bs-toggle="modal"
data-bs-target="#emailModal"
@ -457,17 +466,14 @@
<form method="post" action="{% url 'candidate_interview_view' job.slug %}">
{% csrf_token %}
<<<<<<< HEAD
<div class="modal-body table-responsive">
=======
<div class="modal-body">
>>>>>>> c6fcb276135dc7e87bb0d065a93ff89091ff0207
{{ job.internal_job_id }} {{ job.title}}
<hr>
<<<<<<< HEAD
<table class="table tab table-bordered mt-3">
@ -490,18 +496,7 @@
</table>
=======
<h3>👥 {% trans "Participants" %}</h3>
{{ form.participants.errors }}
{{ form.participants }}
<hr>
<h3>🧑‍💼 {% trans "Users" %}</h3>
{{ form.users.errors }}
{{ form.users }}
>>>>>>> c6fcb276135dc7e87bb0d065a93ff89091ff0207
</div>
<div class="modal-footer">

View File

@ -30,54 +30,113 @@
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
}
/* Card Header and Icon Styling */
/* ------------------------------------------------------------- */
/* CONSOLIDATED CARD HEADER STYLES (for both main and stat cards)*/
/* ------------------------------------------------------------- */
.card-header {
font-weight: 600;
padding: 1.25rem;
/* Consistent, reduced padding for compact look */
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--kaauh-border);
background-color: #f8f9fa;
display: flex;
justify-content: space-between;
align-items: center;
height: auto;
}
.card-header h3, .card-header h2 {
/* Target h2 (for main headers) and h3 (for stat card headers) */
.card-header h1, .card-header h2, .card-header h3, .card-header h6 {
display: flex;
align-items: center;
color: var(--kaauh-primary-text);
font-size: 1.25rem;
font-weight: 600;
margin: 0;
/* Font size for MAIN card titles (e.g., "Data Scope: All Jobs") */
font-size: 1.25rem;
font-weight: 600;
}
/* Override for H3 inside stat cards to make them very small */
.stats .card-header h3 {
font-size: 0.85rem; /* Smallest size for the 9-card layout */
font-weight: 600;
white-space: nowrap; /* Prevent title from wrapping */
overflow: hidden;
text-overflow: ellipsis;
max-width: 100%;
}
.stat-icon {
color: var(--kaauh-teal);
font-size: 1.75rem;
margin-right: 0.75rem;
font-size: 0.8rem; /* Small icon size */
margin-right: 0.25rem;
/* Note: For 9-card density, you might still want to hide this on mobile */
}
/* Stats Grid Layout */
/* ------------------------------------------------------------- */
/* STATS GRID LAYOUT (9 Columns) */
/* ------------------------------------------------------------- */
.stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 1.5rem;
margin-bottom: 3rem;
/* Force 9 columns */
grid-template-columns: repeat(9, 1fr);
gap: 0.75rem;
margin-bottom: 2rem;
}
/* Stat Card Specific Styling */
.stat-value {
font-size: 2.8rem;
/* This is the most important number */
font-size: 1.5rem; /* Increased slightly for focus, was 1rem/1.25rem */
text-align: center;
color: var(--kaauh-teal-dark);
font-weight: 700;
padding: 1rem 1rem 0.5rem;
font-weight: 700;
padding: 0.5rem 0.25rem 0.1rem; /* Very little bottom padding */
line-height: 1.2;
}
.stat-caption {
font-size: 0.9rem;
/* Smallest text for the label below the value */
font-size: 0.7rem; /* Minimized size */
text-align: center;
color: #6c757d;
padding-bottom: 1rem;
padding: 0 0.25rem 0.5rem;
line-height: 1.1;
}
/* ------------------------------------------------------------- */
/* RESPONSIVE DESIGN */
/* ------------------------------------------------------------- */
/* On tablets and smaller laptops (1200px and down) */
@media (max-width: 1200px) {
.stats {
/* Switch to 4 columns on medium screens */
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
}
.stat-value {
font-size: 1.75rem; /* Increase value size slightly when more space is available */
}
.stat-caption {
font-size: 0.8rem;
}
}
/* On phones (576px and down) */
@media (max-width: 576px) {
.stats {
/* Stack to 2 columns on mobile for readability */
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.stat-value {
font-size: 1.5rem;
}
.stat-caption {
font-size: 0.75rem;
}
}
/* Dropdown/Filter Styling */
@ -130,7 +189,7 @@
<h2>
<i class="fas fa-search stat-icon"></i>
{% if current_job %}
{% trans "Data Scope: " %} **{{ current_job.title }}**
{% trans "Data Scope: " %}{{ current_job.title }}
{% else %}
{% trans "Data Scope: All Jobs" %}
{% endif %}
@ -153,9 +212,7 @@
{# STATS CARDS SECTION (12 KPIs) #}
{# -------------------------------------------------------------------------- #}
{% include 'recruitment/partials/stats_cards.html' %}
{# Note: The content of 'recruitment/partials/stats_cards.html' uses h3 which is styled correctly here #}
{# -------------------------------------------------------------------------- #}
{# CHARTS SECTION #}
@ -197,7 +254,7 @@
<h2>
<i class="fas fa-funnel-dollar stat-icon"></i>
{% if current_job %}
{% trans "Pipeline Funnel: " %} **{{ current_job.title }}**
{% trans "Pipeline Funnel: " %}{{ current_job.title }}
{% else %}
{% trans "Total Pipeline Funnel (All Jobs)" %}
{% endif %}
@ -223,28 +280,13 @@
</div>
</div>
<div class="card shadow-sm no-hover mb-4">
<div class="card-header">
<h6 class="mb-0">
<i class="fas fa-chart-pie me-2 text-primary"></i>
{% trans "Candidates From Each Sources" %}
</h6>
</div>
<div class="card-body p-4">
<div style="height: 300px;">
<canvas id="candidatesourceschart"></canvas>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/luxon@3.4.4"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.3.1"></script>
<script>
// Pass context data safely to JavaScript
const totalCandidatesScoped = parseInt('{{ total_candidates|default:0 }}');
@ -462,80 +504,7 @@
// Chart for Candidate Categories and Match Scores
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('candidatesourceschart');
if (!ctx) {
console.warn('Candidates sources chart element not found.');
return;
}
const chartCtx = ctx.getContext('2d');
// Safely get job_category_data from Django context
// Using window.jobChartData to avoid template parsing issues
if (categories.length > 0) { // Only render if there's data
const chart = new Chart(chartCtx, {
type: 'doughnut',
data: {
labels: categories,
datasets: [
{
label: 'Number of Candidates',
data: candidates_count_in_each_source,
backgroundColor: [
'rgba(0, 99, 110, 0.7)', // --kaauh-teal
'rgba(23, 162, 184, 0.7)', // Teal shade
'rgba(0, 150, 136, 0.7)', // Teal green
'rgba(0, 188, 212, 0.7)', // Cyan
'rgba(38, 166, 154, 0.7)', // Turquoise
'rgba(77, 182, 172, 0.7)', // Medium teal
// Add more colors if you expect more categories
],
borderColor: [
'rgba(0, 99, 110, 1)',
'rgba(23, 162, 184, 1)',
'rgba(0, 150, 136, 1)',
'rgba(0, 188, 212, 1)',
'rgba(38, 166, 154, 1)',
'rgba(77, 182, 172, 1)',
// Add more colors if you expect more categories
],
borderWidth: 1,
}
]
},
options: {
responsive: true,
maintainAspectRatio: false, // Important for fixed height container
plugins: {
legend: {
position: 'right', // Position legend for doughnut chart
},
title: {
display: false, // Chart title is handled by the card header
},
tooltip: {
callbacks: {
label: function(context) {
let label = context.label || '';
if (label) {
label += ': ';
}
label += context.parsed + ' candidate(s)';
return label;
}
}
}
}
}
});
} else {
// Display a message if no data is available
chartCtx.canvas.parentNode.innerHTML = '<p class="text-center text-muted mt-4">No candidate category data available for this job.</p>';
}
});
</script>
{% endblock %}

View File

@ -10,7 +10,7 @@
<h3><i class="fas fa-list stat-icon"></i> {% trans "Total Jobs" %}</h3>
</div>
<div class="stat-value">{{ total_jobs_global }}</div>
<div class="stat-caption">{% trans "All Active & Drafted Positions (Global)" %}</div>
<div class="stat-caption">{% trans "All Active & Drafted Positions" %}</div>
</div>
{# SCOPED - 2. Total Active Jobs #}
@ -19,7 +19,7 @@
<h3><i class="fas fa-briefcase stat-icon"></i> {% trans "Active Jobs" %}</h3>
</div>
<div class="stat-value">{{ total_active_jobs }}</div>
<div class="stat-caption">{% trans "Currently Open Requisitions (Scoped)" %}</div>
<div class="stat-caption">{% trans "Currently Open Requisitions" %}</div>
</div>
{# SCOPED - 3. Total Candidates #}
@ -28,7 +28,7 @@
<h3><i class="fas fa-users stat-icon"></i> {% trans "Total Candidates" %}</h3>
</div>
<div class="stat-value">{{ total_candidates }}</div>
<div class="stat-caption">{% trans "Total Profiles in Current Scope" %}</div>
<div class="stat-caption">{% trans "Total applications" %}</div>
</div>
{# SCOPED - 4. Open Positions #}
@ -37,7 +37,7 @@
<h3><i class="fas fa-th-list stat-icon"></i> {% trans "Open Positions" %}</h3>
</div>
<div class="stat-value">{{ total_open_positions }}</div>
<div class="stat-caption">{% trans "Total Slots to be Filled (Scoped)" %}</div>
<div class="stat-caption">{% trans "Total Slots to be Filled " %}</div>
</div>
{# GLOBAL - 5. Total Participants #}
@ -46,11 +46,11 @@
<h3><i class="fas fa-address-book stat-icon"></i> {% trans "Total Participants" %}</h3>
</div>
<div class="stat-value">{{ total_participants }}</div>
<div class="stat-caption">{% trans "Total Recruiters/Interviewers (Global)" %}</div>
<div class="stat-caption">{% trans "Total Recruiters/Interviewers" %}</div>
</div>
{# GLOBAL - 6. Total LinkedIn Posts #}
<div class="card">
{% comment %} <div class="card">
<div class="card-header">
<h3><i class="fab fa-linkedin stat-icon"></i> {% trans "LinkedIn Posts" %}</h3>
</div>
@ -65,13 +65,13 @@
<div class="stat-value">{{ new_candidates_7days }}</div>
<div class="stat-caption">{% trans "Incoming applications last week" %}</div>
</div>
{% endcomment %}
<div class="card">
<div class="card-header">
<h3><i class="fas fa-cogs stat-icon"></i> {% trans "Avg. Apps per Job" %}</h3>
</div>
<div class="stat-value">{{ average_applications|floatformat:1 }}</div>
<div class="stat-caption">{% trans "Average Applications per Job (Scoped)" %}</div>
<div class="stat-caption">{% trans "Average Applications per Job" %}</div>
</div>
{# --- Efficiency & Quality Metrics --- #}
@ -81,7 +81,7 @@
<h3><i class="fas fa-clock stat-icon"></i> {% trans "Time-to-Hire" %}</h3>
</div>
<div class="stat-value">{{ avg_time_to_hire_days }}</div>
<div class="stat-caption">{% trans "Avg. Days (Application to Hired)" %}</div>
<div class="stat-caption">{% trans "Average Days" %}</div>
</div>
<div class="card">
@ -89,7 +89,7 @@
<h3><i class="fas fa-star stat-icon"></i> {% trans "Avg. Match Score" %}</h3>
</div>
<div class="stat-value">{{ avg_match_score|floatformat:1 }}</div>
<div class="stat-caption">{% trans "Average AI Score (Current Scope)" %}</div>
<div class="stat-caption">{% trans "Average AI Score " %}</div>
</div>
<div class="card">
@ -100,13 +100,13 @@
<div class="stat-caption">{% trans "Score ≥ 75% Profiles" %} ({{ high_potential_ratio|floatformat:1 }}%)</div>
</div>
<div class="card">
{% comment %} <div class="card">
<div class="card-header">
<h3><i class="fas fa-calendar-alt stat-icon"></i> {% trans "Meetings This Week" %}</h3>
</div>
<div class="stat-value">{{ meetings_scheduled_this_week }}</div>
<div class="stat-caption">{% trans "Scheduled Interviews (Current Week)" %}</div>
</div>
</div> {% endcomment %}
</div>

View File

@ -146,7 +146,7 @@
<div class="row px-lg-4">
<div class="col-12">
<h1 class="h3 fw-bold dashboard-header">
<i class="fas fa-cogs me-3 text-accent"></i>{% trans "Admin Settings Dashboard" %}
<i class="fas fa-cogs me-3 text-accent"></i>{% trans "Staff Management Dashboard" %}
</h1>
</div>
</div>
@ -165,6 +165,7 @@
</div>
<div class="col-12 table-card">
<div class="table-responsive">
<table class="table table-striped table-hover align-middle mb-0">
@ -180,6 +181,7 @@
</tr>
</thead>
<tbody>
{% for user in staffs %}
<tr>
<td>{{ user.pk }}</td>