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

This commit is contained in:
Faheed 2025-11-19 13:01:22 +03:00
commit 60e3f81620
14 changed files with 941 additions and 271 deletions

1
.gitignore vendored
View File

@ -22,6 +22,7 @@ var/
*.egg-info/ *.egg-info/
.installed.cfg .installed.cfg
*.egg *.egg
.env
# Django stuff: # Django stuff:
*.log *.log

View File

@ -303,6 +303,7 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
processed_attachments = attachments if attachments else [] processed_attachments = attachments if attachments else []
task_ids = [] task_ids = []
job_id=job.id job_id=job.id
sender_user_id=request.user.id if request and hasattr(request, 'user') and request.user.is_authenticated else None sender_user_id=request.user.id if request and hasattr(request, 'user') and request.user.is_authenticated else None
if not from_interview: if not from_interview:
@ -359,6 +360,14 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
else: else:
# --- 3. Handle SYNCHRONOUS Send (No changes needed here, as it was fixed previously) --- # --- 3. Handle SYNCHRONOUS Send (No changes needed here, as it was fixed previously) ---
try:
# NOTE: The synchronous block below should also use the 'customized_sends'
# list for consistency instead of rebuilding messages from 'pure_candidate_emails'
# and 'agency_emails', but keeping your current logic structure to minimize changes.
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa')
is_html = '<' in message and '>' in message
successful_sends = 0
try: try:
# NOTE: The synchronous block below should also use the 'customized_sends' # NOTE: The synchronous block below should also use the 'customized_sends'
# list for consistency instead of rebuilding messages from 'pure_candidate_emails' # list for consistency instead of rebuilding messages from 'pure_candidate_emails'
@ -368,87 +377,67 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
is_html = '<' in message and '>' in message is_html = '<' in message and '>' in message
successful_sends = 0 successful_sends = 0
# Helper Function for Sync Send (as provided) # Helper Function for Sync Send (as provided)
def send_individual_email(recipient, body_message): def send_individual_email(recipient, body_message):
# ... (Existing helper function logic) ... # ... (Existing helper function logic) ...
nonlocal successful_sends nonlocal successful_sends
if is_html: if is_html:
plain_message = strip_tags(body_message) plain_message = strip_tags(body_message)
email_obj = EmailMultiAlternatives(subject=subject, body=plain_message, from_email=from_email, to=[recipient]) email_obj = EmailMultiAlternatives(subject=subject, body=plain_message, from_email=from_email, to=[recipient])
email_obj.attach_alternative(body_message, "text/html") email_obj.attach_alternative(body_message, "text/html")
else: else:
email_obj = EmailMultiAlternatives(subject=subject, body=body_message, from_email=from_email, to=[recipient]) email_obj = EmailMultiAlternatives(subject=subject, body=body_message, from_email=from_email, to=[recipient])
if attachments: if attachments:
for attachment in attachments: for attachment in attachments:
if hasattr(attachment, 'read'): if hasattr(attachment, 'read'):
filename = getattr(attachment, 'name', 'attachment') filename = getattr(attachment, 'name', 'attachment')
content = attachment.read() content = attachment.read()
content_type = getattr(attachment, 'content_type', 'application/octet-stream') content_type = getattr(attachment, 'content_type', 'application/octet-stream')
email_obj.attach(filename, content, content_type) email_obj.attach(filename, content, content_type)
elif isinstance(attachment, tuple) and len(attachment) == 3: elif isinstance(attachment, tuple) and len(attachment) == 3:
filename, content, content_type = attachment filename, content, content_type = attachment
email_obj.attach(filename, content, content_type) email_obj.attach(filename, content, content_type)
try: try:
result=email_obj.send(fail_silently=False) email_obj.send(fail_silently=False)
if result==1: successful_sends += 1
try: except Exception as e:
user=get_object_or_404(User,email=recipient) logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
new_message = Message.objects.create(
sender=request.user,
recipient=user,
job=job,
subject=subject,
content=message, # Store the full HTML or plain content
message_type='DIRECT',
is_read=False, # It's just sent, not read yet
)
logger.info(f"Stored sent message ID {new_message.id} in DB.")
except Exception as e:
logger.error(f"Email sent to {recipient}, but failed to store in DB: {str(e)}")
if not from_interview:
# Send Emails - Pure Candidates
for email in pure_candidate_emails:
candidate_name = Application.objects.filter(email=email).first().first_name
candidate_message = f"Hi, {candidate_name}" + "\n" + message
send_individual_email(email, candidate_message)
else: # Send Emails - Agencies
logger.error("fialed to send email") i = 0
for email in agency_emails:
candidate_email = candidate_through_agency_emails[i]
candidate_name = Application.objects.filter(email=candidate_email).first().first_name
agency_message = f"Hi, {candidate_name}" + "\n" + message
send_individual_email(email, agency_message)
i += 1
successful_sends += 1 logger.info(f"Bulk email processing complete. Sent successfully to {successful_sends} out of {total_recipients} unique recipients.")
except Exception as e: return {
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True) 'success': True,
'recipients_count': successful_sends,
'message': f'Email processing complete. {successful_sends} email(s) were sent successfully to {total_recipients} unique intended recipients.'
}
else:
for email in recipient_list:
send_individual_email(email, message)
if not from_interview: logger.info(f"Interview email processing complete. Sent successfully to {successful_sends} out of {total_recipients} recipients.")
# Send Emails - Pure Candidates return {
for email in pure_candidate_emails: 'success': True,
candidate_name = Application.objects.filter(person__email=email).first().person.full_name 'recipients_count': successful_sends,
candidate_message = f"Hi, {candidate_name}" + "\n" + message 'message': f'Interview emails sent successfully to {successful_sends} recipient(s).'
send_individual_email(email, candidate_message) }
# Send Emails - Agencies
i = 0
for email in agency_emails:
candidate_email = candidate_through_agency_emails[i]
candidate_name = Application.objects.filter(person__email=candidate_email).first().person.full_name
agency_message = f"Hi, {candidate_name}" + "\n" + message
send_individual_email(email, agency_message)
i += 1
logger.info(f"Bulk email processing complete. Sent successfully to {successful_sends} out of {total_recipients} unique recipients.")
return {
'success': True,
'recipients_count': successful_sends,
'message': f'Email processing complete. {successful_sends} email(s) were sent successfully to {total_recipients} unique intended recipients.'
}
else:
for email in recipient_list:
send_individual_email(email, message)
logger.info(f"Interview email processing complete. Sent successfully to {successful_sends} out of {total_recipients} recipients.")
return {
'success': True,
'recipients_count': successful_sends,
'message': f'Interview emails sent successfully to {successful_sends} recipient(s).'
}
except Exception as e: except Exception as e:
error_msg = f"Failed to process bulk email send request: {str(e)}" error_msg = f"Failed to process bulk email send request: {str(e)}"

