email send feature
This commit is contained in:
parent
fde53ce2d3
commit
6aa9ebd279
Binary file not shown.
@ -27,6 +27,7 @@ urlpatterns = [
|
||||
path('application/<slug:template_slug>/submit/', views.application_submit, name='application_submit'),
|
||||
path('application/<slug:slug>/apply/', views.application_detail, name='application_detail'),
|
||||
path('application/<slug:slug>/success/', views.application_success, name='application_success'),
|
||||
path('application/applicant/profile', views.applicant_profile, name='applicant_profile'),
|
||||
|
||||
path('api/templates/', views.list_form_templates, name='list_form_templates'),
|
||||
path('api/templates/save/', views.save_form_template, name='save_form_template'),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1265,7 +1265,22 @@ class ParticipantsSelectForm(forms.ModelForm):
|
||||
|
||||
class CandidateEmailForm(forms.Form):
|
||||
"""Form for composing emails to participants about a candidate"""
|
||||
|
||||
to = forms.MultipleChoiceField(
|
||||
widget=forms.CheckboxSelectMultiple(attrs={
|
||||
'class': 'form-check'
|
||||
}),
|
||||
label=_('Select Candidates'), # Use a descriptive label
|
||||
required=True
|
||||
)
|
||||
|
||||
# to = forms.MultipleChoiceField(
|
||||
# widget=forms.CheckboxSelectMultiple(attrs={
|
||||
# 'class': 'form-check'
|
||||
# }),
|
||||
# label=_('candidates'),
|
||||
# required=True
|
||||
# )
|
||||
|
||||
subject = forms.CharField(
|
||||
max_length=200,
|
||||
widget=forms.TextInput(attrs={
|
||||
@ -1296,30 +1311,30 @@ class CandidateEmailForm(forms.Form):
|
||||
required=True
|
||||
)
|
||||
|
||||
include_candidate_info = forms.BooleanField(
|
||||
widget=forms.CheckboxInput(attrs={
|
||||
'class': 'form-check-input'
|
||||
}),
|
||||
label=_('Include candidate information'),
|
||||
initial=True,
|
||||
required=False
|
||||
)
|
||||
# include_candidate_info = forms.BooleanField(
|
||||
# widget=forms.CheckboxInput(attrs={
|
||||
# 'class': 'form-check-input'
|
||||
# }),
|
||||
# label=_('Include candidate information'),
|
||||
# initial=True,
|
||||
# required=False
|
||||
# )
|
||||
|
||||
include_meeting_details = forms.BooleanField(
|
||||
widget=forms.CheckboxInput(attrs={
|
||||
'class': 'form-check-input'
|
||||
}),
|
||||
label=_('Include meeting details'),
|
||||
initial=True,
|
||||
required=False
|
||||
)
|
||||
# include_meeting_details = forms.BooleanField(
|
||||
# widget=forms.CheckboxInput(attrs={
|
||||
# 'class': 'form-check-input'
|
||||
# }),
|
||||
# label=_('Include meeting details'),
|
||||
# initial=True,
|
||||
# required=False
|
||||
# )
|
||||
|
||||
|
||||
def __init__(self, job, candidate, *args, **kwargs):
|
||||
def __init__(self, job, candidates, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.job = job
|
||||
self.candidate = candidate
|
||||
|
||||
self.candidates=candidates
|
||||
|
||||
# Get all participants and users for this job
|
||||
recipient_choices = []
|
||||
|
||||
@ -1334,12 +1349,22 @@ class CandidateEmailForm(forms.Form):
|
||||
recipient_choices.append(
|
||||
(f'user_{user.id}', f'{user.get_full_name() or user.username} - {user.email} (User)')
|
||||
)
|
||||
|
||||
|
||||
self.fields['recipients'].choices = recipient_choices
|
||||
self.fields['recipients'].initial = [choice[0] for choice in recipient_choices] # Select all by default
|
||||
|
||||
candidate_choices=[]
|
||||
for candidate in candidates:
|
||||
candidate_choices.append(
|
||||
(f'candidate_{candidate.id}', f'{candidate.email}')
|
||||
)
|
||||
|
||||
|
||||
# Set initial subject
|
||||
self.fields['subject'].initial = f'Interview Update: {candidate.name} - {job.title}'
|
||||
self.fields['to'].choices =candidate_choices
|
||||
self.fields['to'].initial = [choice[0] for choice in candidate_choices]
|
||||
|
||||
# # Set initial subject
|
||||
# self.fields['subject'].initial = f'Interview Update: {candidate.name} - {job.title}'
|
||||
|
||||
# Set initial message with candidate and meeting info
|
||||
initial_message = self._get_initial_message()
|
||||
@ -1348,24 +1373,24 @@ class CandidateEmailForm(forms.Form):
|
||||
|
||||
def _get_initial_message(self):
|
||||
"""Generate initial message with candidate and meeting information"""
|
||||
message_parts = []
|
||||
message_parts = ['hiiiiiiii']
|
||||
|
||||
# Add candidate information
|
||||
if self.candidate:
|
||||
message_parts.append(f"Candidate Information:")
|
||||
message_parts.append(f"Name: {self.candidate.name}")
|
||||
message_parts.append(f"Email: {self.candidate.email}")
|
||||
message_parts.append(f"Phone: {self.candidate.phone}")
|
||||
# # Add candidate information
|
||||
# if self.candidate:
|
||||
# message_parts.append(f"Candidate Information:")
|
||||
# message_parts.append(f"Name: {self.candidate.name}")
|
||||
# message_parts.append(f"Email: {self.candidate.email}")
|
||||
# message_parts.append(f"Phone: {self.candidate.phone}")
|
||||
|
||||
# Add latest meeting information if available
|
||||
latest_meeting = self.candidate.get_latest_meeting
|
||||
if latest_meeting:
|
||||
message_parts.append(f"\nMeeting Information:")
|
||||
message_parts.append(f"Topic: {latest_meeting.topic}")
|
||||
message_parts.append(f"Date & Time: {latest_meeting.start_time.strftime('%B %d, %Y at %I:%M %p')}")
|
||||
message_parts.append(f"Duration: {latest_meeting.duration} minutes")
|
||||
if latest_meeting.join_url:
|
||||
message_parts.append(f"Join URL: {latest_meeting.join_url}")
|
||||
# # Add latest meeting information if available
|
||||
# latest_meeting = self.candidate.get_latest_meeting
|
||||
# if latest_meeting:
|
||||
# message_parts.append(f"\nMeeting Information:")
|
||||
# message_parts.append(f"Topic: {latest_meeting.topic}")
|
||||
# message_parts.append(f"Date & Time: {latest_meeting.start_time.strftime('%B %d, %Y at %I:%M %p')}")
|
||||
# message_parts.append(f"Duration: {latest_meeting.duration} minutes")
|
||||
# if latest_meeting.join_url:
|
||||
# message_parts.append(f"Join URL: {latest_meeting.join_url}")
|
||||
|
||||
return '\n'.join(message_parts)
|
||||
|
||||
@ -1375,11 +1400,21 @@ class CandidateEmailForm(forms.Form):
|
||||
if not recipients:
|
||||
raise forms.ValidationError(_('Please select at least one recipient.'))
|
||||
return recipients
|
||||
|
||||
# def clean_to(self):
|
||||
# """Ensure at least one recipient is selected"""
|
||||
# candidates = self.cleaned_data.get('to')
|
||||
# print(candidates)
|
||||
# if not candidates:
|
||||
# raise forms.ValidationError(_('Please select at least one candidate.'))
|
||||
# return candidates
|
||||
|
||||
def get_email_addresses(self):
|
||||
"""Extract email addresses from selected recipients"""
|
||||
email_addresses = []
|
||||
recipients = self.cleaned_data.get('recipients', [])
|
||||
candidates=self.cleaned_data.get('to',[])
|
||||
|
||||
for recipient in recipients:
|
||||
if recipient.startswith('participant_'):
|
||||
participant_id = recipient.split('_')[1]
|
||||
@ -1395,31 +1430,42 @@ class CandidateEmailForm(forms.Form):
|
||||
email_addresses.append(user.email)
|
||||
except User.DoesNotExist:
|
||||
continue
|
||||
|
||||
for candidate in candidates:
|
||||
if candidate.startswith('candidate_'):
|
||||
print("candidadte: {candidate}")
|
||||
candidate_id = candidate.split('_')[1]
|
||||
try:
|
||||
candidate = Candidate.objects.get(id=candidate_id)
|
||||
email_addresses.append(candidate.email)
|
||||
except Candidate.DoesNotExist:
|
||||
continue
|
||||
|
||||
|
||||
return list(set(email_addresses)) # Remove duplicates
|
||||
|
||||
def get_formatted_message(self):
|
||||
"""Get the formatted message with optional additional information"""
|
||||
message = self.cleaned_data.get('message', '')
|
||||
message = self.cleaned_data.get('message', 'mesaage from system user hiii')
|
||||
|
||||
# Add candidate information if requested
|
||||
if self.cleaned_data.get('include_candidate_info') and self.candidate:
|
||||
candidate_info = f"\n\n--- Candidate Information ---\n"
|
||||
candidate_info += f"Name: {self.candidate.name}\n"
|
||||
candidate_info += f"Email: {self.candidate.email}\n"
|
||||
candidate_info += f"Phone: {self.candidate.phone}\n"
|
||||
message += candidate_info
|
||||
# # Add candidate information if requested
|
||||
# if self.cleaned_data.get('include_candidate_info') and self.candidate:
|
||||
# candidate_info = f"\n\n--- Candidate Information ---\n"
|
||||
# candidate_info += f"Name: {self.candidate.name}\n"
|
||||
# candidate_info += f"Email: {self.candidate.email}\n"
|
||||
# candidate_info += f"Phone: {self.candidate.phone}\n"
|
||||
# message += candidate_info
|
||||
|
||||
# Add meeting details if requested
|
||||
if self.cleaned_data.get('include_meeting_details') and self.candidate:
|
||||
latest_meeting = self.candidate.get_latest_meeting
|
||||
if latest_meeting:
|
||||
meeting_info = f"\n\n--- Meeting Details ---\n"
|
||||
meeting_info += f"Topic: {latest_meeting.topic}\n"
|
||||
meeting_info += f"Date & Time: {latest_meeting.start_time.strftime('%B %d, %Y at %I:%M %p')}\n"
|
||||
meeting_info += f"Duration: {latest_meeting.duration} minutes\n"
|
||||
if latest_meeting.join_url:
|
||||
meeting_info += f"Join URL: {latest_meeting.join_url}\n"
|
||||
message += meeting_info
|
||||
# # Add meeting details if requested
|
||||
# if self.cleaned_data.get('include_meeting_details') and self.candidate:
|
||||
# latest_meeting = self.candidate.get_latest_meeting
|
||||
# if latest_meeting:
|
||||
# meeting_info = f"\n\n--- Meeting Details ---\n"
|
||||
# meeting_info += f"Topic: {latest_meeting.topic}\n"
|
||||
# meeting_info += f"Date & Time: {latest_meeting.start_time.strftime('%B %d, %Y at %I:%M %p')}\n"
|
||||
# meeting_info += f"Duration: {latest_meeting.duration} minutes\n"
|
||||
# if latest_meeting.join_url:
|
||||
# meeting_info += f"Join URL: {latest_meeting.join_url}\n"
|
||||
# message += meeting_info
|
||||
|
||||
return message
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from django.db import transaction
|
||||
from django_q.models import Schedule
|
||||
from django_q.tasks import schedule
|
||||
@ -8,7 +9,7 @@ from django_q.tasks import async_task
|
||||
from django.db.models.signals import post_save
|
||||
from django.contrib.auth.models import User
|
||||
from django.utils import timezone
|
||||
from .models import FormField,FormStage,FormTemplate,Candidate,JobPosting,Notification
|
||||
from .models import FormField,FormStage,FormTemplate,Candidate,JobPosting,Notification,AgencyJobAssignment,AgencyAccessLink
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -397,3 +398,12 @@ def notification_created(sender, instance, created, **kwargs):
|
||||
SSE_NOTIFICATION_CACHE[user_id] = SSE_NOTIFICATION_CACHE[user_id][-50:]
|
||||
|
||||
logger.info(f"Notification cached for SSE: {notification_data}")
|
||||
|
||||
@receiver(post_save,sender=AgencyJobAssignment)
|
||||
def create_access_link(sender,instance,created,**kwargs):
|
||||
if created:
|
||||
link=AgencyAccessLink(assignment=instance)
|
||||
link.access_password = link.generate_password()
|
||||
link.unique_token = link.generate_token()
|
||||
link.expires_at = datetime.now() + timedelta(days=4)
|
||||
link.save()
|
||||
@ -231,5 +231,5 @@ urlpatterns = [
|
||||
path('participants/<slug:slug>/delete/', views_frontend.ParticipantsDeleteView.as_view(), name='participants_delete'),
|
||||
|
||||
# Email composition URLs
|
||||
path('jobs/<slug:job_slug>/candidates/<slug:candidate_slug>/compose-email/', views.compose_candidate_email, name='compose_candidate_email'),
|
||||
path('jobs/<slug:job_slug>/candidates/compose_email/', views.compose_candidate_email, name='compose_candidate_email'),
|
||||
]
|
||||
|
||||
@ -565,7 +565,7 @@ def kaauh_career(request):
|
||||
# job detail facing the candidate:
|
||||
def application_detail(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
return render(request, "forms/application_detail.html", {"job": job})
|
||||
return render(request, "applicant/application_detail.html", {"job": job})
|
||||
|
||||
|
||||
from django_q.tasks import async_task
|
||||
@ -872,10 +872,13 @@ def application_submit_form(request, template_slug):
|
||||
|
||||
return render(
|
||||
request,
|
||||
"forms/application_submit_form.html",
|
||||
"applicant/application_submit_form.html",
|
||||
{"template_slug": template_slug, "job_id": job_id},
|
||||
)
|
||||
|
||||
def applicant_profile(request):
|
||||
return render(request,'applicant/applicant_profile.html')
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@require_POST
|
||||
@ -3257,6 +3260,8 @@ def agency_portal_submit_candidate_page(request, slug):
|
||||
slug=slug
|
||||
)
|
||||
|
||||
|
||||
|
||||
if assignment.is_full:
|
||||
messages.error(request, 'Maximum candidate limit reached for this assignment.')
|
||||
return redirect('agency_portal_assignment_detail', slug=assignment.slug)
|
||||
@ -3315,6 +3320,7 @@ def agency_portal_submit_candidate_page(request, slug):
|
||||
'form': form,
|
||||
'assignment': assignment,
|
||||
'total_submitted': total_submitted,
|
||||
'job':assignment.job
|
||||
}
|
||||
return render(request, 'recruitment/agency_portal_submit_candidate.html', context)
|
||||
|
||||
@ -3694,23 +3700,37 @@ def api_candidate_detail(request, candidate_id):
|
||||
|
||||
|
||||
@login_required
|
||||
def compose_candidate_email(request, job_slug, candidate_slug):
|
||||
def compose_candidate_email(request, job_slug):
|
||||
"""Compose email to participants about a candidate"""
|
||||
from .email_service import send_bulk_email
|
||||
|
||||
job = get_object_or_404(JobPosting, slug=job_slug)
|
||||
candidate = get_object_or_404(Candidate, slug=candidate_slug, job=job)
|
||||
candidate_ids=request.GET.getlist('candidate_ids')
|
||||
candidates=Candidate.objects.filter(id__in=candidate_ids)
|
||||
|
||||
print(candidates)
|
||||
|
||||
# candidate = get_object_or_404(Candidate, slug=candidate_slug, job=job)
|
||||
if request.method == 'POST':
|
||||
form = CandidateEmailForm(job, candidate, request.POST)
|
||||
candidate_ids = request.POST.getlist('candidate_ids')
|
||||
print(f"inside the POST {candidate_ids}" )
|
||||
candidates=Candidate.objects.filter(id__in=candidate_ids)
|
||||
form = CandidateEmailForm(job, candidates, request.POST)
|
||||
|
||||
|
||||
print(form)
|
||||
|
||||
if form.is_valid():
|
||||
print("form is valid ...")
|
||||
# Get email addresses
|
||||
email_addresses = form.get_email_addresses()
|
||||
print(f"hiii {email_addresses} ")
|
||||
if not email_addresses:
|
||||
messages.error(request, 'No valid email addresses found for selected recipients.')
|
||||
return render(request, 'includes/email_compose_form.html', {
|
||||
'form': form,
|
||||
'job': job,
|
||||
'candidate': candidate
|
||||
'candidate': candidates
|
||||
})
|
||||
|
||||
# Check if this is an interview invitation
|
||||
@ -3720,18 +3740,18 @@ def compose_candidate_email(request, job_slug, candidate_slug):
|
||||
if is_interview_invitation:
|
||||
# Use HTML template for interview invitations
|
||||
meeting_details = None
|
||||
if form.cleaned_data.get('include_meeting_details'):
|
||||
# Try to get meeting details from candidate
|
||||
meeting_details = {
|
||||
'topic': f'Interview for {job.title}',
|
||||
'date_time': getattr(candidate, 'interview_date', 'To be scheduled'),
|
||||
'duration': '60 minutes',
|
||||
'join_url': getattr(candidate, 'meeting_url', ''),
|
||||
}
|
||||
# if form.cleaned_data.get('include_meeting_details'):
|
||||
# # Try to get meeting details from candidate
|
||||
# meeting_details = {
|
||||
# 'topic': f'Interview for {job.title}',
|
||||
# 'date_time': getattr(candidate, 'interview_date', 'To be scheduled'),
|
||||
# 'duration': '60 minutes',
|
||||
# 'join_url': getattr(candidate, 'meeting_url', ''),
|
||||
# }
|
||||
|
||||
from .email_service import send_interview_invitation_email
|
||||
email_result = send_interview_invitation_email(
|
||||
candidate=candidate,
|
||||
candidate=candidates,
|
||||
job=job,
|
||||
meeting_details=meeting_details,
|
||||
recipient_list=email_addresses
|
||||
@ -3742,6 +3762,7 @@ def compose_candidate_email(request, job_slug, candidate_slug):
|
||||
subject = form.cleaned_data.get('subject')
|
||||
|
||||
# Send emails using email service (no attachments, synchronous to avoid pickle issues)
|
||||
|
||||
email_result = send_bulk_email(
|
||||
subject=subject,
|
||||
message=message,
|
||||
@ -3750,33 +3771,33 @@ def compose_candidate_email(request, job_slug, candidate_slug):
|
||||
async_task_=False # Changed to False to avoid pickle issues
|
||||
)
|
||||
|
||||
if email_result['success']:
|
||||
messages.success(request, f'Email sent successfully to {len(email_addresses)} recipient(s).')
|
||||
if email_result['success']:
|
||||
messages.success(request, f'Email sent successfully to {len(email_addresses)} recipient(s).')
|
||||
|
||||
# For HTMX requests, return success response
|
||||
if 'HX-Request' in request.headers:
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'message': f'Email sent successfully to {len(email_addresses)} recipient(s).'
|
||||
# # For HTMX requests, return success response
|
||||
# if 'HX-Request' in request.headers:
|
||||
# return JsonResponse({
|
||||
# 'success': True,
|
||||
# 'message': f'Email sent successfully to {len(email_addresses)} recipient(s).'
|
||||
# })
|
||||
|
||||
return redirect('candidate_interview_view', slug=job.slug)
|
||||
else:
|
||||
messages.error(request, f'Failed to send email: {email_result.get("message", "Unknown error")}')
|
||||
|
||||
# For HTMX requests, return error response
|
||||
# if 'HX-Request' in request.headers:
|
||||
# return JsonResponse({
|
||||
# 'success': False,
|
||||
# 'error': email_result.get("message", "Failed to send email")
|
||||
# })
|
||||
|
||||
return render(request, 'includes/email_compose_form.html', {
|
||||
'form': form,
|
||||
'job': job,
|
||||
'candidate': candidates
|
||||
})
|
||||
|
||||
return redirect('candidate_interview_view', slug=job.slug)
|
||||
else:
|
||||
messages.error(request, f'Failed to send email: {email_result.get("message", "Unknown error")}')
|
||||
|
||||
# For HTMX requests, return error response
|
||||
if 'HX-Request' in request.headers:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': email_result.get("message", "Failed to send email")
|
||||
})
|
||||
|
||||
return render(request, 'includes/email_compose_form.html', {
|
||||
'form': form,
|
||||
'job': job,
|
||||
'candidate': candidate
|
||||
})
|
||||
|
||||
# except Exception as e:
|
||||
# logger.error(f"Error sending candidate email: {e}")
|
||||
# messages.error(request, f'An error occurred while sending the email: {str(e)}')
|
||||
@ -3795,6 +3816,7 @@ def compose_candidate_email(request, job_slug, candidate_slug):
|
||||
# })
|
||||
else:
|
||||
# Form validation errors
|
||||
print('form is not valid')
|
||||
print(form.errors)
|
||||
messages.error(request, 'Please correct the errors below.')
|
||||
|
||||
@ -3808,17 +3830,30 @@ def compose_candidate_email(request, job_slug, candidate_slug):
|
||||
return render(request, 'includes/email_compose_form.html', {
|
||||
'form': form,
|
||||
'job': job,
|
||||
'candidate': candidate
|
||||
'candidates': candidates
|
||||
})
|
||||
|
||||
else:
|
||||
else:
|
||||
|
||||
# GET request - show the form
|
||||
form = CandidateEmailForm(job, candidate)
|
||||
form = CandidateEmailForm(job, candidates)
|
||||
# try:
|
||||
# l = [x.split("_")[1] for x in candidates]
|
||||
# print(l)
|
||||
# candidates_qs = Candidate.objects.filter(pk__in=l)
|
||||
# print(candidates_qs)
|
||||
# form.initial["to"]. = candidates_qs
|
||||
# except:
|
||||
# pass
|
||||
|
||||
print("GET request made for candidate email form")
|
||||
|
||||
return render(request, 'includes/email_compose_form.html', {
|
||||
'form': form,
|
||||
'job': job,
|
||||
'candidate': candidate
|
||||
'candidates':candidates
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
||||
162
templates/applicant/applicant_profile.html
Normal file
162
templates/applicant/applicant_profile.html
Normal file
@ -0,0 +1,162 @@
|
||||
{% extends 'applicant/partials/candidate_facing_base.html'%}
|
||||
{% load static i18n %}
|
||||
|
||||
|
||||
{% block title %}{% trans "My Profile" %} - {{ block.super }}{% endblock %}
|
||||
|
||||
{% block customCSS %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-5">
|
||||
|
||||
{# Profile Header #}
|
||||
<div class="d-flex justify-content-between align-items-center mb-5 border-bottom pb-4">
|
||||
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-user-circle me-3 text-primary-theme"></i> {% trans "My Candidate Profile" %}
|
||||
</h1>
|
||||
<a href="#" class="btn btn-main-action btn-lg">
|
||||
<i class="fas fa-edit me-2"></i> {% trans "Edit Profile" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{# Profile and Account Management Row #}
|
||||
<div class="row g-5 mb-5">
|
||||
|
||||
{# Candidate Details Card #}
|
||||
<div class="col-lg-5">
|
||||
<div class="card kaauh-card h-100 p-4">
|
||||
<div class="card-body p-0">
|
||||
<div class="d-flex align-items-center mb-4 pb-4 border-bottom">
|
||||
<img src="{% if candidate.profile_picture %}{{ candidate.profile_picture.url }}{% else %}{% static 'image/default_avatar.png' %}{% endif %}"
|
||||
alt="{% trans 'Profile Picture' %}"
|
||||
class="rounded-circle me-4 shadow-sm"
|
||||
style="width: 100px; height: 100px; object-fit: cover; border: 4px solid var(--kaauh-teal-light);">
|
||||
<div>
|
||||
<h3 class="card-title mb-1" style="color: var(--kaauh-teal-dark); font-weight: 600;">{{ candidate.name|default:"N/A" }}</h3>
|
||||
<p class="text-muted mb-0">{{ candidate.email }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="list-unstyled profile-data-list pt-2">
|
||||
<li>
|
||||
<i class="fas fa-phone-alt me-2 text-primary-theme"></i>
|
||||
<strong>{% trans "Phone" %}</strong> {{ candidate.phone|default:"N/A" }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="fas fa-globe me-2 text-primary-theme"></i>
|
||||
<strong>{% trans "Nationality" %}</strong> {{ candidate.get_nationality_display|default:"N/A" }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="fas fa-calendar-alt me-2 text-primary-theme"></i>
|
||||
<strong>{% trans "Date of Birth" %}</strong> {{ candidate.date_of_birth|date:"M d, Y"|default:"N/A" }}
|
||||
</li>
|
||||
<li>
|
||||
<i class="fas fa-file-alt me-2 text-primary-theme"></i>
|
||||
<strong>{% trans "Resume" %}</strong>
|
||||
{% if candidate.resume %}
|
||||
<a href="#" target="_blank" class="text-primary-theme text-decoration-none fw-medium">
|
||||
{% trans "View/Download" %} <i class="fas fa-external-link-alt small ms-1"></i>
|
||||
</a>
|
||||
{% else %}
|
||||
<span class="text-muted">{% trans "Not uploaded" %}</span>
|
||||
{% endif %}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Account Management / Quick Actions Card #}
|
||||
<div class="col-lg-7">
|
||||
<div class="card kaauh-card h-100 p-4">
|
||||
<div class="card-header bg-white border-0 p-0 mb-4 profile-header">
|
||||
<h4 class="mb-0 py-2" style="color: var(--kaauh-teal-dark); font-weight: 600;"><i class="fas fa-cogs me-2"></i> {% trans "Account Settings" %}</h4>
|
||||
</div>
|
||||
<div class="card-body p-0">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
<a href="#" class="btn btn-outline-secondary w-100 py-3 d-flex align-items-center justify-content-center">
|
||||
<i class="fas fa-key me-2"></i> {% trans "Change Password" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<a href="#" class="btn btn-outline-secondary w-100 py-3 d-flex align-items-center justify-content-center">
|
||||
<i class="fas fa-id-card me-2"></i> {% trans "Update Contact Info" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<blockquote class="blockquote small text-muted mt-3 p-3" style="border-left: 3px solid var(--kaauh-teal-light); background-color: var(--kaauh-bg-subtle); border-radius: 0.5rem;">
|
||||
<p class="mb-0">{% trans "Your profile is essential for the application process. Keep your resume and contact information up-to-date for timely communication." %}</p>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Application Tracking Section #}
|
||||
<h2 class="h4 mb-4" style="color: var(--kaauh-teal-dark); border-bottom: 1px solid var(--kaauh-border); padding-bottom: 0.5rem;">
|
||||
<i class="fas fa-list-alt me-2 text-primary-theme"></i> {% trans "My Applications History" %}
|
||||
</h2>
|
||||
|
||||
{% if applications %}
|
||||
<div class="table-responsive kaauh-card shadow-sm">
|
||||
<table class="table table-hover align-middle mb-0 application-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col" style="min-width: 250px;">{% trans "Job Title" %}</th>
|
||||
<th scope="col">{% trans "Applied On" %}</th>
|
||||
<th scope="col">{% trans "Current Stage" %}</th>
|
||||
<th scope="col">{% trans "Status" %}</th>
|
||||
<th scope="col" class="text-end" style="min-width: 120px;">{% trans "Action" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for application in applications %}
|
||||
<tr>
|
||||
<td class="fw-medium">
|
||||
<a href="{% url 'job_detail' application.job.slug %}" class="text-decoration-none text-primary-theme">
|
||||
{{ application.job.title }}
|
||||
</a>
|
||||
</td>
|
||||
<td>{{ application.applied_date|date:"d M Y" }}</td>
|
||||
<td>
|
||||
<span class="badge badge-stage">
|
||||
{{ application.stage }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if application.is_active %}
|
||||
<span class="badge badge-success">{% trans "Active" %}</span>
|
||||
{% else %}
|
||||
<span class="badge badge-warning">{% trans "Closed" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-end">
|
||||
<a href="#" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="fas fa-eye"></i> {% trans "Details" %}
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{# Placeholder for Pagination #}
|
||||
{% comment %} {% include "includes/paginator.html" with page_obj=applications_page %} {% endcomment %}
|
||||
|
||||
{% else %}
|
||||
<div class="alert alert-info text-center kaauh-card p-5" style="border: 1px dashed var(--kaauh-border);">
|
||||
<i class="fas fa-info-circle fa-2x mb-3 text-primary-theme"></i>
|
||||
<h5 class="mb-3">{% trans "You have no active applications." %}</h5>
|
||||
<a href="{% url 'job_list' %}" class="ms-3 btn btn-main-action mt-2">
|
||||
{% trans "View Available Jobs" %} <i class="fas fa-arrow-right ms-2"></i>
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@ -1,4 +1,4 @@
|
||||
{% extends 'forms/partials/candidate_facing_base.html'%}
|
||||
{% extends 'applicant/partials/candidate_facing_base.html'%}
|
||||
{% load static i18n %}
|
||||
{% block content %}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
{% extends 'forms/partials/candidate_facing_base.html'%}
|
||||
{% extends 'applicant/partials/candidate_facing_base.html'%}
|
||||
{% load static i18n %}
|
||||
{% block content %}
|
||||
<style>
|
||||
@ -249,12 +249,12 @@
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
|
||||
{% comment %} <li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="/applications/">{% translate "Applications" %}</a>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="{% url 'applicant_profile' %}">{% translate "Applications" %}</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="/profile/">{% translate "Profile" %}</a>
|
||||
</li> {% endcomment %}
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link text-secondary" href="{% url 'kaauh_career' %}">{% translate "Careers" %}</a>
|
||||
</li>
|
||||
@ -133,7 +133,7 @@
|
||||
<h1 class="h3 mb-0 fw-bold text-primary">
|
||||
<i class="fas fa-file-alt me-2"></i>Create Form Template
|
||||
</h1>
|
||||
<a href="{% url 'form_templates_list' %}" class="btn btn-secondary">
|
||||
<a href="{% url 'form_templates_list' %}" class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-arrow-left me-1"></i>Back to Templates
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -4,32 +4,32 @@
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
<i class="fas fa-envelope me-2"></i>
|
||||
{% trans "Compose Email" %}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="card-body">
|
||||
<form method="post" id="email-compose-form" action="{% url 'compose_candidate_email' job.slug candidate.slug %}">
|
||||
<form hx-boost="true" method="post" id="email-compose-form" action="{% url 'compose_candidate_email' job.slug %}" hx-include="#candidate-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Subject Field -->
|
||||
<!-- Recipients Field -->
|
||||
<!-- Recipients Field -->
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.subject.id_for_label }}" class="form-label fw-bold">
|
||||
{% trans "Subject" %}
|
||||
<label class="form-label fw-bold">
|
||||
{% trans "To" %}
|
||||
</label>
|
||||
{{ form.subject }}
|
||||
{% if form.subject.errors %}
|
||||
<div class="border rounded p-3 bg-light" style="max-height: 200px; overflow-y: auto;">
|
||||
{% for choice in form.to %}
|
||||
<div class="form-check mb-2">
|
||||
{{ choice }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if form.to.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{% for error in form.subject.errors %}
|
||||
{% for error in form.to.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Recipients Field -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label fw-bold">
|
||||
{% trans "Recipients" %}
|
||||
@ -49,6 +49,22 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Subject Field -->
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.subject.id_for_label }}" class="form-label fw-bold">
|
||||
{% trans "Subject" %}
|
||||
</label>
|
||||
{{ form.subject }}
|
||||
{% if form.subject.errors %}
|
||||
<div class="text-danger small mt-1">
|
||||
{% for error in form.subject.errors %}
|
||||
<span>{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Message Field -->
|
||||
<div class="mb-3">
|
||||
@ -64,7 +80,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% comment %}
|
||||
<!-- Options Checkboxes -->
|
||||
<div class="mb-4">
|
||||
<div class="row">
|
||||
@ -93,7 +109,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
|
||||
|
||||
<!-- Form Actions -->
|
||||
|
||||
@ -390,10 +390,10 @@
|
||||
<i class="fas fa-users me-2"></i>
|
||||
{% trans "Recent Candidates" %}
|
||||
</h5>
|
||||
<a href="{% url 'agency_candidates' agency.slug %}" class="btn btn-main-action btn-sm">
|
||||
{% comment %} <a href="{% url 'agency_candidates' agency.slug %}" class="btn btn-main-action btn-sm">
|
||||
{% trans "View All Candidates" %}
|
||||
<i class="fas fa-arrow-right ms-1"></i>
|
||||
</a>
|
||||
</a> {% endcomment %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
|
||||
@ -91,18 +91,19 @@
|
||||
<nav aria-label="breadcrumb" class="mb-4">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'agency_portal_dashboard' %}" class="text-decoration-none">
|
||||
<a href="{% url 'agency_portal_dashboard' %}" class="text-decoration-none text-secondary">
|
||||
<i class="fas fa-home me-1"></i>{% trans "Dashboard" %}
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item">
|
||||
<a href="{% url 'agency_assignment_detail' assignment.slug %}" class="text-decoration-none">
|
||||
{{ assignment.job.title }}
|
||||
</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">
|
||||
|
||||
{% comment %} <li class="breadcrumb-item active" aria-current="page">
|
||||
{% trans "Submit Candidate" %}
|
||||
</li>
|
||||
</li> {% endcomment %}
|
||||
|
||||
<li class="breadcrumb-item active" aria-current="page" style="
|
||||
color: #F43B5E; /* Rosy Accent Color */
|
||||
font-weight: 600; ">
|
||||
{% trans "Submit Candidate" %}</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
@ -114,7 +115,10 @@
|
||||
{% trans "Submit New Candidate" %}
|
||||
</h1>
|
||||
<p class="text-muted mb-0">
|
||||
{% trans "Submit a candidate for" %} {{ assignment.job.title }}
|
||||
<!-- Button trigger modal -->
|
||||
{% trans "Submit a candidate for" %}
|
||||
{{ assignment.job.title }}
|
||||
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
@ -386,6 +390,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
|
||||
@ -239,6 +239,17 @@
|
||||
data-bs-target="#jobAssignmentModal">
|
||||
<i class="fas fa-users-cog me-1"></i> {% trans "Manage Participants" %} ({{participants_count}})
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-outline-info btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
hx-boost='true'
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_candidate_email' job.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
hx-include="#candidate-form"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -374,22 +385,7 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% 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> {% endcomment %}
|
||||
<button type="button" class="btn btn-outline-info btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#emailModal"
|
||||
hx-get="{% url 'compose_candidate_email' job.slug candidate.slug %}"
|
||||
hx-target="#emailModalBody"
|
||||
title="Email Participants">
|
||||
<i class="fas fa-envelope"></i>
|
||||
</button>
|
||||
|
||||
{% if candidate.get_latest_meeting %}
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
@ -399,6 +395,7 @@
|
||||
title="Reschedule">
|
||||
<i class="fas fa-redo-alt"></i>
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#candidateviewModal"
|
||||
@ -407,6 +404,7 @@
|
||||
title="Delete Meeting">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
|
||||
{% else %}
|
||||
<button type="button" class="btn btn-main-action btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
@ -523,6 +521,7 @@
|
||||
<div class="text-center py-5 text-muted">
|
||||
<i class="fas fa-spinner fa-spin fa-2x"></i><br>
|
||||
{% trans "Loading email form..." %}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -212,8 +212,8 @@
|
||||
<option value="Hired">
|
||||
{% trans "To Hired" %}
|
||||
</option>
|
||||
<option value="Rejected">
|
||||
{% trans "To Rejected" %}
|
||||
<option value="Interview">
|
||||
{% trans "To Interview" %}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user