Compare commits

...

10 Commits

Author SHA1 Message Date
fcc74dbac2 messages and email 2025-11-18 19:03:20 +03:00
acee95995c messages creation for email 2025-11-18 18:26:45 +03:00
ee88e8bad8 new change main.cc 2025-11-18 14:12:05 +03:00
8df7b2d42a update the css 2025-11-18 14:05:28 +03:00
0219da4f07 before pull 2025-11-18 14:04:36 +03:00
b4b56d8a9d Merge branch 'main' of http://10.10.1.136:3000/marwan/kaauh_ats into frontend 2025-11-18 13:53:03 +03:00
89ff9a7a59 .. 2025-11-18 13:52:56 +03:00
b61bb5222e merge 2025-11-18 13:52:25 +03:00
f510885092 more changes and bug fixes 2025-11-18 13:22:20 +03:00
90c2cee0a3 Merge pull request 'frontend' (#32) from frontend into main
Reviewed-on: #32
2025-11-18 13:21:44 +03:00
46 changed files with 3379 additions and 360 deletions

2
.env
View File

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

View File

@ -487,3 +487,6 @@ MESSAGE_TAGS = {
# Custom User Model
AUTH_USER_MODEL = "recruitment.CustomUser"
ZOOM_WEBHOOK_API_KEY = "2GNDC5Rvyw9AHoGikHXsQB"

View File

@ -1,15 +1,24 @@
"""
Email service for sending notifications related to agency messaging.
"""
from .models import Application
from django.shortcuts import get_object_or_404
import logging
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.utils.html import strip_tags
from django_q.tasks import async_task # Import needed at the top for clarity
logger = logging.getLogger(__name__)
from django.core.mail import send_mail, EmailMultiAlternatives
from django.conf import settings
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.contrib.auth import get_user_model
import logging
from .models import Message
logger = logging.getLogger(__name__)
User=get_user_model()
class EmailService:
"""
@ -225,17 +234,10 @@ def send_interview_invitation_email(candidate, job, meeting_details=None, recipi
return {'success': False, 'error': error_msg}
from .models import Application
from django.shortcuts import get_object_or_404
import logging
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.utils.html import strip_tags
from django_q.tasks import async_task # Import needed at the top for clarity
logger = logging.getLogger(__name__)
def send_bulk_email(subject, message, recipient_list, request=None, attachments=None, async_task_=False, from_interview=False):
def send_bulk_email(subject, message, recipient_list, request=None, attachments=None, async_task_=False, from_interview=False,job=None):
"""
Send bulk email to multiple recipients with HTML support and attachments,
supporting synchronous or asynchronous dispatch.
@ -301,7 +303,8 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
processed_attachments = attachments if attachments else []
task_ids = []
job_id=job.id
sender_user_id=request.user.id if request and hasattr(request, 'user') and request.user.is_authenticated else None
if not from_interview:
# Loop through ALL final customized sends
for recipient_email, custom_message in customized_sends:
@ -311,7 +314,10 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
custom_message, # Pass the custom message
[recipient_email], # Pass the specific recipient as a list of one
processed_attachments,
hook='recruitment.tasks.email_success_hook'
sender_user_id,
job_id,
hook='recruitment.tasks.email_success_hook',
)
task_ids.append(task_id)
@ -350,80 +356,101 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
except Exception as e:
logger.error(f"Failed to queue async tasks: {str(e)}", exc_info=True)
return {'success': False, 'error': f"Failed to queue async tasks: {str(e)}"}
else:
# --- 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
# Helper Function for Sync Send (as provided)
def send_individual_email(recipient, body_message):
# ... (Existing helper function logic) ...
nonlocal successful_sends
if is_html:
plain_message = strip_tags(body_message)
email_obj = EmailMultiAlternatives(subject=subject, body=plain_message, from_email=from_email, to=[recipient])
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:
email_obj.send(fail_silently=False)
successful_sends += 1
# 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
# Helper Function for Sync Send (as provided)
def send_individual_email(recipient, body_message):
# ... (Existing helper function logic) ...
nonlocal successful_sends
if is_html:
plain_message = strip_tags(body_message)
email_obj = EmailMultiAlternatives(subject=subject, body=plain_message, from_email=from_email, to=[recipient])
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:
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:
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}
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