View File

@ -2453,3 +2453,52 @@ 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 StaffAssignmentForm(forms.ModelForm):
"""Form for assigning staff to a job posting"""
class Meta:
model = JobPosting
fields = ['assigned_to']
widgets = {
'assigned_to': forms.Select(attrs={
'class': 'form-select',
'placeholder': _('Select staff member'),
'required': True
}),
}
labels = {
'assigned_to': _('Assign Staff Member'),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Filter users to only show staff members
self.fields['assigned_to'].queryset = User.objects.filter(
user_type='staff'
).order_by('first_name', 'last_name')
# Add empty choice for unassigning
self.fields['assigned_to'].required = False
self.fields['assigned_to'].empty_label = _('-- Unassign Staff --')
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_class = 'g-3'
self.helper.form_id = 'staff-assignment-form'
self.helper.layout = Layout(
Field('assigned_to', css_class='form-control'),
Div(
Submit('submit', _('Assign Staff'), css_class='btn btn-primary'),
css_class='col-12 mt-3'
),
)
def clean_assigned_to(self):
"""Validate the assigned staff member"""
assigned_to = self.cleaned_data.get('assigned_to')
if assigned_to and assigned_to.user_type != 'staff':
raise forms.ValidationError(_('Only staff members can be assigned to jobs.'))
return assigned_to

View File

@ -22,6 +22,8 @@ from .models import (
) )
from .forms import generate_api_key, generate_api_secret from .forms import generate_api_key, generate_api_secret
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django_q.models import Schedule
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -41,6 +43,7 @@ def format_job(sender, instance, created, **kwargs):
instance.pk, instance.pk,
# hook='myapp.tasks.email_sent_callback' # Optional callback # hook='myapp.tasks.email_sent_callback' # Optional callback
) )
else: else:
existing_schedule = Schedule.objects.filter( existing_schedule = Schedule.objects.filter(
func="recruitment.tasks.form_close", func="recruitment.tasks.form_close",

View File

@ -606,6 +606,8 @@ def form_close(job_id):
job.is_active = False job.is_active = False
job.template_form.is_active = False job.template_form.is_active = False
job.save() job.save()
#TODO:send email to admins
def sync_hired_candidates_task(job_slug): def sync_hired_candidates_task(job_slug):
@ -843,3 +845,5 @@ def email_success_hook(task):
logger.info(f"Task ID {task.id} succeeded. Result: {task.result}") logger.info(f"Task ID {task.id} succeeded. Result: {task.result}")
else: else:
logger.error(f"Task ID {task.id} failed. Error: {task.result}") logger.error(f"Task ID {task.id} failed. Error: {task.result}")

View File

@ -35,6 +35,7 @@ urlpatterns = [
), ),
path("jobs/linkedin/login/", views.linkedin_login, name="linkedin_login"), path("jobs/linkedin/login/", views.linkedin_login, name="linkedin_login"),
path("jobs/linkedin/callback/", views.linkedin_callback, name="linkedin_callback"), path("jobs/linkedin/callback/", views.linkedin_callback, name="linkedin_callback"),
path("jobs/<slug:slug>/staff-assignment/", views.staff_assignment_view, name="staff_assignment_view"),
# Candidate URLs # Candidate URLs
path( path(

View File

@ -30,7 +30,8 @@ from .forms import (
ProfileImageUploadForm, ProfileImageUploadForm,
ParticipantsSelectForm, ParticipantsSelectForm,
ApplicationForm, ApplicationForm,
PasswordResetForm PasswordResetForm,
StaffAssignmentForm,
) )
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
@ -600,7 +601,7 @@ ALLOWED_EXTENSIONS = (".pdf", ".docx")
def job_cvs_download(request, slug): def job_cvs_download(request, slug):
job = get_object_or_404(JobPosting, slug=slug) job = get_object_or_404(JobPosting, slug=slug)
entries = Candidate.objects.filter(job=job) entries = Application.objects.filter(job=job)
# 2. Create an in-memory byte stream (BytesIO) # 2. Create an in-memory byte stream (BytesIO)
zip_buffer = io.BytesIO() zip_buffer = io.BytesIO()
@ -642,7 +643,7 @@ def job_cvs_download(request, slug):
# Set the header for the browser to download the file # Set the header for the browser to download the file
response["Content-Disposition"] = ( response["Content-Disposition"] = (
'attachment; filename=f"all_cvs_for_{job.title}.zip"' f'attachment; filename="all_cvs_for_{job.title}.zip"'
) )
return response return response
@ -2927,6 +2928,34 @@ def admin_settings(request):
context = {"staffs": staffs, "form": form} context = {"staffs": staffs, "form": form}
return render(request, "user/admin_settings.html", context) return render(request, "user/admin_settings.html", context)
@staff_user_required
def staff_assignment_view(request, slug):
"""
View to assign staff to a job posting
"""
job = get_object_or_404(JobPosting, slug=slug)
staff_users = User.objects.filter(user_type="staff", is_superuser=False)
applications = job.applications.all()
if request.method == "POST":
form = StaffAssignmentForm(request.POST)
if form.is_valid():
assignment = form.save(commit=False)
messages.success(request, f"Staff assigned to job '{job.title}' successfully!")
return redirect("job_detail", slug=job.slug)
else:
messages.error(request, "Please correct the errors below.")
else:
form = StaffAssignmentForm()
context = {
"job": job,
"applications": applications,
"staff_users": staff_users,
"form": form,
}
return render(request, "recruitment/staff_assignment_view.html", context)
from django.contrib.auth.forms import SetPasswordForm from django.contrib.auth.forms import SetPasswordForm
@ -3004,6 +3033,8 @@ def zoom_webhook_view(request):
@staff_user_required @staff_user_required
def add_meeting_comment(request, slug): def add_meeting_comment(request, slug):
"""Add a comment to a meeting""" """Add a comment to a meeting"""
# from .forms import MeetingCommentForm
meeting = get_object_or_404(ZoomMeetingDetails, slug=slug) meeting = get_object_or_404(ZoomMeetingDetails, slug=slug)
if request.method == "POST": if request.method == "POST":
@ -4586,7 +4617,34 @@ def message_create(request):
message = form.save(commit=False) message = form.save(commit=False)
message.sender = request.user message.sender = request.user
message.save() message.save()
messages.success(request, "Message sent successfully!") # Send email if message_type is 'email' and recipient has email
if message.message_type == 'email' and message.recipient and message.recipient.email:
try:
from .email_service import send_bulk_email
email_result = send_bulk_email(
subject=message.subject,
message=message.content,
recipient_list=[message.recipient.email],
request=request,
attachments=None,
async_task_=True,
from_interview=False
)
if email_result["success"]:
message.is_email_sent = True
message.email_address = message.recipient.email
message.save(update_fields=['is_email_sent', 'email_address'])
messages.success(request, "Message sent successfully via email!")
else:
messages.warning(request, f"Message saved but email failed: {email_result.get('message', 'Unknown error')}")
except Exception as e:
messages.warning(request, f"Message saved but email sending failed: {str(e)}")
else:
messages.success(request, "Message sent successfully!")
["recipient", "job", "subject", "content", "message_type"] ["recipient", "job", "subject", "content", "message_type"]
recipient_email = form.cleaned_data['recipient'].email # Assuming recipient is a User or Model with an 'email' field recipient_email = form.cleaned_data['recipient'].email # Assuming recipient is a User or Model with an 'email' field
subject = form.cleaned_data['subject'] subject = form.cleaned_data['subject']
@ -4644,7 +4702,34 @@ def message_reply(request, message_id):
message.recipient = parent_message.sender message.recipient = parent_message.sender
message.save() message.save()
messages.success(request, "Reply sent successfully!") # Send email if message_type is 'email' and recipient has email
if message.message_type == 'email' and message.recipient and message.recipient.email:
try:
from .email_service import send_bulk_email
email_result = send_bulk_email(
subject=message.subject,
message=message.content,
recipient_list=[message.recipient.email],
request=request,
attachments=None,
async_task_=True,
from_interview=False
)
if email_result["success"]:
message.is_email_sent = True
message.email_address = message.recipient.email
message.save(update_fields=['is_email_sent', 'email_address'])
messages.success(request, "Reply sent successfully via email!")
else:
messages.warning(request, f"Reply saved but email failed: {email_result.get('message', 'Unknown error')}")
except Exception as e:
messages.warning(request, f"Reply saved but email sending failed: {str(e)}")
else:
messages.success(request, "Reply sent successfully!")
return redirect("message_detail", message_id=parent_message.id) return redirect("message_detail", message_id=parent_message.id)
else: else:
messages.error(request, "Please correct the errors below.") messages.error(request, "Please correct the errors below.")
@ -5147,17 +5232,35 @@ def compose_candidate_email(request, job_slug):
async_task_=True, # Changed to False to avoid pickle issues async_task_=True, # Changed to False to avoid pickle issues
from_interview=False, from_interview=False,
job=job job=job
) )
if email_result["success"]: if email_result["success"]:
for candidate in candidates:
if hasattr(candidate, 'person') and candidate.person:
try:
Message.objects.create(
sender=request.user,
recipient=candidate.person.user,
subject=subject,
content=message,
job=job,
message_type='email',
is_email_sent=True,
email_address=candidate.person.email if candidate.person.email else candidate.email
)
except Exception as e:
# Log error but don't fail the entire process
print(f"Error creating message")
messages.success( messages.success(
request, request,
f"Email sent successfully to {len(email_addresses)} recipient(s).", f"Email will be sent shortly to recipient(s)",
) )
response = HttpResponse(status=200)
response.headers["HX-Refresh"] = "true"
return redirect("candidate_interview_view", slug=job.slug) return response
# return redirect("candidate_interview_view", slug=job.slug)
else: else:
messages.error( messages.error(
request, request,
@ -5185,8 +5288,6 @@ def compose_candidate_email(request, job_slug):
else: else:
# Form validation errors # Form validation errors
print('form is not valid')
print(form.errors)
messages.error(request, "Please correct the errors below.") messages.error(request, "Please correct the errors below.")
# For HTMX requests, return error response # For HTMX requests, return error response
@ -5561,9 +5662,29 @@ def send_interview_email(request, slug):
) )
if email_result["success"]: if email_result["success"]:
# Create Message records for each participant after successful email send
messages_created = 0
for participant in participants:
if hasattr(participant, 'user') and participant.user:
try:
Message.objects.create(
sender=request.user,
recipient=participant.user,
subject=subject,
content=msg_participants,
job=job,
message_type='email',
is_email_sent=True,
email_address=participant.email if hasattr(participant, 'email') else ''
)
messages_created += 1
except Exception as e:
# Log error but don't fail the entire process
print(f"Error creating message for {participant.email if hasattr(participant, 'email') else participant}: {e}")
messages.success( messages.success(
request, request,
f"Email sent successfully to {total_recipients} recipient(s).", f"Email will be sent shortly to {total_recipients} recipient(s).",
) )
return redirect("list_meetings") return redirect("list_meetings")
@ -5683,7 +5804,7 @@ class MeetingListView(ListView):
'details': details, 'details': details,
'type': location.location_type, 'type': location.location_type,
'topic': location.topic, 'topic': location.topic,
# 'slug': interview.slug, 'slug': interview.slug,
'start_time': start_datetime, # Combined datetime object 'start_time': start_datetime, # Combined datetime object
# Duration should ideally be on ScheduledInterview or fetched from details # Duration should ideally be on ScheduledInterview or fetched from details
'duration': getattr(details, 'duration', 'N/A'), 'duration': getattr(details, 'duration', 'N/A'),

