email send feature

This commit is contained in:
Faheed 2025-11-04 18:47:27 +03:00
parent fde53ce2d3
commit 6aa9ebd279
21 changed files with 17015 additions and 1161 deletions

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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'),
]

View File

@ -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
})

View 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 %}

View File

@ -1,4 +1,4 @@
{% extends 'forms/partials/candidate_facing_base.html'%}
{% extends 'applicant/partials/candidate_facing_base.html'%}
{% load static i18n %}
{% block content %}

View File

@ -1,4 +1,4 @@
{% extends 'forms/partials/candidate_facing_base.html'%}
{% extends 'applicant/partials/candidate_facing_base.html'%}
{% load static i18n %}
{% block content %}
<style>

View File

@ -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>

View File

@ -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>

View File

@ -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 -->

View File

@ -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">

View File

@ -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 %}

View File

@ -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>

View File

@ -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>