Merge branch 'main' of http://10.10.1.136:3000/marwan/kaauh_ats into frontend

This commit is contained in:
Faheed 2025-12-21 18:30:15 +03:00
commit 667b4c45f2
19 changed files with 399 additions and 291 deletions

6
.env
View File

@ -1,3 +1,3 @@
DB_NAME=haikal_db DB_NAME=norahuniversity
DB_USER=faheed DB_USER=norahuniversity
DB_PASSWORD=Faheed@215 DB_PASSWORD=norahuniversity

View File

@ -214,16 +214,16 @@ ACCOUNT_FORMS = {"signup": "recruitment.forms.StaffSignupForm"}
# EMAIL_PORT = 2225 # EMAIL_PORT = 2225
# EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" # EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
# EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PASSWORD", "mssp.0Q0rSwb.zr6ke4n2k3e4on12.aHwJqnI") EMAIL_HOST_PASSWORD = os.getenv("EMAIL_PASSWORD", "mssp.0Q0rSwb.zr6ke4n2k3e4on12.aHwJqnI")
# EMAIL_HOST = "smtp.mailersend.net" EMAIL_HOST = "smtp.mailersend.net"
# EMAIL_PORT = 2525 EMAIL_PORT = 2525
# EMAIL_HOST_USER = "MS_lhygCJ@test-65qngkd8nx3lwr12.mlsender.net" EMAIL_HOST_USER = "MS_lhygCJ@test-65qngkd8nx3lwr12.mlsender.net"
# EMAIL_HOST_PASSWORD = "mssp.0Q0rSwb.zr6ke4n2k3e4on12.aHwJqnI" EMAIL_HOST_PASSWORD = "mssp.0Q0rSwb.zr6ke4n2k3e4on12.aHwJqnI"
# EMAIL_USE_TLS = True EMAIL_USE_TLS = True
# EMAIL_HOST = 'sandbox.smtp.mailtrap.io' EMAIL_HOST = 'sandbox.smtp.mailtrap.io'
# EMAIL_HOST_USER = '38e5179debe69a' EMAIL_HOST_USER = '38e5179debe69a'
# EMAIL_HOST_PASSWORD = 'ffa75647d01ecb' EMAIL_HOST_PASSWORD = 'ffa75647d01ecb'
# EMAIL_PORT = '2525' EMAIL_PORT = '2525'

View File

@ -377,6 +377,13 @@ class ApplicationForm(forms.ModelForm):
Submit("submit", _("Submit"), css_class="btn btn-primary"), Submit("submit", _("Submit"), css_class="btn btn-primary"),
) )
def clean_job(self):
job = self.cleaned_data.get("job")
if job.max_applications <= Application.objects.filter(job=job).count():
raise forms.ValidationError(
"The maximum number of applicants for this job has been reached."
)
return job
# def clean(self): # def clean(self):
# cleaned_data = super().clean() # cleaned_data = super().clean()
# job = cleaned_data.get("job") # job = cleaned_data.get("job")
@ -720,7 +727,7 @@ class BulkInterviewTemplateForm(forms.ModelForm):
if end_date and start_date and end_date < start_date: if end_date and start_date and end_date < start_date:
raise forms.ValidationError(_("End date must be after start date")) raise forms.ValidationError(_("End date must be after start date"))
return end_date return end_date
def clean_end_time(self): def clean_end_time(self):
start_time = self.cleaned_data.get("start_time") start_time = self.cleaned_data.get("start_time")
end_time = self.cleaned_data.get("end_time") end_time = self.cleaned_data.get("end_time")
@ -1465,7 +1472,7 @@ class CandidateEmailForm(forms.Form):
f"You will receive a separate email shortly with details regarding your start date, first-day instructions, and onboarding documents.", f"You will receive a separate email shortly with details regarding your start date, first-day instructions, and onboarding documents.",
f"We look forward to seeing you at KAAUH.", f"We look forward to seeing you at KAAUH.",
f"If you have any questions before your start date, please contact [Onboarding Contact].", f"If you have any questions before your start date, please contact [Onboarding Contact].",
] ]
elif candidate: elif candidate:
message_parts="" message_parts=""
@ -1639,7 +1646,7 @@ class MessageForm(forms.ModelForm):
# Validate messaging permissions # Validate messaging permissions
if self.user and cleaned_data.get("recipient"): if self.user and cleaned_data.get("recipient"):
self._validate_messaging_permissions(cleaned_data) self._validate_messaging_permissions(cleaned_data)
if self.cleaned_data.get('recipient')==self.user: if self.cleaned_data.get('recipient')==self.user:
raise forms.ValidationError(_("You cannot message yourself")) raise forms.ValidationError(_("You cannot message yourself"))
@ -1805,6 +1812,27 @@ class PasswordResetForm(forms.Form):
raise forms.ValidationError(_('New passwords do not match.')) raise forms.ValidationError(_('New passwords do not match.'))
return cleaned_data return cleaned_data
class PersonPasswordResetForm(forms.Form):
new_password1 = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
label=_('New Password')
)
new_password2 = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
label=_('Confirm New Password')
)
def clean(self):
"""Custom validation for password reset"""
cleaned_data = super().clean()
new_password1 = cleaned_data.get('new_password1')
new_password2 = cleaned_data.get('new_password2')
if new_password1 and new_password2:
if new_password1 != new_password2:
raise forms.ValidationError(_('New passwords do not match.'))
return cleaned_data
class StaffAssignmentForm(forms.ModelForm): class StaffAssignmentForm(forms.ModelForm):
@ -2225,7 +2253,7 @@ Location: {interview.physical_address}
Room No: {interview.room_number} Room No: {interview.room_number}
This is an onsite schedule. Please arrive 10 minutes early.\n\n""" This is an onsite schedule. Please arrive 10 minutes early.\n\n"""
self.fields['message'].initial = initial_message self.fields['message'].initial = initial_message

