diff --git a/.gitignore b/.gitignore index f765b7b..5617eb3 100644 --- a/.gitignore +++ b/.gitignore @@ -53,7 +53,6 @@ htmlcov/ # Media and Static files (if served locally and not meant for version control) media/ -static/ # Deployment files *.tar.gz diff --git a/recruitment/__pycache__/forms.cpython-313.pyc b/recruitment/__pycache__/forms.cpython-313.pyc index 4aa8345..55879c6 100644 Binary files a/recruitment/__pycache__/forms.cpython-313.pyc and b/recruitment/__pycache__/forms.cpython-313.pyc differ diff --git a/recruitment/forms.py b/recruitment/forms.py index 1a6ba6a..3929bed 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -728,7 +728,7 @@ class InterviewScheduleForm(forms.ModelForm): ) class Meta: - model = InterviewSchedule + model = InterviewSchedule fields = [ 'schedule_interview_type', "applications", @@ -1544,8 +1544,7 @@ class CandidateEmailForm(forms.Form): label=_('Select Candidates'), # Use a descriptive label required=False ) - - + subject = forms.CharField( max_length=200, widget=forms.TextInput(attrs={ @@ -1568,29 +1567,29 @@ class CandidateEmailForm(forms.Form): required=True ) - + def __init__(self, job, candidates, *args, **kwargs): super().__init__(*args, **kwargs) self.job = job self.candidates=candidates - + candidate_choices=[] for candidate in candidates: candidate_choices.append( (f'candidate_{candidate.id}', f'{candidate.email}') ) - + self.fields['to'].choices =candidate_choices - self.fields['to'].initial = [choice[0] for choice in candidate_choices] - - + self.fields['to'].initial = [choice[0] for choice in candidate_choices] + + # Set initial message with candidate and meeting info initial_message = self._get_initial_message() - + if initial_message: self.fields['message'].initial = initial_message @@ -1598,7 +1597,7 @@ class CandidateEmailForm(forms.Form): """Generate initial message with candidate and meeting information""" candidate=self.candidates.first() message_parts=[] - + if candidate and candidate.stage == 'Applied': message_parts = [ f"Than you, for your interest in the {self.job.title} role.", @@ -1618,7 +1617,7 @@ 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.", @@ -1629,7 +1628,7 @@ class CandidateEmailForm(forms.Form): 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.", @@ -1648,9 +1647,9 @@ class CandidateEmailForm(forms.Form): f"If you have any questions before your start date, please contact [Onboarding Contact].", f"Best regards, The KAAUH Hiring team" ] - - - + + + # # Add candidate information # if self.candidate: @@ -1675,9 +1674,9 @@ class CandidateEmailForm(forms.Form): def get_email_addresses(self): """Extract email addresses from selected recipients""" email_addresses = [] - + candidates=self.cleaned_data.get('to',[]) - + if candidates: for candidate in candidates: if candidate.startswith('candidate_'): @@ -1691,7 +1690,7 @@ class CandidateEmailForm(forms.Form): return list(set(email_addresses)) # Remove duplicates - + def get_formatted_message(self): """Get the formatted message with optional additional information""" @@ -1871,7 +1870,7 @@ class InterviewEmailForm(forms.Form): location = meeting.interview_location # --- Data Preparation --- - + # Safely access details through the related InterviewLocation object if location and location.start_time: formatted_date = location.start_time.strftime('%Y-%m-%d') @@ -1884,7 +1883,7 @@ class InterviewEmailForm(forms.Form): formatted_time = "TBD" duration = "N/A" meeting_link = "Not Available" - + job_title = job.title agency_name = candidate.hiring_agency.name if candidate.belong_to_an_agency and candidate.hiring_agency else "Hiring Agency" @@ -1917,7 +1916,7 @@ Best regards, KAAUH Hiring Team """ # ... (Messages for agency and participants remain the same, using the updated safe variables) - + # --- 2. Agency Message (Professional and clear details) --- agency_message = f""" Dear {agency_name}, @@ -1969,44 +1968,44 @@ class OnsiteLocationForm(forms.ModelForm): class Meta: model = OnsiteLocationDetails # Include 'room_number' and update the field list - fields = ['topic', 'physical_address', 'room_number'] + fields = ['topic', 'physical_address', 'room_number'] widgets = { 'topic': forms.TextInput( attrs={'placeholder': 'Enter the Meeting Topic', 'class': 'form-control'} ), - + 'physical_address': forms.TextInput( attrs={'placeholder': 'Physical address (e.g., street address)', 'class': 'form-control'} ), - + 'room_number': forms.TextInput( attrs={'placeholder': 'Room Number/Name (Optional)', 'class': 'form-control'} ), - - + + } class OnsiteReshuduleForm(forms.ModelForm): class Meta: model = OnsiteLocationDetails - fields = ['topic', 'physical_address', 'room_number','start_time','duration','status'] + fields = ['topic', 'physical_address', 'room_number','start_time','duration','status'] widgets = { 'topic': forms.TextInput( attrs={'placeholder': 'Enter the Meeting Topic', 'class': 'form-control'} ), - + 'physical_address': forms.TextInput( attrs={'placeholder': 'Physical address (e.g., street address)', 'class': 'form-control'} ), - + 'room_number': forms.TextInput( attrs={'placeholder': 'Room Number/Name (Optional)', 'class': 'form-control'} ), - - + + } - + class OnsiteScheduleForm(forms.ModelForm): # Add fields for the foreign keys required by ScheduledInterview @@ -2024,8 +2023,8 @@ class OnsiteScheduleForm(forms.ModelForm): class Meta: model = OnsiteLocationDetails # Include all fields from OnsiteLocationDetails plus the new ones - fields = ['topic', 'physical_address', 'room_number', 'start_time', 'duration', 'status', 'application', 'job'] - + fields = ['topic', 'physical_address', 'room_number', 'start_time', 'duration', 'status', 'application', 'job'] + widgets = { 'topic': forms.TextInput( attrs={'placeholder': _('Enter the Meeting Topic'), 'class': 'form-control'} @@ -2036,7 +2035,7 @@ class OnsiteScheduleForm(forms.ModelForm): 'room_number': forms.TextInput( attrs={'placeholder': _('Room Number/Name (Optional)'), 'class': 'form-control'} ), - # You should explicitly set widgets for start_time, duration, and status here + # You should explicitly set widgets for start_time, duration, and status here # if they need Bootstrap classes, otherwise they will use default HTML inputs. # Example: 'start_time': forms.DateTimeInput(attrs={'type': 'datetime-local', 'class': 'form-control'}), diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..e53a1d0 --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,721 @@ +/* + * KAAT-S Theme Styles (V2.0 - Consolidated Global, Nav, and Components) + * This file contains all variables, global layout styles, navigation, and component-specific styles. + */ + +/* ---------------------------------- */ +/* 1. UI Variables and Global Reset */ +/* ---------------------------------- */ +:root { + --kaauh-teal: #00636e; + --kaauh-teal-dark: #004a53; + --kaauh-light-bg: #f9fbfd; + --kaauh-border: #eaeff3; + --kaauh-primary-text: #343a40; + --kaauh-success: #28a745; + --kaauh-info: #17a2b8; + --kaauh-danger: #dc3545; + --kaauh-warning: #ffc107; +} + +/* Primary Color Overrides */ +.text-success { color: var(--kaauh-success) !important; } +.text-info { color: var(--kaauh-info) !important; } +.text-danger { color: var(--kaauh-danger) !important; } +.text-warning { color: var(--kaauh-warning) !important; } +.text-primary-theme { color: var(--kaauh-teal) !important; } +.bg-primary-theme { background-color: var(--kaauh-teal) !important; } + +/* Global Layout Control */ +.max-width-1600 { + max-width: 1600px; + margin-right: auto; + margin-left: auto; + padding-right: var(--bs-gutter-x, 0.75rem); + padding-left: var(--bs-gutter-x, 0.75rem); +} + +/* Global Container Padding for main content */ +.container-fluid.py-4 { padding-top: 1.5rem !important; padding-bottom: 1.5rem !important; } + +/* Main content minimum height */ +main.container-fluid { + min-height: calc(100vh - 200px); + padding: 1.5rem 0; +} + +/* ---------------------------------- */ +/* 2. Navigation and Header */ +/* ---------------------------------- */ + +/* Top Bar (Contact/Social) */ +.top-bar { + background-color: white; + border-bottom: 1px solid var(--kaauh-border); + font-size: 0.825rem; + padding: 0.4rem 0; +} +.top-bar a { text-decoration: none; } +.top-bar .social-icons i { + color: var(--kaauh-teal); + transition: color 0.2s; +} +.top-bar .social-icons i:hover { + color: var(--kaauh-teal-dark); +} +.top-bar .contact-item { + display: flex; + align-items: center; + gap: 0.35rem; + padding: 0.25rem 0.5rem; +} +.top-bar .logo-container img { + height: 60px; + object-fit: contain; +} +@media (max-width: 767.98px) { + .top-bar { + display: none; + } +} + +/* Navbar */ +.navbar-brand { + font-weight: 700; + letter-spacing: -0.5px; + font-size: 1.25rem; + display: flex; + align-items: center; + gap: 0.5rem; +} +.navbar-dark { + background-color: var(--kaauh-teal) !important; + box-shadow: 0 2px 6px rgba(0,0,0,0.12); +} +/* Ensure the inner container of the navbar stretches to allow max-width-1600 */ +.navbar-dark > .container { + max-width: 100%; +} +.nav-link { + font-weight: 500; + transition: all 0.2s ease; + padding: 0.5rem 0.75rem; +} +.nav-link:hover, +.nav-link.active { + color: white !important; + background: rgba(255,255,255,0.12) !important; + border-radius: 4px; +} + +/* Dropdown */ +.dropdown-menu { + backdrop-filter: blur(4px); + background-color: rgba(255, 255, 255, 0.98); + border: 1px solid var(--kaauh-border); + box-shadow: 0 6px 20px rgba(0,0,0,0.12); + border-radius: 8px; + padding: 0.5rem 0; + min-width: 200px; + will-change: transform, opacity; + transition: transform 0.2s ease, opacity 0.2s ease; +} +.dropdown-item { + padding: 0.5rem 1.25rem; + transition: background-color 0.15s; +} +.dropdown-item:hover { + background-color: var(--kaauh-light-bg); + color: var(--kaauh-teal-dark); +} + +/* Language Toggle Button Style */ +.language-toggle-btn { + color: white !important; + background: none !important; + border: none !important; + display: flex; + align-items: center; + gap: 0.3rem; + padding: 0.5rem 0.75rem !important; + font-weight: 500; + transition: all 0.2s ease; +} +.language-toggle-btn:hover { + background: rgba(255,255,255,0.12) !important; + border-radius: 4px; +} + +/* Profile Avatar */ +.profile-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: var(--kaauh-teal); + display: flex; + align-items: center; + justify-content: center; + color: white; + font-weight: bold; + font-size: 0.85rem; + transition: transform 0.1s ease; +} +.navbar-nav .dropdown-toggle:hover .profile-avatar { + transform: scale(1.05); +} +.navbar-nav .dropdown-toggle.p-0:hover { + background: none !important; +} + + +/* ---------------------------------- */ +/* 3. Component Styles (Cards & Forms)*/ +/* ---------------------------------- */ +.kaauh-card { + border: 1px solid var(--kaauh-border); + border-radius: 0.75rem; + box-shadow: 0 4px 12px rgba(0,0,0,0.06); + background-color: white; +} + +/* NEW: Filter Controls Container Style */ +.filter-controls { + border: 1px solid var(--kaauh-border); + border-radius: 0.75rem; + box-shadow: 0 4px 12px rgba(0,0,0,0.06); + background-color: white; + padding: 1.5rem; /* Consistent internal padding */ + margin-bottom: 1.5rem; /* Space below filter */ +} + +/* Typography & Headers */ +.page-header { + color: var(--kaauh-teal-dark); + font-weight: 700; +} +.section-header { + color: var(--kaauh-primary-text); + font-weight: 600; + border-bottom: 2px solid var(--kaauh-border); + padding-bottom: 0.5rem; + margin-bottom: 1rem; +} +label { + font-weight: 500; + color: var(--kaauh-primary-text); + font-size: 0.9rem; + margin-bottom: 0.25rem; +} + +/* Forms - Default Size */ +.form-control, .form-select { + border-radius: 0.5rem; + border: 1px solid #ced4da; + padding: 0.5rem 0.75rem; + font-size: 0.9rem; +} + +/* Forms - Compact Size (for modals/tables) */ +.form-control-sm, +.form-select-sm, +.btn-sm { + padding: 0.25rem 0.5rem !important; /* Adjusted padding */ + font-size: 0.8rem !important; + line-height: 1.25 !important; /* Standard small line height */ + height: auto !important; +} + +.form-select-sm { + padding: 0.25rem 2rem 0.25rem 0.5rem !important; /* Increased right padding for arrow */ + font-size: 0.8rem !important; + line-height: 1.25 !important; + height: auto !important; /* Remove fixed height */ + border-radius: 0.5rem; + border: 1px solid #ced4da; +} + +/* Scrollable Multiple Select Fix */ +.form-group select[multiple] { + max-height: 450px; + overflow-y: auto; + min-height: 250px; + padding: 0; +} + +/* Break Times Section Styling (Schedule Interviews) */ +.break-time-form { + background-color: #f8f9fa; + padding: 0.75rem; + border-radius: 0.5rem; + border: 1px solid var(--kaauh-border); + align-items: flex-end; +} +.note-box { + background-color: #fff3cd; + border-left: 5px solid var(--kaauh-warning); + padding: 1rem; + border-radius: 0.25rem; + font-size: 0.9rem; + margin-bottom: 1rem; +} + +/* Tier Controls (Kept for consistency/future use) */ +.tier-controls { + background-color: var(--kaauh-border); + border-radius: 0.75rem; + padding: 1.25rem; + margin-bottom: 2rem; + border: 1px solid var(--kaauh-border); +} +.tier-controls .form-row { + display: flex; + align-items: end; + gap: 1rem; +} +.tier-controls .form-group { + flex: 1; + margin-bottom: 0; +} + + +/* ---------------------------------- */ +/* 4. Button Styles (Component Themed)*/ +/* ---------------------------------- */ +.btn-main-action { + background-color: var(--kaauh-teal); + border-color: var(--kaauh-teal); + color: white; + font-weight: 600; + transition: all 0.2s ease; +} +.btn-main-action:hover { + background-color: var(--kaauh-teal-dark); + border-color: var(--kaauh-teal-dark); + box-shadow: 0 4px 8px rgba(0,0,0,0.15); +} +.btn-main-action.btn-sm { font-weight: 600 !important; } + +.btn-outline-secondary { + color: var(--kaauh-teal-dark); + border-color: var(--kaauh-teal); +} +.btn-outline-secondary:hover { + background-color: var(--kaauh-teal-dark); + color: white; + border-color: var(--kaauh-teal-dark); +} + +.btn-bulk-pass { + background-color: var(--kaauh-success); + border-color: var(--kaauh-success); + color: white; + font-weight: 500; +} +.btn-bulk-pass:hover { + background-color: #1e7e34; + border-color: #1e7e34; +} + +.btn-bulk-fail { + background-color: var(--kaauh-danger); + border-color: var(--kaauh-danger); + color: white; + font-weight: 500; +} +.btn-bulk-fail:hover { + background-color: #bd2130; + border-color: #bd2130; +} + +.btn-apply { /* From Job Board table */ + background: var(--kaauh-teal); + border: none; + color: white; + padding: 0.45rem 1rem; + font-weight: 600; + border-radius: 6px; + transition: all 0.2s; + white-space: nowrap; +} +.btn-apply:hover { + background: var(--kaauh-teal-dark); + transform: translateY(-1px); + box-shadow: 0 2px 6px rgba(0,0,0,0.15); +} + +/* ---------------------------------- */ +/* 5. Table & Footer Styles */ +/* ---------------------------------- */ +.candidate-table { + table-layout: fixed; + width: 100%; + border-collapse: separate; + border-spacing: 0; + background-color: white; + border-radius: 0.5rem; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0,0,0,0.06); +} +.candidate-table thead { + background-color: var(--kaauh-border); +} +.candidate-table th { + padding: 0.75rem; + font-weight: 600; + color: var(--kaauh-teal-dark); + border-bottom: 2px solid var(--kaauh-teal); + font-size: 0.9rem; + vertical-align: middle; +} +.candidate-table td { + padding: 0.75rem; + border-bottom: 1px solid var(--kaauh-border); + vertical-align: middle; + font-size: 0.9rem; +} +.candidate-table tbody tr:hover { + background-color: #f1f3f4; +} +.candidate-name { + font-weight: 600; + color: var(--kaauh-primary-text); +} +.candidate-details { + font-size: 0.8rem; + color: #6c757d; +} + +/* Job Table Specific Styles */ +.job-table-wrapper { + background: white; + border-radius: 10px; + overflow: hidden; + box-shadow: 0 4px 16px rgba(0,0,0,0.06); + margin-bottom: 2rem; +} +.job-table thead th { + background: var(--kaauh-teal); + color: white; + font-weight: 600; + padding: 1rem; + text-align: center; +} +.job-table td { + padding: 1rem; + vertical-align: middle; + text-align: center; +} +.job-table tr:hover td { + background-color: rgba(0, 99, 110, 0.03); +} + +/* Table Responsiveness */ +@media (max-width: 575.98px) { + .table-responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; + } + .job-table th, + .job-table td { + white-space: nowrap; + font-size: 0.875rem; + } +} + +/* Bulk Action Bar (Interview Management) */ +.bulk-action-bar { + display: flex; + align-items: center; + gap: 0.5rem; + padding-bottom: 0.75rem; + margin-bottom: 1rem; + border-bottom: 1px solid var(--kaauh-border); +} +.action-group { + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* Badges (Adapted for Interview Tiers/Status) */ +.ai-score-badge { /* Used as an all-purpose secondary badge */ + background-color: var(--kaauh-teal-dark) !important; + color: white; + font-weight: 700; + padding: 0.4em 0.8em; + border-radius: 0.4rem; + font-size: 0.8rem; +} +.tier-badge { /* Used for Tier labels */ + font-size: 0.75rem; + padding: 0.125rem 0.5rem; + border-radius: 0.5rem; + font-weight: 600; + margin-left: 0.5rem; + display: inline-block; +} +.tier-1-badge { background-color: var(--kaauh-success); color: white; } +.tier-2-badge { background-color: var(--kaauh-warning); color: #856404; } +.tier-3-badge { background-color: #d1ecf1; color: #0c5460; } +.cd_interview{ color: #00636e; } + +/* NEW: Application Stage Badges */ +.stage-badge { + font-size: 0.8rem; + padding: 0.2em 0.6em; + border-radius: 0.4rem; + font-weight: 600; + display: inline-block; + margin-top: 0.25rem; +} +.stage-Application { background-color: #e9ecef; color: #495057; } +.stage-Screening { background-color: #d1ecf1; color: #0c5460; } +.stage-Exam { background-color: #cce5ff; color: #004085; } +.stage-Interview { background-color: #fff3cd; color: #856404; } +.stage-Offer { background-color: #d4edda; color: #155724; } + +/* NEW: Applicant Status Badges */ +.status-badge { + font-size: 0.8rem; + padding: 0.2em 0.6em; + border-radius: 0.4rem; + font-weight: 500; + display: inline-block; +} +.bg-candidate { background-color: var(--kaauh-teal-dark); color: white; } +.bg-applicant { background-color: #f8f9fa; color: #495057; border: 1px solid #ced4da; } + +/* Table Column Width Fixes */ +.candidate-table th:nth-child(1) { width: 40px; } +.candidate-table th:nth-child(4) { width: 15%; } +.candidate-table th:nth-child(5) { width: 80px; } +.candidate-table th:nth-child(6) { width: 180px; } + +/* Footer & Alerts */ +.footer { + background: var(--kaauh-light-bg); + padding: 1.5rem 0; + border-top: 1px solid var(--kaauh-border); + font-size: 0.9rem; + color: #555; +} +.alert { + border: none; + border-radius: 8px; + box-shadow: 0 2px 6px rgba(0,0,0,0.05); +} + +/* ---------------------------------- */ +/* 6. RTL Support */ +/* ---------------------------------- */ +html[dir="rtl"] { + text-align: right; + direction: rtl; +} +html[dir="rtl"] .navbar-brand { + flex-direction: row-reverse; +} +html[dir="rtl"] .dropdown-menu { + right: auto; + left: 0; +} +/* RTL Spacing adjustments */ +html[dir="rtl"] .me-3 { margin-right: 0 !important; margin-left: 1rem !important; } +html[dir="rtl"] .ms-3 { margin-left: 0 !important; margin-right: 1rem !important; } +html[dir="rtl"] .me-2 { margin-right: 0 !important; margin-left: 0.5rem !important; } +html[dir="rtl"] .ms-2 { margin-left: 0 !important; margin-right: 0.5rem !important; } +html[dir="rtl"] .ms-auto { margin-left: 0 !important; margin-right: auto !important; } +html[dir="rtl"] .me-auto { margin-right: 0 !important; margin-left: auto !important; } + + +/* ================================================= */ +/* 1. THEME VARIABLES AND GLOBAL STYLES */ +/* ================================================= */ +:root { + --kaauh-teal: #00636e; + --kaauh-teal-dark: #004a53; + --kaauh-border: #eaeff3; + --kaauh-primary-text: #343a40; +} + +/* Primary Color Overrides */ +.text-primary { color: var(--kaauh-teal) !important; } +.text-info { color: #17a2b8 !important; } +.text-success { color: #28a745 !important; } +.text-secondary { color: #6c757d !important; } +.text-kaauh-primary { color: var(--kaauh-primary-text); } /* Custom class for primary text color if needed */ + + +/* ---------------------------------- */ +/* 2. Button Styles */ +/* ---------------------------------- */ + +/* Main Action Button Style (Used for Download Resume) */ +.btn-main-action { + background-color: var(--kaauh-teal); + border-color: var(--kaauh-teal); + color: white; + font-weight: 600; + padding: 0.6rem 1.2rem; + transition: all 0.2s ease; + display: inline-flex; + align-items: center; + gap: 0.5rem; +} +.btn-main-action:hover { + background-color: var(--kaauh-teal-dark); + border-color: var(--kaauh-teal-dark); + transform: translateY(-1px); + box-shadow: 0 4px 8px rgba(0,0,0,0.15); +} + +/* Outlined Button Styles */ +.btn-outline-primary { + color: var(--kaauh-teal); + border-color: var(--kaauh-teal); +} +.btn-outline-primary:hover { + background-color: var(--kaauh-teal); + color: white; +} +.btn-outline-secondary { + color: var(--kaauh-teal-dark); + border-color: var(--kaauh-teal); +} +.btn-outline-secondary:hover { + background-color: var(--kaauh-teal-dark); + color: white; + border-color: var(--kaauh-teal-dark); +} + +/* ---------------------------------- */ +/* 3. Card/Modal Styles */ +/* ---------------------------------- */ + +/* Card enhancements */ +.kaauh-card, .card { + border: 1px solid var(--kaauh-border); + border-radius: 0.75rem; + overflow: hidden; + box-shadow: 0 4px 12px rgba(0,0,0,0.06); + background-color: white; +} + +/* Candidate Header Card (The teal header) */ +.candidate-header-card { + background: linear-gradient(135deg, var(--kaauh-teal), #004d57); + color: white; + border-radius: 0.75rem 0.75rem 0 0; + padding: 1.5rem; + box-shadow: 0 4px 10px rgba(0,0,0,0.15); +} +.candidate-header-card h1 { + font-weight: 700; + margin: 0; + font-size: 1.8rem; +} +.candidate-header-card .badge { + font-size: 0.9rem; + padding: 0.4em 0.8em; + border-radius: 0.4rem; + font-weight: 700; +} + +/* ---------------------------------- */ +/* 4. Tab Navigation Styles (Candidate Detail View) */ +/* ---------------------------------- */ + +/* Left Column Tabs (Main Content Tabs) */ +.main-tabs { + border-bottom: 1px solid var(--kaauh-border); + background-color: #f8f9fa; + padding: 0 1.25rem; +} +.main-tabs .nav-link { + border: none; + border-bottom: 3px solid transparent; + color: var(--kaauh-primary-text); + font-weight: 500; + padding: 0.75rem 1rem; + margin-right: 0.5rem; + transition: all 0.2s; +} +.main-tabs .nav-link:hover { + color: var(--kaauh-teal); +} +.main-tabs .nav-link.active { + color: var(--kaauh-teal-dark) !important; + background-color: white !important; + border-bottom: 3px solid var(--kaauh-teal); + font-weight: 600; +} + +/* Right Column Card (General styling for tab content if needed) */ +.right-column-card .tab-content { + padding: 1.5rem 1.25rem; + background-color: white; +} + +/* ---------------------------------- */ +/* 5. Vertical Timeline Styling */ +/* ---------------------------------- */ + +/* Highlight box for the current stage */ +.current-stage { + border: 1px solid var(--kaauh-border); + background-color: #f0f8ff; /* Light, subtle blue background */ +} +.current-stage .text-primary { + color: var(--kaauh-teal) !important; +} + +.timeline { + position: relative; + padding-left: 2rem; +} +.timeline::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 1.25rem; + width: 2px; + background-color: var(--kaauh-border); +} +.timeline-item { + position: relative; + margin-bottom: 2rem; + padding-left: 1.5rem; +} +.timeline-icon { + position: absolute; + left: 0; + top: 0; + width: 32px; + height: 32px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + color: white; + font-size: 0.8rem; + z-index: 10; + border: 4px solid white; +} +.timeline-item:last-child { + margin-bottom: 0; +} + +/* Custom Timeline Background Classes for Stages (Using Bootstrap color palette) */ +.timeline-bg-applied { background-color: var(--kaauh-teal) !important; } +.timeline-bg-exam { background-color: #17a2b8 !important; } +.timeline-bg-interview { background-color: #ffc107 !important; } +.timeline-bg-offer { background-color: #28a745 !important; } +.timeline-bg-rejected { background-color: #dc3545 !important; } +.loading { + background: linear-gradient(135deg, var(--kaauh-teal), #004d57); + color: white; + border-radius: 0.75rem 0.75rem 0 0; + padding: 1.5rem; + box-shadow: 0 4px 10px rgba(0,0,0,0.15); +} diff --git a/static/css/messages.css b/static/css/messages.css new file mode 100644 index 0000000..d47ff89 --- /dev/null +++ b/static/css/messages.css @@ -0,0 +1,786 @@ +/* Unified Messaging System Styles */ + +:root { + --primary-color: #00636e; + --secondary-color: #f8f9fa; + --light-bg: #f8f9fa; + --border-color: #dee2e6; + --text-color: #333; + --text-muted: #6c757d; + --success-color: #28a745; + --warning-color: #ffc107; + --danger-color: #dc3545; + --info-color: #17a2b8; +} + +/* Main Layout */ +.unified-messages { + display: flex; + height: calc(100vh - 120px); + background: white; + border-radius: 0.5rem; + overflow: hidden; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); +} + +/* Sidebar */ +.messages-sidebar { + width: 350px; + border-right: 1px solid var(--border-color); + display: flex; + flex-direction: column; + background: var(--light-bg); +} + +.sidebar-header { + padding: 1rem; + border-bottom: 1px solid var(--border-color); + background: white; +} + +.sidebar-tabs { + display: flex; + gap: 0.5rem; + margin-bottom: 1rem; +} + +.tab-btn { + padding: 0.5rem 1rem; + border: none; + background: transparent; + color: var(--text-muted); + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.875rem; + font-weight: 500; +} + +.tab-btn.active { + background: var(--primary-color); + color: white; +} + +.tab-btn:hover:not(.active) { + background: var(--border-color); +} + +.search-box { + position: relative; +} + +.search-input { + width: 100%; + padding: 0.75rem 1rem 0.75rem 2.5rem; + border: 1px solid var(--border-color); + border-radius: 0.5rem; + font-size: 0.875rem; + transition: border-color 0.2s ease; +} + +.search-input:focus { + outline: none; + border-color: var(--primary-color); +} + +.search-icon { + position: absolute; + left: 0.75rem; + top: 50%; + transform: translateY(-50%); + color: var(--text-muted); +} + +.compose-btn { + width: 100%; + padding: 0.75rem; + background: var(--primary-color); + color: white; + border: none; + border-radius: 0.5rem; + cursor: pointer; + font-weight: 500; + transition: background-color 0.2s ease; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; +} + +.compose-btn:hover { + background: #004d57; +} + +/* Conversation List */ +.conversation-list { + flex: 1; + overflow-y: auto; + padding: 0.5rem; +} + +.conversation-item { + display: flex; + align-items: center; + padding: 0.75rem; + border-radius: 0.5rem; + cursor: pointer; + transition: background-color 0.2s ease; + margin-bottom: 0.25rem; + border: 1px solid transparent; +} + +.conversation-item:hover { + background: white; + border-color: var(--border-color); +} + +.conversation-item.active { + background: white; + border-color: var(--primary-color); + box-shadow: 0 2px 8px rgba(0, 99, 110, 0.1); +} + +.conversation-avatar { + width: 48px; + height: 48px; + border-radius: 50%; + background: var(--primary-color); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 1rem; + margin-right: 0.75rem; + flex-shrink: 0; + position: relative; +} + +.online-indicator { + position: absolute; + bottom: 2px; + right: 2px; + width: 12px; + height: 12px; + background: var(--success-color); + border: 2px solid white; + border-radius: 50%; +} + +.conversation-info { + flex: 1; + min-width: 0; +} + +.conversation-name { + font-weight: 600; + color: var(--text-color); + margin-bottom: 0.25rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.conversation-preview { + font-size: 0.875rem; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.conversation-meta { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 0.25rem; +} + +.conversation-time { + font-size: 0.75rem; + color: var(--text-muted); +} + +.unread-badge { + background: var(--primary-color); + color: white; + border-radius: 50%; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 600; +} + +/* Conversation Detail */ +.conversation-detail { + flex: 1; + display: flex; + flex-direction: column; +} + +.conversation-detail-header { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-color); + background: white; + display: flex; + align-items: center; + justify-content: space-between; +} + +.conversation-detail-info { + display: flex; + align-items: center; +} + +.conversation-detail-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--primary-color); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + margin-right: 0.75rem; +} + +.conversation-detail-name { + font-weight: 600; + color: var(--text-color); + margin-bottom: 0.25rem; +} + +.conversation-detail-status { + font-size: 0.875rem; + color: var(--text-muted); +} + +.conversation-detail-actions { + display: flex; + gap: 0.5rem; +} + +.detail-action-btn { + width: 36px; + height: 36px; + border: none; + background: transparent; + color: var(--text-muted); + border-radius: 50%; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.detail-action-btn:hover { + background: var(--light-bg); + color: var(--primary-color); +} + +/* Messages Container */ +.messages-container { + flex: 1; + overflow-y: auto; + padding: 1rem; + background: var(--light-bg); +} + +.message { + display: flex; + align-items: flex-start; + margin-bottom: 1rem; + padding: 0 1rem; +} + +.message.sent { + flex-direction: row-reverse; +} + +.message.received { + flex-direction: row; +} + +.message.reply { + margin-left: 2rem; + border-left: 2px solid var(--border-color); + padding-left: 1rem; + background: rgba(0, 99, 110, 0.02); + border-radius: 0.5rem; +} + +.message-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + background: var(--primary-color); + color: white; + display: flex; + align-items: center; + justify-content: center; + font-weight: 600; + font-size: 0.875rem; + margin: 0 0.75rem; + flex-shrink: 0; +} + +.message-avatar.reply-avatar { + width: 32px; + height: 32px; + font-size: 0.75rem; + margin: 0 0.5rem; +} + +.message-content { + flex: 1; + display: flex; + flex-direction: column; + align-items: flex-start; + max-width: 70%; +} + +.message.sent .message-content { + align-items: flex-end; +} + +.message.received .message-content { + align-items: flex-start; +} + +.message-bubble { + background: var(--light-bg); + border: 1px solid var(--border-color); + border-radius: 1rem; + padding: 0.75rem 1rem; + margin-bottom: 0.25rem; + position: relative; + word-wrap: break-word; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.message-bubble.reply-bubble { + background: white; + border-radius: 0.75rem; + padding: 0.5rem 0.75rem; + font-size: 0.875rem; +} + +.message.sent .message-bubble { + background: var(--primary-color); + color: white; + border-bottom-right-radius: 0.25rem; +} + +.message.received .message-bubble { + background: white; + color: var(--text-color); + border-bottom-left-radius: 0.25rem; +} + +.thread-indicator { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 0.5rem; + padding: 0.25rem 0.5rem; + background: rgba(0, 99, 110, 0.1); + border-radius: 0.25rem; + font-size: 0.75rem; + color: var(--primary-color); +} + +.reply-info { + display: flex; + align-items: center; + gap: 0.25rem; + margin-bottom: 0.25rem; + font-size: 0.75rem; + color: var(--text-muted); +} + +.message-text { + line-height: 1.5; + margin-bottom: 0.25rem; +} + +.message-time { + font-size: 0.75rem; + color: var(--text-muted); + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* Reply Area */ +.message-reply-area { + padding: 1rem 1.5rem; + border-top: 1px solid var(--border-color); + background: white; +} + +.reply-form { + display: flex; + align-items: flex-end; + gap: 0.75rem; +} + +.reply-input { + flex: 1; + padding: 0.75rem 1rem; + border: 1px solid var(--border-color); + border-radius: 1.5rem; + resize: none; + font-family: inherit; + font-size: 0.875rem; + line-height: 1.5; + max-height: 120px; + min-height: 44px; + transition: border-color 0.2s ease; +} + +.reply-input:focus { + outline: none; + border-color: var(--primary-color); +} + +.reply-tools { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.tool-btn { + width: 36px; + height: 36px; + border: none; + background: transparent; + color: var(--text-muted); + border-radius: 50%; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.tool-btn:hover { + background: var(--light-bg); + color: var(--primary-color); +} + +.send-btn { + width: 36px; + height: 36px; + border: none; + background: var(--primary-color); + color: white; + border-radius: 50%; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.send-btn:hover:not(:disabled) { + background: #004d57; +} + +.send-btn:disabled { + background: var(--text-muted); + cursor: not-allowed; +} + +/* Empty States */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-muted); + text-align: center; + padding: 2rem; +} + +.empty-state i { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.5; +} + +.empty-state h3 { + margin-bottom: 0.5rem; + color: var(--text-color); +} + +.empty-state p { + font-size: 0.875rem; +} + +/* Compose Modal */ +.compose-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.compose-modal-content { + background: white; + border-radius: 0.5rem; + width: 90%; + max-width: 600px; + max-height: 80vh; + overflow-y: auto; + box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); +} + +.compose-modal-header { + padding: 1rem 1.5rem; + border-bottom: 1px solid var(--border-color); + display: flex; + align-items: center; + justify-content: space-between; +} + +.compose-modal-body { + padding: 1.5rem; +} + +.compose-form-group { + margin-bottom: 1rem; +} + +.compose-form-label { + display: block; + margin-bottom: 0.5rem; + font-weight: 500; + color: var(--text-color); +} + +.compose-form-control { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid var(--border-color); + border-radius: 0.5rem; + font-size: 0.875rem; + transition: border-color 0.2s ease; +} + +.compose-form-control:focus { + outline: none; + border-color: var(--primary-color); +} + +.compose-form-textarea { + resize: vertical; + min-height: 120px; + font-family: inherit; + line-height: 1.5; +} + +.compose-modal-footer { + padding: 1rem 1.5rem; + border-top: 1px solid var(--border-color); + display: flex; + justify-content: flex-end; + gap: 0.75rem; +} + +.btn { + padding: 0.75rem 1.5rem; + border: none; + border-radius: 0.5rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + font-size: 0.875rem; +} + +.btn-primary { + background: var(--primary-color); + color: white; +} + +.btn-primary:hover { + background: #004d57; +} + +.btn-secondary { + background: var(--light-bg); + color: var(--text-color); + border: 1px solid var(--border-color); +} + +.btn-secondary:hover { + background: var(--border-color); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .unified-messages { + height: calc(100vh - 60px); + } + + .messages-sidebar { + width: 100%; + position: absolute; + z-index: 10; + transform: translateX(-100%); + transition: transform 0.3s ease; + } + + .messages-sidebar.show { + transform: translateX(0); + } + + .conversation-detail { + width: 100%; + } + + .message-content { + max-width: 85%; + } + + .compose-modal-content { + width: 95%; + margin: 1rem; + } +} + +@media (max-width: 480px) { + .conversation-item { + padding: 0.5rem; + } + + .conversation-avatar { + width: 40px; + height: 40px; + font-size: 0.875rem; + } + + .message { + padding: 0 0.5rem; + } + + .message-avatar { + width: 32px; + height: 32px; + font-size: 0.75rem; + margin: 0 0.5rem; + } + + .message-content { + max-width: 90%; + } + + .message-bubble { + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + } +} + +/* Animations */ +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.message { + animation: slideIn 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.conversation-item { + animation: fadeIn 0.2s ease; +} + +/* Loading States */ +.loading { + display: flex; + align-items: center; + justify-content: center; + padding: 2rem; + color: var(--text-muted); +} + +.spinner { + width: 20px; + height: 20px; + border: 2px solid var(--border-color); + border-top: 2px solid var(--primary-color); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} + +/* Notification Styles */ +.notification { + position: fixed; + top: 20px; + right: 20px; + padding: 1rem 1.5rem; + border-radius: 0.5rem; + color: white; + font-weight: 500; + z-index: 1001; + animation: slideInRight 0.3s ease; + max-width: 300px; +} + +.notification.success { + background: var(--success-color); +} + +.notification.error { + background: var(--danger-color); +} + +.notification.info { + background: var(--info-color); +} + +.notification.warning { + background: var(--warning-color); +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(100%); + } + to { + opacity: 1; + transform: translateX(0); + } +} diff --git a/static/css/update_meeting.css b/static/css/update_meeting.css new file mode 100644 index 0000000..00e0d01 --- /dev/null +++ b/static/css/update_meeting.css @@ -0,0 +1,162 @@ + +/* UI Variables for the KAAT-S Theme */ +:root { + --kaauh-teal: #00636e; + --kaauh-teal-dark: #004a53; + --kaauh-border: #eaeff3; + --kaauh-primary-text: #343a40; + --kaauh-gray: #6c757d; + + /* Status Colors for alerts/messages */ + --kaauh-success: var(--kaauh-teal); + --kaauh-danger: #dc3545; + --kaauh-info: #17a2b8; +} + +/* CONTAINER AND CARD STYLING */ +.container { + padding: 2rem 1rem; +} +.card { + border: 1px solid var(--kaauh-border); + border-radius: 0.75rem; + box-shadow: 0 4px 12px rgba(0,0,0,0.06); + max-width: 600px; + margin: 0 auto; /* Center the card */ + padding: 1.5rem; +} + +/* HEADER STYLING (The section outside the card) */ +.header { + text-align: center; + margin-bottom: 2rem; +} +.header h1 { + font-size: 2rem; + color: var(--kaauh-teal-dark); + font-weight: 700; + margin-bottom: 0.25rem; +} +.header p { + color: var(--kaauh-gray); + font-size: 1rem; +} + +/* CARD TITLE STYLING */ +.card-title { + font-size: 1.25rem; + color: var(--kaauh-teal-dark); + font-weight: 600; + border-bottom: 1px solid var(--kaauh-border); + padding-bottom: 0.75rem; + margin-bottom: 1.5rem; +} + +/* FORM STYLING */ +.form-row { + margin-bottom: 1.5rem; +} +.form-label { + display: block; + font-weight: 600; + color: var(--kaauh-gray); + margin-bottom: 0.5rem; + font-size: 0.9rem; +} +.form-input { + display: block; + width: 100%; + padding: 0.75rem 1rem; + font-size: 1rem; + line-height: 1.5; + color: var(--kaauh-primary-text); + background-color: #fff; + background-clip: padding-box; + border: 1px solid var(--kaauh-border); + border-radius: 0.5rem; + transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; +} +.form-input:focus { + border-color: var(--kaauh-teal); + outline: 0; + box-shadow: 0 0 0 0.1rem rgba(0, 99, 110, 0.25); +} +input[type="datetime-local"] { + font-family: inherit; +} + +/* MESSAGES/ALERTS STYLING */ +.messages { + max-width: 600px; + margin: 0 auto 1.5rem auto; +} +.alert { + padding: 1rem; + border-radius: 0.5rem; + margin-bottom: 0.5rem; + font-weight: 500; +} +.alert-success { + color: white; + background-color: var(--kaauh-success); + border-color: var(--kaauh-success); +} +.alert-danger { + color: white; + background-color: var(--kaauh-danger); + border-color: var(--kaauh-danger); +} +.alert-info { + color: white; + background-color: var(--kaauh-info); + border-color: var(--kaauh-info); +} + +/* BUTTON STYLING */ +.actions { + margin-top: 1.5rem; + display: flex; + gap: 1rem; +} +.btn-base { + display: inline-flex; + align-items: center; + gap: 0.5rem; + text-align: center; + vertical-align: middle; + cursor: pointer; + user-select: none; + padding: 0.5rem 1rem; + font-size: 1rem; + line-height: 1.5; + border-radius: 0.5rem; + font-weight: 600; + border: 1px solid transparent; + transition: all 0.2s ease; + text-decoration: none; +} + +/* Primary Action Button (Update) */ +.btn-main-action { + background-color: var(--kaauh-teal); + border-color: var(--kaauh-teal); + color: white; +} +.btn-main-action:hover { + background-color: var(--kaauh-teal-dark); + border-color: var(--kaauh-teal-dark); + box-shadow: 0 4px 8px rgba(0,0,0,0.15); + color: white; +} + +/* Secondary Button (Cancel) */ +.btn-kaats-outline-secondary { + color: var(--kaauh-secondary); + border-color: var(--kaauh-secondary); + background-color: transparent; +} +.btn-kaats-outline-secondary:hover { + background-color: var(--kaauh-secondary); + color: white; + border-color: var(--kaauh-secondary); +} diff --git a/static/image/hospital_logo copy.png b/static/image/hospital_logo copy.png new file mode 100644 index 0000000..4250a35 Binary files /dev/null and b/static/image/hospital_logo copy.png differ diff --git a/static/image/hospital_logo_1 copy.png b/static/image/hospital_logo_1 copy.png new file mode 100644 index 0000000..ff44820 Binary files /dev/null and b/static/image/hospital_logo_1 copy.png differ diff --git a/static/image/hospital_logo_2 copy.png b/static/image/hospital_logo_2 copy.png new file mode 100644 index 0000000..a1698d4 Binary files /dev/null and b/static/image/hospital_logo_2 copy.png differ diff --git a/static/image/hospital_logo_3 copy.png b/static/image/hospital_logo_3 copy.png new file mode 100644 index 0000000..5a9e883 Binary files /dev/null and b/static/image/hospital_logo_3 copy.png differ diff --git a/static/image/kaauh.jpeg b/static/image/kaauh.jpeg new file mode 100644 index 0000000..f569ae6 Binary files /dev/null and b/static/image/kaauh.jpeg differ diff --git a/static/image/kaauh.png b/static/image/kaauh.png new file mode 100644 index 0000000..bd28a2f Binary files /dev/null and b/static/image/kaauh.png differ diff --git a/static/image/kaauh_banner.png b/static/image/kaauh_banner.png new file mode 100644 index 0000000..4065c35 Binary files /dev/null and b/static/image/kaauh_banner.png differ diff --git a/static/image/kaauh_green.png b/static/image/kaauh_green.png new file mode 100644 index 0000000..561e750 Binary files /dev/null and b/static/image/kaauh_green.png differ diff --git a/static/image/kaauh_green1.png b/static/image/kaauh_green1.png new file mode 100644 index 0000000..10dbccb Binary files /dev/null and b/static/image/kaauh_green1.png differ diff --git a/static/image/vision copy.svg b/static/image/vision copy.svg new file mode 100644 index 0000000..97124a3 --- /dev/null +++ b/static/image/vision copy.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/image/vision2.svg b/static/image/vision2.svg new file mode 100644 index 0000000..43156d8 --- /dev/null +++ b/static/image/vision2.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/static/js/messages.js b/static/js/messages.js new file mode 100644 index 0000000..29f73fd --- /dev/null +++ b/static/js/messages.js @@ -0,0 +1,949 @@ +/** + * Message System JavaScript + * Handles interactive features for the modern message interface + */ + +class MessageSystem { + constructor() { + this.init(); + } + + init() { + this.initSearch(); + this.initFolderNavigation(); + this.initMessageActions(); + this.initComposeFeatures(); + this.initKeyboardShortcuts(); + this.initAutoSave(); + this.initAttachments(); + this.initTooltips(); + } + + /** + * Initialize search functionality + */ + initSearch() { + const searchInputs = document.querySelectorAll('.search-input'); + searchInputs.forEach(input => { + let searchTimeout; + + input.addEventListener('input', (e) => { + clearTimeout(searchTimeout); + const searchTerm = e.target.value.toLowerCase(); + + searchTimeout = setTimeout(() => { + this.performSearch(searchTerm); + }, 300); + }); + + // Clear search on Escape key + input.addEventListener('keydown', (e) => { + if (e.key === 'Escape') { + e.target.value = ''; + this.performSearch(''); + } + }); + }); + } + + /** + * Perform search across message items + */ + performSearch(searchTerm) { + const messageItems = document.querySelectorAll('.message-item'); + let visibleCount = 0; + + messageItems.forEach(item => { + const text = item.textContent.toLowerCase(); + if (text.includes(searchTerm)) { + item.style.display = 'flex'; + visibleCount++; + } else { + item.style.display = 'none'; + } + }); + + // Show no results message if needed + this.updateSearchResults(visibleCount, searchTerm); + } + + /** + * Update search results display + */ + updateSearchResults(count, searchTerm) { + let noResults = document.querySelector('.search-no-results'); + + if (count === 0 && searchTerm) { + if (!noResults) { + noResults = document.createElement('div'); + noResults.className = 'search-no-results'; + noResults.innerHTML = ` +
+ +

