Compare commits
No commits in common. "fcc74dbac2741edac0e899066d6f4f4d392a5115" and "f1499f7be0419f44a355d2599f45850b55cd1a86" have entirely different histories.
fcc74dbac2
...
f1499f7be0
2
.env
2
.env
@ -1,3 +1,3 @@
|
||||
DB_NAME=haikal_db
|
||||
DB_USER=faheed
|
||||
DB_PASSWORD=Faheed@215
|
||||
DB_PASSWORD=Faheed@215
|
||||
|
||||
Binary file not shown.
@ -487,6 +487,3 @@ MESSAGE_TAGS = {
|
||||
|
||||
# Custom User Model
|
||||
AUTH_USER_MODEL = "recruitment.CustomUser"
|
||||
|
||||
|
||||
ZOOM_WEBHOOK_API_KEY = "2GNDC5Rvyw9AHoGikHXsQB"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,24 +1,15 @@
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
@ -234,10 +225,17 @@ 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,job=None):
|
||||
def send_bulk_email(subject, message, recipient_list, request=None, attachments=None, async_task_=False, from_interview=False):
|
||||
"""
|
||||
Send bulk email to multiple recipients with HTML support and attachments,
|
||||
supporting synchronous or asynchronous dispatch.
|
||||
@ -303,8 +301,7 @@ 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:
|
||||
@ -314,10 +311,7 @@ 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,
|
||||
sender_user_id,
|
||||
job_id,
|
||||
hook='recruitment.tasks.email_success_hook',
|
||||
|
||||
hook='recruitment.tasks.email_success_hook'
|
||||
)
|
||||
task_ids.append(task_id)
|
||||
|
||||
@ -356,101 +350,80 @@ 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:
|
||||
# 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).'
|
||||
}
|
||||
|
||||
email_obj.send(fail_silently=False)
|
||||
successful_sends += 1
|
||||
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}
|
||||
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}
|
||||
@ -55,7 +55,7 @@ class SourceForm(forms.ModelForm):
|
||||
|
||||
class Meta:
|
||||
model = Source
|
||||
fields = ["name", "source_type", "description", "ip_address","trusted_ips", "is_active"]
|
||||
fields = ["name", "source_type", "description", "ip_address", "is_active"]
|
||||
widgets = {
|
||||
"name": forms.TextInput(
|
||||
attrs={
|
||||
@ -81,9 +81,6 @@ 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"}),
|
||||
}
|
||||
|
||||
@ -728,7 +725,7 @@ class InterviewScheduleForm(forms.ModelForm):
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = InterviewSchedule
|
||||
model = InterviewSchedule
|
||||
fields = [
|
||||
'schedule_interview_type',
|
||||
"applications",
|
||||
@ -1544,7 +1541,8 @@ class CandidateEmailForm(forms.Form):
|
||||
label=_('Select Candidates'), # Use a descriptive label
|
||||
required=False
|
||||
)
|
||||
|
||||
|
||||
|
||||
subject = forms.CharField(
|
||||
max_length=200,
|
||||
widget=forms.TextInput(attrs={
|
||||
@ -1567,29 +1565,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
|
||||
|
||||
@ -1597,7 +1595,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.",
|
||||
@ -1617,18 +1615,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 you have cleared your exam!",
|
||||
f"We're pleased to inform you that your initial screening was successful!",
|
||||
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.",
|
||||
@ -1647,9 +1645,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:
|
||||
@ -1674,9 +1672,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_'):
|
||||
@ -1690,7 +1688,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"""
|
||||
@ -1870,7 +1868,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')
|
||||
@ -1883,7 +1881,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"
|
||||
|
||||
@ -1916,7 +1914,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},
|
||||
@ -1968,44 +1966,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
|
||||
@ -2023,8 +2021,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'}
|
||||
@ -2035,7 +2033,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'}),
|
||||
@ -2230,7 +2228,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'}),
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
# 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'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -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
|
||||
|
||||
@ -18,9 +18,7 @@ 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__)
|
||||
@ -31,10 +29,11 @@ User = get_user_model()
|
||||
@receiver(post_save, sender=JobPosting)
|
||||
def format_job(sender, instance, created, **kwargs):
|
||||
if created or not instance.ai_parsed:
|
||||
form = getattr(instance, "form_template", None)
|
||||
if not form:
|
||||
try:
|
||||
form_template = instance.form_template
|
||||
except FormTemplate.DoesNotExist:
|
||||
FormTemplate.objects.get_or_create(
|
||||
job=instance, is_active=True, name=instance.title
|
||||
job=instance, is_active=False, name=instance.title
|
||||
)
|
||||
async_task(
|
||||
"recruitment.tasks.format_job_description",
|
||||
@ -470,27 +469,3 @@ 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")
|
||||
|
||||
@ -12,9 +12,8 @@ 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,Message
|
||||
from django.contrib.auth import get_user_model
|
||||
User = get_user_model()
|
||||
from . models import InterviewSchedule,ScheduledInterview,ZoomMeetingDetails
|
||||
|
||||
# Add python-docx import for Word document processing
|
||||
try:
|
||||
from docx import Document
|
||||
@ -756,7 +755,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,sender,job):
|
||||
def _task_send_individual_email(subject, body_message, recipient, attachments):
|
||||
"""Internal helper to create and send a single email."""
|
||||
|
||||
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa')
|
||||
@ -776,36 +775,16 @@ def _task_send_individual_email(subject, body_message, recipient, attachments,se
|
||||
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=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")
|
||||
|
||||
|
||||
email_obj.send(fail_silently=False)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
|
||||
logger.error(f"Task failed to send email to {recipient}: {str(e)}", exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
def send_bulk_email_task(subject, message, recipient_list,attachments=None,sender_user_id=None,job_id=None, hook='recruitment.tasks.email_success_hook'):
|
||||
def send_bulk_email_task(subject, message, recipient_list, attachments=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")
|
||||
@ -814,13 +793,11 @@ def send_bulk_email_task(subject, message, recipient_list,attachments=None,sende
|
||||
|
||||
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,sender,job):
|
||||
if _task_send_individual_email(subject, message, recipient, attachments):
|
||||
successful_sends += 1
|
||||
|
||||
if successful_sends > 0:
|
||||
|
||||
@ -1336,7 +1336,6 @@ 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")
|
||||
|
||||
@ -1347,8 +1346,6 @@ 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,
|
||||
@ -1809,7 +1806,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
|
||||
@ -1857,9 +1854,8 @@ def candidate_screening_view(request, slug):
|
||||
)
|
||||
if gpa:
|
||||
candidates = candidates.filter(
|
||||
person__gpa__gt= gpa
|
||||
person__gpa = gpa
|
||||
)
|
||||
print(candidates)
|
||||
|
||||
if tier1_count > 0:
|
||||
candidates = candidates[:tier1_count]
|
||||
@ -3022,6 +3018,7 @@ 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(
|
||||
@ -3037,7 +3034,7 @@ def create_staff_user(request):
|
||||
|
||||
@staff_user_required
|
||||
def admin_settings(request):
|
||||
staffs = User.objects.filter(user_type="staff",is_superuser=False)
|
||||
staffs = User.objects.filter(is_superuser=False)
|
||||
form = ToggleAccountForm()
|
||||
context = {"staffs": staffs, "form": form}
|
||||
return render(request, "user/admin_settings.html", context)
|
||||
@ -3100,11 +3097,12 @@ def account_toggle_status(request, pk):
|
||||
|
||||
|
||||
@csrf_exempt
|
||||
@staff_user_required
|
||||
def zoom_webhook_view(request):
|
||||
api_key = request.headers.get("X-Zoom-API-KEY")
|
||||
if api_key != settings.ZOOM_WEBHOOK_API_KEY:
|
||||
return HttpResponse(status=405)
|
||||
|
||||
print(request.headers)
|
||||
print(settings.ZOOM_WEBHOOK_API_KEY)
|
||||
# if api_key != settings.ZOOM_WEBHOOK_API_KEY:
|
||||
# return HttpResponse(status=405)
|
||||
if request.method == "POST":
|
||||
try:
|
||||
payload = json.loads(request.body)
|
||||
@ -4692,8 +4690,7 @@ def message_detail(request, message_id):
|
||||
|
||||
@login_required
|
||||
def message_create(request):
|
||||
"""Create a new message"""
|
||||
from .email_service import EmailService
|
||||
"""Create a new message"""
|
||||
if request.method == "POST":
|
||||
form = MessageForm(request.user, request.POST)
|
||||
|
||||
@ -4701,25 +4698,8 @@ 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.")
|
||||
@ -4732,8 +4712,6 @@ 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"""
|
||||
@ -5226,7 +5204,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)
|
||||
@ -5253,16 +5231,14 @@ 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,
|
||||
job=job
|
||||
|
||||
from_interview=False
|
||||
)
|
||||
|
||||
if email_result["success"]:
|
||||
@ -5521,7 +5497,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(
|
||||
@ -5536,7 +5512,7 @@ def candidate_signup(request, slug):
|
||||
phone=phone,
|
||||
gender=gender,
|
||||
nationality=nationality,
|
||||
# gpa=gpa,
|
||||
gpa=gpa,
|
||||
address=address,
|
||||
user = user
|
||||
)
|
||||
|
||||
@ -204,27 +204,26 @@ 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 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({
|
||||
'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)
|
||||
|
||||
@ -245,28 +244,27 @@ 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 redirect('source_detail', pk=source.pk)
|
||||
# return JsonResponse({
|
||||
# 'success': True,
|
||||
# 'is_active': source.is_active,
|
||||
# 'message': f'Source "{source.name}" {status_text} successfully'
|
||||
# })
|
||||
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"""
|
||||
|
||||
@ -1,721 +0,0 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
@ -1,786 +0,0 @@
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
@ -1,162 +0,0 @@
|
||||
|
||||
/* 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.
|
Before Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 231 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 226 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB |
@ -1,120 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 23 KiB |
@ -1,949 +0,0 @@
|
||||
/**
|
||||
* 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 = ``;
|
||||
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);
|
||||
@ -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,145 +43,17 @@
|
||||
|
||||
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;
|
||||
@ -195,7 +67,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) */
|
||||
@ -203,16 +75,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 {
|
||||
@ -232,7 +104,7 @@
|
||||
padding: 10rem 0;
|
||||
}
|
||||
.hero-title {
|
||||
font-size: 5.5rem;
|
||||
font-size: 5.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -281,20 +153,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 */
|
||||
}
|
||||
@ -343,8 +215,7 @@
|
||||
<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="flag-emoji">🇺🇸</span>
|
||||
<span class="language-text">English</span>
|
||||
<span class="me-2">🇺🇸</span> English
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
@ -353,8 +224,7 @@
|
||||
<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="flag-emoji">🇸🇦</span>
|
||||
<span class="language-text">العربية (Arabic)</span>
|
||||
<span class="me-2">🇸🇦</span> العربية (Arabic)
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
@ -369,7 +239,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 }}
|
||||
@ -384,20 +254,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>
|
||||
@ -179,9 +179,8 @@
|
||||
|
||||
|
||||
{% 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 '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>
|
||||
<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>
|
||||
{% 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 %}
|
||||
@ -251,7 +250,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 "Applicant" %}
|
||||
{% trans "Person" %}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@ -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="6" class="candidate-management-header-title">
|
||||
<th scope="col" colspan="5" class="candidate-management-header-title">
|
||||
{% trans "Applicants Metrics" %}
|
||||
</th>
|
||||
</tr>
|
||||
@ -282,11 +282,10 @@
|
||||
<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>
|
||||
@ -312,7 +311,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 {% if job.status != 'ACTIVE' %}disabled{% endif %}" title="{% trans 'Preview' %}">
|
||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary" 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' %}">
|
||||
@ -330,7 +329,6 @@
|
||||
<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 %}
|
||||
|
||||
@ -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 == 'Document Review' or current_stage == 'Offer' or current_stage == 'Hired' %}completed{% endif %}"></div>
|
||||
<div class="stage-connector {% if current_stage == 'Exam' or current_stage == 'Interview' or current_stage == 'Offer' %}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 == 'Document Review' or current_stage == 'Offer' or current_stage == 'Hired' %}completed{% endif %}"></div>
|
||||
<div class="stage-connector {% if current_stage == 'Interview' or current_stage == 'Offer' %}completed{% endif %}"></div>
|
||||
|
||||
{% comment %} STAGE 3: Interview {% endcomment %}
|
||||
<a href="{% url 'candidate_interview_view' job.slug %}"
|
||||
|
||||
@ -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 "Applicants List" %}
|
||||
<i class="fas fa-user-friends me-2"></i> {% trans "People Directory" %}
|
||||
</h1>
|
||||
<a href="{% url 'person_create' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus me-1"></i> {% trans "Add New Applicant" %}
|
||||
<i class="fas fa-plus me-1"></i> {% trans "Add New Person" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@ -163,14 +163,18 @@
|
||||
<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>
|
||||
<div class="input-group input-group-lg">
|
||||
<form method="get" action="" class="w-100">
|
||||
{% include 'includes/search_form.html' %}
|
||||
</form>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div class="col-md-6">
|
||||
@ -196,10 +200,10 @@
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 d-flex">
|
||||
<div class="col-md-4 d-flex justify-content-end align-self-end">
|
||||
<div class="filter-buttons">
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<i class="fas fa-filter me-1"></i> {% trans "Apply Filter" %}
|
||||
<i class="fas fa-filter me-1"></i> {% trans "Apply" %}
|
||||
</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">
|
||||
@ -213,8 +217,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
{% if people_list %}
|
||||
<div id="person-list">
|
||||
<!-- View Switcher -->
|
||||
|
||||
@ -89,7 +89,6 @@
|
||||
|
||||
.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; }
|
||||
@ -120,9 +119,6 @@
|
||||
background-color: #f3e5f5;
|
||||
border-color: #ce93d8;
|
||||
}
|
||||
.card-header{
|
||||
padding: 0.75rem 1.25rem;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
@ -159,7 +155,7 @@
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-md-4 text-end">
|
||||
<span class="status-badge status-{{ application.stage }}">
|
||||
<span class="status-badge status-{{ application.stage|lower }}">
|
||||
{{ application.get_stage_display }}
|
||||
</span>
|
||||
</div>
|
||||
@ -176,9 +172,19 @@
|
||||
<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,Document Review,Offer,Hired,Rejected' %}
|
||||
<div class="progress-step {% if application.stage not in 'Applied,Exam' %}completed{% elif application.stage == 'Exam' %}active{% endif %}">
|
||||
{% 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 %}">
|
||||
<div class="progress-icon">
|
||||
<i class="fas fa-clipboard-check"></i>
|
||||
</div>
|
||||
@ -187,8 +193,8 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- Interview Stage - Show if current stage is Interview or beyond -->
|
||||
{% 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 %}">
|
||||
{% 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 %}">
|
||||
<div class="progress-icon">
|
||||
<i class="fas fa-video"></i>
|
||||
</div>
|
||||
@ -196,19 +202,9 @@
|
||||
</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,Exam,Interview,Document Review,Offer' %}completed{% elif application.stage == 'Offer' %}active{% endif %}">
|
||||
<div class="progress-step {% if application.stage not in 'Applied,Screening,Exam,Interview,Offer' %}completed{% elif application.stage == 'Offer' %}active{% endif %}">
|
||||
<div class="progress-icon">
|
||||
<i class="fas fa-handshake"></i>
|
||||
</div>
|
||||
@ -529,11 +525,6 @@
|
||||
<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>
|
||||
|
||||
@ -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">
|
||||
{% comment %} <a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-primary">
|
||||
<a href="{% url 'candidate_update' candidate.slug %}" class="btn btn-outline-primary">
|
||||
<i class="fas fa-edit"></i> {% trans "Edit Details" %}
|
||||
</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?" %}')">
|
||||
</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?" %}')">
|
||||
<i class="fas fa-trash-alt"></i> {% trans "Delete Candidate" %}
|
||||
</a> {% endcomment %}
|
||||
</a>
|
||||
<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 %}
|
||||
|
||||
{% comment %} <a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
|
||||
<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> {% endcomment %}
|
||||
</a>
|
||||
<a href="{{ candidate.resume.url }}" download class="btn btn-outline-primary">
|
||||
<i class="fas fa-download me-1"></i>
|
||||
{% trans "Download Resume" %}
|
||||
</a>
|
||||
|
||||
{% comment %} <a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
|
||||
<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> {% endcomment %}
|
||||
</a>
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -211,8 +211,8 @@
|
||||
<option selected>
|
||||
----------
|
||||
</option>
|
||||
<option value="Document Review">
|
||||
{% trans "To Document Review" %}
|
||||
<option value="Offer">
|
||||
{% trans "To Offer" %}
|
||||
</option>
|
||||
<option value="Exam">
|
||||
{% trans "To Exam" %}
|
||||
|
||||
@ -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"
|
||||
value="{{ gpa }}" min="0" max="4" step="1"
|
||||
placeholder="e.g., 4" style="width: 120px;">
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.first_name.id_for_label }}" class="form-label">
|
||||
{% trans "First Name" %} <span class="text-danger">*</span>
|
||||
</label>
|
||||
@ -81,19 +81,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.last_name.id_for_label }}" class="form-label">
|
||||
{% trans "Last Name" %} <span class="text-danger">*</span>
|
||||
</label>
|
||||
@ -107,7 +95,19 @@
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-4 mb-3">
|
||||
<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">
|
||||
<label for="{{ form.phone.id_for_label }}" class="form-label">
|
||||
{% trans "Phone Number" %} <span class="text-danger">*</span>
|
||||
</label>
|
||||
@ -118,8 +118,21 @@
|
||||
</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-4 mb-3">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.nationality.id_for_label }}" class="form-label">
|
||||
{% trans "Nationality" %} <span class="text-danger">*</span>
|
||||
</label>
|
||||
@ -131,7 +144,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 mb-3">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="{{ form.gender.id_for_label }}" class="form-label">
|
||||
{% trans "Gender" %} <span class="text-danger">*</span>
|
||||
</label>
|
||||
@ -202,7 +215,7 @@
|
||||
<div class="card-footer text-center">
|
||||
<small class="text-muted">
|
||||
{% trans "Already have an account?" %}
|
||||
<a href="{% url 'account_login' %}?next={% url 'application_submit_form' job.form_template.slug %}" class="text-decoration-none text-kaauh-teal">
|
||||
<a href="{% url 'portal_login' %}" class="text-decoration-none text-kaauh-teal">
|
||||
{% trans "Login here" %}
|
||||
</a>
|
||||
</small>
|
||||
|
||||
@ -13,17 +13,12 @@
|
||||
<a href="{% url 'source_update' source.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
{% comment %} <a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
|
||||
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
|
||||
<i class="fas fa-key"></i> Generate Keys
|
||||
</a> {% endcomment %}
|
||||
<button id="toggle-source-status"
|
||||
type="button"
|
||||
class="btn btn-outline-{{ source.is_active|yesno:'warning,success' }}"
|
||||
</a>
|
||||
<button type="button"
|
||||
class="btn btn-outline-warning"
|
||||
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>
|
||||
@ -98,7 +93,7 @@
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">Status</label>
|
||||
<div id="source-status">
|
||||
<div>
|
||||
{% if source.is_active %}
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% else %}
|
||||
@ -166,7 +161,7 @@
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">API Credentials</h6>
|
||||
</div>
|
||||
<div id="api-credentials" class="card-body">
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">API Key</label>
|
||||
<div class="input-group">
|
||||
@ -195,7 +190,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<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">
|
||||
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning btn-sm">
|
||||
<i class="fas fa-key"></i> Generate New Keys
|
||||
</a>
|
||||
</div>
|
||||
@ -376,7 +371,7 @@
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
function toggleSecretVisibility() {
|
||||
const secretInput = document.getElementById('api-secret');
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
{% load static %}
|
||||
{% 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 }}
|
||||
{{ form.source_type|add_class:"form-select" }}
|
||||
{% if form.source_type.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.source_type.errors %}
|
||||
@ -63,41 +63,7 @@
|
||||
</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 }} <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">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||
{{ form.description.label }}
|
||||
</label>
|
||||
@ -112,6 +78,23 @@
|
||||
<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">
|
||||
@ -186,8 +169,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-main-action">
|
||||
<i class="fas fa-save me-1"></i> {% trans "Save" %}
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="fas fa-save"></i> {{ button_text }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static i18n %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% trans "Sources" %}{% endblock %}
|
||||
{% block title %}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">{% 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>
|
||||
<h1 class="h3 mb-0">Sources</h1>
|
||||
<a href="{% url 'source_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Create Source
|
||||
</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> {% trans "Search" %}
|
||||
<i class="fas fa-search"></i> Search
|
||||
</button>
|
||||
{% if search_query %}
|
||||
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times"></i> {% trans "Clear" %}
|
||||
<i class="fas fa-times"></i> Clear
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -56,12 +56,12 @@
|
||||
<table class="table table-hover">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<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>
|
||||
<th>Name</th>
|
||||
<th>Type</th>
|
||||
<th>Status</th>
|
||||
<th>API Key</th>
|
||||
<th>Created</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@ -71,16 +71,18 @@
|
||||
<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.source_type }}</span>
|
||||
<span class="badge bg-info">{{ source.get_source_type_display }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if source.is_active %}
|
||||
<span class="badge bg-success">{% trans "Active" %}</span>
|
||||
<span class="badge bg-success">Active</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{% trans "Inactive" %}</span>
|
||||
<span class="badge bg-secondary">Inactive</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
@ -99,17 +101,17 @@
|
||||
class="btn btn-sm btn-outline-secondary" title="Edit">
|
||||
<i class="fas fa-edit"></i>
|
||||
</a>
|
||||
{% comment %} <button type="button"
|
||||
<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> {% endcomment %}
|
||||
{% comment %} <a href="{% url 'source_delete' source.pk %}"
|
||||
</button>
|
||||
<a href="{% url 'source_delete' source.pk %}"
|
||||
class="btn btn-sm btn-outline-danger" title="Delete">
|
||||
<i class="fas fa-trash"></i>
|
||||
</a> {% endcomment %}
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -165,16 +167,16 @@
|
||||
{% else %}
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-database fa-3x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">{% trans "No sources found" %}</h5>
|
||||
<h5 class="text-muted">No sources found</h5>
|
||||
<p class="text-muted">
|
||||
{% if search_query %}
|
||||
{% blocktrans with query=query %}No sources match your search criteria "{{ query }}".{% endblocktrans %}
|
||||
No sources match your search criteria.
|
||||
{% else %}
|
||||
{% trans "Get started by creating your first source." %}
|
||||
Get started by creating your first source.
|
||||
{% endif %}
|
||||
</p>
|
||||
<a href="{% url 'source_create' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus"></i> {% trans "Create Source" %}
|
||||
<a href="{% url 'source_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Create Source
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user