View File

@ -28,8 +28,9 @@ class EmailService:
try: try:
# Using EmailMessage for more control (e.g., HTML content) # Using EmailMessage for more control (e.g., HTML content)
from time import sleep
for recipient in recipient_list: for recipient in recipient_list:
sleep(2)
email = EmailMessage( email = EmailMessage(
subject=subject, subject=subject,
body=body, body=body,
@ -46,8 +47,6 @@ class EmailService:
recipient_user=User.objects.filter(email=recipient).first() recipient_user=User.objects.filter(email=recipient).first()
if result and recipient_user and not context["message_created"]: if result and recipient_user and not context["message_created"]:
Message.objects.create(sender=context['sender_user'],recipient=recipient_user,job=context['job'],subject=subject,content=context['email_message'],message_type='DIRECT',is_read=False) Message.objects.create(sender=context['sender_user'],recipient=recipient_user,job=context['job'],subject=subject,content=context['email_message'],message_type='DIRECT',is_read=False)
return len(recipient_list) return len(recipient_list)
except Exception as e: except Exception as e:
@ -111,7 +110,7 @@ class EmailService:
context=context, context=context,
from_email=from_email, from_email=from_email,
html_content=html_content, html_content=html_content,
) )
# Return the count of recipients if successful, or 0 if failure # Return the count of recipients if successful, or 0 if failure

View File