View File

@ -317,7 +317,7 @@
</nav> </nav>
<main class="container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;"> <main id="messageContent" class="container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;">
{% if messages %} {% if messages %}
{% for message in messages %} {% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show mt-2" role="alert"> <div class="alert alert-{{ message.tags }} alert-dismissible fade show mt-2" role="alert">
@ -417,9 +417,21 @@
console.error(e) console.error(e)
} }
function closeOpenBootstrapModal() {
const openModalElement = document.querySelector('.modal.show');
if (openModalElement) {
const modal = bootstrap.Modal.getInstance(openModalElement);
if (modal) {
modal.hide();
} else {
console.warn("Found an open modal element, but could not get the Bootstrap Modal instance.");
}
} else {
console.log("No open Bootstrap Modal found to close.");
}
}
</script> </script>
<!-- Message Count JavaScript -->
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// Update unread message count on page load // Update unread message count on page load

View File

@ -17,7 +17,13 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<form hx-boost="true" method="post" id="email-compose-form" action="{% url 'compose_candidate_email' job.slug %}" hx-include="#candidate-form"> <form hx-boost="true" method="post" id="email-compose-form" action="{% url 'compose_candidate_email' job.slug %}"
hx-include="#candidate-form"
hx-target="#messageContent"
hx-select="#messageContent"
hx-push-url="false"
hx-swap="outerHTML"
hx-on::after-request="new bootstrap.Modal('#emailModal')).hide()">
{% csrf_token %} {% csrf_token %}
<!-- Recipients Field --> <!-- Recipients Field -->
<!-- Recipients Field --> <!-- Recipients Field -->
@ -171,8 +177,8 @@
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('email-compose-form'); const form = document.getElementById('email-compose-form1');
const sendBtn = document.getElementById('send-email-btn'); const sendBtn = document.getElementById('send-email-btn1');
const loadingOverlay = document.getElementById('email-loading-overlay'); const loadingOverlay = document.getElementById('email-loading-overlay');
const messagesContainer = document.getElementById('email-messages-container'); const messagesContainer = document.getElementById('email-messages-container');

