Merge branch 'main' of http://10.10.1.136:3000/marwan/kaauh_ats into frontend
This commit is contained in:
commit
60e3f81620
1
.gitignore
vendored
1
.gitignore
vendored
@ -22,6 +22,7 @@ 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.
@ -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)}"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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'),
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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');
|
||||||
|
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
432
templates/recruitment/staff_assignment_view.html
Normal file
432
templates/recruitment/staff_assignment_view.html
Normal 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 %}
|
||||||
Loading…
x
Reference in New Issue
Block a user