@ -17,6 +17,7 @@ urlpatterns = [
# Job CRUD Operations # Job CRUD Operations
path("jobs/", views.JobListView.as_view(), name="job_list"), path("jobs/", views.JobListView.as_view(), name="job_list"),
path("jobs/create/", views.create_job, name="job_create"), path("jobs/create/", views.create_job, name="job_create"),
path("jobs/bank/", views.job_bank_view, name="job_bank"),
path("jobs/<slug:slug>/", views.job_detail, name="job_detail"), path("jobs/<slug:slug>/", views.job_detail, name="job_detail"),
path("jobs/<slug:slug>/update/", views.edit_job, name="job_update"), path("jobs/<slug:slug>/update/", views.edit_job, name="job_update"),
path("jobs/<slug:slug>/upload-image/", views.job_image_upload, name="job_image_upload"), path("jobs/<slug:slug>/upload-image/", views.job_image_upload, name="job_image_upload"),
@ -25,7 +26,6 @@ urlpatterns = [
path("jobs/<slug:slug>/applicants/", views.job_applicants_view, name="job_applicants"), path("jobs/<slug:slug>/applicants/", views.job_applicants_view, name="job_applicants"),
path("jobs/<slug:slug>/applications/", views.JobApplicationListView.as_view(), name="job_applications_list"), path("jobs/<slug:slug>/applications/", views.JobApplicationListView.as_view(), name="job_applications_list"),
path("jobs/<slug:slug>/calendar/", views.interview_calendar_view, name="interview_calendar"), path("jobs/<slug:slug>/calendar/", views.interview_calendar_view, name="interview_calendar"),
path("jobs/bank/", views.job_bank_view, name="job_bank"),
# Job Actions & Integrations # Job Actions & Integrations
path("jobs/<slug:slug>/post-to-linkedin/", views.post_to_linkedin, name="post_to_linkedin"), path("jobs/<slug:slug>/post-to-linkedin/", views.post_to_linkedin, name="post_to_linkedin"),
@ -103,6 +103,7 @@ urlpatterns = [
path("persons/<slug:slug>/", views.PersonDetailView.as_view(), name="person_detail"), path("persons/<slug:slug>/", views.PersonDetailView.as_view(), name="person_detail"),
path("persons/<slug:slug>/update/", views.PersonUpdateView.as_view(), name="person_update"), path("persons/<slug:slug>/update/", views.PersonUpdateView.as_view(), name="person_update"),
path("persons/<slug:slug>/delete/", views.PersonDeleteView.as_view(), name="person_delete"), path("persons/<slug:slug>/delete/", views.PersonDeleteView.as_view(), name="person_delete"),
path("persons/<slug:slug>/password_reset/", views.password_reset, name="password_reset"),
# ======================================================================== # ========================================================================
# FORM & TEMPLATE MANAGEMENT # FORM & TEMPLATE MANAGEMENT

View File

@ -280,6 +280,11 @@ class PersonDetailView(DetailView, LoginRequiredMixin, StaffRequiredMixin):
template_name = "people/person_detail.html" template_name = "people/person_detail.html"
context_object_name = "person" context_object_name = "person"
def get_context_data(self, **kwargs):
from .forms import PersonPasswordResetForm
context = super().get_context_data(**kwargs)
context['password_form'] = PersonPasswordResetForm()
return context
class PersonUpdateView(UpdateView, LoginRequiredMixin, StaffOrAgencyRequiredMixin): class PersonUpdateView(UpdateView, LoginRequiredMixin, StaffOrAgencyRequiredMixin):
model = Person model = Person
@ -1302,11 +1307,10 @@ def delete_form_template(request, template_id):
# @staff_or_candidate_required # @staff_or_candidate_required
def application_submit_form(request, slug): def application_submit_form(request, slug):
"""Display the form as a step-by-step wizard""" """Display the form as a step-by-step wizard"""
form_template = get_object_or_404(FormTemplate, slug=slug, is_active=True) job = get_object_or_404(JobPosting, slug=slug)
if not request.user.is_authenticated: if not request.user.is_authenticated:
return redirect("application_signup", slug=slug) return redirect("application_signup", slug=slug)
print(form_template.job.slug)
job = get_object_or_404(JobPosting, slug=form_template.job.slug)
if request.user.user_type == "candidate": if request.user.user_type == "candidate":
person = request.user.person_profile person = request.user.person_profile
if job.has_already_applied_to_this_job(person): if job.has_already_applied_to_this_job(person):
@ -1316,23 +1320,16 @@ def application_submit_form(request, slug):
"You have already applied to this job: Multiple applications are not allowed." "You have already applied to this job: Multiple applications are not allowed."
), ),
) )
return redirect("job_application_detail", slug=job.slug) return redirect("job_application_detail", slug=slug)
# template = get_object_or_404(FormTemplate, slug=slug, is_active=True) if job.is_application_limit_reached:
template = job.form_template
stage = template.stages.filter(name="Contact Information")
job_id = template.job.internal_job_id
job = template.job
is_limit_exceeded = job.is_application_limit_reached
if is_limit_exceeded:
messages.error( messages.error(
request, request,
_( _(
"Application limit reached: This job is no longer accepting new applications." "Application limit reached: This job is no longer accepting new applications."
), ),
) )
return redirect("application_detail", slug=job.slug) return redirect("job_application_detail", slug=slug)
if job.is_expired: if job.is_expired:
messages.error( messages.error(
request, request,
@ -1340,12 +1337,12 @@ def application_submit_form(request, slug):
"Application deadline passed: This job is no longer accepting new applications." "Application deadline passed: This job is no longer accepting new applications."
), ),
) )
return redirect("application_detail", slug=job.slug) return redirect("job_application_detail", slug=slug)
return render( return render(
request, request,
"applicant/application_submit_form.html", "applicant/application_submit_form.html",
{"template_slug": template.slug, "job_id": job_id}, {"template_slug": job.form_template.slug, "job_id": job.internal_job_id},
) )
@ -1357,8 +1354,9 @@ def application_submit(request, template_slug):
import re import re
"""Handle form submission""" """Handle form submission"""
if not request.user.is_authenticated: # or request.user.user_type != "candidate": if not request.user.is_authenticated or request.user.user_type != "candidate":
return JsonResponse({"success": False, "message": "Unauthorized access."}) return JsonResponse({"success": False, "message": "Unauthorized access."})
template = get_object_or_404(FormTemplate, slug=template_slug) template = get_object_or_404(FormTemplate, slug=template_slug)
job = template.job job = template.job
if request.method == "POST": if request.method == "POST":
@ -1414,38 +1412,38 @@ def application_submit(request, template_slug):
except FormField.DoesNotExist: except FormField.DoesNotExist:
continue continue
try: try:
gpa = submission.responses.get(field__label="GPA") # gpa = submission.responses.get(field__label="GPA")
if gpa and gpa.value: # if gpa and gpa.value:
gpa_str = gpa.value.replace("/", "").strip() # gpa_str = gpa.value.replace("/", "").strip()
if not re.match(r"^\d+(\.\d+)?$", gpa_str): # if not re.match(r"^\d+(\.\d+)?$", gpa_str):
# --- FIX APPLIED HERE --- # # --- FIX APPLIED HERE ---
return JsonResponse( # return JsonResponse(
{ # {
"success": False, # "success": False,
"message": _("GPA must be a numeric value."), # "message": _("GPA must be a numeric value."),
} # }
) # )
try: # try:
gpa_float = float(gpa_str) # gpa_float = float(gpa_str)
except ValueError: # except ValueError:
# --- FIX APPLIED HERE --- # # --- FIX APPLIED HERE ---
return JsonResponse( # return JsonResponse(
{ # {
"success": False, # "success": False,
"message": _("GPA must be a numeric value."), # "message": _("GPA must be a numeric value."),
} # }
) # )
if not (0.0 <= gpa_float <= 4.0): # if not (0.0 <= gpa_float <= 4.0):
# --- FIX APPLIED HERE --- # # --- FIX APPLIED HERE ---
return JsonResponse( # return JsonResponse(
{ # {
"success": False, # "success": False,
"message": _("GPA must be between 0.0 and 4.0."), # "message": _("GPA must be between 0.0 and 4.0."),
} # }
) # )
resume = submission.responses.get(field__label="Resume Upload") resume = submission.responses.get(field__label="Resume Upload")
@ -1456,7 +1454,7 @@ def application_submit(request, template_slug):
submission.save() submission.save()
# time=timezone.now() # time=timezone.now()
person = request.user.person_profile person = request.user.person_profile
person.gpa = gpa.value if gpa else None # person.gpa = gpa.value if gpa else None
person.save() person.save()
Application.objects.create( Application.objects.create(
person=person, person=person,
@ -2257,60 +2255,60 @@ def reschedule_meeting_for_application(request, slug):
def interview_calendar_view(request, slug): def interview_calendar_view(request, slug):
job = get_object_or_404(JobPosting, slug=slug) job = get_object_or_404(JobPosting, slug=slug)
# # Get all scheduled interviews for this job # Get all scheduled interviews for this job
# scheduled_interviews = ScheduledInterview.objects.filter(job=job).select_related( scheduled_interviews = ScheduledInterview.objects.filter(job=job).select_related(
# "applicaton", "zoom_meeting" "interview","application"
# ) )
# # Convert interviews to calendar events # Convert interviews to calendar events
# events = [] events = []
# for interview in scheduled_interviews: for interview in scheduled_interviews:
# # Create start datetime # Create start datetime
# start_datetime = datetime.combine( start_datetime = datetime.combine(
# interview.interview_date, interview.interview_time interview.interview_date, interview.interview_time
# ) )
# # Calculate end datetime based on interview duration # Calculate end datetime based on interview duration
# duration = interview.zoom_meeting.duration if interview.zoom_meeting else 60 duration = interview.interview.duration if interview.interview else 60
# end_datetime = start_datetime + timedelta(minutes=duration) end_datetime = start_datetime + timedelta(minutes=duration)
# # Determine event color based on status # Determine event color based on status
# color = "#00636e" # Default color color = "#00636e" # Default color
# if interview.status == "confirmed": if interview.status == "confirmed":
# color = "#00a86b" # Green for confirmed color = "#00a86b" # Green for confirmed
# elif interview.status == "cancelled": elif interview.status == "cancelled":
# color = "#e74c3c" # Red for cancelled color = "#e74c3c" # Red for cancelled
# elif interview.status == "completed": elif interview.status == "completed":
# color = "#95a5a6" # Gray for completed color = "#95a5a6" # Gray for completed
# events.append( events.append(
# { {
# "title": f"Interview: {interview.candidate.name}", "title": f"Interview: {interview.application.person.full_name}",
# "start": start_datetime.isoformat(), "start": start_datetime.isoformat(),
# "end": end_datetime.isoformat(), "end": end_datetime.isoformat(),
# "url": f"{request.path}interview/{interview.id}/", "url": f"{request.path}interview/{interview.id}/",
# "color": color, "color": color,
# "extendedProps": { "extendedProps": {
# "candidate": interview.candidate.name, "candidate": interview.application.person.full_name,
# "email": interview.candidate.email, "email": interview.application.person.email,
# "status": interview.status, "status": interview.interview.status,
# "meeting_id": interview.zoom_meeting.meeting_id "meeting_id": interview.interview.meeting_id
# if interview.zoom_meeting if interview.interview
# else None, else None,
# "join_url": interview.zoom_meeting.join_url "join_url": interview.interview.join_url
# if interview.zoom_meeting if interview.interview
# else None, else None,
# }, },
# } }
# ) )
# context = { context = {
# "job": job, "job": job,
# "events": events, "events": events,
# "calendar_color": "#00636e", "calendar_color": "#00636e",
# } }
# return render(request, "recruitment/interview_calendar.html", context) return render(request, "recruitment/interview_calendar.html", context)
def user_profile_image_update(request, pk): def user_profile_image_update(request, pk):
@ -2990,6 +2988,22 @@ def portal_password_reset(request, pk):
for error in errors: for error in errors:
messages.error(request, f"{field}: {error}") messages.error(request, f"{field}: {error}")
@require_POST
def password_reset(request, slug):
from .forms import PersonPasswordResetForm
person = get_object_or_404(Person, slug=slug)
if request.method == "POST":
form = PersonPasswordResetForm(request.POST)
if form.is_valid():
person.user.set_password(form.cleaned_data["new_password1"])
person.user.save()
messages.success(request, "Password reset successfully.")
return redirect("person_detail", slug=person.slug)
else:
for field, errors in form.errors.items():
for error in errors:
messages.error(request, f"{field}: {error}")
def portal_login(request): def portal_login(request):
"""Unified portal login for agency and applicant""" """Unified portal login for agency and applicant"""
@ -4506,7 +4520,7 @@ def source_list(request):
"""List all sources with search and pagination""" """List all sources with search and pagination"""
search_query = request.GET.get("q", "") search_query = request.GET.get("q", "")
sources = Source.objects.all() sources = Source.objects.all()
if search_query: if search_query:
sources = sources.filter( sources = sources.filter(
Q(name__icontains=search_query) Q(name__icontains=search_query)

View File

@ -877,33 +877,39 @@
} }
function renderCurrentStage() { function renderCurrentStage() {
if (state.isPreview) { // Always show stage container and hide preview container initially
renderPreview(); elements.stageContainer.style.display = 'block';
return; elements.previewContainer.style.display = 'none';
}
const currentStage = state.stages[state.currentStage]; if (state.isPreview) {
elements.stageContainer.innerHTML = ''; renderPreview();
elements.previewContainer.style.display = 'none'; return;
}
const stageTitle = document.createElement('h2'); const currentStage = state.stages[state.currentStage];
stageTitle.className = 'stage-title'; elements.stageContainer.innerHTML = '';
stageTitle.textContent = currentStage.name;
elements.stageContainer.appendChild(stageTitle);
currentStage.fields.forEach(field => { const stageTitle = document.createElement('h2');
const fieldElement = createFieldElement(field); stageTitle.className = 'stage-title';
elements.stageContainer.appendChild(fieldElement); stageTitle.textContent = currentStage.name;
}); elements.stageContainer.appendChild(stageTitle);
currentStage.fields.forEach(field => {
const fieldElement = createFieldElement(field);
elements.stageContainer.appendChild(fieldElement);
});
// Update navigation buttons
elements.backBtn.style.display = state.currentStage > 0 ? 'flex' : 'none';
elements.submitBtn.style.display = 'none';
elements.nextBtn.style.display = 'flex';
// Fix: Update the Next button text correctly
elements.nextBtn.innerHTML = state.currentStage === state.stages.length - 1 ?
'Preview <i class="fas fa-arrow-right"></i>' :
'Next <i class="fas fa-arrow-right"></i>';
}
// Update navigation buttons
elements.backBtn.style.display = state.currentStage > 0 ? 'flex' : 'none';
elements.submitBtn.style.display = 'none';
elements.nextBtn.style.display = 'flex';
elements.nextBtn.textContent = state.currentStage === state.stages.length - 1 ?
'Preview' :
'Next'
}
function createFieldElement(field) { function createFieldElement(field) {
const fieldDiv = document.createElement('div'); const fieldDiv = document.createElement('div');
@ -1158,103 +1164,106 @@
} }
function renderPreview() { function renderPreview() {
elements.stageContainer.style.display = 'none'; elements.stageContainer.style.display = 'none';
elements.previewContainer.style.display = 'block'; elements.previewContainer.style.display = 'block';
elements.previewContent.innerHTML = ''; elements.previewContent.innerHTML = '';
// Add applicant info if available // Add applicant info if available
if (state.formData.applicant_name || state.formData.applicant_email) { if (state.formData.applicant_name || state.formData.applicant_email) {
const applicantDiv = document.createElement('div'); const applicantDiv = document.createElement('div');
applicantDiv.className = 'preview-item'; applicantDiv.className = 'preview-item';
applicantDiv.innerHTML = ` applicantDiv.innerHTML = `
<div class="preview-label">Applicant Information</div> <div class="preview-label">Applicant Information</div>
<div class="preview-value"> <div class="preview-value">
${state.formData.applicant_name ? `<strong>Name:</strong> ${state.formData.applicant_name}<br>` : ''} ${state.formData.applicant_name ? `<strong>Name:</strong> ${state.formData.applicant_name}<br>` : ''}
${state.formData.applicant_email ? `<strong>Email:</strong> ${state.formData.applicant_email}` : ''} ${state.formData.applicant_email ? `<strong>Email:</strong> ${state.formData.applicant_email}` : ''}
</div> </div>
`; `;
elements.previewContent.appendChild(applicantDiv); elements.previewContent.appendChild(applicantDiv);
} }
// Add stage data // Add stage data
state.stages.forEach(stage => { state.stages.forEach(stage => {
const stageDiv = document.createElement('div'); const stageDiv = document.createElement('div');
stageDiv.className = 'preview-item'; stageDiv.className = 'preview-item';
const stageTitle = document.createElement('div'); const stageTitle = document.createElement('div');
stageTitle.className = 'preview-label'; stageTitle.className = 'preview-label';
stageTitle.textContent = stage.name; stageTitle.textContent = stage.name;
stageDiv.appendChild(stageTitle); stageDiv.appendChild(stageTitle);
const stageContent = document.createElement('div'); const stageContent = document.createElement('div');
stageContent.className = 'preview-value'; stageContent.className = 'preview-value';
stage.fields.forEach(field => { stage.fields.forEach(field => {
let value = state.formData[field.id]; let value = state.formData[field.id];
if (value === undefined || value === null || value === '') { if (value === undefined || value === null || value === '') {
value = '<em>Not provided</em>'; value = '<em>Not provided</em>';
} else if (field.type === 'file' && value instanceof File) { } else if (field.type === 'file' && value instanceof File) {
value = value.name; value = value.name;
} else if (field.type === 'checkbox' && Array.isArray(value)) { } else if (field.type === 'checkbox' && Array.isArray(value)) {
value = value.join(', '); value = value.join(', ');
} }
const fieldDiv = document.createElement('div'); const fieldDiv = document.createElement('div');
fieldDiv.innerHTML = `<strong>${field.label}:</strong> ${value}`; fieldDiv.innerHTML = `<strong>${field.label}:</strong> ${value}`;
stageContent.appendChild(fieldDiv); stageContent.appendChild(fieldDiv);
}); });
stageDiv.appendChild(stageContent); stageDiv.appendChild(stageContent);
elements.previewContent.appendChild(stageDiv); elements.previewContent.appendChild(stageDiv);
}); });
// Update navigation buttons // Update navigation buttons
elements.backBtn.style.display = 'flex'; elements.backBtn.style.display = 'flex';
elements.nextBtn.style.display = 'none'; elements.nextBtn.style.display = 'none';
elements.submitBtn.style.display = 'flex'; elements.submitBtn.style.display = 'flex';
} }
// Navigation Functions // Navigation Functions
function nextStage() { function nextStage() {
if (state.isPreview) { if (state.isPreview) {
submitForm(); submitForm();
return; return;
} }
if (!validateCurrentStage()) { if (!validateCurrentStage()) {
// Scroll to first error // Scroll to first error
const firstError = document.querySelector('.error-message.show'); const firstError = document.querySelector('.error-message.show');
if (firstError) { if (firstError) {
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' }); firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
} }
return; return;
} }
if (state.currentStage === state.stages.length - 1) { if (state.currentStage === state.stages.length - 1) {
// Go to preview // Go to preview
state.isPreview = true; state.isPreview = true;
renderCurrentStage(); renderCurrentStage();
updateProgress(); updateProgress();
} else { } else {
// Go to next stage // Go to next stage
state.currentStage++; state.currentStage++;
renderCurrentStage(); renderCurrentStage();
updateProgress(); updateProgress();
} }
} }
function prevStage() { function prevStage() {
if (state.isPreview) { if (state.isPreview) {
// Go back to last stage // Go back to last stage from preview
state.isPreview = false; state.isPreview = false;
renderCurrentStage(); // Set to the last form stage
updateProgress(); state.currentStage = state.stages.length - 1;
} else if (state.currentStage > 0) { renderCurrentStage();
state.currentStage--; updateProgress();
renderCurrentStage(); } else if (state.currentStage > 0) {
updateProgress(); // Go to previous stage
} state.currentStage--;
} renderCurrentStage();
updateProgress();
}
}
// Initialize Application // Initialize Application
function init() { function init() {

View File

@ -41,7 +41,7 @@
<i class="fas fa-paper-plane me-2"></i> {% trans "You already applied for this position" %} <i class="fas fa-paper-plane me-2"></i> {% trans "You already applied for this position" %}
</button> </button>
{% else %} {% else %}
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100"> <a href="{% url 'application_submit_form' job.slug %}" class="btn btn-main-action btn-lg w-100">
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %} <i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
</a> </a>
{% endif %} {% endif %}
@ -220,12 +220,10 @@
<i class="fas fa-paper-plane me-2"></i> {% trans "You already applied for this position" %} <i class="fas fa-paper-plane me-2"></i> {% trans "You already applied for this position" %}
</button> </button>
{% else %} {% else %}
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-main-action btn-lg w-100"> <a href="{% url 'application_submit_form' job.slug %}" class="btn btn-main-action btn-lg w-100">
<i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %} <i class="fas fa-paper-plane me-2"></i> {% trans "Apply for this Position" %}
</a> </a>
{% endif %} {% endif %}
</footer> </footer>
{% endif %} {% endif %}
{% endblock content%} {% endblock content%}

View File

@ -234,11 +234,7 @@
</a> </a>
</li> </li>
{% endif %} {% endif %}
<li><hr class="dropdown-divider my-1"></li> <li><hr class="dropdown-divider my-1"></li>
<li> <li>
<form method="post" action="{% url 'account_logout'%}" class="d-inline"> <form method="post" action="{% url 'account_logout'%}" class="d-inline">
{% csrf_token %} {% csrf_token %}

View File

@ -231,7 +231,7 @@
<div class="mt-auto pt-2 border-top"> <div class="mt-auto pt-2 border-top">
<div class="d-flex gap-2 justify-content-end"> <div class="d-flex gap-2 justify-content-end">
<a href="{% url 'application_submit_form' template.slug %}" class="btn btn-outline-primary btn-sm" title="{% trans 'Preview' %}"> <a href="{% url 'application_submit_form' template.job.slug %}" class="btn btn-outline-primary btn-sm" title="{% trans 'Preview' %}">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
<a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'Edit' %}"> <a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary btn-sm" title="{% trans 'Edit' %}">
@ -286,7 +286,7 @@
<td>{{ template.updated_at|date:"M d, Y" }}</td> <td>{{ template.updated_at|date:"M d, Y" }}</td>
<td class="text-end"> <td class="text-end">
<div class="btn-group btn-group-sm" role="group"> <div class="btn-group btn-group-sm" role="group">
<a href="{% url 'application_submit_form' template.slug %}" class="btn btn-outline-primary" title="{% trans 'Preview' %}"> <a href="{% url 'application_submit_form' template.job.slug %}" class="btn btn-outline-primary" title="{% trans 'Preview' %}">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</a> </a>
<a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}"> <a href="{% url 'form_builder' template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">

View File

@ -147,6 +147,30 @@
{% block content %} {% block content %}
<div class="container-fluid py-4"> <div class="container-fluid py-4">
<!-- Modal for Staff Assignment -->
<div class="modal fade" id="staffAssignmentModal" tabindex="-1" aria-labelledby="staffAssignmentModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="staffAssignmentModalLabel">
<i class="fas fa-user-plus me-2"></i> {% trans "Assign Staff Member" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="{% url 'staff_assignment_view' job.slug %}">
{% csrf_token %}
{{staff_form|crispy}}
<div class="d-flex justify-content-end mt-3">
<button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {% trans "Save Assignment" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
<nav aria-label="breadcrumb"> <nav aria-label="breadcrumb">
<ol class="breadcrumb"> <ol class="breadcrumb">
@ -192,10 +216,11 @@
{% else %}bg-secondary{% endif %}'> {% else %}bg-secondary{% endif %}'>
{{ job.get_status_display }} {{ job.get_status_display }}
</span> </span>
{% if user.is_staff and user == application.job.assigned_to or user.is_superuser %}
<button type="button" class="btn btn-outline-light btn-sm ms-2" data-bs-toggle="modal" data-bs-target="#editStatusModal"> <button type="button" class="btn btn-outline-light btn-sm ms-2" data-bs-toggle="modal" data-bs-target="#editStatusModal">
<i class="fas fa-edit"></i> <i class="fas fa-edit"></i>
</button> </button>
{% endif %}
</div> </div>
{# Share Public Link Button #} {# Share Public Link Button #}
@ -223,7 +248,14 @@
{% endif %} {% endif %}
<div class="float-end"> <div class="float-end">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<i class="fas fa-user-tie me-2 text-primary"></i> <strong>{% trans "Assigned to :" %} </strong> {{ job.assigned_to|default:"" }} {% if job.assigned_to %}
<i class="fas fa-user-tie me-2 text-primary"></i>
<strong> {% trans "Assigned to :" %} </strong> {{ job.assigned_to|default:"" }}
{% else %}
<button type="button" class="btn btn-main-action btn-sm" data-bs-toggle="modal" data-bs-target="#staffAssignmentModal">
<i class="fas fa-user-plus me-1"></i> {% trans "Click To Assign" %}
</button>
{% endif %}
</div> </div>
</div> </div>
</h5> </h5>
@ -333,7 +365,10 @@
<a href="{% url 'applications_screening_view' job.slug %}" class="btn btn-main-action"> <a href="{% url 'applications_screening_view' job.slug %}" class="btn btn-main-action">
<i class="fas fa-layer-group me-1"></i> {% trans "Manage Applications" %} <i class="fas fa-layer-group me-1"></i> {% trans "Manage Applications" %}
</a> </a>
<a href="{% url 'interview_calendar' job.slug %}" class="btn btn-main-action">
<i class="fas fa-calendar me-1"></i> {% trans "View Calendar" %}
</a>
{% if not job.form_template.is_active %} {% if not job.form_template.is_active %}
{% if not jobzip_created %} {% if not jobzip_created %}
<a href="{% url 'request_cvs_download' job.slug %}" class="btn btn-main-action"> <a href="{% url 'request_cvs_download' job.slug %}" class="btn btn-main-action">
@ -369,10 +404,11 @@
<p class="text-muted small mb-3"> <p class="text-muted small mb-3">
{% trans "Manage the custom application forms associated with this job posting." %} {% trans "Manage the custom application forms associated with this job posting." %}
</p> </p>
{% if user.is_staff and user == application.job.assigned_to or user.is_superuser %}
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary w-100"> <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 Job Form" %} <i class="fas fa-list-alt me-1"></i> {% trans "Manage Job Form" %}
</a> </a>
{% endif %}
{% comment %} {% if not job.form_template %} {% comment %} {% if not job.form_template %}
<a href="{% url 'create_form_template' %}" class="btn btn-main-action"> <a href="{% url 'create_form_template' %}" class="btn btn-main-action">
<i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %} <i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %}
@ -412,31 +448,6 @@
</button> </button>
{% endif %} {% endif %}
<!-- Modal for Staff Assignment -->
<div class="modal fade" id="staffAssignmentModal" tabindex="-1" aria-labelledby="staffAssignmentModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="staffAssignmentModalLabel">
<i class="fas fa-user-plus me-2"></i> {% trans "Assign Staff Member" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="{% url 'staff_assignment_view' job.slug %}">
{% csrf_token %}
{{staff_form|crispy}}
<div class="d-flex justify-content-end mt-3">
<button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {% trans "Save Assignment" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% if not job.assigned_to %} {% if not job.assigned_to %}
<div class="alert alert-info p-2 small mb-0"> <div class="alert alert-info p-2 small mb-0">
<i class="fas fa-info-circle me-1"></i> {% trans "No staff members assigned to this job yet." %} <i class="fas fa-info-circle me-1"></i> {% trans "No staff members assigned to this job yet." %}

View File

@ -1,5 +1,5 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static i18n %} {% load static i18n crispy_forms_tags %}
{% block title %}{{ person.get_full_name }} - {{ block.super }}{% endblock %} {% block title %}{{ person.get_full_name }} - {{ block.super }}{% endblock %}
@ -536,6 +536,33 @@
</a> </a>
{% if user.is_staff %} {% if user.is_staff %}
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#setStaffPasswordModal">
<i class="fas fa-lock me-1"></i> {% trans "Set Staff Password" %}
</button>
<!-- Set Staff Password Modal -->
<div class="modal fade" id="setStaffPasswordModal" tabindex="-1" aria-labelledby="setStaffPasswordModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="setStaffPasswordModalLabel">
<i class="fas fa-lock me-2"></i> {% trans "Set Staff Password" %}
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form action="{% url 'password_reset' person.slug %}" method="post">
{% csrf_token %}
{{password_form|crispy}}
<div class="d-flex justify-content-end">
<button type="submit" class="btn btn-primary">{% trans "Save" %}</button>
</div>
</form>
</div>
</div>
</div>
</div>
<a href="{% url 'person_update' person.slug %}" class="btn btn-main-action"> <a href="{% url 'person_update' person.slug %}" class="btn btn-main-action">
<i class="fas fa-edit me-1"></i> {% trans "Edit Applicant" %} <i class="fas fa-edit me-1"></i> {% trans "Edit Applicant" %}
</a> </a>
@ -543,8 +570,6 @@
<i class="fas fa-trash-alt me-1"></i> {% trans "Delete" %} <i class="fas fa-trash-alt me-1"></i> {% trans "Delete" %}
</a> </a>
</div>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -231,7 +231,7 @@
<div class="card-footer text-center"> <div class="card-footer text-center">
<small class="text-muted"> <small class="text-muted">
{% trans "Already have an account?" %} {% trans "Already have an account?" %}
<a href="{% url 'account_login' %}?next={% url 'application_submit_form' job.form_template.slug %}" class="text-decoration-none text-kaauh-teal"> <a href="{% url 'account_login' %}?next={% url 'application_submit_form' job.slug %}" class="text-decoration-none text-kaauh-teal">
{% trans "Login here" %} {% trans "Login here" %}
</a> </a>
</small> </small>

View File

@ -298,7 +298,7 @@
</small> </small>
</div> </div>
{# Change Stage button #} {# Change Stage button #}
{% if user.is_staff %} {% if user.is_staff and user == application.job.assigned_to or user.is_superuser %}
<button type="button" class="btn btn-outline-light btn-sm mt-1" data-bs-toggle="modal" data-bs-target="#stageUpdateModal"> <button type="button" class="btn btn-outline-light btn-sm mt-1" data-bs-toggle="modal" data-bs-target="#stageUpdateModal">
<i class="fas fa-exchange-alt"></i> {% trans "Change Stage" %} <i class="fas fa-exchange-alt"></i> {% trans "Change Stage" %}
</button> </button>

View File

@ -402,7 +402,7 @@
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#noteModal" data-bs-target="#noteModal"
hx-get="{% url 'application_add_note' application.slug %}" hx-get="{% url 'application_add_note' application.slug %}"
hx-swap="outerHTML" hx-swap="innerHTML"
hx-target=".notemodal"> hx-target=".notemodal">
<i class="fas fa-calendar-plus me-1"></i> <i class="fas fa-calendar-plus me-1"></i>
Add note Add note

View File

@ -329,7 +329,7 @@
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#noteModal" data-bs-target="#noteModal"
hx-get="{% url 'application_add_note' application.slug %}" hx-get="{% url 'application_add_note' application.slug %}"
hx-swap="outerHTML" hx-swap="innerHTML"
hx-target=".notemodal"> hx-target=".notemodal">
<i class="fas fa-calendar-plus me-1"></i> <i class="fas fa-calendar-plus me-1"></i>
Add note Add note

View File

@ -358,7 +358,7 @@
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#noteModal" data-bs-target="#noteModal"
hx-get="{% url 'application_add_note' application.slug %}" hx-get="{% url 'application_add_note' application.slug %}"
hx-swap="outerHTML" hx-swap="innerHTML"
hx-target=".notemodal"> hx-target=".notemodal">
<i class="fas fa-calendar-plus me-1"></i> <i class="fas fa-calendar-plus me-1"></i>
Add note Add note

View File

@ -383,6 +383,9 @@
<th scope="col" style="width: 5%;"> <th scope="col" style="width: 5%;">
<i class="fas fa-graduation-cap me-1"></i> {% trans "GPA" %} <i class="fas fa-graduation-cap me-1"></i> {% trans "GPA" %}
</th> </th>
<th scope="col" style="width: 5%;">
<i class="fas fa-graduation-cap me-1"></i> {% trans "Years of Experience" %}
</th>
<th scope="col" style="width: 5%;" class="text-center"> <th scope="col" style="width: 5%;" class="text-center">
<i class="fas fa-robot me-1"></i> {% trans "AI Score" %} <i class="fas fa-robot me-1"></i> {% trans "AI Score" %}
</th> </th>
@ -426,6 +429,11 @@
</div> </div>
</td> </td>
<td class="text-center">{{application.person.gpa|default:"0"}}</td> <td class="text-center">{{application.person.gpa|default:"0"}}</td>
<td class="text-center">
<span class="badge ai-score-badge">
{{ application.years_of_experience }}
</span>
</td>
<td class="text-center"> <td class="text-center">
{% if application.is_resume_parsed %} {% if application.is_resume_parsed %}
{% if application.match_score %} {% if application.match_score %}
@ -473,7 +481,7 @@
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#noteModal" data-bs-target="#noteModal"
hx-get="{% url 'application_add_note' application.slug %}" hx-get="{% url 'application_add_note' application.slug %}"
hx-swap="outerHTML" hx-swap="innerHTML"
hx-target=".notemodal"> hx-target=".notemodal">
<i class="fas fa-calendar-plus me-1"></i> <i class="fas fa-calendar-plus me-1"></i>
Add note Add note

View File

@ -161,7 +161,7 @@
</a> </a>
</div> </div>
<div class="col-md-4 mb-4"> <div class="col-md-4 mb-4">
<a href="{% url "easy_logs" %}" class="text-decoration-none"> <a href="{% url 'easy_logs' %}" class="text-decoration-none">
<div class="kaauh-card shadow-sm p-4 h-100" style="border-left: 5px solid var(--kaauh-teal);"> <div class="kaauh-card shadow-sm p-4 h-100" style="border-left: 5px solid var(--kaauh-teal);">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<i class="fas fa-file-alt fa-3x text-primary-theme me-4"></i> <i class="fas fa-file-alt fa-3x text-primary-theme me-4"></i>
@ -199,25 +199,44 @@
</div> </div>
<div class="text-end mt-3"> <div class="text-end mt-3">
{% if not request.session.linkedin_authenticated %} {% if not request.session.linkedin_authenticated %}
<a class="text-decoration-none text-teal" href="{% url 'linkedin_login' %}">
<a class="text-decoration-none text-teal" href="{% url 'linkedin_login' %}"> <button class="btn btn-sm btn-outline-secondary">
{% trans "Sign to linkedin" %}<i class="fas fa-arrow-right ms-1"></i>
<button class="btn btn-sm btn-outline-secondary"> </button>
{% trans "Sign to linkedin" %}<i class="fas fa-arrow-right ms-1"></i> </a>
</button> {% else %}
<p class="text-primary">
</a> <i class="fab fa-linkedin me-2"></i>
{% trans "LinkedIn Connected" %}
{% else %} </p>
<p class="text-primary">
<i class="fab fa-linkedin me-2"></i>
{% trans "LinkedIn Connected" %}
</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>
</a> </a>
</div> </div>
<div class="col-md-4 mb-4">
<a href="{% url 'job_bank' %}" class="text-decoration-none">
<div class="kaauh-card shadow-sm p-4 h-100" style="border-left: 5px solid var(--kaauh-teal);">
<div class="d-flex align-items-center">
<i class="fas fa-database fa-3x text-primary-theme me-4"></i>
<div>
<h5 class="fw-bold mb-1" style="color: var(--kaauh-teal-dark);">
{% trans "Job Bank" %}
</h5>
<p class="text-muted small mb-0">
{% trans "Store your job postings in our Job Bank to reuse them later." %}
</p>
</div>
</div>
<div class="text-end mt-3">
<button class="btn btn-sm btn-outline-secondary">
{% trans "Go to Job Bank" %}<i class="fas fa-arrow-right ms-1"></i>
</button>
</div>
</div>
</a>
</div>
</div> </div>
{% endblock %} {% endblock %}