@ -55,7 +55,7 @@ class SourceForm(forms.ModelForm):
class Meta:
model = Source
fields = ["name", "source_type", "description", "ip_address", "is_active"]
fields = ["name", "source_type", "description", "ip_address","trusted_ips", "is_active"]
widgets = {
"name": forms.TextInput(
attrs={
@ -81,6 +81,9 @@ class SourceForm(forms.ModelForm):
"ip_address": forms.TextInput(
attrs={"class": "form-control", "placeholder": "192.168.1.100"}
),
"trusted_ips":forms.TextInput(
attrs={"class": "form-control", "placeholder": "192.168.1.100","required": False}
),
"is_active": forms.CheckboxInput(attrs={"class": "form-check-input"}),
}
@ -725,7 +728,7 @@ class InterviewScheduleForm(forms.ModelForm):
)
class Meta:
model = InterviewSchedule
model = InterviewSchedule
fields = [
'schedule_interview_type',
"applications",
@ -1541,8 +1544,7 @@ class CandidateEmailForm(forms.Form):
label=_('Select Candidates'), # Use a descriptive label
required=False
)
subject = forms.CharField(
max_length=200,
widget=forms.TextInput(attrs={
@ -1565,29 +1567,29 @@ class CandidateEmailForm(forms.Form):
required=True
)
def __init__(self, job, candidates, *args, **kwargs):
super().__init__(*args, **kwargs)
self.job = job
self.candidates=candidates
candidate_choices=[]
for candidate in candidates:
candidate_choices.append(
(f'candidate_{candidate.id}', f'{candidate.email}')
)
self.fields['to'].choices =candidate_choices
self.fields['to'].initial = [choice[0] for choice in candidate_choices]
self.fields['to'].initial = [choice[0] for choice in candidate_choices]
# Set initial message with candidate and meeting info
initial_message = self._get_initial_message()
if initial_message:
self.fields['message'].initial = initial_message
@ -1595,7 +1597,7 @@ class CandidateEmailForm(forms.Form):
"""Generate initial message with candidate and meeting information"""
candidate=self.candidates.first()
message_parts=[]
if candidate and candidate.stage == 'Applied':
message_parts = [
f"Than you, for your interest in the {self.job.title} role.",
@ -1615,18 +1617,18 @@ class CandidateEmailForm(forms.Form):
f"We look forward to reviewing your results.",
f"Best regards, The KAAUH Hiring team"
]
elif candidate and candidate.stage == 'Interview':
message_parts = [
f"Than you, for your interest in the {self.job.title} role.",
f"We're pleased to inform you that your initial screening was successful!",
f"We're pleased to inform you that you have cleared your exam!",
f"The next step is the mandatory online assessment exam.",
f"Please complete the assessment by using the following link:",
f"https://kaauh/hire/exam",
f"We look forward to reviewing your results.",
f"Best regards, The KAAUH Hiring team"
]
elif candidate and candidate.stage == 'Offer':
message_parts = [
f"Congratulations, ! We are delighted to inform you that we are extending a formal offer of employment for the {self.job.title} role.",
@ -1645,9 +1647,9 @@ class CandidateEmailForm(forms.Form):
f"If you have any questions before your start date, please contact [Onboarding Contact].",
f"Best regards, The KAAUH Hiring team"
]
# # Add candidate information
# if self.candidate:
@ -1672,9 +1674,9 @@ class CandidateEmailForm(forms.Form):
def get_email_addresses(self):
"""Extract email addresses from selected recipients"""
email_addresses = []
candidates=self.cleaned_data.get('to',[])
if candidates:
for candidate in candidates:
if candidate.startswith('candidate_'):
@ -1688,7 +1690,7 @@ class CandidateEmailForm(forms.Form):
return list(set(email_addresses)) # Remove duplicates
def get_formatted_message(self):
"""Get the formatted message with optional additional information"""
@ -1868,7 +1870,7 @@ class InterviewEmailForm(forms.Form):
location = meeting.interview_location
# --- Data Preparation ---
# Safely access details through the related InterviewLocation object
if location and location.start_time:
formatted_date = location.start_time.strftime('%Y-%m-%d')
@ -1881,7 +1883,7 @@ class InterviewEmailForm(forms.Form):
formatted_time = "TBD"
duration = "N/A"
meeting_link = "Not Available"
job_title = job.title
agency_name = candidate.hiring_agency.name if candidate.belong_to_an_agency and candidate.hiring_agency else "Hiring Agency"
@ -1914,7 +1916,7 @@ Best regards,
KAAUH Hiring Team
"""
# ... (Messages for agency and participants remain the same, using the updated safe variables)
# --- 2. Agency Message (Professional and clear details) ---
agency_message = f"""
Dear {agency_name},
@ -1966,44 +1968,44 @@ class OnsiteLocationForm(forms.ModelForm):
class Meta:
model = OnsiteLocationDetails
# Include 'room_number' and update the field list
fields = ['topic', 'physical_address', 'room_number']
fields = ['topic', 'physical_address', 'room_number']
widgets = {
'topic': forms.TextInput(
attrs={'placeholder': 'Enter the Meeting Topic', 'class': 'form-control'}
),
'physical_address': forms.TextInput(
attrs={'placeholder': 'Physical address (e.g., street address)', 'class': 'form-control'}
),
'room_number': forms.TextInput(
attrs={'placeholder': 'Room Number/Name (Optional)', 'class': 'form-control'}
),
}
class OnsiteReshuduleForm(forms.ModelForm):
class Meta:
model = OnsiteLocationDetails
fields = ['topic', 'physical_address', 'room_number','start_time','duration','status']
fields = ['topic', 'physical_address', 'room_number','start_time','duration','status']
widgets = {
'topic': forms.TextInput(
attrs={'placeholder': 'Enter the Meeting Topic', 'class': 'form-control'}
),
'physical_address': forms.TextInput(
attrs={'placeholder': 'Physical address (e.g., street address)', 'class': 'form-control'}
),
'room_number': forms.TextInput(
attrs={'placeholder': 'Room Number/Name (Optional)', 'class': 'form-control'}
),
}
class OnsiteScheduleForm(forms.ModelForm):
# Add fields for the foreign keys required by ScheduledInterview
@ -2021,8 +2023,8 @@ class OnsiteScheduleForm(forms.ModelForm):
class Meta:
model = OnsiteLocationDetails
# Include all fields from OnsiteLocationDetails plus the new ones
fields = ['topic', 'physical_address', 'room_number', 'start_time', 'duration', 'status', 'application', 'job']
fields = ['topic', 'physical_address', 'room_number', 'start_time', 'duration', 'status', 'application', 'job']
widgets = {
'topic': forms.TextInput(
attrs={'placeholder': _('Enter the Meeting Topic'), 'class': 'form-control'}
@ -2033,7 +2035,7 @@ class OnsiteScheduleForm(forms.ModelForm):
'room_number': forms.TextInput(
attrs={'placeholder': _('Room Number/Name (Optional)'), 'class': 'form-control'}
),
# You should explicitly set widgets for start_time, duration, and status here
# You should explicitly set widgets for start_time, duration, and status here
# if they need Bootstrap classes, otherwise they will use default HTML inputs.
# Example:
'start_time': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}),
@ -2228,7 +2230,7 @@ class CandidateSignupForm(forms.ModelForm):
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'phone': forms.TextInput(attrs={'class': 'form-control'}),
'gpa': forms.TextInput(attrs={'class': 'form-control'}),
# 'gpa': forms.TextInput(attrs={'class': 'form-control'}),
"nationality": forms.Select(attrs={'class': 'form-control select2'}),
'date_of_birth': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
'gender': forms.Select(attrs={'class': 'form-control'}),

View File

@ -0,0 +1,29 @@
# Generated by Django 5.2.6 on 2025-11-18 10:24
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='jobposting',
name='job_type',
field=models.CharField(choices=[('Full-time', 'Full-time'), ('Part-time', 'Part-time'), ('Contract', 'Contract'), ('Internship', 'Internship'), ('Faculty', 'Faculty'), ('Temporary', 'Temporary')], default='Full-time', max_length=20),
),
migrations.AlterField(
model_name='jobposting',
name='workplace_type',
field=models.CharField(choices=[('On-site', 'On-site'), ('Remote', 'Remote'), ('Hybrid', 'Hybrid')], default='On-site', max_length=20),
),
migrations.AlterField(
model_name='scheduledinterview',
name='interview_location',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scheduled_interview', to='recruitment.interviewlocation', verbose_name='Meeting/Location Details'),
),
]

View File

@ -99,9 +99,9 @@ class JobPosting(Base):
# Core Fields
title = models.CharField(max_length=200)
department = models.CharField(max_length=100, blank=True)
job_type = models.CharField(max_length=20, choices=JOB_TYPES, default="FULL_TIME")
job_type = models.CharField(max_length=20, choices=JOB_TYPES, default="Full-time")
workplace_type = models.CharField(
max_length=20, choices=WORKPLACE_TYPES, default="ON_SITE"
max_length=20, choices=WORKPLACE_TYPES, default="On-site"
)
# Location

View File

@ -18,7 +18,9 @@ from .models import (
Notification,
HiringAgency,
Person,
Source,
)
from .forms import generate_api_key, generate_api_secret
from django.contrib.auth import get_user_model
logger = logging.getLogger(__name__)
@ -29,11 +31,10 @@ User = get_user_model()
@receiver(post_save, sender=JobPosting)
def format_job(sender, instance, created, **kwargs):
if created or not instance.ai_parsed:
try:
form_template = instance.form_template
except FormTemplate.DoesNotExist:
form = getattr(instance, "form_template", None)
if not form:
FormTemplate.objects.get_or_create(
job=instance, is_active=False, name=instance.title
job=instance, is_active=True, name=instance.title
)
async_task(
"recruitment.tasks.format_job_description",
@ -469,3 +470,27 @@ def person_created(sender, instance, created, **kwargs):
)
instance.user = user
instance.save()
@receiver(post_save, sender=Source)
def source_created(sender, instance, created, **kwargs):
"""
Automatically generate API key and API secret when a new Source is created.
"""
if created:
# Only generate keys if they don't already exist
if not instance.api_key and not instance.api_secret:
logger.info(f"Generating API keys for new Source: {instance.pk} - {instance.name}")
# Generate API key and secret using existing secure functions
api_key = generate_api_key()
api_secret = generate_api_secret()
# Update the source with generated keys
instance.api_key = api_key
instance.api_secret = api_secret
instance.save(update_fields=['api_key', 'api_secret'])
logger.info(f"API keys generated successfully for Source: {instance.name} (Key: {api_key[:8]}...)")
else:
logger.info(f"Source {instance.name} already has API keys, skipping generation")

View File

@ -12,8 +12,9 @@ from . linkedin_service import LinkedInService
from django.shortcuts import get_object_or_404
from . models import JobPosting
from django.utils import timezone
from . models import InterviewSchedule,ScheduledInterview,ZoomMeetingDetails
from . models import InterviewSchedule,ScheduledInterview,ZoomMeetingDetails,Message
from django.contrib.auth import get_user_model
User = get_user_model()
# Add python-docx import for Word document processing
try:
from docx import Document
@ -755,7 +756,7 @@ from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.utils.html import strip_tags
def _task_send_individual_email(subject, body_message, recipient, attachments):
def _task_send_individual_email(subject, body_message, recipient, attachments,sender,job):
"""Internal helper to create and send a single email."""
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa')
@ -775,16 +776,36 @@ def _task_send_individual_email(subject, body_message, recipient, attachments):
email_obj.attach(filename, content, content_type)
try:
email_obj.send(fail_silently=False)
return True
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=sender,
recipient=user,
job=job,
subject=subject,
content=body_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")
except Exception as e:
logger.error(f"Task failed to send email to {recipient}: {str(e)}", exc_info=True)
return False
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
def send_bulk_email_task(subject, message, recipient_list, attachments=None, hook='recruitment.tasks.email_success_hook'):
def send_bulk_email_task(subject, message, recipient_list,attachments=None,sender_user_id=None,job_id=None, hook='recruitment.tasks.email_success_hook'):
"""
Django-Q background task to send pre-formatted email to a list of recipients.
Django-Q background task to send pre-formatted email to a list of recipients.,
Receives arguments directly from the async_task call.
"""
logger.info(f"Starting bulk email task for {len(recipient_list)} recipients")
@ -793,11 +814,13 @@ def send_bulk_email_task(subject, message, recipient_list, attachments=None, hoo
if not recipient_list:
return {'success': False, 'error': 'No recipients provided to task.'}
sender=get_object_or_404(User,pk=sender_user_id)
job=get_object_or_404(JobPosting,pk=job_id)
# Since the async caller sends one task per recipient, total_recipients should be 1.
for recipient in recipient_list:
# The 'message' is the custom message specific to this recipient.
if _task_send_individual_email(subject, message, recipient, attachments):
if _task_send_individual_email(subject, message, recipient, attachments,sender,job):
successful_sends += 1
if successful_sends > 0:

View File

@ -1336,6 +1336,7 @@ def application_submit(request, template_slug):
# email = submission.responses.get(field__label="Email Address")
# phone = submission.responses.get(field__label="Phone Number")
# address = submission.responses.get(field__label="Address")
gpa = submission.responses.get(field__label="GPA")
resume = submission.responses.get(field__label="Resume Upload")
@ -1346,6 +1347,8 @@ def application_submit(request, template_slug):
submission.save()
# time=timezone.now()
person = request.user.person_profile
person.gpa = gpa.value if gpa else None
person.save()
Application.objects.create(
person = person,
resume=resume.get_file if resume.is_file else None,
@ -1806,7 +1809,7 @@ def candidate_screening_view(request, slug):
min_experience_str = request.GET.get("min_experience")
screening_rating = request.GET.get("screening_rating")
tier1_count_str = request.GET.get("tier1_count")
gpa = request.GET.get("gpa")
gpa = request.GET.get("GPA")
try:
# Check if the string value exists and is not an empty string before conversion
@ -1854,8 +1857,9 @@ def candidate_screening_view(request, slug):
)
if gpa:
candidates = candidates.filter(
person__gpa = gpa
person__gpa__gt= gpa
)
print(candidates)
if tier1_count > 0:
candidates = candidates[:tier1_count]
@ -3018,7 +3022,6 @@ def is_superuser_check(user):
def create_staff_user(request):
if request.method == "POST":
form = StaffUserCreationForm(request.POST)
print(form)
if form.is_valid():
form.save()
messages.success(
@ -3034,7 +3037,7 @@ def create_staff_user(request):
@staff_user_required
def admin_settings(request):
staffs = User.objects.filter(is_superuser=False)
staffs = User.objects.filter(user_type="staff",is_superuser=False)
form = ToggleAccountForm()
context = {"staffs": staffs, "form": form}
return render(request, "user/admin_settings.html", context)
@ -3097,12 +3100,11 @@ def account_toggle_status(request, pk):
@csrf_exempt
@staff_user_required
def zoom_webhook_view(request):
print(request.headers)
print(settings.ZOOM_WEBHOOK_API_KEY)
# if api_key != settings.ZOOM_WEBHOOK_API_KEY:
# return HttpResponse(status=405)
api_key = request.headers.get("X-Zoom-API-KEY")
if api_key != settings.ZOOM_WEBHOOK_API_KEY:
return HttpResponse(status=405)
if request.method == "POST":
try:
payload = json.loads(request.body)
@ -4690,7 +4692,8 @@ def message_detail(request, message_id):
@login_required
def message_create(request):
"""Create a new message"""
"""Create a new message"""
from .email_service import EmailService
if request.method == "POST":
form = MessageForm(request.user, request.POST)
@ -4698,8 +4701,25 @@ def message_create(request):
message = form.save(commit=False)
message.sender = request.user
message.save()
messages.success(request, "Message sent successfully!")
["recipient", "job", "subject", "content", "message_type"]
recipient_email = form.cleaned_data['recipient'].email # Assuming recipient is a User or Model with an 'email' field
subject = form.cleaned_data['subject']
custom_message = form.cleaned_data['content']
job_id = form.cleaned_data['job'].id if 'job' in form.cleaned_data and form.cleaned_data['job'] else None
sender_user_id = request.user.id
task_id = async_task(
'recruitment.tasks.send_bulk_email_task',
subject,
custom_message, # Pass the custom message
[recipient_email], # Pass the specific recipient as a list of one
sender_user_id=sender_user_id,
job_id=job_id,
hook='recruitment.tasks.email_success_hook')
logger.info(f"{task_id} queued.")
return redirect("message_list")
else:
messages.error(request, "Please correct the errors below.")
@ -4712,6 +4732,8 @@ def message_create(request):
if request.user.user_type != "staff":
return render(request, "messages/candidate_message_form.html", context)
return render(request, "messages/message_form.html", context)
@login_required
def message_reply(request, message_id):
"""Reply to a message"""
@ -5204,7 +5226,7 @@ def compose_candidate_email(request, job_slug):
if request.method == 'POST':
print("........................................................inside candidate conpose.............")
candidate_ids = request.POST.getlist('candidate_ids')
candidates=Application.objects.filter(id__in=candidate_ids)
form = CandidateEmailForm(job, candidates, request.POST)
@ -5231,14 +5253,16 @@ def compose_candidate_email(request, job_slug):
# Send emails using email service (no attachments, synchronous to avoid pickle issues)
email_result = send_bulk_email(
email_result = send_bulk_email( #
subject=subject,
message=message,
recipient_list=email_addresses,
request=request,
attachments=None,
async_task_=True, # Changed to False to avoid pickle issues
from_interview=False
from_interview=False,
job=job
)
if email_result["success"]:
@ -5497,7 +5521,7 @@ def candidate_signup(request, slug):
gender = form.cleaned_data["gender"]
nationality = form.cleaned_data["nationality"]
address = form.cleaned_data["address"]
gpa = form.cleaned_data["gpa"]
# gpa = form.cleaned_data["gpa"]
password = form.cleaned_data["password"]
user = User.objects.create_user(
@ -5512,7 +5536,7 @@ def candidate_signup(request, slug):
phone=phone,
gender=gender,
nationality=nationality,
gpa=gpa,
# gpa=gpa,
address=address,
user = user
)

View File

@ -204,26 +204,27 @@ def generate_api_keys_view(request, pk):
source.save()
# Log the key regeneration
IntegrationLog.objects.create(
source=source,
action=IntegrationLog.ActionChoices.CREATE,
endpoint=f'/api/sources/{source.pk}/generate-keys/',
method='POST',
request_data={
'name': source.name,
'old_api_key': old_api_key[:8] + '...' if old_api_key else None,
'new_api_key': new_api_key[:8] + '...'
},
ip_address=request.META.get('REMOTE_ADDR'),
user_agent=request.META.get('HTTP_USER_AGENT', '')
)
# IntegrationLog.objects.create(
# source=source,
# action=IntegrationLog.ActionChoices.CREATE,
# endpoint=f'/api/sources/{source.pk}/generate-keys/',
# method='POST',
# request_data={
# 'name': source.name,
# 'old_api_key': old_api_key[:8] + '...' if old_api_key else None,
# 'new_api_key': new_api_key[:8] + '...'
# },
# ip_address=request.META.get('REMOTE_ADDR'),
# user_agent=request.META.get('HTTP_USER_AGENT', '')
# )
return JsonResponse({
'success': True,
'api_key': new_api_key,
'api_secret': new_api_secret,
'message': 'API keys regenerated successfully'
})
return redirect('source_detail', pk=source.pk)
# return JsonResponse({
# 'success': True,
# 'api_key': new_api_key,
# 'api_secret': new_api_secret,
# 'message': 'API keys regenerated successfully'
# })
return JsonResponse({'error': 'Invalid request method'}, status=405)
@ -244,27 +245,28 @@ def toggle_source_status_view(request, pk):
source.save()
# Log the status change
IntegrationLog.objects.create(
source=source,
action=IntegrationLog.ActionChoices.SYNC,
endpoint=f'/api/sources/{source.pk}/toggle-status/',
method='POST',
request_data={
'name': source.name,
'old_status': old_status,
'new_status': source.is_active
},
ip_address=request.META.get('REMOTE_ADDR'),
user_agent=request.META.get('HTTP_USER_AGENT', '')
)
# IntegrationLog.objects.create(
# source=source,
# action=IntegrationLog.ActionChoices.SYNC,
# endpoint=f'/api/sources/{source.pk}/toggle-status/',
# method='POST',
# request_data={
# 'name': source.name,
# 'old_status': old_status,
# 'new_status': source.is_active
# },
# ip_address=request.META.get('REMOTE_ADDR'),
# user_agent=request.META.get('HTTP_USER_AGENT', '')
# )
status_text = 'activated' if source.is_active else 'deactivated'
return JsonResponse({
'success': True,
'is_active': source.is_active,
'message': f'Source "{source.name}" {status_text} successfully'
})
return redirect('source_detail', pk=source.pk)
# return JsonResponse({
# 'success': True,
# 'is_active': source.is_active,
# 'message': f'Source "{source.name}" {status_text} successfully'
# })
def copy_to_clipboard_view(request):
"""HTMX endpoint to copy text to clipboard"""

721
static/css/main.css Normal file
View File

@ -0,0 +1,721 @@
/*
* KAAT-S Theme Styles (V2.0 - Consolidated Global, Nav, and Components)
* This file contains all variables, global layout styles, navigation, and component-specific styles.
*/
/* ---------------------------------- */
/* 1. UI Variables and Global Reset */
/* ---------------------------------- */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-light-bg: #f9fbfd;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-success: #28a745;
--kaauh-info: #17a2b8;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
/* Primary Color Overrides */
.text-success { color: var(--kaauh-success) !important; }
.text-info { color: var(--kaauh-info) !important; }
.text-danger { color: var(--kaauh-danger) !important; }
.text-warning { color: var(--kaauh-warning) !important; }
.text-primary-theme { color: var(--kaauh-teal) !important; }
.bg-primary-theme { background-color: var(--kaauh-teal) !important; }
/* Global Layout Control */
.max-width-1600 {
max-width: 1600px;
margin-right: auto;
margin-left: auto;
padding-right: var(--bs-gutter-x, 0.75rem);
padding-left: var(--bs-gutter-x, 0.75rem);
}
/* Global Container Padding for main content */
.container-fluid.py-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; }
/* Main content minimum height */
main.container-fluid {
min-height: calc(100vh - 200px);
padding: 1.5rem 0;
}
/* ---------------------------------- */
/* 2. Navigation and Header */
/* ---------------------------------- */
/* Top Bar (Contact/Social) */
.top-bar {
background-color: white;
border-bottom: 1px solid var(--kaauh-border);
font-size: 0.825rem;
padding: 0.4rem 0;
}
.top-bar a { text-decoration: none; }
.top-bar .social-icons i {
color: var(--kaauh-teal);
transition: color 0.2s;
}
.top-bar .social-icons i:hover {
color: var(--kaauh-teal-dark);
}
.top-bar .contact-item {
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.25rem 0.5rem;
}
.top-bar .logo-container img {
height: 60px;
object-fit: contain;
}
@media (max-width: 767.98px) {
.top-bar {
display: none;
}
}
/* Navbar */
.navbar-brand {
font-weight: 700;
letter-spacing: -0.5px;
font-size: 1.25rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.navbar-dark {
background-color: var(--kaauh-teal) !important;
box-shadow: 0 2px 6px rgba(0,0,0,0.12);
}
/* Ensure the inner container of the navbar stretches to allow max-width-1600 */
.navbar-dark > .container {
max-width: 100%;
}
.nav-link {
font-weight: 500;
transition: all 0.2s ease;
padding: 0.5rem 0.75rem;
}
.nav-link:hover,
.nav-link.active {
color: white !important;
background: rgba(255,255,255,0.12) !important;
border-radius: 4px;
}
/* Dropdown */
.dropdown-menu {
backdrop-filter: blur(4px);
background-color: rgba(255, 255, 255, 0.98);
border: 1px solid var(--kaauh-border);
box-shadow: 0 6px 20px rgba(0,0,0,0.12);
border-radius: 8px;
padding: 0.5rem 0;
min-width: 200px;
will-change: transform, opacity;
transition: transform 0.2s ease, opacity 0.2s ease;
}
.dropdown-item {
padding: 0.5rem 1.25rem;
transition: background-color 0.15s;
}
.dropdown-item:hover {
background-color: var(--kaauh-light-bg);
color: var(--kaauh-teal-dark);
}
/* Language Toggle Button Style */
.language-toggle-btn {
color: white !important;
background: none !important;
border: none !important;
display: flex;
align-items: center;
gap: 0.3rem;
padding: 0.5rem 0.75rem !important;
font-weight: 500;
transition: all 0.2s ease;
}
.language-toggle-btn:hover {
background: rgba(255,255,255,0.12) !important;
border-radius: 4px;
}
/* Profile Avatar */
.profile-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--kaauh-teal);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
font-size: 0.85rem;
transition: transform 0.1s ease;
}
.navbar-nav .dropdown-toggle:hover .profile-avatar {
transform: scale(1.05);
}
.navbar-nav .dropdown-toggle.p-0:hover {
background: none !important;
}
/* ---------------------------------- */
/* 3. Component Styles (Cards & Forms)*/
/* ---------------------------------- */
.kaauh-card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
/* NEW: Filter Controls Container Style */
.filter-controls {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
padding: 1.5rem; /* Consistent internal padding */
margin-bottom: 1.5rem; /* Space below filter */
}
/* Typography & Headers */
.page-header {
color: var(--kaauh-teal-dark);
font-weight: 700;
}
.section-header {
color: var(--kaauh-primary-text);
font-weight: 600;
border-bottom: 2px solid var(--kaauh-border);
padding-bottom: 0.5rem;
margin-bottom: 1rem;
}
label {
font-weight: 500;
color: var(--kaauh-primary-text);
font-size: 0.9rem;
margin-bottom: 0.25rem;
}
/* Forms - Default Size */
.form-control, .form-select {
border-radius: 0.5rem;
border: 1px solid #ced4da;
padding: 0.5rem 0.75rem;
font-size: 0.9rem;
}
/* Forms - Compact Size (for modals/tables) */
.form-control-sm,
.form-select-sm,
.btn-sm {
padding: 0.25rem 0.5rem !important; /* Adjusted padding */
font-size: 0.8rem !important;
line-height: 1.25 !important; /* Standard small line height */
height: auto !important;
}
.form-select-sm {
padding: 0.25rem 2rem 0.25rem 0.5rem !important; /* Increased right padding for arrow */
font-size: 0.8rem !important;
line-height: 1.25 !important;
height: auto !important; /* Remove fixed height */
border-radius: 0.5rem;
border: 1px solid #ced4da;
}
/* Scrollable Multiple Select Fix */
.form-group select[multiple] {
max-height: 450px;
overflow-y: auto;
min-height: 250px;
padding: 0;
}
/* Break Times Section Styling (Schedule Interviews) */
.break-time-form {
background-color: #f8f9fa;
padding: 0.75rem;
border-radius: 0.5rem;
border: 1px solid var(--kaauh-border);
align-items: flex-end;
}
.note-box {
background-color: #fff3cd;
border-left: 5px solid var(--kaauh-warning);
padding: 1rem;
border-radius: 0.25rem;
font-size: 0.9rem;
margin-bottom: 1rem;
}
/* Tier Controls (Kept for consistency/future use) */
.tier-controls {
background-color: var(--kaauh-border);
border-radius: 0.75rem;
padding: 1.25rem;
margin-bottom: 2rem;
border: 1px solid var(--kaauh-border);
}
.tier-controls .form-row {
display: flex;
align-items: end;
gap: 1rem;
}
.tier-controls .form-group {
flex: 1;
margin-bottom: 0;
}
/* ---------------------------------- */
/* 4. Button Styles (Component Themed)*/
/* ---------------------------------- */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
transition: all 0.2s ease;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.btn-main-action.btn-sm { font-weight: 600 !important; }
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
.btn-bulk-pass {
background-color: var(--kaauh-success);
border-color: var(--kaauh-success);
color: white;
font-weight: 500;
}
.btn-bulk-pass:hover {
background-color: #1e7e34;
border-color: #1e7e34;
}
.btn-bulk-fail {
background-color: var(--kaauh-danger);
border-color: var(--kaauh-danger);
color: white;
font-weight: 500;
}
.btn-bulk-fail:hover {
background-color: #bd2130;
border-color: #bd2130;
}
.btn-apply { /* From Job Board table */
background: var(--kaauh-teal);
border: none;
color: white;
padding: 0.45rem 1rem;
font-weight: 600;
border-radius: 6px;
transition: all 0.2s;
white-space: nowrap;
}
.btn-apply:hover {
background: var(--kaauh-teal-dark);
transform: translateY(-1px);
box-shadow: 0 2px 6px rgba(0,0,0,0.15);
}
/* ---------------------------------- */
/* 5. Table & Footer Styles */
/* ---------------------------------- */
.candidate-table {
table-layout: fixed;
width: 100%;
border-collapse: separate;
border-spacing: 0;
background-color: white;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
}
.candidate-table thead {
background-color: var(--kaauh-border);
}
.candidate-table th {
padding: 0.75rem;
font-weight: 600;
color: var(--kaauh-teal-dark);
border-bottom: 2px solid var(--kaauh-teal);
font-size: 0.9rem;
vertical-align: middle;
}
.candidate-table td {
padding: 0.75rem;
border-bottom: 1px solid var(--kaauh-border);
vertical-align: middle;
font-size: 0.9rem;
}
.candidate-table tbody tr:hover {
background-color: #f1f3f4;
}
.candidate-name {
font-weight: 600;
color: var(--kaauh-primary-text);
}
.candidate-details {
font-size: 0.8rem;
color: #6c757d;
}
/* Job Table Specific Styles */
.job-table-wrapper {
background: white;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 16px rgba(0,0,0,0.06);
margin-bottom: 2rem;
}
.job-table thead th {
background: var(--kaauh-teal);
color: white;
font-weight: 600;
padding: 1rem;
text-align: center;
}
.job-table td {
padding: 1rem;
vertical-align: middle;
text-align: center;
}
.job-table tr:hover td {
background-color: rgba(0, 99, 110, 0.03);
}
/* Table Responsiveness */
@media (max-width: 575.98px) {
.table-responsive {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.job-table th,
.job-table td {
white-space: nowrap;
font-size: 0.875rem;
}
}
/* Bulk Action Bar (Interview Management) */
.bulk-action-bar {
display: flex;
align-items: center;
gap: 0.5rem;
padding-bottom: 0.75rem;
margin-bottom: 1rem;
border-bottom: 1px solid var(--kaauh-border);
}
.action-group {
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Badges (Adapted for Interview Tiers/Status) */
.ai-score-badge { /* Used as an all-purpose secondary badge */
background-color: var(--kaauh-teal-dark) !important;
color: white;
font-weight: 700;
padding: 0.4em 0.8em;
border-radius: 0.4rem;
font-size: 0.8rem;
}
.tier-badge { /* Used for Tier labels */
font-size: 0.75rem;
padding: 0.125rem 0.5rem;
border-radius: 0.5rem;
font-weight: 600;
margin-left: 0.5rem;
display: inline-block;
}
.tier-1-badge { background-color: var(--kaauh-success); color: white; }
.tier-2-badge { background-color: var(--kaauh-warning); color: #856404; }
.tier-3-badge { background-color: #d1ecf1; color: #0c5460; }
.cd_interview{ color: #00636e; }
/* NEW: Application Stage Badges */
.stage-badge {
font-size: 0.8rem;
padding: 0.2em 0.6em;
border-radius: 0.4rem;
font-weight: 600;
display: inline-block;
margin-top: 0.25rem;
}
.stage-Application { background-color: #e9ecef; color: #495057; }
.stage-Screening { background-color: #d1ecf1; color: #0c5460; }
.stage-Exam { background-color: #cce5ff; color: #004085; }
.stage-Interview { background-color: #fff3cd; color: #856404; }
.stage-Offer { background-color: #d4edda; color: #155724; }
/* NEW: Applicant Status Badges */
.status-badge {
font-size: 0.8rem;
padding: 0.2em 0.6em;
border-radius: 0.4rem;
font-weight: 500;
display: inline-block;
}
.bg-candidate { background-color: var(--kaauh-teal-dark); color: white; }
.bg-applicant { background-color: #f8f9fa; color: #495057; border: 1px solid #ced4da; }
/* Table Column Width Fixes */
.candidate-table th:nth-child(1) { width: 40px; }
.candidate-table th:nth-child(4) { width: 15%; }
.candidate-table th:nth-child(5) { width: 80px; }
.candidate-table th:nth-child(6) { width: 180px; }
/* Footer & Alerts */
.footer {
background: var(--kaauh-light-bg);
padding: 1.5rem 0;
border-top: 1px solid var(--kaauh-border);
font-size: 0.9rem;
color: #555;
}
.alert {
border: none;
border-radius: 8px;
box-shadow: 0 2px 6px rgba(0,0,0,0.05);
}
/* ---------------------------------- */
/* 6. RTL Support */
/* ---------------------------------- */
html[dir="rtl"] {
text-align: right;
direction: rtl;
}
html[dir="rtl"] .navbar-brand {
flex-direction: row-reverse;
}
html[dir="rtl"] .dropdown-menu {
right: auto;
left: 0;
}
/* RTL Spacing adjustments */
html[dir="rtl"] .me-3 { margin-right: 0 !important; margin-left: 1rem !important; }
html[dir="rtl"] .ms-3 { margin-left: 0 !important; margin-right: 1rem !important; }
html[dir="rtl"] .me-2 { margin-right: 0 !important; margin-left: 0.5rem !important; }
html[dir="rtl"] .ms-2 { margin-left: 0 !important; margin-right: 0.5rem !important; }
html[dir="rtl"] .ms-auto { margin-left: 0 !important; margin-right: auto !important; }
html[dir="rtl"] .me-auto { margin-right: 0 !important; margin-left: auto !important; }
/* ================================================= */
/* 1. 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; }
.text-info { color: #17a2b8 !important; }
.text-success { color: #28a745 !important; }
.text-secondary { color: #6c757d !important; }
.text-kaauh-primary { color: var(--kaauh-primary-text); } /* Custom class for primary text color if needed */
/* ---------------------------------- */
/* 2. Button Styles */
/* ---------------------------------- */
/* Main Action Button Style (Used for Download Resume) */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
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);
border-color: var(--kaauh-teal-dark);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Outlined Button Styles */
.btn-outline-primary {
color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
}
.btn-outline-primary:hover {
background-color: var(--kaauh-teal);
color: white;
}
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
/* ---------------------------------- */
/* 3. Card/Modal Styles */
/* ---------------------------------- */
/* 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;
}
/* Candidate Header Card (The teal header) */
.candidate-header-card {
background: linear-gradient(135deg, var(--kaauh-teal), #004d57);
color: white;
border-radius: 0.75rem 0.75rem 0 0;
padding: 1.5rem;
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
}
.candidate-header-card h1 {
font-weight: 700;
margin: 0;
font-size: 1.8rem;
}
.candidate-header-card .badge {
font-size: 0.9rem;
padding: 0.4em 0.8em;
border-radius: 0.4rem;
font-weight: 700;
}
/* ---------------------------------- */
/* 4. Tab Navigation Styles (Candidate Detail View) */
/* ---------------------------------- */
/* Left Column Tabs (Main Content Tabs) */
.main-tabs {
border-bottom: 1px solid var(--kaauh-border);
background-color: #f8f9fa;
padding: 0 1.25rem;
}
.main-tabs .nav-link {
border: none;
border-bottom: 3px solid transparent;
color: var(--kaauh-primary-text);
font-weight: 500;
padding: 0.75rem 1rem;
margin-right: 0.5rem;
transition: all 0.2s;
}
.main-tabs .nav-link:hover {
color: var(--kaauh-teal);
}
.main-tabs .nav-link.active {
color: var(--kaauh-teal-dark) !important;
background-color: white !important;
border-bottom: 3px solid var(--kaauh-teal);
font-weight: 600;
}
/* Right Column Card (General styling for tab content if needed) */
.right-column-card .tab-content {
padding: 1.5rem 1.25rem;
background-color: white;
}
/* ---------------------------------- */
/* 5. Vertical Timeline Styling */
/* ---------------------------------- */
/* Highlight box for the current stage */
.current-stage {
border: 1px solid var(--kaauh-border);
background-color: #f0f8ff; /* Light, subtle blue background */
}
.current-stage .text-primary {
color: var(--kaauh-teal) !important;
}
.timeline {
position: relative;
padding-left: 2rem;
}
.timeline::before {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: 1.25rem;
width: 2px;
background-color: var(--kaauh-border);
}
.timeline-item {
position: relative;
margin-bottom: 2rem;
padding-left: 1.5rem;
}
.timeline-icon {
position: absolute;
left: 0;
top: 0;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 0.8rem;
z-index: 10;
border: 4px solid white;
}
.timeline-item:last-child {
margin-bottom: 0;
}
/* Custom Timeline Background Classes for Stages (Using Bootstrap color palette) */
.timeline-bg-applied { background-color: var(--kaauh-teal) !important; }
.timeline-bg-exam { background-color: #17a2b8 !important; }
.timeline-bg-interview { background-color: #ffc107 !important; }
.timeline-bg-offer { background-color: #28a745 !important; }
.timeline-bg-rejected { background-color: #dc3545 !important; }
.loading {
background: linear-gradient(135deg, var(--kaauh-teal), #004d57);
color: white;
border-radius: 0.75rem 0.75rem 0 0;
padding: 1.5rem;
box-shadow: 0 4px 10px rgba(0,0,0,0.15);
}

786
static/css/messages.css Normal file
View File

@ -0,0 +1,786 @@
/* Unified Messaging System Styles */
:root {
--primary-color: #00636e;
--secondary-color: #f8f9fa;
--light-bg: #f8f9fa;
--border-color: #dee2e6;
--text-color: #333;
--text-muted: #6c757d;
--success-color: #28a745;
--warning-color: #ffc107;
--danger-color: #dc3545;
--info-color: #17a2b8;
}
/* Main Layout */
.unified-messages {
display: flex;
height: calc(100vh - 120px);
background: white;
border-radius: 0.5rem;
overflow: hidden;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
}
/* Sidebar */
.messages-sidebar {
width: 350px;
border-right: 1px solid var(--border-color);
display: flex;
flex-direction: column;
background: var(--light-bg);
}
.sidebar-header {
padding: 1rem;
border-bottom: 1px solid var(--border-color);
background: white;
}
.sidebar-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tab-btn {
padding: 0.5rem 1rem;
border: none;
background: transparent;
color: var(--text-muted);
border-radius: 0.5rem;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.875rem;
font-weight: 500;
}
.tab-btn.active {
background: var(--primary-color);
color: white;
}
.tab-btn:hover:not(.active) {
background: var(--border-color);
}
.search-box {
position: relative;
}
.search-input {
width: 100%;
padding: 0.75rem 1rem 0.75rem 2.5rem;
border: 1px solid var(--border-color);
border-radius: 0.5rem;
font-size: 0.875rem;
transition: border-color 0.2s ease;
}
.search-input:focus {
outline: none;
border-color: var(--primary-color);
}
.search-icon {
position: absolute;
left: 0.75rem;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
}
.compose-btn {
width: 100%;
padding: 0.75rem;
background: var(--primary-color);
color: white;
border: none;
border-radius: 0.5rem;
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
.compose-btn:hover {
background: #004d57;
}
/* Conversation List */
.conversation-list {
flex: 1;
overflow-y: auto;
padding: 0.5rem;
}
.conversation-item {
display: flex;
align-items: center;
padding: 0.75rem;
border-radius: 0.5rem;
cursor: pointer;
transition: background-color 0.2s ease;
margin-bottom: 0.25rem;
border: 1px solid transparent;
}
.conversation-item:hover {
background: white;
border-color: var(--border-color);
}
.conversation-item.active {
background: white;
border-color: var(--primary-color);
box-shadow: 0 2px 8px rgba(0, 99, 110, 0.1);
}
.conversation-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--primary-color);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 1rem;
margin-right: 0.75rem;
flex-shrink: 0;
position: relative;
}
.online-indicator {
position: absolute;
bottom: 2px;
right: 2px;
width: 12px;
height: 12px;
background: var(--success-color);
border: 2px solid white;
border-radius: 50%;
}
.conversation-info {
flex: 1;
min-width: 0;
}
.conversation-name {
font-weight: 600;
color: var(--text-color);
margin-bottom: 0.25rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conversation-preview {
font-size: 0.875rem;
color: var(--text-muted);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.conversation-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 0.25rem;
}
.conversation-time {
font-size: 0.75rem;
color: var(--text-muted);
}
.unread-badge {
background: var(--primary-color);
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
}
/* Conversation Detail */
.conversation-detail {
flex: 1;
display: flex;
flex-direction: column;
}
.conversation-detail-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
background: white;
display: flex;
align-items: center;
justify-content: space-between;
}
.conversation-detail-info {
display: flex;
align-items: center;
}
.conversation-detail-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--primary-color);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
margin-right: 0.75rem;
}
.conversation-detail-name {
font-weight: 600;
color: var(--text-color);
margin-bottom: 0.25rem;
}
.conversation-detail-status {
font-size: 0.875rem;
color: var(--text-muted);
}
.conversation-detail-actions {
display: flex;
gap: 0.5rem;
}
.detail-action-btn {
width: 36px;
height: 36px;
border: none;
background: transparent;
color: var(--text-muted);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.detail-action-btn:hover {
background: var(--light-bg);
color: var(--primary-color);
}
/* Messages Container */
.messages-container {
flex: 1;
overflow-y: auto;
padding: 1rem;
background: var(--light-bg);
}
.message {
display: flex;
align-items: flex-start;
margin-bottom: 1rem;
padding: 0 1rem;
}
.message.sent {
flex-direction: row-reverse;
}
.message.received {
flex-direction: row;
}
.message.reply {
margin-left: 2rem;
border-left: 2px solid var(--border-color);
padding-left: 1rem;
background: rgba(0, 99, 110, 0.02);
border-radius: 0.5rem;
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--primary-color);
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.875rem;
margin: 0 0.75rem;
flex-shrink: 0;
}
.message-avatar.reply-avatar {
width: 32px;
height: 32px;
font-size: 0.75rem;
margin: 0 0.5rem;
}
.message-content {
flex: 1;
display: flex;
flex-direction: column;
align-items: flex-start;
max-width: 70%;
}
.message.sent .message-content {
align-items: flex-end;
}
.message.received .message-content {
align-items: flex-start;
}
.message-bubble {
background: var(--light-bg);
border: 1px solid var(--border-color);
border-radius: 1rem;
padding: 0.75rem 1rem;
margin-bottom: 0.25rem;
position: relative;
word-wrap: break-word;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.message-bubble.reply-bubble {
background: white;
border-radius: 0.75rem;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
}
.message.sent .message-bubble {
background: var(--primary-color);
color: white;
border-bottom-right-radius: 0.25rem;
}
.message.received .message-bubble {
background: white;
color: var(--text-color);
border-bottom-left-radius: 0.25rem;
}
.thread-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
padding: 0.25rem 0.5rem;
background: rgba(0, 99, 110, 0.1);
border-radius: 0.25rem;
font-size: 0.75rem;
color: var(--primary-color);
}
.reply-info {
display: flex;
align-items: center;
gap: 0.25rem;
margin-bottom: 0.25rem;
font-size: 0.75rem;
color: var(--text-muted);
}
.message-text {
line-height: 1.5;
margin-bottom: 0.25rem;
}
.message-time {
font-size: 0.75rem;
color: var(--text-muted);
display: flex;
align-items: center;
gap: 0.5rem;
}
/* Reply Area */
.message-reply-area {
padding: 1rem 1.5rem;
border-top: 1px solid var(--border-color);
background: white;
}
.reply-form {
display: flex;
align-items: flex-end;
gap: 0.75rem;
}
.reply-input {
flex: 1;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: 1.5rem;
resize: none;
font-family: inherit;
font-size: 0.875rem;
line-height: 1.5;
max-height: 120px;
min-height: 44px;
transition: border-color 0.2s ease;
}
.reply-input:focus {
outline: none;
border-color: var(--primary-color);
}
.reply-tools {
display: flex;
align-items: center;
gap: 0.5rem;
}
.tool-btn {
width: 36px;
height: 36px;
border: none;
background: transparent;
color: var(--text-muted);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.tool-btn:hover {
background: var(--light-bg);
color: var(--primary-color);
}
.send-btn {
width: 36px;
height: 36px;
border: none;
background: var(--primary-color);
color: white;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
}
.send-btn:hover:not(:disabled) {
background: #004d57;
}
.send-btn:disabled {
background: var(--text-muted);
cursor: not-allowed;
}
/* Empty States */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: var(--text-muted);
text-align: center;
padding: 2rem;
}
.empty-state i {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state h3 {
margin-bottom: 0.5rem;
color: var(--text-color);
}
.empty-state p {
font-size: 0.875rem;
}
/* Compose Modal */
.compose-modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.compose-modal-content {
background: white;
border-radius: 0.5rem;
width: 90%;
max-width: 600px;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}
.compose-modal-header {
padding: 1rem 1.5rem;
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
}
.compose-modal-body {
padding: 1.5rem;
}
.compose-form-group {
margin-bottom: 1rem;
}
.compose-form-label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: var(--text-color);
}
.compose-form-control {
width: 100%;
padding: 0.75rem 1rem;
border: 1px solid var(--border-color);
border-radius: 0.5rem;
font-size: 0.875rem;
transition: border-color 0.2s ease;
}
.compose-form-control:focus {
outline: none;
border-color: var(--primary-color);
}
.compose-form-textarea {
resize: vertical;
min-height: 120px;
font-family: inherit;
line-height: 1.5;
}
.compose-modal-footer {
padding: 1rem 1.5rem;
border-top: 1px solid var(--border-color);
display: flex;
justify-content: flex-end;
gap: 0.75rem;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 0.5rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
font-size: 0.875rem;
}
.btn-primary {
background: var(--primary-color);
color: white;
}
.btn-primary:hover {
background: #004d57;
}
.btn-secondary {
background: var(--light-bg);
color: var(--text-color);
border: 1px solid var(--border-color);
}
.btn-secondary:hover {
background: var(--border-color);
}
/* Responsive Design */
@media (max-width: 768px) {
.unified-messages {
height: calc(100vh - 60px);
}
.messages-sidebar {
width: 100%;
position: absolute;
z-index: 10;
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.messages-sidebar.show {
transform: translateX(0);
}
.conversation-detail {
width: 100%;
}
.message-content {
max-width: 85%;
}
.compose-modal-content {
width: 95%;
margin: 1rem;
}
}
@media (max-width: 480px) {
.conversation-item {
padding: 0.5rem;
}
.conversation-avatar {
width: 40px;
height: 40px;
font-size: 0.875rem;
}
.message {
padding: 0 0.5rem;
}
.message-avatar {
width: 32px;
height: 32px;
font-size: 0.75rem;
margin: 0 0.5rem;
}
.message-content {
max-width: 90%;
}
.message-bubble {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
}
}
/* Animations */
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.message {
animation: slideIn 0.3s ease;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.conversation-item {
animation: fadeIn 0.2s ease;
}
/* Loading States */
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
color: var(--text-muted);
}
.spinner {
width: 20px;
height: 20px;
border: 2px solid var(--border-color);
border-top: 2px solid var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Notification Styles */
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 1rem 1.5rem;
border-radius: 0.5rem;
color: white;
font-weight: 500;
z-index: 1001;
animation: slideInRight 0.3s ease;
max-width: 300px;
}
.notification.success {
background: var(--success-color);
}
.notification.error {
background: var(--danger-color);
}
.notification.info {
background: var(--info-color);
}
.notification.warning {
background: var(--warning-color);
}
@keyframes slideInRight {
from {
opacity: 0;
transform: translateX(100%);
}
to {
opacity: 1;
transform: translateX(0);
}
}

View File

@ -0,0 +1,162 @@
/* UI Variables for the KAAT-S Theme */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-gray: #6c757d;
/* Status Colors for alerts/messages */
--kaauh-success: var(--kaauh-teal);
--kaauh-danger: #dc3545;
--kaauh-info: #17a2b8;
}
/* CONTAINER AND CARD STYLING */
.container {
padding: 2rem 1rem;
}
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
max-width: 600px;
margin: 0 auto; /* Center the card */
padding: 1.5rem;
}
/* HEADER STYLING (The section outside the card) */
.header {
text-align: center;
margin-bottom: 2rem;
}
.header h1 {
font-size: 2rem;
color: var(--kaauh-teal-dark);
font-weight: 700;
margin-bottom: 0.25rem;
}
.header p {
color: var(--kaauh-gray);
font-size: 1rem;
}
/* CARD TITLE STYLING */
.card-title {
font-size: 1.25rem;
color: var(--kaauh-teal-dark);
font-weight: 600;
border-bottom: 1px solid var(--kaauh-border);
padding-bottom: 0.75rem;
margin-bottom: 1.5rem;
}
/* FORM STYLING */
.form-row {
margin-bottom: 1.5rem;
}
.form-label {
display: block;
font-weight: 600;
color: var(--kaauh-gray);
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.form-input {
display: block;
width: 100%;
padding: 0.75rem 1rem;
font-size: 1rem;
line-height: 1.5;
color: var(--kaauh-primary-text);
background-color: #fff;
background-clip: padding-box;
border: 1px solid var(--kaauh-border);
border-radius: 0.5rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-input:focus {
border-color: var(--kaauh-teal);
outline: 0;
box-shadow: 0 0 0 0.1rem rgba(0, 99, 110, 0.25);
}
input[type="datetime-local"] {
font-family: inherit;
}
/* MESSAGES/ALERTS STYLING */
.messages {
max-width: 600px;
margin: 0 auto 1.5rem auto;
}
.alert {
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 0.5rem;
font-weight: 500;
}
.alert-success {
color: white;
background-color: var(--kaauh-success);
border-color: var(--kaauh-success);
}
.alert-danger {
color: white;
background-color: var(--kaauh-danger);
border-color: var(--kaauh-danger);
}
.alert-info {
color: white;
background-color: var(--kaauh-info);
border-color: var(--kaauh-info);
}
/* BUTTON STYLING */
.actions {
margin-top: 1.5rem;
display: flex;
gap: 1rem;
}
.btn-base {
display: inline-flex;
align-items: center;
gap: 0.5rem;
text-align: center;
vertical-align: middle;
cursor: pointer;
user-select: none;
padding: 0.5rem 1rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.5rem;
font-weight: 600;
border: 1px solid transparent;
transition: all 0.2s ease;
text-decoration: none;
}
/* Primary Action Button (Update) */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
color: white;
}
/* Secondary Button (Cancel) */
.btn-kaats-outline-secondary {
color: var(--kaauh-secondary);
border-color: var(--kaauh-secondary);
background-color: transparent;
}
.btn-kaats-outline-secondary:hover {
background-color: var(--kaauh-secondary);
color: white;
border-color: var(--kaauh-secondary);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

View File

@ -0,0 +1,120 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 198.425 132.744" style="version:1">
<switch>
<foreignObject width="1" height="1" x="0" y="0" requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/"/>
<g>
<path fill="#ADA9AE" d="M141.468 75.372v0.145c0 9.575-6.977 16.611-18.315 16.611 -9.157 0-15.48-3.627-19.84-8.777l7.704-7.327c3.488 3.918 7.122 6.093 12.282 6.093 4.215 0 7.195-2.393 7.195-6.165 -0.056-0.1 0.822-6.6-9.957-6.6h-4.651l-1.745-7.11 16.32-16.385 -6.54 4.2h-17.628v-9.574h34.375v8.414l-12.863 12.258C134.709 62.316 141.468 65.942 141.468 75.372M109.563 17.265c0-4.597 3.302-8.297 7.936-8.297h5.222l-1.367 0.681c0.062 12.6-0.25 14.187 0.556 16.029l-4.353-0.001c-4.634 0-7.994-3.758-7.994-8.354V17.265zM117.499 27.788h4.637v-1.667c0.338 0.586 0.793 1.041 1.464 1.359 1.475 0.7 0.403 0.26 42.703 0.39V6.858h-2.231v18.785c0 0-38.678 0.149-39.413-0.181 -0.638-0.287-0.87-0.943-0.976-1.642 -0.169-1.135-0.066-1.156-0.097-16.962h-6.029c-6.169 0-10.369 4.885-10.369 10.465v0.058C107.188 22.96 111.33 27.788 117.499 27.788M182.746 4.63h-4.888c0.106-1.616-0.642-4.63 2.651-4.63h1.574v1.552c-1.524 0.113-2.401-0.371-2.401 0.846v0.515h3.064V4.63zM188.396 28.097c-0.595 0-1.2-0.083-1.813-0.251l0.586-2.143c1.009 0.287 2.299 0.304 2.831-0.598 0.621-1.068 0.399-1.444 0.432-18.247h2.231c-0.064 15.685 0.154 16.123-0.209 17.941C192.026 26.926 190.736 28.097 188.396 28.097M123.586 4.63h-2.232V2.403h2.232V4.63zM172.692 12.314c1.047-2.039 3.245-3.174 5.492-3.198V9.112h3.605l-1.274 0.635v10c-1.756-0.052-2.99 0.167-4.616-0.349 -2.22-0.698-3.724-2.406-3.724-4.885C172.175 13.716 172.347 12.982 172.692 12.314M172.692 20.206c2.905 2.07 6.866 1.728 7.073 1.767l0.75-1.571c-0.121 1.523 0.419 3.651-1.004 4.717 -1.203 0.899-3.348 0.87-4.672 0.556 -0.548-0.129-1.176-0.352-1.883-0.667l-0.697 2.143c1.414 0.521 2.766 0.946 4.267 0.946 2.176 0 3.756-0.515 4.742-1.544 0.985-1.031 1.478-2.418 1.478-4.162V6.858c-2.746 0.086-4.829-0.277-7.335 0.585C168.696 9.752 168.562 17.266 172.692 20.206M119.123 4.63h-2.232V2.403h2.232V4.63zM96.028 6.834h2.304v20.911h-1.886L81.607 8.936l1.312 3.048v15.761h-2.305V6.834h2.215l14.17 17.99 -0.971-2.566V6.834zM9.009 27.894L0 6.834h2.634c0.134 0.324 7.314 17.679 8.042 19.439l-0.228-2.298 7.121-17.141h2.543l-9.008 21.06H9.009zM48.932 6.834h2.364v20.91h-2.364V6.834zM74.29 17.349c0 4.75-3.411 8.573-8.2 8.573s-8.261-3.883-8.261-8.632v-0.061c0-4.749 3.413-8.572 8.201-8.572 4.789 0 8.26 3.883 8.26 8.633V17.349zM66.09 6.476c-6.375 0-10.715 5.048-10.715 10.814v0.059c0 5.765 4.28 10.754 10.655 10.754s10.715-5.048 10.715-10.813v-0.061C76.745 11.465 72.465 6.476 66.09 6.476M45.031 22.069v0.059c0 3.584-2.993 5.915-7.153 5.915 -3.322 0-6.045-1.105-8.559-3.346l1.466-1.732c2.185 1.971 4.28 2.956 7.184 2.956 2.813 0 4.668-1.492 4.668-3.554v-0.059c0-5.836-12.48-1.986-12.48-10.008v-0.059c0-3.286 2.903-5.706 6.884-5.706 3.052 0 5.237 0.867 7.362 2.57l-1.376 1.821c-4.75-3.864-10.506-2.384-10.506 1.106v0.06c0 1.971 1.077 3.076 5.687 4.062C42.877 17.17 45.031 18.873 45.031 22.069M25.688 27.745h-2.364V6.835h2.364V27.745zM163.293 30.096h2.232v2.227h-2.232V30.096zM158.83 30.096h2.232v2.227h-2.232V30.096zM18.71 81.609h19.398v9.649H0.461v-8.85c18.811-15.964 25.728-19.084 25.728-26.04 0-4.28-2.835-6.601-6.832-6.601 -3.925 0-6.614 2.175-10.393 6.817l-7.849-6.309C6.13 43.456 11 39.756 20.084 39.756c10.538 0 17.515 6.166 17.515 15.669v0.144c0 11.916-9.021 15.757-24.49 28.223l-2.16 1.664L18.71 81.609zM186.725 66.015c0 8.704-6.25 15.813-15.263 15.813 -9.012 0-15.407-7.254-15.407-15.959v-0.145c0-8.704 6.251-15.812 15.262-15.812 9.011 0 15.408 7.254 15.408 15.957V66.015zM171.462 39.612c-15.697 0-27.108 11.823-27.108 26.257v0.146c0 14.435 11.264 26.113 26.963 26.113s27.108-11.824 27.108-26.259v-0.145C198.425 51.29 187.16 39.612 171.462 39.612"/>
<path fill="#29367B" d="M68.518 36.272L71.095 38.74 68.622 41.311 66.046 38.843"/>
<path fill="#B1AEB3" d="M74.415 38.843L76.99 41.311 79.463 38.74 76.887 36.272"/>
<path fill="#D0DA33" d="M70.787 42.084L72.794 44.005 74.718 42.003 72.713 40.082"/>
<path fill="#71BA44" d="M78.142 43.183L80.148 45.105 82.072 43.103 80.066 41.181"/>
<path fill="#D0DA33" d="M67.507 45.145L69.513 47.067 71.437 45.065 69.433 43.143"/>
<path fill="#27B8BE" d="M74.238 45.378L76.243 47.3 78.168 45.298 76.162 43.376"/>
<path fill="#71BA44" d="M71.605 47.681L73.016 49.033 74.37 47.624 72.959 46.272"/>
<path fill="#58B75E" d="M76.928 48.742L78.339 50.094 79.694 48.685 78.283 47.334"/>
<path fill="#3088C8" d="M88.918 37.593H93.36800000000001V42.035000000000004H88.918z" transform="rotate(-10.39 91.135 39.818)"/>
<path fill="#1E3871" d="M83.133 39.508H86.70299999999999V43.072H83.133z" transform="rotate(-10.376 84.927 41.294)"/>
<path fill="#27B8BE" d="M89.92 44.395H93.49V47.959H89.92z" transform="rotate(-10.39 91.7 46.17)"/>
<path fill="#D0DA33" d="M85.014 44.984H87.794V47.758H85.014z" transform="rotate(-10.382 86.413 46.375)"/>
<path fill="#B1AEB3" d="M90.334 50.169H93.114V52.943999999999996H90.334z" transform="rotate(-10.382 91.733 51.56)"/>
<path fill="#27B8BE" d="M80.917 46.094H83.69800000000001V48.869H80.917z" transform="rotate(-10.382 82.315 47.485)"/>
<path fill="#71BA44" d="M85.88 49.669H88.66V52.443999999999996H85.88z" transform="rotate(-10.382 87.28 51.06)"/>
<path fill="#B1AEB3" d="M82.329 50.08H84.285V52.032H82.329z" transform="rotate(-10.39 83.314 51.066)"/>
<path fill="#58B75E" d="M86.024 54.05H87.979V56.001H86.024z" transform="rotate(-10.36 87.01 55.033)"/>
<path fill="#29367B" d="M101.572 52.342L99.659 56.354 103.679 58.262 105.59 54.251"/>
<path fill="#27B8BE" d="M95.867 50.489L94.333 53.707 97.557 55.238 99.091 52.02"/>
<path fill="#71BA44" d="M98.505 58.416L96.971 61.635 100.196 63.165 101.73 59.947"/>
<path fill="#D0DA33" d="M94.28 56.002L93.086 58.508 95.597 59.7 96.792 57.194"/>
<path fill="#D0DA33" d="M95.555 63.315L94.36 65.821 96.87 67.013 98.066 64.507"/>
<path fill="#3088C8" d="M90.306 54.51L89.111 57.016 91.622 58.208 92.817 55.702"/>
<path fill="#B1AEB3" d="M92.237 60.308L91.042 62.814 93.552 64.006 94.748 61.5"/>
<path fill="#71BA44" d="M89.217 58.541L88.376 60.304 90.142 61.142 90.983 59.38"/>
<path fill="#27B8BE" d="M89.372 64.241H91.324V66.196H89.372z" transform="rotate(-64.552 90.342 65.217)"/>
<path fill="#3088C8" d="M103.737 71.551L99.835 73.687 101.976 77.582 105.878 75.446"/>
<path fill="#27B8BE" d="M100.198 66.717L97.068 68.431 98.785 71.555 101.915 69.841"/>
<path fill="#27B8BE" d="M97.691 74.686L94.56 76.4 96.278 79.525 99.408 77.811"/>
<path fill="#29367B" d="M95.679 70.261L93.242 71.596 94.578 74.028 97.017 72.694"/>
<path fill="#58B75E" d="M92.426 76.936L89.988 78.271 91.325 80.703 93.763 79.369"/>
<path fill="#1E3871" d="M93.331 66.731L90.893 68.065 92.23 70.499 94.669 69.164"/>
<path fill="#D0DA33" d="M91.498 72.56L89.06 73.895 90.397 76.327 92.835 74.993"/>
<path fill="#B1AEB3" d="M90.084 69.364L88.369 70.303 89.31 72.014 91.024 71.076"/>
<path fill="#27B8BE" d="M87.475 74.114L85.76 75.053 86.7 76.764 88.415 75.826"/>
<path fill="#71BA44" d="M89.529 88.1H93.972V92.55099999999999H89.529z" transform="rotate(-82.958 91.74 90.32)"/>
<path fill="#1E3871" d="M90.424 82.17H93.988V85.74H90.424z" transform="rotate(-82.935 92.21 83.955)"/>
<path fill="#27B8BE" d="M83.719 87.169H87.28299999999999V90.74H83.719z" transform="rotate(-82.935 85.504 88.955)"/>
<path fill="#71BA44" d="M85.516 82.459H88.29100000000001V85.239H85.516z" transform="rotate(-82.952 86.912 83.852)"/>
<path fill="#B1AEB3" d="M78.966 85.97H81.741V88.75H78.966z" transform="rotate(-82.952 80.36 87.365)"/>
<path fill="#B1AEB3" d="M85.681 78.224H88.457V81.00500000000001H85.681z" transform="rotate(-82.935 87.072 79.615)"/>
<path fill="#71BA44" d="M80.778 81.88H83.55300000000001V84.661H80.778z" transform="rotate(-82.935 82.168 83.27)"/>
<path fill="#B1AEB3" d="M82.377 78.518H84.329V80.473H82.377z" transform="rotate(-82.946 83.36 79.498)"/>
<path fill="#B1AEB3" d="M77.476 80.846H79.428V82.802H77.476z" transform="rotate(-82.952 78.455 81.827)"/>
<path fill="#D0DA33" d="M76.652 96.502L73.39 93.479 70.362 96.735 73.624 99.757"/>
<path fill="#71BA44" d="M76.051 34.279L72.789 31.256 69.761 34.512 73.023 37.534"/>
<path fill="#27B8BE" d="M80.133 91.625L77.516 89.2 75.087 91.812 77.704 94.236"/>
<path fill="#71BA44" d="M71.765 91.764L69.147 89.339 66.718 91.951 69.335 94.375"/>
<path fill="#3088C8" d="M75.337 88.463L73.299 86.575 71.408 88.608 73.445 90.496"/>
<path fill="#D0DA33" d="M67.965 87.486L65.928 85.598 64.037 87.631 66.075 89.52"/>
<path fill="#D0DA33" d="M77.954 85.126L75.916 83.238 74.025 85.271 76.062 87.159"/>
<path fill="#B1AEB3" d="M71.833 85.227L69.795 83.339 67.904 85.372 69.941 87.26"/>
<path fill="#1E3871" d="M74.426 82.881L72.993 81.552 71.663 82.983 73.096 84.311"/>
<path fill="#B1AEB3" d="M69.087 81.907L67.654 80.579 66.323 82.009 67.756 83.337"/>
<path fill="#3088C8" d="M52.797 88.848H57.248V93.28999999999999H52.797z" transform="rotate(-11.333 55.022 91.072)"/>
<path fill="#27B8BE" d="M59.437 87.709H63.007V91.273H59.437z" transform="rotate(-11.333 61.207 89.493)"/>
<path fill="#71BA44" d="M52.569 82.934H56.139V86.49799999999999H52.569z" transform="rotate(-11.344 54.34 84.698)"/>
<path fill="#D0DA33" d="M58.262 83.048H61.042V85.82300000000001H58.262z" transform="rotate(-11.333 59.64 84.44)"/>
<path fill="#29367B" d="M62.34 81.869H65.12V84.645H62.34z" transform="rotate(-11.333 63.72 83.263)"/>
<path fill="#3088C8" d="M57.317 78.377H60.097V81.152H57.317z" transform="rotate(-11.333 58.696 79.77)"/>
<path fill="#71BA44" d="M61.693 78.724H63.649V80.676H61.693z" transform="rotate(-11.314 62.682 79.71)"/>
<path fill="#B1AEB3" d="M57.934 74.815H59.888999999999996V76.767H57.934z" transform="rotate(-11.344 58.916 75.795)"/>
<path fill="#27B8BE" d="M44.388 78.715L46.233 74.673 42.182 72.831 40.337 76.873"/>
<path fill="#D0DA33" d="M50.122 80.474L51.602 77.231 48.353 75.754 46.872 78.997"/>
<path fill="#3088C8" d="M47.351 72.592L48.832 69.349 45.582 67.871 44.102 71.115"/>
<path fill="#27B8BE" d="M49.54 71.708H52.316V74.487H49.54z" transform="rotate(-65.485 50.93 73.102)"/>
<path fill="#B1AEB3" d="M55.002 81.249L56.155 78.724 53.625 77.574 52.472 80.099"/>
<path fill="#29367B" d="M48.145 64.418H50.92100000000001V67.197H48.145z" transform="rotate(-65.474 49.535 65.806)"/>
<path fill="#58B75E" d="M53.538 73.134H56.31399999999999V75.913H53.538z" transform="rotate(-65.474 54.928 74.522)"/>
<path fill="#D0DA33" d="M51.512 67.369H54.288V70.148H51.512z" transform="rotate(-65.455 52.905 68.762)"/>
<path fill="#1E3871" d="M56.637 72.312L57.448 70.536 55.668 69.726 54.857 71.503"/>
<path fill="#71BA44" d="M55.879 66.948L56.69 65.171 54.91 64.362 54.099 66.139"/>
<path fill="#27B8BE" d="M41.902 59.544L45.769 57.344 43.563 53.485 39.697 55.685"/>
<path fill="#71BA44" d="M45.522 64.32L48.624 62.555 46.854 59.459 43.753 61.225"/>
<path fill="#1E3871" d="M47.897 56.31L50.999 54.545 49.229 51.448 46.128 53.214"/>
<path fill="#3088C8" d="M49.982 60.701L52.396 59.326 51.019 56.916 48.604 58.29"/>
<path fill="#71BA44" d="M53.124 53.974L55.538 52.599 54.161 50.189 51.746 51.563"/>
<path fill="#27B8BE" d="M52.387 64.192L54.803 62.818 53.426 60.407 51.01 61.782"/>
<path fill="#D0DA33" d="M54.124 58.334L56.539 56.96 55.162 54.549 52.747 55.924"/>
<path fill="#B1AEB3" d="M55.591 61.506L57.29 60.539 56.321 58.844 54.622 59.81"/>
<path fill="#71BA44" d="M58.12 56.713L59.82 55.746 58.851 54.051 57.151 55.018"/>
<path fill="#71BA44" d="M51.354 38.35H55.796V42.800000000000004H51.354z" transform="rotate(-83.898 53.578 40.576)"/>
<path fill="#B1AEB3" d="M51.445 45.166H55.008V48.736999999999995H51.445z" transform="rotate(-83.892 53.227 46.953)"/>
<path fill="#27B8BE" d="M58.066 40.057H61.63V43.628H58.066z" transform="rotate(-83.892 59.848 41.844)"/>
<path fill="#D0DA33" d="M57.142 45.581H59.917V48.361000000000004H57.142z" transform="rotate(-83.886 58.533 46.972)"/>
<path fill="#3088C8" d="M63.634 41.961H66.409V44.741H63.634z" transform="rotate(-83.91 65.016 43.35)"/>
<path fill="#27B8BE" d="M57.047 49.818H59.821999999999996V52.598H57.047z" transform="rotate(-83.886 58.435 51.21)"/>
<path fill="#71BA44" d="M61.889 46.08H64.664V48.86H61.889z" transform="rotate(-83.886 63.278 47.47)"/>
<path fill="#1E3871" d="M61.176 50.287H63.128V52.243H61.176z" transform="rotate(-83.892 62.153 51.266)"/>
<path fill="#B1AEB3" d="M66.039 47.877H67.991V49.834H66.039z" transform="rotate(-83.927 67.016 48.854)"/>
<path fill="#727A82" d="M86.941 68.235c-2.33-1.342-5.572-1.732-8.931-0.502 -1.468 0.555-2.938 1.524-4.178 2.473 -0.781-4.735-0.4-9.13-0.218-11.289 0.001-0.012 0.015-0.018 0.026-0.011 1.677 1.055 1.207 4.003 0.855 4.846 -0.007 0.017 0.018 0.028 0.03 0.014 2.09-2.468 1.069-5.193 0.64-5.883 -0.078-0.125 0.395 0.254 0.783 0.751 1.461 1.86 0.786 3.949 0.949 3.9 0.004-0.001 0.009 0 0.011-0.005 0.971-1.905 0.659-4.321-1.338-6.066 -0.091-0.077 0.265 0.039 0.378 0.079 1.703 0.591 2.591 2.345 2.579 3.909 0 0.344 1.15-2.938-1.63-4.606 -0.52-0.311-1.054-0.482-1.403-0.524 -0.272-0.029 1.489-0.294 2.726 0.544 0.743 0.501 0.938 1.107 0.919 0.937 -0.216-2.197-2.806-3.065-4.376-2.406 -0.166 0.083 0.618-1.079 2.134-0.989 0.292 0.017 0.562 0.08 0.778 0.183 0.015 0.007 0.029-0.013 0.018-0.026 -1.145-1.309-3.029-1.336-4.029 0.178 -0.007 0.01-0.022 0.01-0.028 0 -0.379-0.679-0.696-1.548-0.833-2.368 -0.033-0.21-0.103 1.015-0.863 2.368 -0.006 0.01-0.02 0.01-0.027 0 -1.003-1.518-2.885-1.488-4.029-0.178 -0.012 0.013 0.002 0.033 0.018 0.026 0.764-0.363 2.207-0.256 2.93 0.781 0.009 0.014-0.004 0.032-0.02 0.025 -1.618-0.68-4.16 0.243-4.375 2.406 -0.001 0.018 0.023 0.028 0.032 0.012 0.072-0.135 0.25-0.423 0.583-0.715 1.294-1.14 3.329-0.81 3.03-0.778 -0.795 0.098-2.427 0.806-3.03 2.424 -0.493 1.322-0.018 3.063 0 2.632 0.016-1.471 0.859-3.441 2.94-3.943 0.016-0.004 0.028 0.018 0.015 0.029 -3.234 2.843-1.278 6.334-1.308 6.058 -0.1-1.127-0.183-3.139 1.686-4.658 0.014-0.011 0.036 0.004 0.026 0.02 -0.429 0.69-1.449 3.415 0.64 5.883 0.012 0.014 0.037 0.003 0.03-0.014 -0.351-0.843-0.821-3.791 0.855-4.846 0.011-0.007 0.025-0.001 0.026 0.011 0.183 2.159 0.564 6.554-0.218 11.289 -2.656-2.03-5.271-3.361-8.639-3.213 -1.694 0.074-3.301 0.567-4.47 1.242 -0.042 0.024-0.011 0.085 0.034 0.071 4.899-1.566 9.139 0.334 13.081 3.614 -1.991 1.735-3.798 3.551-4.848 4.099 -0.245-0.335-0.67-0.528-1.135-0.427 -0.876 0.193-1.085 1.29-0.61 1.762 1.721 1.717 4.412-1.69 7.631-4.538 3.098 2.738 5.913 6.254 7.631 4.538 0.137-0.137 0.296-0.589 0.214-0.948 -0.207-0.896-1.398-1.155-1.959-0.387 -1.01-0.509-2.783-2.301-4.848-4.099 3.97-3.309 8.164-5.156 13.08-3.614C86.952 68.32 86.983 68.259 86.941 68.235"/>
<path fill="#ADA9AE" d="M71.265 115.108h1.277v-11.436h-1.277V115.108zM37.804 113.833l-1.995-4.891c2.064-1.747 4.625-1.426 6.273 0l-1.995 4.891H37.804zM30.764 113.833c-1.004-0.03-1.71 0.095-2.642-0.198 -3.451-1.09-2.513-5.888 1.349-5.888h1.293V113.833zM66.268 113.833c-2.057-0.063-3.91 0.363-4.853-1.274 0.186-0.818 0.084-1.552 0.112-5.306H60.25v4.318c0 3.169-3.824 2.963-4.726 1.06 -0.362-0.765-0.141-1.085-0.207-5.378h-1.278v4.414c0 1.44-0.901 2.166-2.155 2.166H49.41v-6.58h-1.277v6.58h-6.705l2.235-5.337c-1.268-1.108-3.033-2.151-4.725-2.151 -1.719 0-3.45 0.998-4.726 2.151l2.251 5.337h-4.422v-7.376h-2.043c-2.888 0-5.285 1.66-5.285 4.381 0 4.843 5.941 4.246 6.051 4.27 -0.02 0.157 0.159 1.251-0.575 1.801 -0.686 0.514-1.911 0.499-2.674 0.319 -0.314-0.075-0.673-0.202-1.077-0.383l-0.399 1.227c0.81 0.298 1.583 0.542 2.442 0.542 2.434 0 3.56-1.214 3.56-3.267v-0.239c20.842-0.131 20.226 0.3 21.496-0.398 0.484-0.265 0.875-0.621 1.173-1.068 0.309 0.51 0.729 0.909 1.261 1.196 1.117 0.599 2.821 0.543 3.856 0.031 0.537-0.265 0.95-0.674 1.237-1.227 0.493 0.842 1.258 1.383 2.794 1.451v0.015h3.687v-11.436h-1.277V113.833zM180.858 113.268c-1.13 2.641-5.02 2.717-6.146-0.04 -0.331-0.809-0.331-1.768 0-2.589 1.363-3.378 6.41-2.408 6.41 1.299C181.122 112.415 181.034 112.859 180.858 113.268M168.47 113.268c-0.977 2.27-3.921 2.606-5.46 1.02 -1.263-1.306-1.226-3.433-0.007-4.716 0.993-1.055 2.524-1.225 3.695-0.742C168.544 109.6 169.159 111.667 168.47 113.268M137.412 113.833c-0.977-0.03-1.685 0.098-2.611-0.206 -3.363-1.105-2.561-5.896 1.349-5.896h1.262V113.833zM186.389 113.833H182c1.37-3.007-0.758-6.532-4.263-6.532 -3.228 0-5.486 3.308-4.167 6.532h-3.959c1.335-2.919-0.688-6.532-4.262-6.532 -3.23 0-5.486 3.309-4.167 6.532h-4.055v-10.161h-1.277v10.161h-4.07c-0.07-1.309 0.297-3.246-0.799-4.469 -0.743-0.838-1.57-1.099-2.698-1.099h-5.651l2.404-4.593h-1.389l-2.452 4.577c0.079 0.508 0.233 0.905 0.575 1.29 6.387 0.054 6.804-0.134 7.559 0.184 0.592 0.245 0.953 0.692 1.078 1.235 0.152 0.654 0.075 1.146 0.096 2.875h-11.814v-7.376h-2.012c-3.384 0-5.268 2.013-5.268 4.333 0 2.907 2.338 4.318 5.268 4.318h25.335c1.879 2.024 5.042 1.852 6.769 0h5.619c1.873 2.018 5.033 1.859 6.77 0h6.497v-11.436h-1.278V113.833zM191.386 115.108h1.277v-11.436h-1.277V115.108zM97.288 117.914H98.6v-1.309h-1.312V117.914zM109.774 113.833l-1.996-4.891c2.048-1.734 4.605-1.441 6.274 0l-1.995 4.891H109.774zM118.825 113.833h-5.427l2.234-5.337c-1.271-1.114-3.042-2.151-4.725-2.151 -1.725 0-3.451 1.003-4.726 2.151l2.251 5.337h-4.756v-7.376h-1.278v8.922c0 1.127-0.082 1.96-1.213 1.96 -0.263 0-0.402-0.023-0.654-0.096l-0.335 1.228c1.067 0.292 2.327 0.194 2.953-0.726 0.566-0.833 0.499-1.983 0.527-2.637h16.427v-11.436h-1.278V113.833zM21.872 107.572c-1.42-1.55-3.064-0.988-3.624-1.115v1.274c0.932 0.036 1.363-0.103 1.995 0.199 0.532 0.256 0.908 0.669 1.062 1.251 0.173 0.669 0.076 1.191 0.104 4.652h-5.987v1.275h7.264c-0.053-4.83 0.132-5.342-0.2-6.421C22.353 108.252 22.148 107.88 21.872 107.572M92.619 117.929h1.312v-1.309h-1.312V117.929zM84.854 113.833c-0.975-0.03-1.684 0.098-2.611-0.206 -3.367-1.106-2.556-5.896 1.349-5.896h1.262V113.833zM98.583 106.457h-1.277v7.376h-4.821v-7.376h-1.277v7.376h-5.077v-7.376c-1.606 0.05-4.002-0.325-5.843 1.234 -2.61 2.211-1.937 7.417 3.832 7.417h14.463V106.457zM90.382 117.929h1.312v-1.309h-1.312V117.929zM5.037 103.672H3.725v1.309h1.312V103.672zM136.446 103.672h-1.312v1.309h1.312V103.672zM86.125 103.672h-1.312v1.309h1.312V103.672zM12.571 117.929h1.312v-1.309h-1.312V117.929zM83.888 103.672h-1.312v1.309h1.312V103.672zM7.274 103.672H5.962v1.309h1.312V103.672zM10.334 117.929h1.312v-1.309h-1.312V117.929zM6.003 113.833c-0.973-0.03-1.686 0.098-2.61-0.206 -3.361-1.102-2.567-5.896 1.349-5.896h1.261V113.833zM13.634 106.457h-1.277v7.376H7.28v-7.376c-1.531 0.047-2.748-0.153-4.183 0.326 -1.203 0.404-2.186 1.147-2.722 2.287 -0.558 1.186-0.457 2.696 0.057 3.664 0.925 1.746 2.86 2.374 4.836 2.374h8.366V106.457zM123.822 115.108h1.277v-11.436h-1.277V115.108zM138.682 103.672h-1.312v1.309h1.312V103.672z"/>
<path fill="#ADA9AE" d="M1.251 123.484L2.854 123.484 2.854 128.062 7.193 123.484 9.161 123.484 5.369 127.399 9.33 132.588 7.389 132.588 4.275 128.491 2.854 129.948 2.854 132.588 1.251 132.588"/>
<path fill="#ADA9AE" d="M10.792 123.484H12.395V132.588H10.792z"/>
<path fill="#ADA9AE" d="M14.863 123.484L16.349 123.484 21.236 129.779 21.236 123.484 22.812 123.484 22.812 132.588 21.47 132.588 16.44 126.111 16.44 132.588 14.863 132.588"/>
<path fill="#ADA9AE" d="M24.797 128.062v-0.026c0-2.549 1.955-4.709 4.704-4.709 1.59 0 2.568 0.443 3.506 1.236l-1.016 1.21c-2.47-2.098-5.512-0.587-5.512 2.237v0.026c0 1.873 1.237 3.251 3.1 3.251 0.861 0 1.643-0.273 2.203-0.689v-1.704h-2.333v-1.391h3.884v3.823C30.071 134.113 24.797 132.681 24.797 128.062"/>
<path fill="#ADA9AE" d="M38.948 131.131c4.254 0 4.203-6.191 0-6.191H37.15v6.191H38.948zM35.547 123.484h3.401c6.508 0 6.42 9.104 0 9.104h-3.401V123.484z"/>
<path fill="#ADA9AE" d="M53.155 128.062v-0.026c0-1.769-1.29-3.238-3.102-3.238 -1.811 0-3.075 1.443-3.075 3.212v0.026c0 1.769 1.29 3.239 3.101 3.239C51.891 131.275 53.155 129.831 53.155 128.062M45.297 128.062v-0.026c0-2.562 1.981-4.709 4.782-4.709 2.802 0 4.757 2.121 4.757 4.683v0.026c0 2.562-1.98 4.708-4.783 4.708C47.252 132.744 45.297 130.624 45.297 128.062"/>
<path fill="#ADA9AE" d="M56.827 123.484L58.534 123.484 61.31 127.789 64.085 123.484 65.792 123.484 65.792 132.588 64.189 132.588 64.189 126.059 61.31 130.351 61.258 130.351 58.404 126.085 58.404 132.588 56.827 132.588"/>
<path fill="#ADA9AE" d="M79.506 128.062v-0.026c0-1.769-1.29-3.238-3.102-3.238 -1.811 0-3.075 1.443-3.075 3.212v0.026c0 1.769 1.29 3.239 3.101 3.239C78.242 131.275 79.506 129.831 79.506 128.062M71.648 128.062v-0.026c0-2.562 1.981-4.709 4.782-4.709 2.802 0 4.757 2.121 4.757 4.683v0.026c0 2.562-1.981 4.708-4.783 4.708C73.603 132.744 71.648 130.624 71.648 128.062"/>
<path fill="#ADA9AE" d="M83.179 123.484L89.968 123.484 89.968 124.941 84.782 124.941 84.782 127.425 89.382 127.425 89.382 128.881 84.782 128.881 84.782 132.588 83.179 132.588"/>
<path fill="#ADA9AE" d="M95.027 131.262l0.964-1.145c0.873 0.755 1.746 1.184 2.88 1.184 0.99 0 1.616-0.455 1.616-1.145v-0.025c0-2.034-5.096-0.646-5.096-4.111v-0.026c0-2.794 3.911-3.507 6.425-1.495l-0.86 1.209c-2.098-1.561-3.961-0.947-3.961 0.144v0.026c0 2.005 5.095 0.737 5.095 4.096V130C102.09 133.192 97.564 133.521 95.027 131.262"/>
<path fill="#ADA9AE" d="M109.296 128.973l-1.577-3.642 -1.564 3.642H109.296zM107.002 123.419h1.485l4.014 9.169h-1.694l-0.925-2.198h-4.314l-0.937 2.198h-1.643L107.002 123.419z"/>
<path fill="#ADA9AE" d="M113.444 128.726v-5.242h1.603v5.177c0 1.691 0.873 2.601 2.306 2.601 1.421 0 2.294-0.858 2.294-2.536v-5.242h1.603v5.164c0 2.718-1.538 4.084-3.923 4.084C114.955 132.732 113.444 131.366 113.444 128.726"/>
<path fill="#ADA9AE" d="M126.997 131.131c4.254 0 4.204-6.191 0-6.191h-1.798v6.191H126.997zM123.596 123.484h3.401c6.508 0 6.42 9.104 0 9.104h-3.401V123.484z"/>
<path fill="#ADA9AE" d="M133.875 123.484H135.478V132.588H133.875z"/>
<path fill="#ADA9AE" d="M147.352 128.973l-1.577-3.642 -1.564 3.642H147.352zM145.058 123.419h1.485l4.014 9.169h-1.694l-0.925-2.198h-4.314l-0.937 2.198h-1.643L145.058 123.419z"/>
<path fill="#ADA9AE" d="M156.02 127.997c1.147 0 1.877-0.598 1.877-1.522v-0.026c0-0.975-0.704-1.509-1.89-1.509h-2.332v3.057H156.02zM152.072 123.484h4.065c1.928 0 3.389 0.969 3.389 2.874 -0.037 0.1 0.19 2.108-2.177 2.784l2.463 3.446h-1.89l-2.241-3.174c-0.175 0-2.109 0-2.006 0v3.174h-1.603V123.484z"/>
<path fill="#ADA9AE" d="M167.299 128.973l-1.577-3.642 -1.564 3.642H167.299zM165.005 123.419h1.486l4.013 9.169h-1.694l-0.925-2.198h-4.314l-0.937 2.198h-1.643L165.005 123.419z"/>
<path fill="#ADA9AE" d="M176.253 131.171c1.095 0 1.759-0.43 1.759-1.249v-0.026c0-0.767-0.612-1.223-1.876-1.223h-2.541v2.498H176.253zM175.784 127.321c1.029 0 1.72-0.403 1.72-1.236v-0.026c0-0.715-0.573-1.157-1.603-1.157h-2.306v2.419H175.784zM172.018 123.484h4.092c1.604 0 2.997 0.75 2.997 2.315 -0.037 0.1 0.164 1.294-1.303 2.055 1.069 0.364 1.811 0.976 1.811 2.211v0.026c0 1.626-1.342 2.497-3.375 2.497h-4.222V123.484z"/>
<path fill="#ADA9AE" d="M181.6 123.484H183.203V132.588H181.6z"/>
<path fill="#ADA9AE" d="M191.041 128.973l-1.577-3.642 -1.564 3.642H191.041zM188.747 123.419h1.485l4.014 9.169h-1.694l-0.925-2.198h-4.314l-0.937 2.198h-1.643L188.747 123.419z"/>
</g>
</switch>
</svg>

After

Width:  |  Height:  |  Size: 23 KiB

949
static/js/messages.js Normal file
View File

@ -0,0 +1,949 @@
/**
* Message System JavaScript
* Handles interactive features for the modern message interface
*/
class MessageSystem {
constructor() {
this.init();
}
init() {
this.initSearch();
this.initFolderNavigation();
this.initMessageActions();
this.initComposeFeatures();
this.initKeyboardShortcuts();
this.initAutoSave();
this.initAttachments();
this.initTooltips();
}
/**
* Initialize search functionality
*/
initSearch() {
const searchInputs = document.querySelectorAll('.search-input');
searchInputs.forEach(input => {
let searchTimeout;
input.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
const searchTerm = e.target.value.toLowerCase();
searchTimeout = setTimeout(() => {
this.performSearch(searchTerm);
}, 300);
});
// Clear search on Escape key
input.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
e.target.value = '';
this.performSearch('');
}
});
});
}
/**
* Perform search across message items
*/
performSearch(searchTerm) {
const messageItems = document.querySelectorAll('.message-item');
let visibleCount = 0;
messageItems.forEach(item => {
const text = item.textContent.toLowerCase();
if (text.includes(searchTerm)) {
item.style.display = 'flex';
visibleCount++;
} else {
item.style.display = 'none';
}
});
// Show no results message if needed
this.updateSearchResults(visibleCount, searchTerm);
}
/**
* Update search results display
*/
updateSearchResults(count, searchTerm) {
let noResults = document.querySelector('.search-no-results');
if (count === 0 && searchTerm) {
if (!noResults) {
noResults = document.createElement('div');
noResults.className = 'search-no-results';
noResults.innerHTML = `
<div class="text-center py-8">
<i class="fas fa-search text-4xl text-gray-400 mb-4"></i>
<h3 class="text-lg font-semibold text-gray-700 mb-2">
{% trans "No messages found" %}
</h3>
<p class="text-gray-500">
{% trans "No messages match your search for" %} "${searchTerm}"
</p>
</div>
`;
const messagesList = document.querySelector('.messages-list');
if (messagesList) {
messagesList.appendChild(noResults);
}
}
} else if (noResults) {
noResults.remove();
}
}
/**
* Initialize folder navigation
*/
initFolderNavigation() {
const folderItems = document.querySelectorAll('.folder-item');
folderItems.forEach(item => {
item.addEventListener('click', (e) => {
// Remove active class from all items
folderItems.forEach(f => f.classList.remove('active'));
// Add active class to clicked item
item.classList.add('active');
// Add loading state
this.showLoadingState();
// Navigate to folder (if it's a link)
if (item.tagName === 'A') {
// Let the link handle navigation
return;
}
});
});
}
/**
* Initialize message actions
*/
initMessageActions() {
// Refresh button
const refreshBtn = document.querySelector('.action-btn[title="Refresh"]');
if (refreshBtn) {
refreshBtn.addEventListener('click', () => {
this.refreshMessages();
});
}
// Mark as read functionality
const markReadBtns = document.querySelectorAll('[onclick*="markAsRead"]');
markReadBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
this.markAsRead(btn);
});
});
// Delete message functionality
const deleteBtns = document.querySelectorAll('[onclick*="confirm"]');
deleteBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
if (!this.confirmDelete()) {
e.preventDefault();
}
});
});
}
/**
* Initialize compose features
*/
initComposeFeatures() {
const form = document.getElementById('composeForm');
if (!form) return;
// Auto-resize textarea
const textarea = form.querySelector('textarea[name="content"]');
if (textarea) {
textarea.addEventListener('input', () => {
this.autoResizeTextarea(textarea);
});
}
// Rich text toolbar
const toolbarBtns = document.querySelectorAll('.toolbar-btn');
toolbarBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
this.handleToolbarAction(btn);
});
});
// Save draft button
const saveDraftBtn = document.getElementById('saveDraftBtn');
if (saveDraftBtn) {
saveDraftBtn.addEventListener('click', () => {
this.saveDraft();
});
}
// Form submission
form.addEventListener('submit', (e) => {
this.handleFormSubmit(e);
});
}
/**
* Initialize keyboard shortcuts
*/
initKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// Ctrl/Cmd + K for search
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
const searchInput = document.querySelector('.search-input');
if (searchInput) {
searchInput.focus();
}
}
// Ctrl/Cmd + N for new message
if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
e.preventDefault();
const composeBtn = document.querySelector('.compose-btn');
if (composeBtn) {
window.location.href = composeBtn.href;
}
}
// Escape to close modals
if (e.key === 'Escape') {
this.closeModals();
}
});
}
/**
* Initialize auto-save functionality
*/
initAutoSave() {
const form = document.getElementById('composeForm');
if (!form) return;
let autoSaveTimer;
const inputs = form.querySelectorAll('input, textarea, select');
inputs.forEach(input => {
input.addEventListener('input', () => {
clearTimeout(autoSaveTimer);
autoSaveTimer = setTimeout(() => {
this.autoSave();
}, 30000); // Auto-save after 30 seconds
});
});
}
/**
* Initialize attachment handling
*/
initAttachments() {
const attachBtn = document.querySelector('.toolbar-btn[title*="Attach"]');
if (attachBtn) {
attachBtn.addEventListener('click', () => {
this.showAttachmentDialog();
});
}
}
/**
* Initialize tooltips
*/
initTooltips() {
const tooltipElements = document.querySelectorAll('[title]');
tooltipElements.forEach(element => {
element.addEventListener('mouseenter', (e) => {
this.showTooltip(e.target);
});
element.addEventListener('mouseleave', (e) => {
this.hideTooltip(e.target);
});
});
}
/**
* Refresh messages with animation
*/
refreshMessages() {
const refreshBtn = document.querySelector('.action-btn[title="Refresh"]');
if (refreshBtn) {
const icon = refreshBtn.querySelector('i');
icon.classList.add('fa-spin');
setTimeout(() => {
icon.classList.remove('fa-spin');
location.reload();
}, 1000);
}
}
/**
* Mark message as read
*/
async markAsRead(button) {
const messageId = button.getAttribute('data-message-id');
if (!messageId) return;
try {
const response = await fetch(`/messages/${messageId}/mark-read/`, {
method: 'POST',
headers: {
'X-CSRFToken': this.getCSRFToken(),
'Content-Type': 'application/json',
},
});
const data = await response.json();
if (data.success) {
this.showNotification('{% trans "Message marked as read" %}', 'success');
location.reload();
} else {
this.showNotification('{% trans "Failed to mark message as read" %}', 'error');
}
} catch (error) {
console.error('Error marking message as read:', error);
this.showNotification('{% trans "An error occurred" %}', 'error');
}
}
/**
* Confirm delete action
*/
confirmDelete() {
return confirm('{% trans "Are you sure you want to delete this message?" %}');
}
/**
* Auto-resize textarea
*/
autoResizeTextarea(textarea) {
textarea.style.height = 'auto';
textarea.style.height = textarea.scrollHeight + 'px';
}
/**
* Handle toolbar actions
*/
handleToolbarAction(button) {
const action = button.getAttribute('title');
const textarea = document.querySelector('textarea[name="content"]');
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end);
switch (action) {
case '{% trans "Bold" %}':
this.wrapText(textarea, '**', '**');
break;
case '{% trans "Italic" %}':
this.wrapText(textarea, '*', '*');
break;
case '{% trans "Underline" %}':
this.wrapText(textarea, '__', '__');
break;
case '{% trans "Bullet List" %}':
this.insertList(textarea, '- ');
break;
case '{% trans "Numbered List" %}':
this.insertList(textarea, '1. ');
break;
case '{% trans "Insert Link" %}':
this.insertLink(textarea);
break;
case '{% trans "Insert Image" %}':
this.insertImage(textarea);
break;
case '{% trans "Attach File" %}':
this.showAttachmentDialog();
break;
}
}
/**
* Wrap selected text with formatting
*/
wrapText(textarea, before, after) {
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = textarea.value.substring(start, end);
const replacement = before + selectedText + after;
textarea.value = textarea.value.substring(0, start) + replacement + textarea.value.substring(end);
textarea.selectionStart = start + before.length;
textarea.selectionEnd = start + before.length + selectedText.length;
textarea.focus();
}
/**
* Insert list
*/
insertList(textarea, marker) {
const start = textarea.selectionStart;
const text = marker + '\n';
textarea.value = textarea.value.substring(0, start) + text + textarea.value.substring(start);
textarea.selectionStart = textarea.selectionEnd = start + text.length;
textarea.focus();
}
/**
* Insert link
*/
insertLink(textarea) {
const url = prompt('{% trans "Enter URL:" %}', 'https://');
if (url) {
const link = `[${url}](${url})`;
this.insertAtCursor(textarea, link);
}
}
/**
* Insert image
*/
insertImage(textarea) {
const url = prompt('{% trans "Enter image URL:" %}', 'https://');
if (url) {
const image = `![Image](${url})`;
this.insertAtCursor(textarea, image);
}
}
/**
* Insert text at cursor position
*/
insertAtCursor(textarea, text) {
const start = textarea.selectionStart;
textarea.value = textarea.value.substring(0, start) + text + textarea.value.substring(start);
textarea.selectionStart = textarea.selectionEnd = start + text.length;
textarea.focus();
}
/**
* Save draft
*/
async saveDraft() {
const form = document.getElementById('composeForm');
if (!form) return;
const formData = new FormData(form);
try {
const response = await fetch('/messages/save-draft/', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': this.getCSRFToken(),
},
});
const data = await response.json();
if (data.success) {
this.showNotification('{% trans "Draft saved" %}', 'success');
} else {
this.showNotification('{% trans "Failed to save draft" %}', 'error');
}
} catch (error) {
console.error('Error saving draft:', error);
this.showNotification('{% trans "An error occurred" %}', 'error');
}
}
/**
* Auto-save draft
*/
async autoSave() {
const form = document.getElementById('composeForm');
if (!form) return;
// Only auto-save if form has content
const hasContent = form.querySelector('textarea[name="content"]').value.trim() ||
form.querySelector('input[name="subject"]').value.trim();
if (!hasContent) return;
try {
const formData = new FormData(form);
await fetch('/messages/auto-save-draft/', {
method: 'POST',
body: formData,
headers: {
'X-CSRFToken': this.getCSRFToken(),
},
});
console.log('Draft auto-saved');
} catch (error) {
console.error('Error auto-saving draft:', error);
}
}
/**
* Handle form submission
*/
handleFormSubmit(e) {
const form = e.target;
const submitBtn = form.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>{% trans "Sending..." %}';
}
}
/**
* Show attachment dialog
*/
showAttachmentDialog() {
const input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.accept = 'image/*,.pdf,.doc,.docx,.txt';
input.addEventListener('change', (e) => {
this.handleFileSelect(e.target.files);
});
input.click();
}
/**
* Handle file selection
*/
handleFileSelect(files) {
const attachmentsSection = document.getElementById('attachmentsSection');
const attachmentList = document.getElementById('attachmentList');
if (!attachmentsSection || !attachmentList) return;
attachmentsSection.style.display = 'block';
Array.from(files).forEach(file => {
const attachmentItem = this.createAttachmentItem(file);
attachmentList.appendChild(attachmentItem);
});
}
/**
* Create attachment item
*/
createAttachmentItem(file) {
const item = document.createElement('div');
item.className = 'attachment-item';
const icon = this.getFileIcon(file.type);
const size = this.formatFileSize(file.size);
item.innerHTML = `
<i class="fas ${icon} attachment-icon"></i>
<span class="attachment-name">${file.name}</span>
<span class="attachment-size">${size}</span>
<i class="fas fa-times attachment-remove" onclick="this.parentElement.remove()"></i>
`;
return item;
}
/**
* Get file icon based on MIME type
*/
getFileIcon(mimeType) {
if (mimeType.startsWith('image/')) return 'fa-image';
if (mimeType.includes('pdf')) return 'fa-file-pdf';
if (mimeType.includes('word')) return 'fa-file-word';
if (mimeType.includes('text')) return 'fa-file-alt';
return 'fa-file';
}
/**
* Format file size
*/
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
/**
* Show loading state
*/
showLoadingState() {
const messagesList = document.querySelector('.messages-list');
if (messagesList) {
messagesList.style.opacity = '0.5';
}
}
/**
* Show notification
*/
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `notification notification-${type}`;
notification.innerHTML = `
<i class="fas ${this.getNotificationIcon(type)} me-2"></i>
${message}
`;
document.body.appendChild(notification);
setTimeout(() => {
notification.classList.add('show');
}, 100);
setTimeout(() => {
notification.classList.remove('show');
setTimeout(() => {
notification.remove();
}, 300);
}, 3000);
}
/**
* Get notification icon
*/
getNotificationIcon(type) {
const icons = {
success: 'fa-check-circle',
error: 'fa-exclamation-circle',
warning: 'fa-exclamation-triangle',
info: 'fa-info-circle'
};
return icons[type] || icons.info;
}
/**
* Show tooltip
*/
showTooltip(element) {
const title = element.getAttribute('title');
if (!title) return;
const tooltip = document.createElement('div');
tooltip.className = 'tooltip';
tooltip.textContent = title;
document.body.appendChild(tooltip);
const rect = element.getBoundingClientRect();
tooltip.style.left = rect.left + (rect.width / 2) - (tooltip.offsetWidth / 2) + 'px';
tooltip.style.top = rect.top - tooltip.offsetHeight - 5 + 'px';
element.setAttribute('data-original-title', title);
element.removeAttribute('title');
}
/**
* Hide tooltip
*/
hideTooltip(element) {
const tooltip = document.querySelector('.tooltip');
if (tooltip) {
tooltip.remove();
}
const originalTitle = element.getAttribute('data-original-title');
if (originalTitle) {
element.setAttribute('title', originalTitle);
element.removeAttribute('data-original-title');
}
}
/**
* Close modals
*/
closeModals() {
const modals = document.querySelectorAll('.modal');
modals.forEach(modal => {
modal.style.display = 'none';
});
}
/**
* Get CSRF token
*/
getCSRFToken() {
const cookie = document.cookie.split(';').find(c => c.trim().startsWith('csrftoken='));
return cookie ? cookie.split('=')[1] : '';
}
/**
* Initialize reply functionality
*/
initReplyFunctionality() {
// Handle reply button clicks
const replyBtns = document.querySelectorAll('.reply-btn');
replyBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
this.handleReplyClick(btn);
});
});
// Handle reply form submissions
const replyForms = document.querySelectorAll('#reply-form');
replyForms.forEach(form => {
form.addEventListener('submit', (e) => {
e.preventDefault();
this.handleReplySubmit(form);
});
});
// Handle cancel button in reply forms
const cancelBtns = document.querySelectorAll('[onclick*="hideReplyForm"]');
cancelBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
e.preventDefault();
this.hideReplyForm(btn.closest('#reply-section'));
});
});
}
/**
* Handle reply button click
*/
async handleReplyClick(button) {
const messageId = button.getAttribute('data-message-id');
if (!messageId) return;
try {
// Show loading state
const originalText = button.innerHTML;
button.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Loading...';
button.disabled = true;
// Fetch reply form via AJAX
const response = await fetch(`/messages/${messageId}/reply/`, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': this.getCSRFToken(),
},
});
const data = await response.json();
if (data.success) {
// Show reply form
const replySection = document.getElementById('reply-section');
if (replySection) {
replySection.innerHTML = data.html;
// Focus on the textarea
const textarea = replySection.querySelector('textarea[name="content"]');
if (textarea) {
setTimeout(() => textarea.focus(), 100);
}
}
} else {
this.showNotification('Failed to load reply form', 'error');
}
// Restore button state
button.innerHTML = originalText;
button.disabled = false;
} catch (error) {
console.error('Error loading reply form:', error);
this.showNotification('An error occurred while loading reply form', 'error');
// Restore button state
button.innerHTML = originalText;
button.disabled = false;
}
}
/**
* Handle reply form submission
*/
async handleReplySubmit(form) {
const submitBtn = form.querySelector('button[type="submit"]');
const textarea = form.querySelector('textarea[name="content"]');
const content = textarea.value.trim();
if (!content) {
this.showNotification('Reply content cannot be empty', 'error');
textarea.focus();
return;
}
try {
// Show loading state
const originalText = submitBtn.innerHTML;
submitBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i> Sending...';
submitBtn.disabled = true;
const formData = new FormData(form);
const response = await fetch(form.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRFToken': this.getCSRFToken(),
},
});
const data = await response.json();
if (data.success) {
this.showNotification(data.message, 'success');
// Add the reply to the conversation (if on detail page)
this.addReplyToConversation(data);
// Hide the reply form
this.hideReplyForm(form.closest('#reply-section'));
} else {
this.showNotification(data.error || 'Failed to send reply', 'error');
}
// Restore button state
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
} catch (error) {
console.error('Error sending reply:', error);
this.showNotification('An error occurred while sending reply', 'error');
// Restore button state
submitBtn.innerHTML = originalText;
submitBtn.disabled = false;
}
}
/**
* Add reply to conversation
*/
addReplyToConversation(replyData) {
const conversationContainer = document.querySelector('.conversation-container');
if (!conversationContainer) return;
// Create new reply element
const replyElement = document.createElement('div');
replyElement.className = 'message-reply fade-in';
replyElement.innerHTML = `
<div class="flex items-start space-x-3 mb-4">
<div class="flex-shrink-0">
<div class="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center">
<span class="text-white text-sm font-medium">${replyData.sender_name || 'You'}</span>
</div>
</div>
<div class="flex-grow">
<div class="bg-gray-50 rounded-lg p-4">
<div class="flex items-center justify-between mb-2">
<h4 class="text-sm font-medium text-gray-900">Reply</h4>
<span class="text-xs text-gray-500">${replyData.reply_time}</span>
</div>
<p class="text-sm text-gray-700 whitespace-pre-wrap">${replyData.reply_content}</p>
</div>
</div>
</div>
`;
conversationContainer.appendChild(replyElement);
// Scroll to the new reply
replyElement.scrollIntoView({ behavior: 'smooth', block: 'end' });
}
/**
* Hide reply form
*/
hideReplyForm(replySection) {
if (replySection) {
replySection.innerHTML = '';
}
}
}
// Initialize the message system when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
window.messageSystem = new MessageSystem();
// Initialize reply functionality
if (window.messageSystem) {
window.messageSystem.initReplyFunctionality();
}
});
// Add notification styles
const notificationStyles = `
.notification {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 8px;
color: white;
font-weight: 500;
z-index: 9999;
transform: translateX(100%);
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.notification.show {
transform: translateX(0);
}
.notification-success {
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
}
.notification-error {
background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
}
.notification-warning {
background: linear-gradient(135deg, #f59e0b 0%, #d97706 100%);
}
.notification-info {
background: linear-gradient(135deg, #3b82f6 0%, #2563eb 100%);
}
.tooltip {
position: absolute;
background: #1f2937;
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 12px;
white-space: nowrap;
z-index: 9999;
pointer-events: none;
}
.tooltip::after {
content: '';
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
border: 5px solid transparent;
border-top-color: #1f2937;
}
`;
// Add styles to head
const styleSheet = document.createElement('style');
styleSheet.textContent = notificationStyles;
document.head.appendChild(styleSheet);