+ {% trans "No messages found" %} +

+

+ {% trans "No messages match your search for" %} "${searchTerm}" +

+
+ `; + + const messagesList = document.querySelector('.messages-list'); + if (messagesList) { + messagesList.appendChild(noResults); + } + } + } else if (noResults) { + noResults.remove(); + } + } + + /** + * Initialize folder navigation + */ + initFolderNavigation() { + const folderItems = document.querySelectorAll('.folder-item'); + + folderItems.forEach(item => { + item.addEventListener('click', (e) => { + // Remove active class from all items + folderItems.forEach(f => f.classList.remove('active')); + + // Add active class to clicked item + item.classList.add('active'); + + // Add loading state + this.showLoadingState(); + + // Navigate to folder (if it's a link) + if (item.tagName === 'A') { + // Let the link handle navigation + return; + } + }); + }); + } + + /** + * Initialize message actions + */ + initMessageActions() { + // Refresh button + const refreshBtn = document.querySelector('.action-btn[title="Refresh"]'); + if (refreshBtn) { + refreshBtn.addEventListener('click', () => { + this.refreshMessages(); + }); + } + + // Mark as read functionality + const markReadBtns = document.querySelectorAll('[onclick*="markAsRead"]'); + markReadBtns.forEach(btn => { + btn.addEventListener('click', (e) => { + e.preventDefault(); + this.markAsRead(btn); + }); + }); + + // Delete message functionality + const deleteBtns = document.querySelectorAll('[onclick*="confirm"]'); + deleteBtns.forEach(btn => { + btn.addEventListener('click', (e) => { + if (!this.confirmDelete()) { + e.preventDefault(); + } + }); + }); + } + + /** + * Initialize compose features + */ + initComposeFeatures() { + const form = document.getElementById('composeForm'); + if (!form) return; + + // Auto-resize textarea + const textarea = form.querySelector('textarea[name="content"]'); + if (textarea) { + textarea.addEventListener('input', () => { + this.autoResizeTextarea(textarea); + }); + } + + // Rich text toolbar + const toolbarBtns = document.querySelectorAll('.toolbar-btn'); + toolbarBtns.forEach(btn => { + btn.addEventListener('click', (e) => { + e.preventDefault(); + this.handleToolbarAction(btn); + }); + }); + + // Save draft button + const saveDraftBtn = document.getElementById('saveDraftBtn'); + if (saveDraftBtn) { + saveDraftBtn.addEventListener('click', () => { + this.saveDraft(); + }); + } + + // Form submission + form.addEventListener('submit', (e) => { + this.handleFormSubmit(e); + }); + } + + /** + * Initialize keyboard shortcuts + */ + initKeyboardShortcuts() { + document.addEventListener('keydown', (e) => { + // Ctrl/Cmd + K for search + if ((e.ctrlKey || e.metaKey) && e.key === 'k') { + e.preventDefault(); + const searchInput = document.querySelector('.search-input'); + if (searchInput) { + searchInput.focus(); + } + } + + // Ctrl/Cmd + N for new message + if ((e.ctrlKey || e.metaKey) && e.key === 'n') { + e.preventDefault(); + const composeBtn = document.querySelector('.compose-btn'); + if (composeBtn) { + window.location.href = composeBtn.href; + } + } + + // Escape to close modals + if (e.key === 'Escape') { + this.closeModals(); + } + }); + } + + /** + * Initialize auto-save functionality + */ + initAutoSave() { + const form = document.getElementById('composeForm'); + if (!form) return; + + let autoSaveTimer; + const inputs = form.querySelectorAll('input, textarea, select'); + + inputs.forEach(input => { + input.addEventListener('input', () => { + clearTimeout(autoSaveTimer); + autoSaveTimer = setTimeout(() => { + this.autoSave(); + }, 30000); // Auto-save after 30 seconds + }); + }); + } + + /** + * Initialize attachment handling + */ + initAttachments() { + const attachBtn = document.querySelector('.toolbar-btn[title*="Attach"]'); + if (attachBtn) { + attachBtn.addEventListener('click', () => { + this.showAttachmentDialog(); + }); + } + } + + /** + * Initialize tooltips + */ + initTooltips() { + const tooltipElements = document.querySelectorAll('[title]'); + tooltipElements.forEach(element => { + element.addEventListener('mouseenter', (e) => { + this.showTooltip(e.target); + }); + + element.addEventListener('mouseleave', (e) => { + this.hideTooltip(e.target); + }); + }); + } + + /** + * Refresh messages with animation + */ + refreshMessages() { + const refreshBtn = document.querySelector('.action-btn[title="Refresh"]'); + if (refreshBtn) { + const icon = refreshBtn.querySelector('i'); + icon.classList.add('fa-spin'); + + setTimeout(() => { + icon.classList.remove('fa-spin'); + location.reload(); + }, 1000); + } + } + + /** + * Mark message as read + */ + async markAsRead(button) { + const messageId = button.getAttribute('data-message-id'); + if (!messageId) return; + + try { + const response = await fetch(`/messages/${messageId}/mark-read/`, { + method: 'POST', + headers: { + 'X-CSRFToken': this.getCSRFToken(), + 'Content-Type': 'application/json', + }, + }); + + const data = await response.json(); + if (data.success) { + this.showNotification('{% trans "Message marked as read" %}', 'success'); + location.reload(); + } else { + this.showNotification('{% trans "Failed to mark message as read" %}', 'error'); + } + } catch (error) { + console.error('Error marking message as read:', error); + this.showNotification('{% trans "An error occurred" %}', 'error'); + } + } + + /** + * Confirm delete action + */ + confirmDelete() { + return confirm('{% trans "Are you sure you want to delete this message?" %}'); + } + + /** + * Auto-resize textarea + */ + autoResizeTextarea(textarea) { + textarea.style.height = 'auto'; + textarea.style.height = textarea.scrollHeight + 'px'; + } + + /** + * Handle toolbar actions + */ + handleToolbarAction(button) { + const action = button.getAttribute('title'); + const textarea = document.querySelector('textarea[name="content"]'); + + if (!textarea) return; + + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const selectedText = textarea.value.substring(start, end); + + switch (action) { + case '{% trans "Bold" %}': + this.wrapText(textarea, '**', '**'); + break; + case '{% trans "Italic" %}': + this.wrapText(textarea, '*', '*'); + break; + case '{% trans "Underline" %}': + this.wrapText(textarea, '__', '__'); + break; + case '{% trans "Bullet List" %}': + this.insertList(textarea, '- '); + break; + case '{% trans "Numbered List" %}': + this.insertList(textarea, '1. '); + break; + case '{% trans "Insert Link" %}': + this.insertLink(textarea); + break; + case '{% trans "Insert Image" %}': + this.insertImage(textarea); + break; + case '{% trans "Attach File" %}': + this.showAttachmentDialog(); + break; + } + } + + /** + * Wrap selected text with formatting + */ + wrapText(textarea, before, after) { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const selectedText = textarea.value.substring(start, end); + const replacement = before + selectedText + after; + + textarea.value = textarea.value.substring(0, start) + replacement + textarea.value.substring(end); + textarea.selectionStart = start + before.length; + textarea.selectionEnd = start + before.length + selectedText.length; + textarea.focus(); + } + + /** + * Insert list + */ + insertList(textarea, marker) { + const start = textarea.selectionStart; + const text = marker + '\n'; + + textarea.value = textarea.value.substring(0, start) + text + textarea.value.substring(start); + textarea.selectionStart = textarea.selectionEnd = start + text.length; + textarea.focus(); + } + + /** + * Insert link + */ + insertLink(textarea) { + const url = prompt('{% trans "Enter URL:" %}', 'https://'); + if (url) { + const link = `[${url}](${url})`; + this.insertAtCursor(textarea, link); + } + } + + /** + * Insert image + */ + insertImage(textarea) { + const url = prompt('{% trans "Enter image URL:" %}', 'https://'); + if (url) { + const image = `![Image](${url})`; + this.insertAtCursor(textarea, image); + } + } + + /** + * Insert text at cursor position + */ + insertAtCursor(textarea, text) { + const start = textarea.selectionStart; + textarea.value = textarea.value.substring(0, start) + text + textarea.value.substring(start); + textarea.selectionStart = textarea.selectionEnd = start + text.length; + textarea.focus(); + } + + /** + * Save draft + */ + async saveDraft() { + const form = document.getElementById('composeForm'); + if (!form) return; + + const formData = new FormData(form); + + try { + const response = await fetch('/messages/save-draft/', { + method: 'POST', + body: formData, + headers: { + 'X-CSRFToken': this.getCSRFToken(), + }, + }); + + const data = await response.json(); + if (data.success) { + this.showNotification('{% trans "Draft saved" %}', 'success'); + } else { + this.showNotification('{% trans "Failed to save draft" %}', 'error'); + } + } catch (error) { + console.error('Error saving draft:', error); + this.showNotification('{% trans "An error occurred" %}', 'error'); + } + } + + /** + * Auto-save draft + */ + async autoSave() { + const form = document.getElementById('composeForm'); + if (!form) return; + + // Only auto-save if form has content + const hasContent = form.querySelector('textarea[name="content"]').value.trim() || + form.querySelector('input[name="subject"]').value.trim(); + + if (!hasContent) return; + + try { + const formData = new FormData(form); + await fetch('/messages/auto-save-draft/', { + method: 'POST', + body: formData, + headers: { + 'X-CSRFToken': this.getCSRFToken(), + }, + }); + + console.log('Draft auto-saved'); + } catch (error) { + console.error('Error auto-saving draft:', error); + } + } + + /** + * Handle form submission + */ + handleFormSubmit(e) { + const form = e.target; + const submitBtn = form.querySelector('button[type="submit"]'); + + if (submitBtn) { + submitBtn.disabled = true; + submitBtn.innerHTML = '{% 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 = ` + + ${file.name} + ${size} + + `; + + 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 = ` + + ${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 = ' 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 = ' 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 = ` +
+
+
+ ${replyData.sender_name || 'You'} +
+
+
+
+
+

Reply

+ ${replyData.reply_time} +
+

${replyData.reply_content}

+
+
+
+ `; + + 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); diff --git a/templates/base.html b/templates/base.html index 27e5f33..e5d3209 100644 --- a/templates/base.html +++ b/templates/base.html @@ -179,8 +179,9 @@ {% if request.user.is_superuser %} -
  • {% trans "Settings" %}
  • -
  • {% trans "Activity Log" %}
  • +
  • {% trans "Settings" %}
  • +
  • {% trans "Integration" %}
  • +
  • {% trans "Activity Log" %}
  • {% comment %}
  • {% trans "Help & Support" %}
  • {% endcomment %} {% endif %} {% endif %} diff --git a/templates/recruitment/source_list.html b/templates/recruitment/source_list.html index adccd5d..5e0cbc0 100644 --- a/templates/recruitment/source_list.html +++ b/templates/recruitment/source_list.html @@ -1,16 +1,16 @@ {% extends "base.html" %} -{% load static %} +{% load static i18n %} -{% block title %}Sources{% endblock %} +{% block title %}{% trans "Sources" %}{% endblock %} {% block content %}
    -

    Sources

    +

    {% trans "Integration Sources" %}

    - Create Source + {% trans "Create Source for Integration" %}
    @@ -29,11 +29,11 @@
    {% if search_query %} - Clear + {% trans "Clear" %} {% endif %}
    @@ -56,12 +56,12 @@ - - - - - - + + + + + + @@ -71,16 +71,16 @@ {{ source.name }} - +
    NameTypeStatusAPI KeyCreatedActions{% trans "Name" %}{% trans "Type" %}{% trans "Status" %}{% trans "API Key" %}{% trans "Created" %}{% trans "Actions" %}
    {{ source.source_type }} {% if source.is_active %} - Active + {% trans "Active" %} {% else %} - Inactive + {% trans "Inactive" %} {% endif %} @@ -165,16 +165,16 @@ {% else %}
    -
    No sources found
    +
    {% trans "No sources found" %}

    {% if search_query %} - No sources match your search criteria. + {% blocktrans with query=query %}No sources match your search criteria "{{ query }}".{% endblocktrans %} {% else %} - Get started by creating your first source. + {% trans "Get started by creating your first source." %} {% endif %}

    - Create Source + {% trans "Create Source" %}
    {% endif %}