Compare commits

..

No commits in common. "9b2bf34431d843ebc8bccd0c5d64c45db8f8500c" and "3934d1cebe2b8876b4764322a5241d7da4aa7feb" have entirely different histories.

94 changed files with 286 additions and 980 deletions

3
.env
View File

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

1
.gitignore vendored
View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -303,7 +303,6 @@ 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:
@ -360,78 +359,98 @@ 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: 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'
# and 'agency_emails', but keeping your current logic structure to minimize changes. # and 'agency_emails', but keeping your current logic structure to minimize changes.
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa') from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa')
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:
email_obj = EmailMultiAlternatives(subject=subject, body=body_message, from_email=from_email, to=[recipient])
if attachments:
for attachment in attachments:
if hasattr(attachment, 'read'):
filename = getattr(attachment, 'name', 'attachment')
content = attachment.read()
content_type = getattr(attachment, 'content_type', 'application/octet-stream')
email_obj.attach(filename, content, content_type)
elif isinstance(attachment, tuple) and len(attachment) == 3:
filename, content, content_type = attachment
email_obj.attach(filename, content, content_type)
try:
result=email_obj.send(fail_silently=False)
if result==1:
try:
user=get_object_or_404(User,email=recipient)
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)}")
else:
logger.error("fialed to send email")
successful_sends += 1
except Exception as e:
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
if not from_interview:
# Send Emails - Pure Candidates
for email in pure_candidate_emails:
candidate_name = Application.objects.filter(person__email=email).first().person.full_name
candidate_message = f"Hi, {candidate_name}" + "\n" + message
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: else:
email_obj = EmailMultiAlternatives(subject=subject, body=body_message, from_email=from_email, to=[recipient]) for email in recipient_list:
send_individual_email(email, message)
if attachments: logger.info(f"Interview email processing complete. Sent successfully to {successful_sends} out of {total_recipients} recipients.")
for attachment in attachments: return {
if hasattr(attachment, 'read'): 'success': True,
filename = getattr(attachment, 'name', 'attachment') 'recipients_count': successful_sends,
content = attachment.read() 'message': f'Interview emails sent successfully to {successful_sends} recipient(s).'
content_type = getattr(attachment, 'content_type', 'application/octet-stream') }
email_obj.attach(filename, content, content_type)
elif isinstance(attachment, tuple) and len(attachment) == 3:
filename, content, content_type = attachment
email_obj.attach(filename, content, content_type)
try: except Exception as e:
email_obj.send(fail_silently=False) error_msg = f"Failed to process bulk email send request: {str(e)}"
successful_sends += 1 logger.error(error_msg, exc_info=True)
except Exception as e: return {'success': False, 'error': error_msg}
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
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)
# Send Emails - Agencies
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
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:
error_msg = f"Failed to process bulk email send request: {str(e)}"
logger.error(error_msg, exc_info=True)
return {'success': False, 'error': error_msg}

View File

@ -2453,52 +2453,3 @@ 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

@ -985,10 +985,6 @@ class Application(Base):
content_type = ContentType.objects.get_for_model(self.__class__) content_type = ContentType.objects.get_for_model(self.__class__)
return Document.objects.filter(content_type=content_type, object_id=self.id) return Document.objects.filter(content_type=content_type, object_id=self.id)
# @property
# def belong_to_agency(self):
class TrainingMaterial(Base): class TrainingMaterial(Base):
title = models.CharField(max_length=255, verbose_name=_("Title")) title = models.CharField(max_length=255, verbose_name=_("Title"))

View File