View File

@ -32,8 +32,8 @@
--gray-text: #6c757d;
--kaauh-border: #d0d7de; /* Cleaner border color */
--kaauh-shadow: 0 15px 35px rgba(0, 0, 0, 0.1); /* Deeper shadow for premium look */
--kaauh-dark-bg: #0d0d0d;
--kaauh-dark-contrast: #1c1c1c;
--kaauh-dark-bg: #0d0d0d;
--kaauh-dark-contrast: #1c1c1c;
/* CALCULATED STICKY HEIGHTS (As provided in base) */
--navbar-height: 56px;
@ -43,17 +43,145 @@
body {
min-height: 100vh;
background-color: #f0f0f5;
background-color: #f0f0f5;
padding-top: 0;
}
.text-primary-theme { color: var(--kaauh-teal) !important; }
.text-primary-theme-hover:hover { color: var(--kaauh-teal-dark) !important; }
/* Language Dropdown Styles */
.language-toggle-btn {
background-color: transparent;
border: 1px solid var(--kaauh-border);
color: var(--kaauh-teal);
padding: 0.5rem 1rem;
border-radius: 0.5rem;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
font-weight: 500;
min-width: 120px;
justify-content: center;
}
.language-toggle-btn:hover {
background-color: var(--kaauh-teal-light);
border-color: var(--kaauh-teal);
color: var(--kaauh-teal-dark);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 99, 110, 0.15);
}
.language-toggle-btn:focus {
outline: none;
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
border-color: var(--kaauh-teal);
}
.language-toggle-btn::after {
margin-left: 0.5rem;
}
.dropdown-menu {
border: 1px solid var(--kaauh-border);
border-radius: 0.5rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
padding: 0.5rem;
min-width: 180px;
}
.dropdown-menu .dropdown-item {
padding: 0.75rem 1rem;
transition: all 0.2s ease;
border-radius: 0.375rem;
margin: 0.25rem 0;
display: flex;
align-items: center;
gap: 0.75rem;
font-weight: 500;
border: none;
background: transparent;
width: 100%;
text-align: left;
}
.dropdown-menu .dropdown-item:hover {
background-color: var(--kaauh-teal);
color: white;
transform: translateX(4px);
}
.dropdown-menu .dropdown-item:focus {
outline: none;
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
.dropdown-menu .dropdown-item.active {
background-color: var(--kaauh-teal);
color: white;
font-weight: 600;
box-shadow: 0 2px 4px rgba(0, 99, 110, 0.2);
}
.dropdown-menu .dropdown-item.active:hover {
background-color: var(--kaauh-teal-dark);
}
.flag-emoji {
font-size: 1.2rem;
line-height: 1;
min-width: 24px;
text-align: center;
}
.language-text {
font-size: 0.9rem;
font-weight: 500;
}
/* RTL Support for Language Dropdown */
html[dir="rtl"] .language-toggle-btn {
flex-direction: row-reverse;
}
html[dir="rtl"] .dropdown-menu .dropdown-item {
flex-direction: row-reverse;
text-align: right;
}
html[dir="rtl"] .dropdown-menu .dropdown-item:hover {
transform: translateX(-4px);
}
/* Mobile Responsiveness */
@media (max-width: 768px) {
.language-toggle-btn {
min-width: 100px;
padding: 0.4rem 0.8rem;
font-size: 0.9rem;
}
.dropdown-menu {
min-width: 160px;
}
.dropdown-menu .dropdown-item {
padding: 0.6rem 0.8rem;
font-size: 0.85rem;
}
.flag-emoji {
font-size: 1rem;
min-width: 20px;
}
}
.bg-kaauh-teal {
background-color: #00636e;
}
.btn-main-action {
background-color: var(--kaauh-teal);
color: white;
@ -67,7 +195,7 @@
background-color: var(--kaauh-teal-dark);
color: white;
transform: translateY(-2px); /* More pronounced lift */
box-shadow: 0 10px 20px rgba(0, 99, 110, 0.5);
box-shadow: 0 10px 20px rgba(0, 99, 110, 0.5);
}
/* ---------------------------------------------------------------------- */
/* 1. DARK HERO STYLING (High Contrast) */
@ -75,16 +203,16 @@
.hero-section {
background: linear-gradient(135deg, var(--kaauh-dark-contrast) 0%, var(--kaauh-dark-bg) 100%);
padding: 4rem 0; /* Reduced from 8rem to 4rem */
margin-top: -1px;
color: white;
position: relative;
margin-top: -1px;
color: white;
position: relative;
overflow: hidden;
}
.hero-title {
font-size: 2.5rem; /* Reduced from 3.5rem to 2.5rem */
font-weight: 800; /* Extra bold */
line-height: 1.1;
letter-spacing: -0.05em;
letter-spacing: -0.05em;
max-width: 900px;
}
.hero-section .lead {
@ -104,7 +232,7 @@
padding: 10rem 0;
}
.hero-title {
font-size: 5.5rem;
font-size: 5.5rem;
}
}
@ -153,20 +281,20 @@
background-color: #f0f0f5; /* Separates the job list from the white path section */
padding-top: 3rem;
}
.job-listing-card {
border: 1px solid var(--kaauh-border);
border-left: 6px solid var(--kaauh-teal);
border: 1px solid var(--kaauh-border);
border-left: 6px solid var(--kaauh-teal);
border-radius: 0.75rem;
padding: 2rem !important;
padding: 2rem !important;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05); /* Lighter default shadow */
}
.job-listing-card:hover {
transform: translateY(-3px); /* Increased lift */
box-shadow: 0 12px 25px rgba(0, 99, 110, 0.15); /* Stronger hover shadow */
background-color: var(--kaauh-teal-light);
background-color: var(--kaauh-teal-light);
}
.card.sticky-top-filters {
box-shadow: var(--kaauh-shadow); /* Uses the deeper card shadow */
}
@ -215,7 +343,8 @@
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button name="language" value="en" class="dropdown-item {% if LANGUAGE_CODE == 'en' %}active bg-light-subtle{% endif %}" type="submit">
<span class="me-2">🇺🇸</span> English
<span class="flag-emoji">🇺🇸</span>
<span class="language-text">English</span>
</button>
</form>
</li>
@ -224,7 +353,8 @@
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
<button name="language" value="ar" class="dropdown-item {% if LANGUAGE_CODE == 'ar' %}active bg-light-subtle{% endif %}" type="submit">
<span class="me-2">🇸🇦</span> العربية (Arabic)
<span class="flag-emoji">🇸🇦</span>
<span class="language-text">العربية (Arabic)</span>
</button>
</form>
</li>
@ -239,7 +369,7 @@
<div class="container message-container mt-3">
<div class="row">
{# Use responsive columns matching the main content block for alignment #}
<div class="col-lg-12 order-lg-1 col-12 mx-auto">
<div class="col-lg-12 order-lg-1 col-12 mx-auto">
{% for message in messages %}
<div class="alert alert-{{ message.tags|default:'info' }} alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle me-2"></i> {{ message }}
@ -254,20 +384,20 @@
{# ================================================= #}
{# DJANGO MESSAGE BLOCK - Placed directly below the main navbar #}
{# ================================================= #}
{# ================================================= #}
{% block content %}
{% endblock content %}
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
{% block customJS %}
{% endblock %}
</body>
</html>
</html>

View File

@ -179,8 +179,9 @@
{% if request.user.is_superuser %}
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'admin_settings' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Settings" %}</span></a></li>
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'easy_logs' %}"><i class="fas fa-history me-3 fs-5"></i> <span>{% trans "Activity Log" %}</span></a></li>
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'admin_settings' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Settings" %}</span></a></li>
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'source_list' %}"><i class="fas fa-cog me-3 fs-5"></i> <span>{% trans "Integration" %}</span></a></li>
<li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none text-teal" href="{% url 'easy_logs' %}"><i class="fas fa-history me-3 fs-5"></i> <span>{% trans "Activity Log" %}</span></a></li>
{% comment %} <li><a class="dropdown-item py-2 px-4 d-flex align-items-center text-decoration-none" href="#"><i class="fas fa-question-circle me-3 text-primary fs-5"></i> <span>{% trans "Help & Support" %}</span></a></li> {% endcomment %}
{% endif %}
{% endif %}
@ -250,7 +251,7 @@
<a class="nav-link {% if request.resolver_match.url_name == 'person_list' %}active{% endif %}" href="{% url 'person_list' %}">
<span class="d-flex align-items-center gap-2">
{% include "icons/users.html" %}
{% trans "Person" %}
{% trans "Applicant" %}
</span>
</a>
</li>

View File

@ -272,7 +272,7 @@
<th scope="col" rowspan="2">{% trans "Actions" %}</th>
<th scope="col" rowspan="2" class="text-center">{% trans "Manage Forms" %}</th>
<th scope="col" colspan="5" class="candidate-management-header-title">
<th scope="col" colspan="6" class="candidate-management-header-title">
{% trans "Applicants Metrics" %}
</th>
</tr>
@ -282,10 +282,11 @@
<th style="width: calc(50% / 7);">{% trans "Screened" %}</th>
<th style="width: calc(50% / 7 * 2);">{% trans "Exam" %}</th>
<th style="width: calc(50% / 7 * 2);">{% trans "Interview" %}</th>
<th style="width: calc(50% / 7 * 2);">{% trans "Documets Review" %}</th>
<th style="width: calc(50% / 7);">{% trans "Offer" %}</th>
</tr>
</thead>
<tbody>
{% for job in jobs %}
<tr>
@ -311,7 +312,7 @@
<td class="text-center">
<div class="btn-group btn-group-sm" role="group">
{% if job.form_template %}
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Preview' %}">
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary {% if job.status != 'ACTIVE' %}disabled{% endif %}" title="{% trans 'Preview' %}">
<i class="fas fa-eye"></i>
</a>
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary" title="{% trans 'Edit' %}">
@ -329,6 +330,7 @@
<td class="candidate-data-cell text-info"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-info">{% if job.screening_candidates.count %}{{ job.screening_candidates.count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_exam_view' job.slug %}" class="text-success">{% if job.exam_candidates.count %}{{ job.exam_candidates.count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_interview_view' job.slug %}" class="text-success">{% if job.interview_candidates.count %}{{ job.interview_candidates.count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_document_review_view' job.slug %}" class="text-success">{% if job.document_review_candidates.count %}{{ job.document_review_candidates.count }}{% else %}-{% endif %}</a></td>
<td class="candidate-data-cell text-success"><a href="{% url 'candidate_offer_view' job.slug %}" class="text-success">{% if job.offer_candidates.count %}{{ job.offer_candidates.count }}{% else %}-{% endif %}</a></td>
</tr>
{% endfor %}

View File

@ -113,7 +113,7 @@
</a>
{% comment %} CONNECTOR 1 -> 2 {% endcomment %}
<div class="stage-connector {% if current_stage == 'Exam' or current_stage == 'Interview' or current_stage == 'Offer' %}completed{% endif %}"></div>
<div class="stage-connector {% if current_stage == 'Exam' or current_stage == 'Interview' or current_stage == 'Document Review' or current_stage == 'Offer' or current_stage == 'Hired' %}completed{% endif %}"></div>
{% comment %} STAGE 2: Exam {% endcomment %}
<a href="{% url 'candidate_exam_view' job.slug %}"
@ -127,7 +127,7 @@
</a>
{% comment %} CONNECTOR 2 -> 3 {% endcomment %}
<div class="stage-connector {% if current_stage == 'Interview' or current_stage == 'Offer' %}completed{% endif %}"></div>
<div class="stage-connector {% if current_stage == 'Interview' or current_stage == 'Document Review' or current_stage == 'Offer' or current_stage == 'Hired' %}completed{% endif %}"></div>
{% comment %} STAGE 3: Interview {% endcomment %}
<a href="{% url 'candidate_interview_view' job.slug %}"

View File

@ -152,10 +152,10 @@
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-user-friends me-2"></i> {% trans "People Directory" %}
<i class="fas fa-user-friends me-2"></i> {% trans "Applicants List" %}
</h1>
<a href="{% url 'person_create' %}" class="btn btn-main-action">
<i class="fas fa-plus me-1"></i> {% trans "Add New Person" %}
<i class="fas fa-plus me-1"></i> {% trans "Add New Applicant" %}
</a>
</div>
@ -163,18 +163,14 @@
<div class="card mb-4 shadow-sm no-hover">
<div class="card-body">
<div class="row g-4">
<div class="col-md-6">
<label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label>
<form method="get" action="" class="w-100">
<div class="input-group input-group-lg">
<input type="text" name="q" class="form-control" id="search"
placeholder="{% trans 'Search people...' %}"
value="{{ request.GET.q }}">
<button class="btn btn-main-action" type="submit">
<i class="fas fa-search"></i>
</button>
</div>
</form>
<div class="input-group input-group-lg">
<form method="get" action="" class="w-100">
{% include 'includes/search_form.html' %}
</form>
</div>
</div>
<div class="col-md-6">
@ -200,10 +196,10 @@
</select>
</div>
<div class="col-md-4 d-flex justify-content-end align-self-end">
<div class="col-md-4 d-flex">
<div class="filter-buttons">
<button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-filter me-1"></i> {% trans "Apply" %}
<i class="fas fa-filter me-1"></i> {% trans "Apply Filter" %}
</button>
{% if request.GET.q or request.GET.nationality or request.GET.gender %}
<a href="{% url 'person_list' %}" class="btn btn-outline-secondary btn-sm">
@ -217,7 +213,8 @@
</div>
</div>
</div>
{% if people_list %}
<div id="person-list">
<!-- View Switcher -->

View File

@ -89,6 +89,7 @@
.status-applied { background: #e3f2fd; color: #1976d2; }
.status-screening { background: #fff3e0; color: #f57c00; }
.status-document_review { background: #f3e5f5; color: #7b1fa2; }
.status-exam { background: #f3e5f5; color: #7b1fa2; }
.status-interview { background: #e8f5e8; color: #388e3c; }
.status-offer { background: #fff8e1; color: #f9a825; }
@ -119,6 +120,9 @@
background-color: #f3e5f5;
border-color: #ce93d8;
}
.card-header{
padding: 0.75rem 1.25rem;
}
</style>
{% endblock %}
@ -155,7 +159,7 @@
</p>
</div>
<div class="col-md-4 text-end">
<span class="status-badge status-{{ application.stage|lower }}">
<span class="status-badge status-{{ application.stage }}">
{{ application.get_stage_display }}
</span>
</div>
@ -172,19 +176,9 @@
<div class="progress-label">{% trans "Applied" %}</div>
</div>
<!-- Screening Stage - Show if current stage is Screening or beyond -->
{% if application.stage in 'Screening,Exam,Interview,Offer,Hired,Rejected' %}
<div class="progress-step {% if application.stage not in 'Applied,Screening' %}completed{% elif application.stage == 'Screening' %}active{% endif %}">
<div class="progress-icon">
<i class="fas fa-search"></i>
</div>
<div class="progress-label">{% trans "Screening" %}</div>
</div>
{% endif %}
<!-- Exam Stage - Show if current stage is Exam or beyond -->
{% if application.stage in 'Exam,Interview,Offer,Hired,Rejected' %}
<div class="progress-step {% if application.stage not in 'Applied,Screening,Exam' %}completed{% elif application.stage == 'Exam' %}active{% endif %}">
{% if application.stage in 'Exam,Interview,Document Review,Offer,Hired,Rejected' %}
<div class="progress-step {% if application.stage not in 'Applied,Exam' %}completed{% elif application.stage == 'Exam' %}active{% endif %}">
<div class="progress-icon">
<i class="fas fa-clipboard-check"></i>
</div>
@ -193,8 +187,8 @@
{% endif %}
<!-- Interview Stage - Show if current stage is Interview or beyond -->
{% if application.stage in 'Interview,Offer,Hired,Rejected' %}
<div class="progress-step {% if application.stage not in 'Applied,Screening,Exam,Interview' %}completed{% elif application.stage == 'Interview' %}active{% endif %}">
{% if application.stage in 'Interview,Document Review,Offer,Hired,Rejected' %}
<div class="progress-step {% if application.stage not in 'Applied,Exam,Interview' %}completed{% elif application.stage == 'Interview' %}active{% endif %}">
<div class="progress-icon">
<i class="fas fa-video"></i>
</div>
@ -202,9 +196,19 @@
</div>
{% endif %}
<!-- Document Review Stage - Show if current stage is Document Review or beyond -->
{% if application.stage in 'Document Review,Offer,Hired,Rejected' %}
<div class="progress-step {% if application.stage not in 'Applied,Exam,Interview,Document Review' %}completed{% elif application.stage == 'Document Review' %}active{% endif %}">
<div class="progress-icon">
<i class="fas fa-file-alt"></i>
</div>
<div class="progress-label">{% trans "Document Review" %}</div>
</div>
{% endif %}
<!-- Offer Stage - Show if current stage is Offer or beyond -->
{% if application.stage in 'Offer,Hired,Rejected' %}
<div class="progress-step {% if application.stage not in 'Applied,Screening,Exam,Interview,Offer' %}completed{% elif application.stage == 'Offer' %}active{% endif %}">
<div class="progress-step {% if application.stage not in 'Applied,Exam,Interview,Document Review,Offer' %}completed{% elif application.stage == 'Offer' %}active{% endif %}">
<div class="progress-icon">
<i class="fas fa-handshake"></i>
</div>
@ -525,6 +529,11 @@
<i class="fas fa-search me-2"></i>
{% trans "Your application is currently under screening. We are evaluating your qualifications against the job requirements." %}
</div>
{% elif application.stage == 'Document Review' %}
<div class="alert alert-purple">
<i class="fas fa-file-alt me-2"></i>
{% trans "Please upload the required documents for review. Our team will evaluate your submitted materials." %}
</div>
{% elif application.stage == 'Exam' %}
<div class="alert alert-purple">
<i class="fas fa-clipboard-check me-2"></i>

View File

@ -647,30 +647,30 @@
<div class="card shadow-sm mb-2 p-2">
<h5 class="text-muted mb-3"><i class="fas fa-cog me-2"></i>{% trans "Management Actions" %}</h5>
<div class="d-grid gap-2">
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-primary">
{% comment %} <a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-primary">
<i class="fas fa-edit"></i> {% trans "Edit Details" %}
</a>
<a href="{% url 'candidate_delete' candidate.slug %}" class="btn btn-outline-danger" onclick="return confirm('{% trans "Are you sure you want to delete this candidate?" %}')">
</a> {% endcomment %}
{% comment %} <a href="{% url 'candidate_delete' candidate.slug %}" class="btn btn-outline-danger" onclick="return confirm('{% trans "Are you sure you want to delete this candidate?" %}')">
<i class="fas fa-trash-alt"></i> {% trans "Delete Candidate" %}
</a>
</a> {% endcomment %}
<a href="{% url 'candidate_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {% trans "Back to List" %}
</a>
{% if candidate.resume %}
<a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
{% comment %} <a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
<i class="fas fa-eye me-1"></i>
{% trans "View Actual Resume" %}
</a>
</a> {% endcomment %}
<a href="{{ candidate.resume.url }}" download class="btn btn-outline-primary">
<i class="fas fa-download me-1"></i>
{% trans "Download Resume" %}
</a>
<a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
{% comment %} <a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
<i class="fas fa-file-alt me-1"></i>
{% trans "View Resume AI Overview" %}
</a>
</a> {% endcomment %}
{% endif %}
</div>

View File

@ -211,8 +211,8 @@
<option selected>
----------
</option>
<option value="Offer">
{% trans "To Offer" %}
<option value="Document Review">
{% trans "To Document Review" %}
</option>
<option value="Exam">
{% trans "To Exam" %}

View File

@ -252,7 +252,7 @@
{% trans "GPA" %}
</label>
<input type="number" name="GPA" id="gpa" class="form-control form-control-sm"
value="{{ gpa }}" min="0" max="4" step="1"
value="{{ gpa }}" min="0" max="4"
placeholder="e.g., 4" style="width: 120px;">
</div>
<div class="col-auto">

View File

@ -69,7 +69,7 @@
{% csrf_token %}
<div class="row">
<div class="col-md-6 mb-3">
<div class="col-md-4 mb-3">
<label for="{{ form.first_name.id_for_label }}" class="form-label">
{% trans "First Name" %} <span class="text-danger">*</span>
</label>
@ -81,7 +81,19 @@
{% endif %}
</div>
<div class="col-md-6 mb-3">
<div class="col-md-4 mb-3">
<label for="{{ form.middle_name.id_for_label }}" class="form-label">
{% trans "Middle Name" %}
</label>
{{ form.middle_name }}
{% if form.middle_name.errors %}
<div class="text-danger small">
{{ form.middle_name.errors.0 }}
</div>
{% endif %}
</div>
<div class="col-md-4 mb-3">
<label for="{{ form.last_name.id_for_label }}" class="form-label">
{% trans "Last Name" %} <span class="text-danger">*</span>
</label>
@ -95,19 +107,7 @@
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.middle_name.id_for_label }}" class="form-label">
{% trans "Middle Name" %}
</label>
{{ form.middle_name }}
{% if form.middle_name.errors %}
<div class="text-danger small">
{{ form.middle_name.errors.0 }}
</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<div class="col-md-4 mb-3">
<label for="{{ form.phone.id_for_label }}" class="form-label">
{% trans "Phone Number" %} <span class="text-danger">*</span>
</label>
@ -118,21 +118,8 @@
</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="{{ form.gpa.id_for_label }}" class="form-label">
{% trans "GPA" %} <span class="text-danger">*</span>
</label>
{{ form.gpa }}
{% if form.nationality.errors %}
<div class="text-danger small">
{{ form.gpa.errors.0 }}
</div>
{% endif %}
</div>
<div class="col-md-6 mb-3">
<div class="col-md-4 mb-3">
<label for="{{ form.nationality.id_for_label }}" class="form-label">
{% trans "Nationality" %} <span class="text-danger">*</span>
</label>
@ -144,7 +131,7 @@
{% endif %}
</div>
<div class="col-md-6 mb-3">
<div class="col-md-4 mb-3">
<label for="{{ form.gender.id_for_label }}" class="form-label">
{% trans "Gender" %} <span class="text-danger">*</span>
</label>
@ -215,7 +202,7 @@
<div class="card-footer text-center">
<small class="text-muted">
{% trans "Already have an account?" %}
<a href="{% url 'portal_login' %}" class="text-decoration-none text-kaauh-teal">
<a href="{% url 'account_login' %}?next={% url 'application_submit_form' job.form_template.slug %}" class="text-decoration-none text-kaauh-teal">
{% trans "Login here" %}
</a>
</small>

View File

@ -13,12 +13,17 @@
<a href="{% url 'source_update' source.pk %}" class="btn btn-outline-secondary">
<i class="fas fa-edit"></i> Edit
</a>
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
{% comment %} <a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
<i class="fas fa-key"></i> Generate Keys
</a>
<button type="button"
class="btn btn-outline-warning"
</a> {% endcomment %}
<button id="toggle-source-status"
type="button"
class="btn btn-outline-{{ source.is_active|yesno:'warning,success' }}"
hx-post="{% url 'toggle_source_status' source.pk %}"
hx-target="#toggle-source-status"
hx-select="#toggle-source-status"
hx-select-oob="#source-status"
hx-swap="outerHTML"
hx-confirm="Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?"
title="{{ source.is_active|yesno:'Deactivate,Activate' }}">
<i class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i>
@ -93,7 +98,7 @@
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted">Status</label>
<div>
<div id="source-status">
{% if source.is_active %}
<span class="badge bg-success">Active</span>
{% else %}
@ -161,7 +166,7 @@
<div class="card-header">
<h6 class="mb-0">API Credentials</h6>
</div>
<div class="card-body">
<div id="api-credentials" class="card-body">
<div class="mb-3">
<label class="form-label text-muted">API Key</label>
<div class="input-group">
@ -190,7 +195,7 @@
</div>
</div>
<div class="text-end">
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning btn-sm">
<a hx-post="{% url 'generate_api_keys' source.pk %}" hx-target="#api-credentials" hx-select="#api-credentials" hx-swap="outerHTML" class="btn btn-main-action btn-sm">
<i class="fas fa-key"></i> Generate New Keys
</a>
</div>
@ -371,7 +376,7 @@
{% endfor %}
{% endblock %}
{% block extra_js %}
{% block customJS %}
<script>
function toggleSecretVisibility() {
const secretInput = document.getElementById('api-secret');

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% load static %}
{% load static i18n %}
{% load widget_tweaks %}
{% block title %}{{ title }}{% endblock %}
@ -50,7 +50,7 @@
<label for="{{ form.source_type.id_for_label }}" class="form-label">
{{ form.source_type.label }} <span class="text-danger">*</span>
</label>
{{ form.source_type|add_class:"form-select" }}
{{ form.source_type }}
{% if form.source_type.errors %}
<div class="invalid-feedback d-block">
{% for error in form.source_type.errors %}
@ -63,7 +63,41 @@
</div>
</div>
<div class="mb-3">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
{{ form.ip_address.label }} <span class="text-danger">*</span>
</label>
{{ form.ip_address|add_class:"form-control" }}
{% if form.ip_address.errors %}
<div class="invalid-feedback d-block">
{% for error in form.ip_address.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.ip_address.help_text }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
{{ form.trusted_ips.label }} <span class="text-danger">*</span>
</label>
{{ form.trusted_ips|add_class:"form-control" }}
{% if form.trusted_ips.errors %}
<div class="invalid-feedback d-block">
{% for error in form.trusted_ips.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.trusted_ips.help_text }}</div>
</div>
</div>
<div class="mb-3">
<label for="{{ form.description.id_for_label }}" class="form-label">
{{ form.description.label }}
</label>
@ -78,23 +112,6 @@
<div class="form-text">{{ form.description.help_text }}</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
{{ form.ip_address.label }}
</label>
{{ form.ip_address|add_class:"form-control" }}
{% if form.ip_address.errors %}
<div class="invalid-feedback d-block">
{% for error in form.ip_address.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<div class="form-text">{{ form.ip_address.help_text }}</div>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<div class="form-check">
@ -169,8 +186,8 @@
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times"></i> Cancel
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i> {{ button_text }}
<button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {% trans "Save" %}
</button>
</div>
</form>

View File

@ -1,16 +1,16 @@
{% extends "base.html" %}
{% load static %}
{% load static i18n %}
{% block title %}Sources{% endblock %}
{% block title %}{% trans "Sources" %}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">Sources</h1>
<a href="{% url 'source_create' %}" class="btn btn-primary">
<i class="fas fa-plus"></i> Create Source
<h1 class="h3 mb-0">{% trans "Integration Sources" %}</h1>
<a href="{% url 'source_create' %}" class="btn btn-main-action">
{% trans "Create Source for Integration" %} <i class="fas fa-plus"></i>
</a>
</div>
@ -29,11 +29,11 @@
</div>
<div class="col-md-4">
<button type="submit" class="btn btn-outline-primary">
<i class="fas fa-search"></i> Search
<i class="fas fa-search"></i> {% trans "Search" %}
</button>
{% if search_query %}
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times"></i> Clear
<i class="fas fa-times"></i> {% trans "Clear" %}
</a>
{% endif %}
</div>
@ -56,12 +56,12 @@
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Name</th>
<th>Type</th>
<th>Status</th>
<th>API Key</th>
<th>Created</th>
<th>Actions</th>
<th>{% trans "Name" %}</th>
<th>{% trans "Type" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "API Key" %}</th>
<th>{% trans "Created" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
@ -71,18 +71,16 @@
<a href="{% url 'source_detail' source.pk %}" class="text-decoration-none">
<strong>{{ source.name }}</strong>
</a>
{% if source.description %}
<br><small class="text-muted">{{ source.description|truncatechars:50 }}</small>
{% endif %}
</td>
<td>
<span class="badge bg-info">{{ source.get_source_type_display }}</span>
<span class="badge bg-info">{{ source.source_type }}</span>
</td>
<td>
{% if source.is_active %}
<span class="badge bg-success">Active</span>
<span class="badge bg-success">{% trans "Active" %}</span>
{% else %}
<span class="badge bg-secondary">Inactive</span>
<span class="badge bg-secondary">{% trans "Inactive" %}</span>
{% endif %}
</td>
<td>
@ -101,17 +99,17 @@
class="btn btn-sm btn-outline-secondary" title="Edit">
<i class="fas fa-edit"></i>
</a>
<button type="button"
{% comment %} <button type="button"
class="btn btn-sm btn-outline-warning"
hx-post="{% url 'toggle_source_status' source.pk %}"
hx-confirm="Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?"
title="{{ source.is_active|yesno:'Deactivate,Activate' }}">
<i class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i>
</button>
<a href="{% url 'source_delete' source.pk %}"
</button> {% endcomment %}
{% comment %} <a href="{% url 'source_delete' source.pk %}"
class="btn btn-sm btn-outline-danger" title="Delete">
<i class="fas fa-trash"></i>
</a>
</a> {% endcomment %}
</div>
</td>
</tr>
@ -167,16 +165,16 @@
{% else %}
<div class="text-center py-5">
<i class="fas fa-database fa-3x text-muted mb-3"></i>
<h5 class="text-muted">No sources found</h5>
<h5 class="text-muted">{% trans "No sources found" %}</h5>
<p class="text-muted">
{% if search_query %}
No sources match your search criteria.
{% blocktrans with query=query %}No sources match your search criteria "{{ query }}".{% endblocktrans %}
{% else %}
Get started by creating your first source.
{% trans "Get started by creating your first source." %}
{% endif %}
</p>
<a href="{% url 'source_create' %}" class="btn btn-primary">
<i class="fas fa-plus"></i> Create Source
<a href="{% url 'source_create' %}" class="btn btn-main-action">
<i class="fas fa-plus"></i> {% trans "Create Source" %}
</a>
</div>
{% endif %}