View File

@ -301,6 +301,11 @@
<i class="fas fa-cogs me-1"></i> {% trans "Form Template" %} <i class="fas fa-cogs me-1"></i> {% trans "Form Template" %}
</button> </button>
</li> </li>
<li class="nav-item flex-fill" role="presentation">
<button class="nav-link" id="staff-tab" data-bs-toggle="tab" data-bs-target="#staff-pane" type="button" role="tab" aria-controls="staff-pane" aria-selected="false">
<i class="fas fa-user-tie me-1 text-primary"></i> {% trans "Staff" %}
</button>
</li>
<li class="nav-item flex-fill" role="presentation"> <li class="nav-item flex-fill" role="presentation">
<button class="nav-link" id="linkedin-tab" data-bs-toggle="tab" data-bs-target="#linkedin-pane" type="button" role="tab" aria-controls="linkedin-pane" aria-selected="false"> <button class="nav-link" id="linkedin-tab" data-bs-toggle="tab" data-bs-target="#linkedin-pane" type="button" role="tab" aria-controls="linkedin-pane" aria-selected="false">
<i class="fab fa-linkedin me-1 text-info"></i> {% trans "LinkedIn" %} <i class="fab fa-linkedin me-1 text-info"></i> {% trans "LinkedIn" %}
@ -369,7 +374,54 @@
</div> </div>
</div> </div>
{# TAB 4: LINKEDIN INTEGRATION CONTENT #} {# TAB 4: STAFF ASSIGNMENT CONTENT #}
<div class="tab-pane fade" id="staff-pane" role="tabpanel" aria-labelledby="staff-tab">
<h5 class="mb-3"><i class="fas fa-user-tie me-2 text-primary"></i>{% trans "Staff Assignment" %}</h5>
<div class="d-grid gap-3">
<p class="text-muted small mb-3">
{% trans "Assign staff members to manage this job posting and track applications." %}
</p>
<a href="{% url 'staff_assignment_view' job.slug %}" class="btn btn-main-action">
<i class="fas fa-user-plus me-1"></i> {% trans "Assign Staff Member" %}
</a>
{% if job.staff_assignments.exists %}
<div class="mt-3">
<h6 class="text-muted">{% trans "Current Assignments" %}</h6>
{% for assignment in job.staff_assignments.all %}
<div class="card mb-2">
<div class="card-body p-2">
<div class="d-flex justify-content-between align-items-center">
<div>
<strong>{{ assignment.staff.get_full_name|default:assignment.staff.username }}</strong>
<br>
<small class="text-muted">{{ assignment.staff.email }}</small>
</div>
<div>
{% if assignment.staff.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-danger">Inactive</span>
{% endif %}
</div>
</div>
{% if assignment.notes %}
<small class="text-muted d-block mt-1">{{ assignment.notes }}</small>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<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." %}
</div>
{% endif %}
</div>
</div>
{# TAB 5: LINKEDIN INTEGRATION CONTENT #}
<div class="tab-pane fade" id="linkedin-pane" role="tabpanel" aria-labelledby="linkedin-tab"> <div class="tab-pane fade" id="linkedin-pane" role="tabpanel" aria-labelledby="linkedin-tab">
<h5 class="mb-3"><i class="fab fa-linkedin me-2 text-info"></i>{% trans "LinkedIn Integration" %}</h5> <h5 class="mb-3"><i class="fab fa-linkedin me-2 text-info"></i>{% trans "LinkedIn Integration" %}</h5>
<div class="d-grid gap-3"> <div class="d-grid gap-3">

View File

@ -0,0 +1,432 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Assign Staff to {{ job.title }}{% endblock %}
{% block customCSS %}
<style>
/* Theme Variables and Global Styles */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
}
/* Primary Color Overrides */
.text-primary { color: var(--kaauh-teal) !important; }
.bg-primary { background-color: var(--kaauh-teal) !important; }
.btn-main-action {
background-color: var(--kaauh-teal) !important;
border-color: var(--kaauh-teal) !important;
color: white !important;
font-weight: 600;
padding: 0.6rem 1.2rem;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark) !important;
border-color: var(--kaauh-teal-dark) !important;
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.btn-outline-secondary {
color: var(--kaauh-teal-dark) !important;
border-color: var(--kaauh-teal) !important;
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark) !important;
color: white !important;
border-color: var(--kaauh-teal-dark) !important;
}
/* Header styling */
.job-header-card {
background: linear-gradient(135deg, var(--kaauh-teal), var(--kaauh-teal-dark));
color: white;
border-radius: 0.75rem 0.75rem 0 0;
padding: 1.5rem;
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
}
.job-header-card h1 {
font-weight: 700;
margin: 0;
font-size: 1.8rem;
}
/* Card enhancements */
.kaauh-card, .card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
.kaauh-card:not(.no-hover):hover,
.card:not(.no-hover):hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
}
/* Standard Card Header */
.card-header {
font-weight: 600;
padding: 1rem 1.25rem;
background-color: #f8f9fa;
border-bottom: 1px solid var(--kaauh-border);
}
/* Form styling */
.form-control, .form-select {
border-radius: 0.5rem;
border: 1px solid #ced4da;
padding: 0.5rem 0.75rem;
font-size: 0.9rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus, .form-select:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
/* Staff member cards */
.staff-card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
transition: all 0.2s ease;
height: 100%;
}
.staff-card:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
border-color: var(--kaauh-teal);
}
/* Badge styling */
.badge {
font-size: 0.8rem;
padding: 0.4em 0.8em;
border-radius: 0.4rem;
font-weight: 600;
}
/* Table styling */
.table {
margin-bottom: 0;
}
.table th {
background-color: var(--kaauh-border);
font-weight: 600;
color: var(--kaauh-teal-dark);
border-bottom: 2px solid var(--kaauh-teal);
}
.table tbody tr:hover {
background-color: #f1f3f4;
}
/* Page header styling */
.page-header {
color: var(--kaauh-teal-dark);
font-weight: 700;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row">
<div class="col-12">
<!-- Page Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="page-header h3 mb-1">Assign Staff to Job</h1>
<p class="text-secondary mb-0">Job: {{ job.title }} ({{ job.internal_job_id }})</p>
</div>
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-2"></i>Back to Job Details
</a>
</div>
<!-- Job Information Card -->
<div class="kaauh-card mb-4">
<div class="job-header-card">
<h5 class="mb-0"><i class="fas fa-briefcase me-2"></i>Job Information</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>Department:</strong> {{ job.get_department_display|default:job.department }}</p>
<p><strong>Job Type:</strong> {{ job.get_job_type_display }}</p>
<p><strong>Workplace Type:</strong> {{ job.get_workplace_type_display }}</p>
</div>
<div class="col-md-6">
<p><strong>Status:</strong>
<span class="badge {% if job.status == 'ACTIVE' %}bg-success{% elif job.status == 'CLOSED' %}bg-danger{% else %}bg-secondary{% endif %}">
{{ job.get_status_display }}
</span>
</p>
<p><strong>Applications:</strong> {{ applications.count }}</p>
<p><strong>Created:</strong> {{ job.created_at|date:"M d, Y" }}</p>
</div>
</div>
</div>
</div>
<!-- Staff Assignment Form -->
<div class="kaauh-card">
<div class="card-header">
<h5 class="mb-0 text-primary"><i class="fas fa-user-tie me-2"></i>Staff Assignment</h5>
</div>
<div class="card-body">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
</div>
{% endfor %}
{% endif %}
<form method="post" id="staffAssignmentForm">
{% csrf_token %}
<div class="row mb-3">
<div class="col-md-12">
<label for="{{ form.staff.id_for_label }}" class="form-label fw-bold">
{{ form.staff.label }} <span class="text-danger">*</span>
</label>
{{ form.staff }}
{% if form.staff.errors %}
<div class="text-danger small mt-1">
{% for error in form.staff.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">Select a staff member to assign to this job posting.</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<label for="{{ form.assignment_date.id_for_label }}" class="form-label fw-bold">
{{ form.assignment_date.label }}
</label>
{{ form.assignment_date }}
{% if form.assignment_date.errors %}
<div class="text-danger small mt-1">
{% for error in form.assignment_date.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">Date when staff assignment becomes effective.</div>
</div>
</div>
<div class="row mb-3">
<div class="col-md-12">
<label for="{{ form.notes.id_for_label }}" class="form-label fw-bold">
{{ form.notes.label }}
</label>
{{ form.notes }}
{% if form.notes.errors %}
<div class="text-danger small mt-1">
{% for error in form.notes.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">Optional notes about this staff assignment.</div>
</div>
</div>
<div class="row">
<div class="col-12">
<button type="submit" class="btn btn-main-action">
<i class="fas fa-user-plus me-2"></i>Assign Staff to Job
</button>
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary ms-2">
<i class="fas fa-times me-2"></i>Cancel
</a>
</div>
</div>
</form>
</div>
</div>
<!-- Current Staff Assignments -->
{% if job.staff_assignments.exists %}
<div class="kaauh-card mt-4">
<div class="card-header">
<h5 class="mb-0 text-primary"><i class="fas fa-users me-2"></i>Current Staff Assignments</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Staff Member</th>
<th>Email</th>
<th>Assignment Date</th>
<th>Notes</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for assignment in job.staff_assignments.all %}
<tr>
<td>
<strong>{{ assignment.staff.get_full_name|default:assignment.staff.username }}</strong>
{% if assignment.staff.is_active %}
<span class="badge bg-success ms-2">Active</span>
{% else %}
<span class="badge bg-danger ms-2">Inactive</span>
{% endif %}
</td>
<td>{{ assignment.staff.email }}</td>
<td>{{ assignment.assignment_date|date:"M d, Y" }}</td>
<td>{{ assignment.notes|default:"-" }}</td>
<td>
<span class="badge bg-primary">Assigned</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endif %}
<!-- Available Staff Members -->
<div class="kaauh-card mt-4">
<div class="card-header">
<h5 class="mb-0 text-primary"><i class="fas fa-users me-2"></i>Available Staff Members</h5>
</div>
<div class="card-body">
<div class="row">
{% for staff_user in staff_users %}
<div class="col-md-6 col-lg-4 mb-3">
<div class="staff-card card h-100">
<div class="card-body">
<div class="d-flex align-items-center mb-2">
<div class="rounded-circle text-white d-flex align-items-center justify-content-center me-3" style="width: 40px; height: 40px; background-color: var(--kaauh-teal);">
{% if staff_user.first_name %}
{{ staff_user.first_name.0 }}{{ staff_user.last_name.0 }}
{% else %}
{{ staff_user.username.0|upper }}
{% endif %}
</div>
<div>
<h6 class="mb-0">{{ staff_user.get_full_name|default:staff_user.username }}</h6>
<small class="text-muted">{{ staff_user.email }}</small>
</div>
</div>
<div class="mt-2">
{% if staff_user.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
<span class="badge bg-danger">Inactive</span>
{% endif %}
<span class="badge bg-primary ms-1">Staff</span>
</div>
</div>
</div>
</div>
{% empty %}
<div class="col-12">
<div class="alert alert-warning">
<i class="fas fa-exclamation-triangle me-2"></i>
No staff members available. Please create staff accounts first.
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal for Staff Assignment Confirmation -->
<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>Confirm Staff Assignment
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p>Are you sure you want to assign this staff member to the job <strong>{{ job.title }}</strong>?</p>
<div id="selectedStaffInfo"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-main-action" id="confirmAssignmentBtn">
<i class="fas fa-check me-2"></i>Confirm Assignment
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('staffAssignmentForm');
const modal = new bootstrap.Modal(document.getElementById('staffAssignmentModal'));
const confirmBtn = document.getElementById('confirmAssignmentBtn');
const selectedStaffInfo = document.getElementById('selectedStaffInfo');
// Handle form submission with modal confirmation
if (form) {
form.addEventListener('submit', function(e) {
e.preventDefault();
const staffSelect = document.getElementById('{{ form.staff.id_for_label }}');
const selectedOption = staffSelect.options[staffSelect.selectedIndex];
if (selectedOption.value) {
// Show selected staff info in modal
selectedStaffInfo.innerHTML = `
<div class="alert alert-info">
<strong>Selected Staff Member:</strong><br>
Name: ${selectedOption.text}<br>
This assignment will take effect immediately.
</div>
`;
// Show modal
modal.show();
} else {
alert('Please select a staff member.');
}
});
}
// Handle modal confirmation
if (confirmBtn) {
confirmBtn.addEventListener('click', function() {
// Hide modal
modal.hide();
// Submit the form
form.submit();
});
}
// Auto-focus on staff select field
const staffSelect = document.getElementById('{{ form.staff.id_for_label }}');
if (staffSelect) {
staffSelect.focus();
}
});
</script>
{% endblock %}