@ -22,8 +22,6 @@ 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__)
@ -43,7 +41,6 @@ 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,8 +606,6 @@ 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):
@ -845,5 +843,3 @@ 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,7 +35,6 @@ 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,8 +30,7 @@ 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
@ -601,7 +600,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 = Application.objects.filter(job=job) entries = Candidate.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()
@ -643,7 +642,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"] = (
f'attachment; filename="all_cvs_for_{job.title}.zip"' 'attachment; filename=f"all_cvs_for_{job.title}.zip"'
) )
return response return response
@ -2928,34 +2927,6 @@ 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
@ -3033,8 +3004,6 @@ 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":
@ -4617,34 +4586,7 @@ 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()
# Send email if message_type is 'email' and recipient has email messages.success(request, "Message sent successfully!")
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']
@ -4702,34 +4644,7 @@ def message_reply(request, message_id):
message.recipient = parent_message.sender message.recipient = parent_message.sender
message.save() message.save()
# Send email if message_type is 'email' and recipient has email messages.success(request, "Reply sent successfully!")
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.")
@ -5232,35 +5147,17 @@ 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 will be sent shortly to recipient(s)", f"Email sent successfully to {len(email_addresses)} recipient(s).",
) )
response = HttpResponse(status=200)
response.headers["HX-Refresh"] = "true"
return response return redirect("candidate_interview_view", slug=job.slug)
# return redirect("candidate_interview_view", slug=job.slug)
else: else:
messages.error( messages.error(
request, request,
@ -5288,6 +5185,8 @@ 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
@ -5662,29 +5561,9 @@ 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 will be sent shortly to {total_recipients} recipient(s).", f"Email sent successfully to {total_recipients} recipient(s).",
) )
return redirect("list_meetings") return redirect("list_meetings")
@ -5804,7 +5683,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 id="messageContent" class="container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;"> <main 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,21 +417,9 @@
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,13 +17,7 @@
<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 %}" <form hx-boost="true" method="post" id="email-compose-form" action="{% url 'compose_candidate_email' job.slug %}" hx-include="#candidate-form">
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 -->
@ -177,8 +171,8 @@
<script> <script>
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('email-compose-form1'); const form = document.getElementById('email-compose-form');
const sendBtn = document.getElementById('send-email-btn1'); const sendBtn = document.getElementById('send-email-btn');
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

@ -364,37 +364,12 @@ body { background-color: #f0f2f5; font-family: 'Inter', sans-serif; }
</div> </div>
<form method="post" action="{% url 'create_interview_participants' interview.slug %}"> <form method="post" action="{% url 'create_interview_participants' interview.slug %}">
{% csrf_token %} {% csrf_token %}
<div class="modal-body table-responsive"> <div class="modal-body">
{{ form.participants.errors }}
{{ meeting.name }} {{ form.participants }}
{{ form.system_users.errors }}
<hr> {{ form.system_users }}
</div>
<table class="table tab table-bordered mt-3">
<thead>
<th class="col">👥 {% trans "Participants" %}</th>
<th class="col">🧑‍💼 {% trans "Users" %}</th>
</thead>
<tbody>
<tr>
<td>
{{ form.participants.errors }}
{{ form.participants }}
</td>
<td> {{ form.system_users.errors }}
{{ form.system_users }}
</td>
</tr>
</table>
</div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-danger-red" data-bs-dismiss="modal">{% trans "Close" %}</button> <button type="button" class="btn btn-danger-red" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="submit" class="btn btn-primary-teal">{% trans "Save" %}</button> <button type="submit" class="btn btn-primary-teal">{% trans "Save" %}</button>
@ -412,7 +387,7 @@ body { background-color: #f0f2f5; font-family: 'Inter', sans-serif; }
<h5 class="modal-title">📧 {% trans "Compose Interview Invitation" %}</h5> <h5 class="modal-title">📧 {% trans "Compose Interview Invitation" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<form method="post" action="{% url 'send_interview_email' interview.slug %}"> <form method="post" action="{% url 'send_interview_email' interview.pk %}">
{% csrf_token %} {% csrf_token %}
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">

View File

@ -301,11 +301,6 @@
<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" %}
@ -374,54 +369,7 @@
</div> </div>
</div> </div>
{# TAB 4: STAFF ASSIGNMENT CONTENT #} {# TAB 4: LINKEDIN INTEGRATION 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

@ -1,432 +0,0 @@
{% 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 %}