Compare commits
8 Commits
f4bddfc391
...
406ba3b3b6
| Author | SHA1 | Date | |
|---|---|---|---|
| 406ba3b3b6 | |||
| 7e5dfad1dd | |||
| 58d72844a6 | |||
| c369b8bedc | |||
| 2428034684 | |||
| dace6bc0c3 | |||
| 7bbca7d746 | |||
| eb122da037 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -27,7 +27,7 @@ var/
|
|||||||
*.log
|
*.log
|
||||||
*.pot
|
*.pot
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
local_settings.py
|
settings.py
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
|
|
||||||
# Virtual environment
|
# Virtual environment
|
||||||
@ -95,7 +95,7 @@ coverage.xml
|
|||||||
# Django stuff:
|
# Django stuff:
|
||||||
|
|
||||||
# Local settings
|
# Local settings
|
||||||
local_settings.py
|
settings.py
|
||||||
|
|
||||||
# Database sqlite files:
|
# Database sqlite files:
|
||||||
# The base directory for relative paths in .gitignore
|
# The base directory for relative paths in .gitignore
|
||||||
|
|||||||
Binary file not shown.
@ -66,7 +66,7 @@ INSTALLED_APPS = [
|
|||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
|
|
||||||
|
|
||||||
LOGIN_REDIRECT_URL = '/'
|
LOGIN_REDIRECT_URL = 'dashboard'
|
||||||
|
|
||||||
|
|
||||||
ACCOUNT_LOGOUT_REDIRECT_URL = '/'
|
ACCOUNT_LOGOUT_REDIRECT_URL = '/'
|
||||||
@ -135,9 +135,9 @@ WSGI_APPLICATION = 'NorahUniversity.wsgi.application'
|
|||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||||
'NAME': 'norahuniversity',
|
'NAME': 'haikal_db',
|
||||||
'USER': 'norahuniversity',
|
'USER': 'faheed',
|
||||||
'PASSWORD': 'norahuniversity',
|
'PASSWORD': 'Faheed@215',
|
||||||
'HOST': '127.0.0.1',
|
'HOST': '127.0.0.1',
|
||||||
'PORT': '5432',
|
'PORT': '5432',
|
||||||
}
|
}
|
||||||
@ -183,7 +183,6 @@ ACCOUNT_LOGIN_METHODS = ['email']
|
|||||||
ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*']
|
ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*']
|
||||||
|
|
||||||
ACCOUNT_UNIQUE_EMAIL = True
|
ACCOUNT_UNIQUE_EMAIL = True
|
||||||
ACCOUNT_EMAIL_VERIFICATION = 'none'
|
|
||||||
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
|
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
|
||||||
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
|
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
|
||||||
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
||||||
@ -192,23 +191,11 @@ ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
|||||||
ACCOUNT_FORMS = {'signup': 'recruitment.forms.StaffSignupForm'}
|
ACCOUNT_FORMS = {'signup': 'recruitment.forms.StaffSignupForm'}
|
||||||
|
|
||||||
|
|
||||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||||
EMAIL_HOST = '10.10.1.110' #'smtp.gmail.com'
|
|
||||||
EMAIL_PORT = 2225 #587
|
|
||||||
EMAIL_USE_TLS = False
|
|
||||||
EMAIL_USE_SSL = False
|
|
||||||
EMAIL_TIMEOUT = 10
|
|
||||||
|
|
||||||
DEFAULT_FROM_EMAIL = 'norahuniversity@example.com'
|
|
||||||
|
|
||||||
# Gmail SMTP credentials
|
|
||||||
# Remove the comment below if you want to use Gmail SMTP server
|
|
||||||
# EMAIL_HOST_USER = 'your_email@gmail.com'
|
|
||||||
# EMAIL_HOST_PASSWORD = 'your_password'
|
|
||||||
|
|
||||||
# Crispy Forms Configuration
|
# Crispy Forms Configuration
|
||||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
CRISPY_TEMPLATE_PACK = "bootstrapconsole5"
|
||||||
|
|
||||||
# Bootstrap 5 Configuration
|
# Bootstrap 5 Configuration
|
||||||
CRISPY_BS5 = {
|
CRISPY_BS5 = {
|
||||||
|
|||||||
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.
@ -162,11 +162,18 @@ def send_interview_invitation_email(candidate, job, meeting_details=None, recipi
|
|||||||
try:
|
try:
|
||||||
# Prepare recipient list
|
# Prepare recipient list
|
||||||
recipients = []
|
recipients = []
|
||||||
if candidate.email:
|
if candidate.hiring_source == "Agency":
|
||||||
|
try:
|
||||||
|
recipients.append(candidate.hiring_agency.email)
|
||||||
|
except :
|
||||||
|
pass
|
||||||
|
else:
|
||||||
recipients.append(candidate.email)
|
recipients.append(candidate.email)
|
||||||
|
|
||||||
if recipient_list:
|
if recipient_list:
|
||||||
recipients.extend(recipient_list)
|
recipients.extend(recipient_list)
|
||||||
|
|
||||||
|
|
||||||
if not recipients:
|
if not recipients:
|
||||||
return {'success': False, 'error': 'No recipient email addresses provided'}
|
return {'success': False, 'error': 'No recipient email addresses provided'}
|
||||||
|
|
||||||
|
|||||||
@ -31,7 +31,70 @@ def generate_api_secret(length=64):
|
|||||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||||
|
|
||||||
class SourceForm(forms.ModelForm):
|
class SourceForm(forms.ModelForm):
|
||||||
"""Form for creating and editing sources with API key generation"""
|
"""Simple form for creating and editing sources"""
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Source
|
||||||
|
fields = [
|
||||||
|
'name', 'source_type', 'description', 'ip_address', 'is_active'
|
||||||
|
]
|
||||||
|
widgets = {
|
||||||
|
'name': forms.TextInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'e.g., ATS System, ERP Integration',
|
||||||
|
'required': True
|
||||||
|
}),
|
||||||
|
'source_type': forms.TextInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'e.g., ATS, ERP, API',
|
||||||
|
'required': True
|
||||||
|
}),
|
||||||
|
'description': forms.Textarea(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'rows': 3,
|
||||||
|
'placeholder': 'Brief description of the source system'
|
||||||
|
}),
|
||||||
|
'ip_address': forms.TextInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': '192.168.1.100'
|
||||||
|
}),
|
||||||
|
'is_active': forms.CheckboxInput(attrs={
|
||||||
|
'class': 'form-check-input'
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.helper = FormHelper()
|
||||||
|
self.helper.form_method = 'post'
|
||||||
|
self.helper.form_class = 'form-horizontal'
|
||||||
|
self.helper.label_class = 'col-md-3'
|
||||||
|
self.helper.field_class = 'col-md-9'
|
||||||
|
|
||||||
|
self.helper.layout = Layout(
|
||||||
|
Field('name', css_class='form-control'),
|
||||||
|
Field('source_type', css_class='form-control'),
|
||||||
|
Field('ip_address', css_class='form-control'),
|
||||||
|
Field('is_active', css_class='form-check-input'),
|
||||||
|
Submit('submit', 'Save Source', css_class='btn btn-primary mt-3')
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_name(self):
|
||||||
|
"""Ensure source name is unique"""
|
||||||
|
name = self.cleaned_data.get('name')
|
||||||
|
if name:
|
||||||
|
# Check for duplicates excluding current instance if editing
|
||||||
|
instance = self.instance
|
||||||
|
if not instance.pk: # Creating new instance
|
||||||
|
if Source.objects.filter(name=name).exists():
|
||||||
|
raise ValidationError('A source with this name already exists.')
|
||||||
|
else: # Editing existing instance
|
||||||
|
if Source.objects.filter(name=name).exclude(pk=instance.pk).exists():
|
||||||
|
raise ValidationError('A source with this name already exists.')
|
||||||
|
return name
|
||||||
|
|
||||||
|
class SourceAdvancedForm(forms.ModelForm):
|
||||||
|
"""Advanced form for creating and editing sources with API key generation"""
|
||||||
|
|
||||||
# Hidden field to trigger API key generation
|
# Hidden field to trigger API key generation
|
||||||
generate_keys = forms.CharField(
|
generate_keys = forms.CharField(
|
||||||
|
|||||||
@ -683,6 +683,29 @@ class Candidate(Base):
|
|||||||
def scoring_timeout(self):
|
def scoring_timeout(self):
|
||||||
return timezone.now() <= (self.created_at + timezone.timedelta(minutes=5))
|
return timezone.now() <= (self.created_at + timezone.timedelta(minutes=5))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_interview_date(self):
|
||||||
|
if hasattr(self, 'scheduled_interview') and self.scheduled_interview:
|
||||||
|
return self.scheduled_interviews.first().interview_date
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def get_interview_time(self):
|
||||||
|
if hasattr(self, 'scheduled_interview') and self.scheduled_interview:
|
||||||
|
return self.scheduled_interviews.first().interview_time
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_to_hire_days(self):
|
||||||
|
if self.hired_date and self.created_at:
|
||||||
|
time_to_hire = self.hired_date - self.created_at.date()
|
||||||
|
return time_to_hire.days
|
||||||
|
return 0
|
||||||
|
|
||||||
class TrainingMaterial(Base):
|
class TrainingMaterial(Base):
|
||||||
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
||||||
|
|||||||
@ -5,7 +5,7 @@ from . import views_integration
|
|||||||
from . import views_source
|
from . import views_source
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views_frontend.dashboard_view, name='dashboard'),
|
path('dashboard/', views_frontend.dashboard_view, name='dashboard'),
|
||||||
|
|
||||||
# Job URLs (using JobPosting model)
|
# Job URLs (using JobPosting model)
|
||||||
path('jobs/', views_frontend.JobListView.as_view(), name='job_list'),
|
path('jobs/', views_frontend.JobListView.as_view(), name='job_list'),
|
||||||
@ -62,6 +62,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# Form Preview URLs
|
# Form Preview URLs
|
||||||
# path('forms/', views.form_list, name='form_list'),
|
# path('forms/', views.form_list, name='form_list'),
|
||||||
|
|
||||||
path('forms/builder/', views.form_builder, name='form_builder'),
|
path('forms/builder/', views.form_builder, name='form_builder'),
|
||||||
path('forms/builder/<slug:template_slug>/', views.form_builder, name='form_builder'),
|
path('forms/builder/<slug:template_slug>/', views.form_builder, name='form_builder'),
|
||||||
path('forms/', views.form_templates_list, name='form_templates_list'),
|
path('forms/', views.form_templates_list, name='form_templates_list'),
|
||||||
@ -140,7 +141,8 @@ urlpatterns = [
|
|||||||
path('sources/<int:pk>/', views_source.SourceDetailView.as_view(), name='source_detail'),
|
path('sources/<int:pk>/', views_source.SourceDetailView.as_view(), name='source_detail'),
|
||||||
path('sources/<int:pk>/update/', views_source.SourceUpdateView.as_view(), name='source_update'),
|
path('sources/<int:pk>/update/', views_source.SourceUpdateView.as_view(), name='source_update'),
|
||||||
path('sources/<int:pk>/delete/', views_source.SourceDeleteView.as_view(), name='source_delete'),
|
path('sources/<int:pk>/delete/', views_source.SourceDeleteView.as_view(), name='source_delete'),
|
||||||
path('sources/api/generate-keys/', views_source.generate_api_keys_view, name='generate_api_keys'),
|
path('sources/<int:pk>/generate-keys/', views_source.generate_api_keys_view, name='generate_api_keys'),
|
||||||
|
path('sources/<int:pk>/toggle-status/', views_source.toggle_source_status_view, name='toggle_source_status'),
|
||||||
path('sources/api/copy-to-clipboard/', views_source.copy_to_clipboard_view, name='copy_to_clipboard'),
|
path('sources/api/copy-to-clipboard/', views_source.copy_to_clipboard_view, name='copy_to_clipboard'),
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -449,7 +449,7 @@ def schedule_interviews(schedule):
|
|||||||
interview_date=slot['date'],
|
interview_date=slot['date'],
|
||||||
interview_time=slot['time']
|
interview_time=slot['time']
|
||||||
)
|
)
|
||||||
|
candidate.interview_date=interview_datetime
|
||||||
# Send email to candidate
|
# Send email to candidate
|
||||||
send_interview_email(scheduled_interview)
|
send_interview_email(scheduled_interview)
|
||||||
|
|
||||||
|
|||||||
@ -42,7 +42,8 @@ from .forms import (
|
|||||||
AgencyJobAssignmentForm,
|
AgencyJobAssignmentForm,
|
||||||
LinkedPostContentForm,
|
LinkedPostContentForm,
|
||||||
ParticipantsSelectForm,
|
ParticipantsSelectForm,
|
||||||
CandidateEmailForm
|
CandidateEmailForm,
|
||||||
|
SourceForm
|
||||||
)
|
)
|
||||||
from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent
|
from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent
|
||||||
from rest_framework import viewsets
|
from rest_framework import viewsets
|
||||||
@ -80,7 +81,8 @@ from .models import (
|
|||||||
Profile,MeetingComment,HiringAgency,
|
Profile,MeetingComment,HiringAgency,
|
||||||
AgencyJobAssignment,
|
AgencyJobAssignment,
|
||||||
AgencyAccessLink,
|
AgencyAccessLink,
|
||||||
Notification
|
Notification,
|
||||||
|
Source
|
||||||
)
|
)
|
||||||
import logging
|
import logging
|
||||||
from datastar_py.django import (
|
from datastar_py.django import (
|
||||||
@ -858,13 +860,13 @@ def application_submit_form(request, template_slug):
|
|||||||
if is_limit_exceeded:
|
if is_limit_exceeded:
|
||||||
messages.error(
|
messages.error(
|
||||||
request,
|
request,
|
||||||
'Application limit reached: This job is no longer accepting new applications. Please explore other available positions.'
|
_('Application limit reached: This job is no longer accepting new applications.')
|
||||||
)
|
)
|
||||||
return redirect('application_detail',slug=job.slug)
|
return redirect('application_detail',slug=job.slug)
|
||||||
if job.is_expired:
|
if job.is_expired:
|
||||||
messages.error(
|
messages.error(
|
||||||
request,
|
request,
|
||||||
'Application deadline passed: This job is no longer accepting new applications. Please explore other available positions.'
|
_('Application deadline passed: This job is no longer accepting new applications.')
|
||||||
)
|
)
|
||||||
return redirect('application_detail',slug=job.slug)
|
return redirect('application_detail',slug=job.slug)
|
||||||
|
|
||||||
@ -1422,10 +1424,26 @@ def candidate_set_exam_date(request, slug):
|
|||||||
def candidate_update_status(request, slug):
|
def candidate_update_status(request, slug):
|
||||||
job = get_object_or_404(JobPosting, slug=slug)
|
job = get_object_or_404(JobPosting, slug=slug)
|
||||||
mark_as = request.POST.get('mark_as')
|
mark_as = request.POST.get('mark_as')
|
||||||
|
|
||||||
if mark_as != '----------':
|
if mark_as != '----------':
|
||||||
candidate_ids = request.POST.getlist("candidate_ids")
|
candidate_ids = request.POST.getlist("candidate_ids")
|
||||||
|
print(candidate_ids)
|
||||||
if c := Candidate.objects.filter(pk__in = candidate_ids):
|
if c := Candidate.objects.filter(pk__in = candidate_ids):
|
||||||
c.update(stage=mark_as,exam_date=timezone.now(),applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
|
|
||||||
|
if mark_as=='Exam':
|
||||||
|
c.update(exam_date=timezone.now(),interview_date=None,offer_date=None,hired_date=None,stage=mark_as,applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
|
||||||
|
elif mark_as=='Interview':
|
||||||
|
# interview_date update when scheduling the interview
|
||||||
|
c.update(stage=mark_as,offer_date=None,hired_date=None,applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
|
||||||
|
elif mark_as=='Offer':
|
||||||
|
c.update(stage=mark_as,offer_date=timezone.now(),hired_date=None,applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
|
||||||
|
elif mark_as=='Hired':
|
||||||
|
print('hired')
|
||||||
|
c.update(stage=mark_as,hired_date=timezone.now(),applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
|
||||||
|
else:
|
||||||
|
c.update(stage=mark_as,exam_date=None,interview_date=None,offer_date=None,hired_date=None,applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
messages.success(request, f"Candidates Updated")
|
messages.success(request, f"Candidates Updated")
|
||||||
response = HttpResponse(redirect("candidate_screening_view", slug=job.slug))
|
response = HttpResponse(redirect("candidate_screening_view", slug=job.slug))
|
||||||
@ -2971,11 +2989,12 @@ def agency_assignment_create(request,slug=None):
|
|||||||
messages.success(request, f'Assignment created for {assignment.agency.name} - {assignment.job.title}!')
|
messages.success(request, f'Assignment created for {assignment.agency.name} - {assignment.job.title}!')
|
||||||
return redirect('agency_assignment_detail', slug=assignment.slug)
|
return redirect('agency_assignment_detail', slug=assignment.slug)
|
||||||
else:
|
else:
|
||||||
messages.error(request, 'Please correct the errors below.')
|
messages.error(request, f'Please correct the errors below.{form.errors.as_text()}')
|
||||||
|
print(form.errors.as_json())
|
||||||
else:
|
else:
|
||||||
form = AgencyJobAssignmentForm()
|
form = AgencyJobAssignmentForm()
|
||||||
try:
|
try:
|
||||||
from django.forms import HiddenInput
|
# from django.forms import HiddenInput
|
||||||
form.initial['agency'] = agency
|
form.initial['agency'] = agency
|
||||||
# form.fields['agency'].widget = HiddenInput()
|
# form.fields['agency'].widget = HiddenInput()
|
||||||
except HiringAgency.DoesNotExist:
|
except HiringAgency.DoesNotExist:
|
||||||
@ -3085,6 +3104,7 @@ def agency_access_link_detail(request, slug):
|
|||||||
slug=slug
|
slug=slug
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'access_link': access_link,
|
'access_link': access_link,
|
||||||
}
|
}
|
||||||
@ -3800,3 +3820,170 @@ def compose_candidate_email(request, job_slug, candidate_slug):
|
|||||||
'job': job,
|
'job': job,
|
||||||
'candidate': candidate
|
'candidate': candidate
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# Source CRUD Views
|
||||||
|
@login_required
|
||||||
|
def source_list(request):
|
||||||
|
"""List all sources with search and pagination"""
|
||||||
|
search_query = request.GET.get('q', '')
|
||||||
|
sources = Source.objects.all()
|
||||||
|
|
||||||
|
if search_query:
|
||||||
|
sources = sources.filter(
|
||||||
|
Q(name__icontains=search_query) |
|
||||||
|
Q(source_type__icontains=search_query) |
|
||||||
|
Q(description__icontains=search_query)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Order by most recently created
|
||||||
|
sources = sources.order_by('-created_at')
|
||||||
|
|
||||||
|
# Pagination
|
||||||
|
paginator = Paginator(sources, 15) # Show 15 sources per page
|
||||||
|
page_number = request.GET.get('page')
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'page_obj': page_obj,
|
||||||
|
'search_query': search_query,
|
||||||
|
'total_sources': sources.count(),
|
||||||
|
}
|
||||||
|
return render(request, 'recruitment/source_list.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def source_create(request):
|
||||||
|
"""Create a new source"""
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = SourceForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
source = form.save()
|
||||||
|
messages.success(request, f'Source "{source.name}" created successfully!')
|
||||||
|
return redirect('source_detail', slug=source.slug)
|
||||||
|
else:
|
||||||
|
messages.error(request, 'Please correct the errors below.')
|
||||||
|
else:
|
||||||
|
form = SourceForm()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'form': form,
|
||||||
|
'title': 'Create New Source',
|
||||||
|
'button_text': 'Create Source',
|
||||||
|
}
|
||||||
|
return render(request, 'recruitment/source_form.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def source_detail(request, slug):
|
||||||
|
"""View details of a specific source"""
|
||||||
|
source = get_object_or_404(Source, slug=slug)
|
||||||
|
|
||||||
|
# Get integration logs for this source
|
||||||
|
integration_logs = source.integration_logs.order_by('-created_at')[:10] # Show recent 10 logs
|
||||||
|
|
||||||
|
# Statistics
|
||||||
|
total_logs = source.integration_logs.count()
|
||||||
|
successful_logs = source.integration_logs.filter(method='POST').count()
|
||||||
|
failed_logs = source.integration_logs.filter(method='POST', status_code__gte=400).count()
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'source': source,
|
||||||
|
'integration_logs': integration_logs,
|
||||||
|
'total_logs': total_logs,
|
||||||
|
'successful_logs': successful_logs,
|
||||||
|
'failed_logs': failed_logs,
|
||||||
|
}
|
||||||
|
return render(request, 'recruitment/source_detail.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def source_update(request, slug):
|
||||||
|
"""Update an existing source"""
|
||||||
|
source = get_object_or_404(Source, slug=slug)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = SourceForm(request.POST, instance=source)
|
||||||
|
if form.is_valid():
|
||||||
|
source = form.save()
|
||||||
|
messages.success(request, f'Source "{source.name}" updated successfully!')
|
||||||
|
return redirect('source_detail', slug=source.slug)
|
||||||
|
else:
|
||||||
|
messages.error(request, 'Please correct the errors below.')
|
||||||
|
else:
|
||||||
|
form = SourceForm(instance=source)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'form': form,
|
||||||
|
'source': source,
|
||||||
|
'title': f'Edit Source: {source.name}',
|
||||||
|
'button_text': 'Update Source',
|
||||||
|
}
|
||||||
|
return render(request, 'recruitment/source_form.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def source_delete(request, slug):
|
||||||
|
"""Delete a source"""
|
||||||
|
source = get_object_or_404(Source, slug=slug)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
source_name = source.name
|
||||||
|
source.delete()
|
||||||
|
messages.success(request, f'Source "{source_name}" deleted successfully!')
|
||||||
|
return redirect('source_list')
|
||||||
|
|
||||||
|
context = {
|
||||||
|
'source': source,
|
||||||
|
'title': 'Delete Source',
|
||||||
|
'message': f'Are you sure you want to delete the source "{source.name}"?',
|
||||||
|
'cancel_url': reverse('source_detail', kwargs={'slug': source.slug}),
|
||||||
|
}
|
||||||
|
return render(request, 'recruitment/source_confirm_delete.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def source_generate_keys(request, slug):
|
||||||
|
"""Generate new API keys for a source"""
|
||||||
|
source = get_object_or_404(Source, slug=slug)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
# Generate new API key and secret
|
||||||
|
from .forms import generate_api_key, generate_api_secret
|
||||||
|
source.api_key = generate_api_key()
|
||||||
|
source.api_secret = generate_api_secret()
|
||||||
|
source.save(update_fields=['api_key', 'api_secret'])
|
||||||
|
|
||||||
|
messages.success(request, f'New API keys generated for "{source.name}"!')
|
||||||
|
return redirect('source_detail', slug=source.slug)
|
||||||
|
|
||||||
|
# For GET requests, show confirmation page
|
||||||
|
context = {
|
||||||
|
'source': source,
|
||||||
|
'title': 'Generate New API Keys',
|
||||||
|
'message': f'Are you sure you want to generate new API keys for "{source.name}"? This will invalidate the existing keys.',
|
||||||
|
'cancel_url': reverse('source_detail', kwargs={'slug': source.slug}),
|
||||||
|
}
|
||||||
|
return render(request, 'recruitment/source_confirm_generate_keys.html', context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def source_toggle_status(request, slug):
|
||||||
|
"""Toggle active status of a source"""
|
||||||
|
source = get_object_or_404(Source, slug=slug)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
source.is_active = not source.is_active
|
||||||
|
source.save(update_fields=['is_active'])
|
||||||
|
|
||||||
|
status_text = 'activated' if source.is_active else 'deactivated'
|
||||||
|
messages.success(request, f'Source "{source.name}" has been {status_text}!')
|
||||||
|
|
||||||
|
# Handle HTMX requests
|
||||||
|
if 'HX-Request' in request.headers:
|
||||||
|
return HttpResponse(status=200) # HTMX success response
|
||||||
|
|
||||||
|
return redirect('source_detail', slug=source.slug)
|
||||||
|
|
||||||
|
# For GET requests, return error
|
||||||
|
return JsonResponse({'success': False, 'error': 'Method not allowed'})
|
||||||
|
|||||||
@ -257,6 +257,7 @@ def candidate_detail(request, slug):
|
|||||||
if request.user.is_staff:
|
if request.user.is_staff:
|
||||||
stage_form = forms.CandidateStageForm()
|
stage_form = forms.CandidateStageForm()
|
||||||
|
|
||||||
|
|
||||||
# parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False)
|
# parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False)
|
||||||
# parsed = json_to_markdown_table([parsed])
|
# parsed = json_to_markdown_table([parsed])
|
||||||
return render(request, 'recruitment/candidate_detail.html', {
|
return render(request, 'recruitment/candidate_detail.html', {
|
||||||
@ -458,19 +459,25 @@ def dashboard_view(request):
|
|||||||
|
|
||||||
# B. Efficiency & Conversion Metrics (Scoped)
|
# B. Efficiency & Conversion Metrics (Scoped)
|
||||||
hired_candidates = candidate_queryset.filter(
|
hired_candidates = candidate_queryset.filter(
|
||||||
Q(offer_status="Accepted") | Q(stage='HIRED'),
|
stage='Hired'
|
||||||
join_date__isnull=False
|
|
||||||
)
|
)
|
||||||
|
print(hired_candidates)
|
||||||
|
lst=[c.time_to_hire_days for c in hired_candidates]
|
||||||
|
print(lst)
|
||||||
time_to_hire_query = hired_candidates.annotate(
|
time_to_hire_query = hired_candidates.annotate(
|
||||||
time_diff=ExpressionWrapper(
|
time_diff=ExpressionWrapper(
|
||||||
F('join_date') - F('created_at__date'),
|
F('hired_date') - F('created_at__date'),
|
||||||
output_field=fields.DurationField()
|
output_field=fields.DurationField()
|
||||||
)
|
)
|
||||||
).aggregate(avg_time_to_hire=Avg('time_diff'))
|
).aggregate(avg_time_to_hire=Avg('time_diff'))
|
||||||
|
|
||||||
|
print(time_to_hire_query)
|
||||||
|
|
||||||
avg_time_to_hire_days = (
|
avg_time_to_hire_days = (
|
||||||
time_to_hire_query.get('avg_time_to_hire').days
|
time_to_hire_query.get('avg_time_to_hire').days
|
||||||
if time_to_hire_query.get('avg_time_to_hire') else 0
|
if time_to_hire_query.get('avg_time_to_hire') else 0
|
||||||
)
|
)
|
||||||
|
print(avg_time_to_hire_days)
|
||||||
|
|
||||||
applied_count = candidate_queryset.filter(stage='Applied').count()
|
applied_count = candidate_queryset.filter(stage='Applied').count()
|
||||||
advanced_count = candidate_queryset.filter(stage__in=['Exam', 'Interview', 'Offer']).count()
|
advanced_count = candidate_queryset.filter(stage__in=['Exam', 'Interview', 'Offer']).count()
|
||||||
|
|||||||
@ -182,24 +182,90 @@ class SourceDeleteView(LoginRequiredMixin, UserPassesTestMixin, DeleteView):
|
|||||||
messages.success(request, f'Source "{self.object.name}" deleted successfully!')
|
messages.success(request, f'Source "{self.object.name}" deleted successfully!')
|
||||||
return super().delete(request, *args, **kwargs)
|
return super().delete(request, *args, **kwargs)
|
||||||
|
|
||||||
def generate_api_keys_view(request):
|
def generate_api_keys_view(request, pk):
|
||||||
"""API endpoint to generate API keys"""
|
"""Generate new API keys for a specific source"""
|
||||||
if not request.user.is_staff:
|
if not request.user.is_staff:
|
||||||
return JsonResponse({'error': 'Permission denied'}, status=403)
|
return JsonResponse({'error': 'Permission denied'}, status=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
source = get_object_or_404(Source, pk=pk)
|
||||||
|
except Source.DoesNotExist:
|
||||||
|
return JsonResponse({'error': 'Source not found'}, status=404)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
api_key = generate_api_key()
|
# Generate new API keys
|
||||||
api_secret = generate_api_secret()
|
new_api_key = generate_api_key()
|
||||||
|
new_api_secret = generate_api_secret()
|
||||||
|
|
||||||
|
# Update the source with new keys
|
||||||
|
old_api_key = source.api_key
|
||||||
|
source.api_key = new_api_key
|
||||||
|
source.api_secret = new_api_secret
|
||||||
|
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', '')
|
||||||
|
)
|
||||||
|
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': True,
|
'success': True,
|
||||||
'api_key': api_key,
|
'api_key': new_api_key,
|
||||||
'api_secret': api_secret,
|
'api_secret': new_api_secret,
|
||||||
'message': 'API keys generated successfully'
|
'message': 'API keys regenerated successfully'
|
||||||
})
|
})
|
||||||
|
|
||||||
return JsonResponse({'error': 'Invalid request method'}, status=405)
|
return JsonResponse({'error': 'Invalid request method'}, status=405)
|
||||||
|
|
||||||
|
def toggle_source_status_view(request, pk):
|
||||||
|
"""Toggle the active status of a source"""
|
||||||
|
if not request.user.is_staff:
|
||||||
|
return JsonResponse({'error': 'Permission denied'}, status=403)
|
||||||
|
|
||||||
|
try:
|
||||||
|
source = get_object_or_404(Source, pk=pk)
|
||||||
|
except Source.DoesNotExist:
|
||||||
|
return JsonResponse({'error': 'Source not found'}, status=404)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
# Toggle the status
|
||||||
|
old_status = source.is_active
|
||||||
|
source.is_active = not source.is_active
|
||||||
|
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', '')
|
||||||
|
)
|
||||||
|
|
||||||
|
status_text = 'activated' if source.is_active else 'deactivated'
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'is_active': source.is_active,
|
||||||
|
'message': f'Source "{source.name}" {status_text} successfully'
|
||||||
|
})
|
||||||
|
|
||||||
def copy_to_clipboard_view(request):
|
def copy_to_clipboard_view(request):
|
||||||
"""HTMX endpoint to copy text to clipboard"""
|
"""HTMX endpoint to copy text to clipboard"""
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% translate "Application Submitted - Thank You" %}</title>
|
<title>{% trans "Application Submitted - Thank You" %}</title>
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
<style>
|
<style>
|
||||||
@ -166,25 +166,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="text-success-header">{% translate "Thank You!" %}</h1>
|
<h1 class="text-success-header">{% translate "Thank You!" %}</h1>
|
||||||
<h2 class="h4" style="color: #333;">{% translate "Your application has been submitted successfully" %}</h2>
|
<h2 class="h4" style="color: #333;">{% trans "Your application has been submitted successfully" %}</h2>
|
||||||
|
|
||||||
{% comment %} {# JOB INFO BLOCK #}
|
{% comment %} {# JOB INFO BLOCK #}
|
||||||
<div class="job-info-block">
|
<div class="job-info-block">
|
||||||
<p class="mb-2"><strong>{% translate "Position" %}:</strong> <span class="fw-bold">{{ job.title }}</span></p>
|
<p class="mb-2"><strong>{% trans "Position" %}:</strong> <span class="fw-bold">{{ job.title }}</span></p>
|
||||||
<p class="mb-2"><strong>{% translate "Job ID" %}:</strong> {{ job.internal_job_id }}</p>
|
<p class="mb-2"><strong>{% trans "Job ID" %}:</strong> {{ job.internal_job_id }}</p>
|
||||||
<p class="mb-2"><strong>{% translate "Department" %}:</strong> {{ job.department|default:"Not specified" }}</p>
|
<p class="mb-2"><strong>{% trans "Department" %}:</strong> {{ job.department|default:"Not specified" }}</p>
|
||||||
{% if job.application_deadline %}
|
{% if job.application_deadline %}
|
||||||
<p><strong>{% translate "Application Deadline" %}:</strong> {{ job.application_deadline|date:"F j, Y" }}</p>
|
<p><strong>{% trans "Application Deadline" %}:</strong> {{ job.application_deadline|date:"F j, Y" }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div> {% endcomment %}
|
</div> {% endcomment %}
|
||||||
|
|
||||||
<p style="font-size: 1rem; line-height: 1.6; color: #555;">
|
<p style="font-size: 1rem; line-height: 1.6; color: #555;">
|
||||||
{% translate "We appreciate your interest in joining our team. Our hiring team will review your application and contact you if there's a potential match for this position." %}
|
{% trans "We appreciate your interest in joining our team. Our hiring team will review your application and contact you if there's a potential match for this position." %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div style="margin-top: 30px;">
|
<div style="margin-top: 30px;">
|
||||||
<a href="https://kaauh.edu.sa/career" class="btn btn-main-action btn-lg">
|
<a href="{% url 'kaauh_career' %}" class="btn btn-main-action btn-lg">
|
||||||
<i class="fas fa-arrow-left me-2"></i> {% translate "Return to Job Listings" %}
|
<i class="fas fa-arrow-left me-2"></i> {% trans "Return to Job Listings" %}
|
||||||
</a>
|
</a>
|
||||||
{# You can add a link to view the saved application here if applicable #}
|
{# You can add a link to view the saved application here if applicable #}
|
||||||
{% comment %} <a href="{% url 'jobs:job_detail' job.internal_job_id %}" class="btn btn-secondary">View Job Details</a> {% endcomment %}
|
{% comment %} <a href="{% url 'jobs:job_detail' job.internal_job_id %}" class="btn btn-secondary">View Job Details</a> {% endcomment %}
|
||||||
|
|||||||
@ -251,25 +251,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# Description Blocks (Main Content) #}
|
{# Description Blocks (Main Content) #}
|
||||||
{% if job.description %}
|
{% if job.has_description_content %}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h5>{% trans "Job Description" %}</h5>
|
<h5>{% trans "Job Description" %}</h5>
|
||||||
<div class="text-secondary">{{ job.description|safe }}</div>
|
<div class="text-secondary">{{ job.description|safe }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if job.qualifications %}
|
{% if job.has_qualifications_content %}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h5>{% trans "Required Qualifications" %}</h5>
|
<h5>{% trans "Required Qualifications" %}</h5>
|
||||||
<div class="text-secondary">{{ job.qualifications|safe }}</div>
|
<div class="text-secondary">{{ job.qualifications|safe }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if job.benefits %}
|
{% if job.has_benefits_content %}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h5>{% trans "Benefits" %}</h5>
|
<h5>{% trans "Benefits" %}</h5>
|
||||||
<div class="text-secondary">{{ job.benefits|safe}}</div>
|
<div class="text-secondary">{{ job.benefits|safe}}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if job.application_instructions %}
|
{% if job.has_application_instructions_content %}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h5>{% trans "Application Instructions" %}</h5>
|
<h5>{% trans "Application Instructions" %}</h5>
|
||||||
<div class="text-secondary">{{ job.application_instructions|safe }}</div>
|
<div class="text-secondary">{{ job.application_instructions|safe }}</div>
|
||||||
@ -347,11 +347,14 @@
|
|||||||
<i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %}
|
<i class="fas fa-plus-circle me-1"></i> {% trans "Create New Form Template" %}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if job.form_template.is_active %}
|
{% if job.form_template.is_active %}
|
||||||
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
|
<a href="{% url 'application_submit_form' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
|
||||||
<i class="fas fa-list-alt me-1"></i> {% trans "View Form Template" %}
|
<i class="fas fa-list-alt me-1"></i> {% trans "View Form Template" %}
|
||||||
</a>
|
</a>
|
||||||
{% else %}
|
<a href="{% url 'form_builder' job.form_template.slug %}" class="btn btn-outline-secondary w-100">
|
||||||
|
<i class="fas fa-list-alt me-1"></i> {% trans "Manage Form Template" %}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
<p>{% trans "This job status is not active, the form will appear once the job is made active"%}</p>
|
<p>{% trans "This job status is not active, the form will appear once the job is made active"%}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@ -446,7 +449,7 @@
|
|||||||
<div class="row g-3 stats-grid">
|
<div class="row g-3 stats-grid">
|
||||||
|
|
||||||
{# 1. Job Avg. Score #}
|
{# 1. Job Avg. Score #}
|
||||||
<div class="col-6">
|
<div class="col-4">
|
||||||
<div class="card text-center h-100 kpi-card">
|
<div class="card text-center h-100 kpi-card">
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
<i class="fas fa-star text-primary mb-1 d-block" style="font-size: 1.2rem;"></i>
|
<i class="fas fa-star text-primary mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||||
@ -457,7 +460,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# 2. High Potential Count #}
|
{# 2. High Potential Count #}
|
||||||
<div class="col-6">
|
<div class="col-4">
|
||||||
<div class="card text-center h-100">
|
<div class="card text-center h-100">
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
<i class="fas fa-trophy text-success mb-1 d-block" style="font-size: 1.2rem;"></i>
|
<i class="fas fa-trophy text-success mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||||
@ -468,7 +471,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# 3. Avg. Time to Interview #}
|
{# 3. Avg. Time to Interview #}
|
||||||
<div class="col-6">
|
{% comment %} <div class="col-6">
|
||||||
<div class="card text-center h-100">
|
<div class="card text-center h-100">
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
<i class="fas fa-calendar-alt text-info mb-1 d-block" style="font-size: 1.2rem;"></i>
|
<i class="fas fa-calendar-alt text-info mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||||
@ -487,9 +490,9 @@
|
|||||||
<small class="text-muted d-block">{% trans "Avg. Exam Review" %}</small>
|
<small class="text-muted d-block">{% trans "Avg. Exam Review" %}</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> {% endcomment %}
|
||||||
<!--Vacancy fill rate-->
|
<!--Vacancy fill rate-->
|
||||||
<div class="col-6">
|
<div class="col-4">
|
||||||
<div class="card text-center h-100">
|
<div class="card text-center h-100">
|
||||||
<div class="card-body p-2">
|
<div class="card-body p-2">
|
||||||
<i class="fas fa-trophy text-secondary mb-1 d-block" style="font-size: 1.2rem;"></i>
|
<i class="fas fa-trophy text-secondary mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||||
|
|||||||
@ -325,7 +325,7 @@
|
|||||||
</td>
|
</td>
|
||||||
|
|
||||||
{# CANDIDATE MANAGEMENT DATA - URLS NEUTRALIZED #}
|
{# CANDIDATE MANAGEMENT DATA - URLS NEUTRALIZED #}
|
||||||
<td class="candidate-data-cell text-primary-theme"><a href="#" class="text-primary-theme">{% if job.all_candidates.count %}{{ job.all_candidates.count }}{% else %}-{% endif %}</a></td>
|
<td class="candidate-data-cell text-primary-theme"><a href="{% url 'candidate_screening_view' job.slug %}" class="text-primary-theme">{% if job.all_candidates.count %}{{ job.all_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||||
<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-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_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_interview_view' job.slug %}" class="text-success">{% if job.interview_candidates.count %}{{ job.interview_candidates.count }}{% else %}-{% endif %}</a></td>
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid py-4">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4 px-3 py-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||||
<i class="fas fa-link me-2"></i>
|
<i class="fas fa-link me-2"></i>
|
||||||
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-8">
|
<div class="col-md-8">
|
||||||
<div class="kaauh-card shadow-sm mb-4">
|
<div class="kaauh-card shadow-sm mb-4 px-3 py-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
<h5 class="card-title mb-0">
|
<h5 class="card-title mb-0">
|
||||||
@ -77,7 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="kaauh-card shadow-sm">
|
<div class="kaauh-card shadow-sm">
|
||||||
<div class="card-body">
|
<div class="card-body px-3 py-3">
|
||||||
<h5 class="card-title mb-3">
|
<h5 class="card-title mb-3">
|
||||||
<i class="fas fa-key me-2 text-warning"></i>
|
<i class="fas fa-key me-2 text-warning"></i>
|
||||||
{% trans "Access Credentials" %}
|
{% trans "Access Credentials" %}
|
||||||
@ -125,7 +125,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="kaauh-card shadow-sm mb-4">
|
<div class="kaauh-card shadow-sm mb-4 px-3 py-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title mb-3">
|
<h5 class="card-title mb-3">
|
||||||
<i class="fas fa-chart-line me-2 text-info"></i>
|
<i class="fas fa-chart-line me-2 text-info"></i>
|
||||||
@ -161,7 +161,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="kaauh-card shadow-sm">
|
<div class="kaauh-card shadow-sm px-3 py-3">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title mb-3">
|
<h5 class="card-title mb-3">
|
||||||
<i class="fas fa-cog me-2 text-secondary"></i>
|
<i class="fas fa-cog me-2 text-secondary"></i>
|
||||||
|
|||||||
@ -473,7 +473,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<div class="card kaauh-card mb-4">
|
{% comment %} <div class="card kaauh-card mb-4">
|
||||||
<div class="card-header bg-white border-bottom">
|
<div class="card-header bg-white border-bottom">
|
||||||
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
|
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
|
||||||
<i class="fas fa-bolt me-2"></i>
|
<i class="fas fa-bolt me-2"></i>
|
||||||
@ -503,7 +503,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> {% endcomment %}
|
||||||
|
|
||||||
<!-- Agency Information -->
|
<!-- Agency Information -->
|
||||||
<div class="card kaauh-card">
|
<div class="card kaauh-card">
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
--kaauh-info: #17a2b8;
|
--kaauh-info: #17a2b8;
|
||||||
--kaauh-danger: #dc3545;
|
--kaauh-danger: #dc3545;
|
||||||
--kaauh-warning: #ffc107;
|
--kaauh-warning: #ffc107;
|
||||||
|
--kaauh-gray-light: #f8f9fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Primary Color Overrides */
|
/* Primary Color Overrides */
|
||||||
@ -53,6 +54,17 @@
|
|||||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Secondary Button Style */
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
|
||||||
/* Search Form Styling */
|
/* Search Form Styling */
|
||||||
.search-form {
|
.search-form {
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
@ -71,6 +83,41 @@
|
|||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Table View Styling */
|
||||||
|
.table-view .table thead th {
|
||||||
|
background-color: var(--kaauh-teal-dark);
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
border-color: var(--kaauh-border);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.table-view .table tbody td {
|
||||||
|
vertical-align: middle;
|
||||||
|
padding: 1rem;
|
||||||
|
border-color: var(--kaauh-border);
|
||||||
|
}
|
||||||
|
.table-view .table tbody tr:hover {
|
||||||
|
background-color: var(--kaauh-gray-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card View Specific Styles */
|
||||||
|
.card-view .card {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.card-view .card-title {
|
||||||
|
color: var(--kaauh-teal-dark);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.card-view .card-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -115,7 +162,7 @@
|
|||||||
value="{{ search_query }}">
|
value="{{ search_query }}">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-2">
|
<div class="col-md-1">
|
||||||
<button type="submit" class="btn btn-main-action w-100">
|
<button type="submit" class="btn btn-main-action w-100">
|
||||||
<i class="fas fa-search me-1"></i> {% trans "Search" %}
|
<i class="fas fa-search me-1"></i> {% trans "Search" %}
|
||||||
</button>
|
</button>
|
||||||
@ -125,78 +172,168 @@
|
|||||||
|
|
||||||
<!-- Agencies List -->
|
<!-- Agencies List -->
|
||||||
{% if page_obj %}
|
{% if page_obj %}
|
||||||
<div class="row">
|
<div id="agency-list">
|
||||||
{% for agency in page_obj %}
|
{% include "includes/_list_view_switcher.html" with list_id="agency-list" %}
|
||||||
<div class="col-lg-4 col-md-6 mb-4">
|
|
||||||
<div class="card kaauh-card agency-card h-100">
|
|
||||||
<div class="card-body">
|
|
||||||
<!-- Agency Header -->
|
|
||||||
<div class="d-flex justify-content-between align-items-start mb-3">
|
|
||||||
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
|
|
||||||
{{ agency.name }}
|
|
||||||
</h5>
|
|
||||||
{% if agency.email %}
|
|
||||||
<a href="mailto:{{ agency.email }}" class="text-muted" title="{{ agency.email }}">
|
|
||||||
<i class="fas fa-envelope"></i>
|
|
||||||
</a>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contact Information -->
|
<!-- Table View -->
|
||||||
{% if agency.contact_person %}
|
<div class="table-view">
|
||||||
<p class="card-text mb-2">
|
<div class="table-responsive">
|
||||||
<i class="fas fa-user text-muted me-2"></i>
|
<table class="table table-hover align-middle mb-0">
|
||||||
<strong>{% trans "Contact:" %}</strong> {{ agency.contact_person }}
|
<thead>
|
||||||
</p>
|
<tr>
|
||||||
{% endif %}
|
<th scope="col">{% trans "Agency Name" %}</th>
|
||||||
|
<th scope="col">{% trans "Contact Person" %}</th>
|
||||||
|
<th scope="col">{% trans "Email" %}</th>
|
||||||
|
<th scope="col">{% trans "Phone" %}</th>
|
||||||
|
<th scope="col">{% trans "Country" %}</th>
|
||||||
|
<th scope="col">{% trans "Website" %}</th>
|
||||||
|
<th scope="col">{% trans "Created" %}</th>
|
||||||
|
<th scope="col" class="text-end">{% trans "Actions" %}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for agency in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td class="fw-medium">
|
||||||
|
<a href="{% url 'agency_detail' agency.slug %}"
|
||||||
|
class="text-decoration-none text-primary-theme">
|
||||||
|
{{ agency.name }}
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td>{{ agency.contact_person|default:"-" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if agency.email %}
|
||||||
|
<a href="mailto:{{ agency.email }}"
|
||||||
|
class="text-decoration-none"
|
||||||
|
title="{{ agency.email }}">
|
||||||
|
<i class="fas fa-envelope me-1"></i>
|
||||||
|
{{ agency.email|truncatechars:20 }}
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ agency.phone|default:"-" }}</td>
|
||||||
|
<td>
|
||||||
|
{% if agency.country %}
|
||||||
|
<i class="fas fa-globe text-muted me-1"></i>
|
||||||
|
{{ agency.get_country_display }}
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if agency.website %}
|
||||||
|
<a href="{{ agency.website }}"
|
||||||
|
target="_blank"
|
||||||
|
class="text-decoration-none text-secondary">
|
||||||
|
{{ agency.website|truncatechars:25 }}
|
||||||
|
<i class="fas fa-external-link-alt ms-1 small"></i>
|
||||||
|
</a>
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="stats-badge">
|
||||||
|
{{ agency.created_at|date:"M d, Y" }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="text-end">
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<a href="{% url 'agency_detail' agency.slug %}"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
title="{% trans 'View' %}">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'agency_update' agency.slug %}"
|
||||||
|
class="btn btn-outline-secondary"
|
||||||
|
title="{% trans 'Edit' %}">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if agency.phone %}
|
<!-- Card View -->
|
||||||
<p class="card-text mb-2">
|
<div class="card-view row g-4">
|
||||||
<i class="fas fa-phone text-muted me-2"></i>
|
{% for agency in page_obj %}
|
||||||
{{ agency.phone }}
|
<div class="col-lg-4 col-md-6 mb-4">
|
||||||
</p>
|
<div class="card kaauh-card agency-card h-100">
|
||||||
{% endif %}
|
<div class="card-body">
|
||||||
|
<!-- Agency Header -->
|
||||||
{% if agency.country %}
|
<div class="d-flex justify-content-between align-items-start mb-3">
|
||||||
<p class="card-text mb-2">
|
<h5 class="card-title mb-0" style="color: var(--kaauh-teal-dark);">
|
||||||
<i class="fas fa-globe text-muted me-2"></i>
|
{{ agency.name }}
|
||||||
{{ agency.get_country_display }}
|
</h5>
|
||||||
</p>
|
{% if agency.email %}
|
||||||
{% endif %}
|
<a href="mailto:{{ agency.email }}" class="text-muted" title="{{ agency.email }}">
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
<!-- Website Link -->
|
|
||||||
{% if agency.website %}
|
|
||||||
<p class="card-text mb-3">
|
|
||||||
<i class="fas fa-link text-muted me-2"></i>
|
|
||||||
<a href="{{ agency.website }}" target="_blank" class="text-decoration-none text-secondary">
|
|
||||||
{{ agency.website|truncatechars:30 }}
|
|
||||||
<i class="fas fa-external-link-alt ms-1 small"></i>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="d-flex justify-content-between align-items-center mt-auto">
|
|
||||||
<div>
|
|
||||||
<a href="{% url 'agency_detail' agency.slug %}"
|
|
||||||
class="btn btn-main-action btn-sm me-2">
|
|
||||||
<i class="fas fa-eye me-1"></i> {% trans "View" %}
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'agency_update' agency.slug %}"
|
|
||||||
class="btn btn-outline-secondary btn-sm">
|
|
||||||
<i class="fas fa-edit me-1"></i> {% trans "Edit" %}
|
|
||||||
</a>
|
</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<span class="stats-badge">
|
<!-- Contact Information -->
|
||||||
{% trans "Created" %} {{ agency.created_at|date:"M d, Y" }}
|
{% if agency.contact_person %}
|
||||||
</span>
|
<p class="card-text mb-2">
|
||||||
|
<i class="fas fa-user text-muted me-2"></i>
|
||||||
|
<strong>{% trans "Contact:" %}</strong> {{ agency.contact_person }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if agency.phone %}
|
||||||
|
<p class="card-text mb-2">
|
||||||
|
<i class="fas fa-phone text-muted me-2"></i>
|
||||||
|
{{ agency.phone }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if agency.country %}
|
||||||
|
<p class="card-text mb-2">
|
||||||
|
<i class="fas fa-globe text-muted me-2"></i>
|
||||||
|
{{ agency.get_country_display }}
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Website Link -->
|
||||||
|
{% if agency.website %}
|
||||||
|
<p class="card-text mb-3">
|
||||||
|
<i class="fas fa-link text-muted me-2"></i>
|
||||||
|
<a href="{{ agency.website }}" target="_blank" class="text-decoration-none text-secondary">
|
||||||
|
{{ agency.website|truncatechars:30 }}
|
||||||
|
<i class="fas fa-external-link-alt ms-1 small"></i>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center mt-auto">
|
||||||
|
<div>
|
||||||
|
<a href="{% url 'agency_detail' agency.slug %}"
|
||||||
|
class="btn btn-main-action btn-sm me-2">
|
||||||
|
<i class="fas fa-eye me-1"></i> {% trans "View" %}
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'agency_update' agency.slug %}"
|
||||||
|
class="btn btn-outline-secondary btn-sm">
|
||||||
|
<i class="fas fa-edit me-1"></i> {% trans "Edit" %}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="stats-badge">
|
||||||
|
{% trans "Created" %} {{ agency.created_at|date:"M d, Y" }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{% endfor %}
|
||||||
{% endfor %}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination -->
|
<!-- Pagination -->
|
||||||
|
|||||||
@ -417,15 +417,16 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if candidate.interview_date %}
|
|
||||||
|
{% if candidate.get_interview_date %}
|
||||||
<div class="timeline-item">
|
<div class="timeline-item">
|
||||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-comments"></i></div>
|
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-comments"></i></div>
|
||||||
<div class="timeline-content">
|
<div class="timeline-content">
|
||||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Interview" %}</p>
|
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Interview" %}</p>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.interview_date|date:"M d, Y" }}
|
<i class="far fa-calendar-alt me-1"></i> {{ candidate.get_interview_date}}
|
||||||
<span class="ms-2">|</span>
|
<span class="ms-2">|</span>
|
||||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.interview_date|date:"h:i A" }}
|
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.get_interview_time}}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -439,8 +440,21 @@
|
|||||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
|
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
<i class="far fa-calendar-alt me-1"></i> {{ candidate.offer_date|date:"M d, Y" }}
|
<i class="far fa-calendar-alt me-1"></i> {{ candidate.offer_date|date:"M d, Y" }}
|
||||||
<span class="ms-2">|</span>
|
|
||||||
<i class="far fa-clock ms-2 me-1"></i> {{ candidate.offer_date|date:"h:i A" }}
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if candidate.hired_date %}
|
||||||
|
<div class="timeline-item">
|
||||||
|
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-handshake"></i></div>
|
||||||
|
<div class="timeline-content">
|
||||||
|
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Offer" %}</p>
|
||||||
|
<small class="text-muted">
|
||||||
|
<i class="far fa-calendar-alt me-1"></i> {{ candidate.hired_date|date:"M d, Y" }}
|
||||||
|
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -651,7 +665,16 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card shadow-sm mb-4 p-2">
|
<div class="card shadow-sm mb-4 p-2">
|
||||||
<h5 class="text-muted mb-3"><i class="fas fa-clock me-2"></i>{% trans "Time to Hire: " %}{{candidate.time_to_hire|default:100}} days</h5>
|
<h5 class="text-muted mb-3"><i class="fas fa-clock me-2"></i>{% trans "Time to Hire:" %}
|
||||||
|
|
||||||
|
{% with days=candidate.time_to_hire_days %}
|
||||||
|
{% if days > 0 %}
|
||||||
|
{{ days }} day{{ days|pluralize }}
|
||||||
|
{% else %}
|
||||||
|
N/A
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
</h5>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -675,7 +698,8 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
|
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
|
||||||
<button type="submit" class="btn btn-sm btn-main-action" hx-get="{% url 'candidate_retry_scoring' candidate.slug %}" hx-select=".resume-parsed-section" hx-target=".resume-parsed-section" hx-swap="outerHTML" hx-on:click="this.disabled=true;this.innerHTML=`Scoring Resume , Please Wait.. <i class='fa-solid fa-spinner fa-spin'></i>`">
|
<button type="submit" class="btn btn-sm btn-main-action" hx-get="{% url 'candidate_retry_scoring' candidate.slug %}" hx-select=".resume-parsed-section" hx-target=".resume-parsed-section" hx-swap="outerHTML" hx-on:click="this.disabled=true;this.innerHTML=`Scoring Resume , Please Wait.. <i class='fa-solid fa-spinner fa-spin'></i>`">
|
||||||
{% trans "Retry AI Scoring" %}
|
<i class="fas fa-redo-alt me-1"></i>
|
||||||
|
{% trans "Unable to Parse Resume , click to retry" %}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -265,7 +265,7 @@
|
|||||||
<th style="width: 10%"><i class="fas fa-calendar me-1"></i> {% trans "Meeting Date" %}</th>
|
<th style="width: 10%"><i class="fas fa-calendar me-1"></i> {% trans "Meeting Date" %}</th>
|
||||||
<th style="width: 7%"><i class="fas fa-video me-1"></i> {% trans "Link" %}</th>
|
<th style="width: 7%"><i class="fas fa-video me-1"></i> {% trans "Link" %}</th>
|
||||||
<th style="width: 8%"><i class="fas fa-check-circle me-1"></i> {% trans "Meeting Status" %}</th>
|
<th style="width: 8%"><i class="fas fa-check-circle me-1"></i> {% trans "Meeting Status" %}</th>
|
||||||
<th style="width: 5%"><i class="fas fa-check-circle me-1"></i> {% trans "Interview Result" %}</th>
|
<th style="width: 5%"><i class="fas fa-check-circle me-1"></i> {% trans "Interview Result"%}</th>
|
||||||
<th style="width: 10%"><i class="fas fa-cog me-1"></i> {% trans "Actions" %}</th>
|
<th style="width: 10%"><i class="fas fa-cog me-1"></i> {% trans "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -276,11 +276,20 @@
|
|||||||
<div class="form-check">
|
<div class="form-check">
|
||||||
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="candidate-name">
|
<button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#candidateviewModal"
|
||||||
|
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||||
|
hx-target="#candidateviewModalBody"
|
||||||
|
title="View Profile">
|
||||||
|
{{ candidate.name }}<i class="fas fa-eye ms-1"></i>
|
||||||
|
</button>
|
||||||
|
{% comment %} <div class="candidate-name">
|
||||||
{{ candidate.name }}
|
{{ candidate.name }}
|
||||||
</div>
|
</div> {% endcomment %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="candidate-details">
|
<div class="candidate-details">
|
||||||
@ -365,14 +374,14 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm"
|
{% comment %} <button type="button" class="btn btn-outline-secondary btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#candidateviewModal"
|
data-bs-target="#candidateviewModal"
|
||||||
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||||
hx-target="#candidateviewModalBody"
|
hx-target="#candidateviewModalBody"
|
||||||
title="View Profile">
|
title="View Profile">
|
||||||
<i class="fas fa-eye"></i>
|
<i class="fas fa-eye"></i>
|
||||||
</button>
|
</button> {% endcomment %}
|
||||||
<button type="button" class="btn btn-outline-info btn-sm"
|
<button type="button" class="btn btn-outline-info btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#emailModal"
|
data-bs-target="#emailModal"
|
||||||
@ -457,17 +466,14 @@
|
|||||||
|
|
||||||
<form method="post" action="{% url 'candidate_interview_view' job.slug %}">
|
<form method="post" action="{% url 'candidate_interview_view' job.slug %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<<<<<<< HEAD
|
|
||||||
|
|
||||||
<div class="modal-body table-responsive">
|
<div class="modal-body table-responsive">
|
||||||
=======
|
|
||||||
|
|
||||||
<div class="modal-body">
|
|
||||||
>>>>>>> c6fcb276135dc7e87bb0d065a93ff89091ff0207
|
|
||||||
{{ job.internal_job_id }} {{ job.title}}
|
{{ job.internal_job_id }} {{ job.title}}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
<<<<<<< HEAD
|
|
||||||
|
|
||||||
|
|
||||||
<table class="table tab table-bordered mt-3">
|
<table class="table tab table-bordered mt-3">
|
||||||
@ -490,18 +496,7 @@
|
|||||||
|
|
||||||
|
|
||||||
</table>
|
</table>
|
||||||
=======
|
|
||||||
|
|
||||||
<h3>👥 {% trans "Participants" %}</h3>
|
|
||||||
{{ form.participants.errors }}
|
|
||||||
{{ form.participants }}
|
|
||||||
|
|
||||||
<hr>
|
|
||||||
|
|
||||||
<h3>🧑💼 {% trans "Users" %}</h3>
|
|
||||||
{{ form.users.errors }}
|
|
||||||
{{ form.users }}
|
|
||||||
>>>>>>> c6fcb276135dc7e87bb0d065a93ff89091ff0207
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
@ -298,8 +298,6 @@
|
|||||||
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"
|
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"
|
||||||
style="animation: kaats-spinner-dash 1.5s ease-in-out infinite;"></circle>
|
style="animation: kaats-spinner-dash 1.5s ease-in-out infinite;"></circle>
|
||||||
</svg>
|
</svg>
|
||||||
{# CRITICAL: Remove the DIV and the text-nowrap class #}
|
|
||||||
<span class="text-teal-primary">{% trans "AI Scoring..." %}</span>
|
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@ -30,54 +30,113 @@
|
|||||||
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
box-shadow: 0 6px 16px rgba(0,0,0,0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Card Header and Icon Styling */
|
/* ------------------------------------------------------------- */
|
||||||
|
/* CONSOLIDATED CARD HEADER STYLES (for both main and stat cards)*/
|
||||||
|
/* ------------------------------------------------------------- */
|
||||||
.card-header {
|
.card-header {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 1.25rem;
|
/* Consistent, reduced padding for compact look */
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
border-bottom: 1px solid var(--kaauh-border);
|
border-bottom: 1px solid var(--kaauh-border);
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-header h3, .card-header h2 {
|
/* Target h2 (for main headers) and h3 (for stat card headers) */
|
||||||
|
.card-header h1, .card-header h2, .card-header h3, .card-header h6 {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--kaauh-primary-text);
|
color: var(--kaauh-primary-text);
|
||||||
|
margin: 0;
|
||||||
|
|
||||||
|
/* Font size for MAIN card titles (e.g., "Data Scope: All Jobs") */
|
||||||
font-size: 1.25rem;
|
font-size: 1.25rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: 0;
|
}
|
||||||
|
|
||||||
|
/* Override for H3 inside stat cards to make them very small */
|
||||||
|
.stats .card-header h3 {
|
||||||
|
font-size: 0.85rem; /* Smallest size for the 9-card layout */
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap; /* Prevent title from wrapping */
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-icon {
|
.stat-icon {
|
||||||
color: var(--kaauh-teal);
|
color: var(--kaauh-teal);
|
||||||
font-size: 1.75rem;
|
font-size: 0.8rem; /* Small icon size */
|
||||||
margin-right: 0.75rem;
|
margin-right: 0.25rem;
|
||||||
|
/* Note: For 9-card density, you might still want to hide this on mobile */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stats Grid Layout */
|
/* ------------------------------------------------------------- */
|
||||||
|
/* STATS GRID LAYOUT (9 Columns) */
|
||||||
|
/* ------------------------------------------------------------- */
|
||||||
.stats {
|
.stats {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
/* Force 9 columns */
|
||||||
gap: 1.5rem;
|
grid-template-columns: repeat(9, 1fr);
|
||||||
margin-bottom: 3rem;
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stat Card Specific Styling */
|
/* Stat Card Specific Styling */
|
||||||
.stat-value {
|
.stat-value {
|
||||||
font-size: 2.8rem;
|
/* This is the most important number */
|
||||||
|
font-size: 1.5rem; /* Increased slightly for focus, was 1rem/1.25rem */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: var(--kaauh-teal-dark);
|
color: var(--kaauh-teal-dark);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 1rem 1rem 0.5rem;
|
padding: 0.5rem 0.25rem 0.1rem; /* Very little bottom padding */
|
||||||
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stat-caption {
|
.stat-caption {
|
||||||
font-size: 0.9rem;
|
/* Smallest text for the label below the value */
|
||||||
|
font-size: 0.7rem; /* Minimized size */
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #6c757d;
|
color: #6c757d;
|
||||||
padding-bottom: 1rem;
|
padding: 0 0.25rem 0.5rem;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------- */
|
||||||
|
/* RESPONSIVE DESIGN */
|
||||||
|
/* ------------------------------------------------------------- */
|
||||||
|
|
||||||
|
/* On tablets and smaller laptops (1200px and down) */
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.stats {
|
||||||
|
/* Switch to 4 columns on medium screens */
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.75rem; /* Increase value size slightly when more space is available */
|
||||||
|
}
|
||||||
|
.stat-caption {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On phones (576px and down) */
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.stats {
|
||||||
|
/* Stack to 2 columns on mobile for readability */
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
.stat-caption {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dropdown/Filter Styling */
|
/* Dropdown/Filter Styling */
|
||||||
@ -130,7 +189,7 @@
|
|||||||
<h2>
|
<h2>
|
||||||
<i class="fas fa-search stat-icon"></i>
|
<i class="fas fa-search stat-icon"></i>
|
||||||
{% if current_job %}
|
{% if current_job %}
|
||||||
{% trans "Data Scope: " %} **{{ current_job.title }}**
|
{% trans "Data Scope: " %}{{ current_job.title }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans "Data Scope: All Jobs" %}
|
{% trans "Data Scope: All Jobs" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -153,9 +212,7 @@
|
|||||||
{# STATS CARDS SECTION (12 KPIs) #}
|
{# STATS CARDS SECTION (12 KPIs) #}
|
||||||
{# -------------------------------------------------------------------------- #}
|
{# -------------------------------------------------------------------------- #}
|
||||||
{% include 'recruitment/partials/stats_cards.html' %}
|
{% include 'recruitment/partials/stats_cards.html' %}
|
||||||
|
{# Note: The content of 'recruitment/partials/stats_cards.html' uses h3 which is styled correctly here #}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{# -------------------------------------------------------------------------- #}
|
{# -------------------------------------------------------------------------- #}
|
||||||
{# CHARTS SECTION #}
|
{# CHARTS SECTION #}
|
||||||
@ -197,7 +254,7 @@
|
|||||||
<h2>
|
<h2>
|
||||||
<i class="fas fa-funnel-dollar stat-icon"></i>
|
<i class="fas fa-funnel-dollar stat-icon"></i>
|
||||||
{% if current_job %}
|
{% if current_job %}
|
||||||
{% trans "Pipeline Funnel: " %} **{{ current_job.title }}**
|
{% trans "Pipeline Funnel: " %}{{ current_job.title }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% trans "Total Pipeline Funnel (All Jobs)" %}
|
{% trans "Total Pipeline Funnel (All Jobs)" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -223,28 +280,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="card shadow-sm no-hover mb-4">
|
|
||||||
<div class="card-header">
|
|
||||||
<h6 class="mb-0">
|
|
||||||
<i class="fas fa-chart-pie me-2 text-primary"></i>
|
|
||||||
{% trans "Candidates From Each Sources" %}
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body p-4">
|
|
||||||
<div style="height: 300px;">
|
|
||||||
<canvas id="candidatesourceschart"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/luxon@3.4.4"></script>
|
<script src="https://cdn.jsdelivr.net/npm/luxon@3.4.4"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.3.1"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-luxon@1.3.1"></script>
|
||||||
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Pass context data safely to JavaScript
|
// Pass context data safely to JavaScript
|
||||||
const totalCandidatesScoped = parseInt('{{ total_candidates|default:0 }}');
|
const totalCandidatesScoped = parseInt('{{ total_candidates|default:0 }}');
|
||||||
@ -462,80 +504,7 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Chart for Candidate Categories and Match Scores
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const ctx = document.getElementById('candidatesourceschart');
|
|
||||||
if (!ctx) {
|
|
||||||
console.warn('Candidates sources chart element not found.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const chartCtx = ctx.getContext('2d');
|
|
||||||
|
|
||||||
// Safely get job_category_data from Django context
|
|
||||||
// Using window.jobChartData to avoid template parsing issues
|
|
||||||
|
|
||||||
|
|
||||||
if (categories.length > 0) { // Only render if there's data
|
|
||||||
const chart = new Chart(chartCtx, {
|
|
||||||
type: 'doughnut',
|
|
||||||
data: {
|
|
||||||
labels: categories,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: 'Number of Candidates',
|
|
||||||
data: candidates_count_in_each_source,
|
|
||||||
backgroundColor: [
|
|
||||||
'rgba(0, 99, 110, 0.7)', // --kaauh-teal
|
|
||||||
'rgba(23, 162, 184, 0.7)', // Teal shade
|
|
||||||
'rgba(0, 150, 136, 0.7)', // Teal green
|
|
||||||
'rgba(0, 188, 212, 0.7)', // Cyan
|
|
||||||
'rgba(38, 166, 154, 0.7)', // Turquoise
|
|
||||||
'rgba(77, 182, 172, 0.7)', // Medium teal
|
|
||||||
// Add more colors if you expect more categories
|
|
||||||
],
|
|
||||||
borderColor: [
|
|
||||||
'rgba(0, 99, 110, 1)',
|
|
||||||
'rgba(23, 162, 184, 1)',
|
|
||||||
'rgba(0, 150, 136, 1)',
|
|
||||||
'rgba(0, 188, 212, 1)',
|
|
||||||
'rgba(38, 166, 154, 1)',
|
|
||||||
'rgba(77, 182, 172, 1)',
|
|
||||||
// Add more colors if you expect more categories
|
|
||||||
],
|
|
||||||
borderWidth: 1,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
maintainAspectRatio: false, // Important for fixed height container
|
|
||||||
plugins: {
|
|
||||||
legend: {
|
|
||||||
position: 'right', // Position legend for doughnut chart
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: false, // Chart title is handled by the card header
|
|
||||||
},
|
|
||||||
tooltip: {
|
|
||||||
callbacks: {
|
|
||||||
label: function(context) {
|
|
||||||
let label = context.label || '';
|
|
||||||
if (label) {
|
|
||||||
label += ': ';
|
|
||||||
}
|
|
||||||
label += context.parsed + ' candidate(s)';
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Display a message if no data is available
|
|
||||||
chartCtx.canvas.parentNode.innerHTML = '<p class="text-center text-muted mt-4">No candidate category data available for this job.</p>';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -10,7 +10,7 @@
|
|||||||
<h3><i class="fas fa-list stat-icon"></i> {% trans "Total Jobs" %}</h3>
|
<h3><i class="fas fa-list stat-icon"></i> {% trans "Total Jobs" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ total_jobs_global }}</div>
|
<div class="stat-value">{{ total_jobs_global }}</div>
|
||||||
<div class="stat-caption">{% trans "All Active & Drafted Positions (Global)" %}</div>
|
<div class="stat-caption">{% trans "All Active & Drafted Positions" %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# SCOPED - 2. Total Active Jobs #}
|
{# SCOPED - 2. Total Active Jobs #}
|
||||||
@ -19,7 +19,7 @@
|
|||||||
<h3><i class="fas fa-briefcase stat-icon"></i> {% trans "Active Jobs" %}</h3>
|
<h3><i class="fas fa-briefcase stat-icon"></i> {% trans "Active Jobs" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ total_active_jobs }}</div>
|
<div class="stat-value">{{ total_active_jobs }}</div>
|
||||||
<div class="stat-caption">{% trans "Currently Open Requisitions (Scoped)" %}</div>
|
<div class="stat-caption">{% trans "Currently Open Requisitions" %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# SCOPED - 3. Total Candidates #}
|
{# SCOPED - 3. Total Candidates #}
|
||||||
@ -28,7 +28,7 @@
|
|||||||
<h3><i class="fas fa-users stat-icon"></i> {% trans "Total Candidates" %}</h3>
|
<h3><i class="fas fa-users stat-icon"></i> {% trans "Total Candidates" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ total_candidates }}</div>
|
<div class="stat-value">{{ total_candidates }}</div>
|
||||||
<div class="stat-caption">{% trans "Total Profiles in Current Scope" %}</div>
|
<div class="stat-caption">{% trans "Total applications" %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# SCOPED - 4. Open Positions #}
|
{# SCOPED - 4. Open Positions #}
|
||||||
@ -37,7 +37,7 @@
|
|||||||
<h3><i class="fas fa-th-list stat-icon"></i> {% trans "Open Positions" %}</h3>
|
<h3><i class="fas fa-th-list stat-icon"></i> {% trans "Open Positions" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ total_open_positions }}</div>
|
<div class="stat-value">{{ total_open_positions }}</div>
|
||||||
<div class="stat-caption">{% trans "Total Slots to be Filled (Scoped)" %}</div>
|
<div class="stat-caption">{% trans "Total Slots to be Filled " %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# GLOBAL - 5. Total Participants #}
|
{# GLOBAL - 5. Total Participants #}
|
||||||
@ -46,11 +46,11 @@
|
|||||||
<h3><i class="fas fa-address-book stat-icon"></i> {% trans "Total Participants" %}</h3>
|
<h3><i class="fas fa-address-book stat-icon"></i> {% trans "Total Participants" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ total_participants }}</div>
|
<div class="stat-value">{{ total_participants }}</div>
|
||||||
<div class="stat-caption">{% trans "Total Recruiters/Interviewers (Global)" %}</div>
|
<div class="stat-caption">{% trans "Total Recruiters/Interviewers" %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# GLOBAL - 6. Total LinkedIn Posts #}
|
{# GLOBAL - 6. Total LinkedIn Posts #}
|
||||||
<div class="card">
|
{% comment %} <div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3><i class="fab fa-linkedin stat-icon"></i> {% trans "LinkedIn Posts" %}</h3>
|
<h3><i class="fab fa-linkedin stat-icon"></i> {% trans "LinkedIn Posts" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
@ -65,13 +65,13 @@
|
|||||||
<div class="stat-value">{{ new_candidates_7days }}</div>
|
<div class="stat-value">{{ new_candidates_7days }}</div>
|
||||||
<div class="stat-caption">{% trans "Incoming applications last week" %}</div>
|
<div class="stat-caption">{% trans "Incoming applications last week" %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endcomment %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3><i class="fas fa-cogs stat-icon"></i> {% trans "Avg. Apps per Job" %}</h3>
|
<h3><i class="fas fa-cogs stat-icon"></i> {% trans "Avg. Apps per Job" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ average_applications|floatformat:1 }}</div>
|
<div class="stat-value">{{ average_applications|floatformat:1 }}</div>
|
||||||
<div class="stat-caption">{% trans "Average Applications per Job (Scoped)" %}</div>
|
<div class="stat-caption">{% trans "Average Applications per Job" %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{# --- Efficiency & Quality Metrics --- #}
|
{# --- Efficiency & Quality Metrics --- #}
|
||||||
@ -81,7 +81,7 @@
|
|||||||
<h3><i class="fas fa-clock stat-icon"></i> {% trans "Time-to-Hire" %}</h3>
|
<h3><i class="fas fa-clock stat-icon"></i> {% trans "Time-to-Hire" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ avg_time_to_hire_days }}</div>
|
<div class="stat-value">{{ avg_time_to_hire_days }}</div>
|
||||||
<div class="stat-caption">{% trans "Avg. Days (Application to Hired)" %}</div>
|
<div class="stat-caption">{% trans "Average Days" %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@ -89,7 +89,7 @@
|
|||||||
<h3><i class="fas fa-star stat-icon"></i> {% trans "Avg. Match Score" %}</h3>
|
<h3><i class="fas fa-star stat-icon"></i> {% trans "Avg. Match Score" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ avg_match_score|floatformat:1 }}</div>
|
<div class="stat-value">{{ avg_match_score|floatformat:1 }}</div>
|
||||||
<div class="stat-caption">{% trans "Average AI Score (Current Scope)" %}</div>
|
<div class="stat-caption">{% trans "Average AI Score " %}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@ -100,13 +100,13 @@
|
|||||||
<div class="stat-caption">{% trans "Score ≥ 75% Profiles" %} ({{ high_potential_ratio|floatformat:1 }}%)</div>
|
<div class="stat-caption">{% trans "Score ≥ 75% Profiles" %} ({{ high_potential_ratio|floatformat:1 }}%)</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
{% comment %} <div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h3><i class="fas fa-calendar-alt stat-icon"></i> {% trans "Meetings This Week" %}</h3>
|
<h3><i class="fas fa-calendar-alt stat-icon"></i> {% trans "Meetings This Week" %}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-value">{{ meetings_scheduled_this_week }}</div>
|
<div class="stat-value">{{ meetings_scheduled_this_week }}</div>
|
||||||
<div class="stat-caption">{% trans "Scheduled Interviews (Current Week)" %}</div>
|
<div class="stat-caption">{% trans "Scheduled Interviews (Current Week)" %}</div>
|
||||||
</div>
|
</div> {% endcomment %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,106 +1,105 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
{% load i18n %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}{{ title }}{% endblock %}
|
||||||
{% trans "Delete Source" %} | {% trans "Recruitment System" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div class="card-header py-3">
|
<h1 class="h3 mb-0">{{ title }}</h1>
|
||||||
<h6 class="m-0 font-weight-bold text-danger">
|
<a href="{% url 'source_detail' source.slug %}" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
<i class="fas fa-arrow-left"></i> Back to Source
|
||||||
{% trans "Delete Source" %}
|
</a>
|
||||||
</h6>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="alert alert-warning">
|
|
||||||
<h5 class="alert-heading">{% trans "Confirm Deletion" %}</h5>
|
|
||||||
<p>{% trans "Are you sure you want to delete the following source? This action cannot be undone." %}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source Information -->
|
<div class="row">
|
||||||
<div class="row mb-4">
|
<div class="col-md-8">
|
||||||
<div class="col-12">
|
<div class="card">
|
||||||
<h5>{% trans "Source Information" %}</h5>
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<div class="alert alert-warning d-flex align-items-center">
|
||||||
<table class="table table-borderless">
|
<i class="fas fa-exclamation-triangle fa-2x me-3"></i>
|
||||||
<tr>
|
|
||||||
<th width="30%">{% trans "Name" %}</th>
|
|
||||||
<td>{{ source.name }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Type" %}</th>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-info">{{ source.source_type }}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Status" %}</th>
|
|
||||||
<td>
|
|
||||||
{% if source.is_active %}
|
|
||||||
<span class="badge bg-success">
|
|
||||||
<i class="fas fa-check-circle me-1"></i>
|
|
||||||
{% trans "Active" %}
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-danger">
|
|
||||||
<i class="fas fa-times-circle me-1"></i>
|
|
||||||
{% trans "Inactive" %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Created By" %}</th>
|
|
||||||
<td>{{ source.created_by }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Created At" %}</th>
|
|
||||||
<td>{{ source.created_at|date:"M d, Y H:i" }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Warning Messages -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="alert alert-danger">
|
|
||||||
<h5 class="alert-heading">
|
|
||||||
<i class="fas fa-exclamation-circle me-2"></i>
|
|
||||||
{% trans "Important Note" %}
|
|
||||||
</h5>
|
|
||||||
<ul>
|
|
||||||
<li>{% trans "All associated API keys will be permanently deleted." %}</li>
|
|
||||||
<li>{% trans "Integration logs related to this source will remain but will show 'Source deleted'." %}</li>
|
|
||||||
<li>{% trans "Any active integrations using this source will be disconnected." %}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<div>
|
<div>
|
||||||
<a href="{% url 'source_detail' source.pk %}" class="btn btn-secondary">
|
<strong>Warning:</strong> This action cannot be undone.
|
||||||
<i class="fas fa-arrow-left me-2"></i>
|
Deleting this source will also remove all associated integration logs and API credentials.
|
||||||
{% trans "Cancel" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="">
|
</div>
|
||||||
{% csrf_token %}
|
|
||||||
|
<div class="mb-4">
|
||||||
|
<h5>Source to be deleted:</h5>
|
||||||
|
<div class="card bg-light">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Name:</strong><br>
|
||||||
|
{{ source.name }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Type:</strong><br>
|
||||||
|
<span class="badge bg-info">{{ source.get_source_type_display }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if source.description %}
|
||||||
|
<hr>
|
||||||
|
<div>
|
||||||
|
<strong>Description:</strong><br>
|
||||||
|
{{ source.description|linebreaks }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<hr>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Created:</strong><br>
|
||||||
|
{{ source.created_at|date:"M d, Y H:i" }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Total API Calls:</strong><br>
|
||||||
|
{{ source.integration_logs.count }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" class="d-inline">
|
||||||
|
{% csrf_token %}
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{{ cancel_url }}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-times"></i> Cancel
|
||||||
|
</a>
|
||||||
<button type="submit" class="btn btn-danger">
|
<button type="submit" class="btn btn-danger">
|
||||||
<i class="fas fa-trash me-2"></i>
|
<i class="fas fa-trash"></i> Delete Source
|
||||||
{% trans "Delete Source" %}
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">Impact Summary</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Integration Logs</label>
|
||||||
|
<div class="h5 mb-0 text-danger">
|
||||||
|
{{ source.integration_logs.count }} will be deleted
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">API Credentials</label>
|
||||||
|
<div class="h5 mb-0 text-danger">
|
||||||
|
API Key & Secret will be permanently lost
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Active Integrations</label>
|
||||||
|
<div class="h5 mb-0 text-warning">
|
||||||
|
Any systems using this API will lose access
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -110,19 +109,3 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
|
||||||
<script>
|
|
||||||
// Add confirmation dialog
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const deleteForm = document.querySelector('form[action*="delete"]');
|
|
||||||
if (deleteForm) {
|
|
||||||
deleteForm.addEventListener('submit', function(e) {
|
|
||||||
if (!confirm('{% trans "Are you sure you want to delete this source? This action cannot be undone." %}')) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@ -1,228 +1,287 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
{% load i18n %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}{{ source.name }} - Source Details{% endblock %}
|
||||||
{{ source.name }} | {% trans "Source Details" %} | {% trans "Recruitment System" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid">
|
||||||
<!-- Page Header -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<div>
|
|
||||||
<h1 class="h3 mb-1">{{ source.name }}</h1>
|
|
||||||
<nav aria-label="breadcrumb">
|
|
||||||
<ol class="breadcrumb">
|
|
||||||
<li class="breadcrumb-item">
|
|
||||||
<a href="{% url 'source_list' %}">{% trans "Sources" %}</a>
|
|
||||||
</li>
|
|
||||||
<li class="breadcrumb-item active" aria-current="page">{{ source.name }}</li>
|
|
||||||
</ol>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
<div class="btn-group">
|
|
||||||
<a href="{% url 'source_update' source.pk %}" class="btn btn-primary">
|
|
||||||
<i class="fas fa-edit me-2"></i>
|
|
||||||
{% trans "Edit" %}
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'source_delete' source.pk %}" class="btn btn-danger">
|
|
||||||
<i class="fas fa-trash me-2"></i>
|
|
||||||
{% trans "Delete" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Source Information Card -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card shadow">
|
|
||||||
<div class="card-header py-3">
|
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
|
||||||
<i class="fas fa-info-circle me-2"></i>
|
|
||||||
{% trans "Source Information" %}
|
|
||||||
</h6>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-6">
|
|
||||||
<table class="table table-borderless">
|
|
||||||
<tr>
|
|
||||||
<th width="30%">{% trans "Name" %}</th>
|
|
||||||
<td>{{ source.name }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Type" %}</th>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-info">{{ source.source_type }}</span>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Status" %}</th>
|
|
||||||
<td>
|
|
||||||
{% if source.is_active %}
|
|
||||||
<span class="badge bg-success">
|
|
||||||
<i class="fas fa-check-circle me-1"></i>
|
|
||||||
{% trans "Active" %}
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-danger">
|
|
||||||
<i class="fas fa-times-circle me-1"></i>
|
|
||||||
{% trans "Inactive" %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-6">
|
|
||||||
<table class="table table-borderless">
|
|
||||||
<tr>
|
|
||||||
<th width="30%">{% trans "Created By" %}</th>
|
|
||||||
<td>{{ source.created_by }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Created At" %}</th>
|
|
||||||
<td>{{ source.created_at|date:"M d, Y H:i" }}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Updated At" %}</th>
|
|
||||||
<td>{{ source.updated_at|date:"M d, Y H:i" }}</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row mt-3">
|
|
||||||
<div class="col-12">
|
|
||||||
<h6>{% trans "Description" %}</h6>
|
|
||||||
<p class="text-muted">{{ source.description|default:"-" }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<!-- Network Configuration -->
|
<div class="col-12">
|
||||||
<div class="col-lg-6 mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div class="card shadow">
|
<h1 class="h3 mb-0">{{ source.name }}</h1>
|
||||||
<div class="card-header py-3">
|
<div class="btn-group">
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
<a href="{% url 'source_update' source.pk %}" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-network-wired me-2"></i>
|
<i class="fas fa-edit"></i> Edit
|
||||||
{% trans "Network Configuration" %}
|
</a>
|
||||||
</h6>
|
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
|
||||||
|
<i class="fas fa-key"></i> Generate Keys
|
||||||
|
</a>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-outline-warning"
|
||||||
|
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>
|
||||||
|
{{ source.is_active|yesno:'Deactivate,Activate' }}
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'source_delete' source.pk %}" class="btn btn-outline-danger">
|
||||||
|
<i class="fas fa-trash"></i> Delete
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<strong>{% trans "IP Address" %}:</strong>
|
<!-- Source Information -->
|
||||||
<code>{{ source.ip_address|default:"Not specified" }}</code>
|
<div class="row">
|
||||||
</div>
|
<div class="col-md-8">
|
||||||
<div class="mb-3">
|
<div class="card mb-4">
|
||||||
<strong>{% trans "Trusted IPs" %}:</strong>
|
<div class="card-header">
|
||||||
<div class="mt-2">
|
<h6 class="mb-0">Source Information</h6>
|
||||||
{% if source.trusted_ips %}
|
</div>
|
||||||
<div class="d-flex flex-wrap gap-2">
|
<div class="card-body">
|
||||||
{% for ip in source.trusted_ips|split:"," %}
|
<div class="row">
|
||||||
<span class="badge bg-secondary">{{ ip|strip }}</span>
|
<div class="col-md-6">
|
||||||
{% endfor %}
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Name</label>
|
||||||
|
<div class="fw-bold">{{ source.name }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Type</label>
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-info">{{ source.get_source_type_display }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if source.description %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Description</label>
|
||||||
|
<div>{{ source.description|linebreaks }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Contact Email</label>
|
||||||
|
<div>
|
||||||
|
{% if source.contact_email %}
|
||||||
|
<a href="mailto:{{ source.contact_email }}">{{ source.contact_email }}</a>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Not specified</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Contact Phone</label>
|
||||||
|
<div>
|
||||||
|
{% if source.contact_phone %}
|
||||||
|
{{ source.contact_phone }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Not specified</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Status</label>
|
||||||
|
<div>
|
||||||
|
{% if source.is_active %}
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Requires Authentication</label>
|
||||||
|
<div>
|
||||||
|
{% if source.requires_auth %}
|
||||||
|
<span class="badge bg-warning">Yes</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">No</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if source.webhook_url %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Webhook URL</label>
|
||||||
|
<div><code>{{ source.webhook_url }}</code></div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if source.api_timeout %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">API Timeout</label>
|
||||||
|
<div>{{ source.api_timeout }} seconds</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if source.notes %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Notes</label>
|
||||||
|
<div>{{ source.notes|linebreaks }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Created</label>
|
||||||
|
<div>{{ source.created_at|date:"M d, Y H:i" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Last Updated</label>
|
||||||
|
<div>{{ source.updated_at|date:"M d, Y H:i" }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<!-- API Credentials -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">API Credentials</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">API Key</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" value="{{ source.api_key }}" readonly>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
hx-post="{% url 'copy_to_clipboard' %}"
|
||||||
|
hx-vals='{"text": "{{ source.api_key }}"}'
|
||||||
|
title="Copy to clipboard">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">API Secret</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" class="form-control" value="{{ source.api_secret }}" readonly id="api-secret">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="toggleSecretVisibility()">
|
||||||
|
<i class="fas fa-eye" id="secret-toggle-icon"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
hx-post="{% url 'copy_to_clipboard' %}"
|
||||||
|
hx-vals='{"text": "{{ source.api_secret }}"}'
|
||||||
|
title="Copy to clipboard">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-end">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Statistics -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">Integration Statistics</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Total API Calls</label>
|
||||||
|
<div class="h5 mb-0">{{ total_logs }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Successful Calls</label>
|
||||||
|
<div class="h5 mb-0 text-success">{{ successful_logs }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Failed Calls</label>
|
||||||
|
<div class="h5 mb-0 text-danger">{{ failed_logs }}</div>
|
||||||
|
</div>
|
||||||
|
{% if total_logs > 0 %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label text-muted">Success Rate</label>
|
||||||
|
<div class="h5 mb-0">
|
||||||
|
{% widthratio successful_logs total_logs 100 %}%
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">Not specified</span>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- API Configuration -->
|
<!-- Integration Logs -->
|
||||||
<div class="col-lg-6 mb-4">
|
<div class="card">
|
||||||
<div class="card shadow">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<div class="card-header py-3">
|
<h6 class="mb-0">Recent Integration Logs</h6>
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
<small class="text-muted">Last 10 logs</small>
|
||||||
<i class="fas fa-key me-2"></i>
|
|
||||||
{% trans "API Configuration" %}
|
|
||||||
</h6>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="mb-3">
|
{% if integration_logs %}
|
||||||
<strong>{% trans "Integration Version" %}:</strong>
|
|
||||||
<span class="badge bg-primary">{{ source.integration_version|default:"Not specified" }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>{% trans "API Key" %}:</strong>
|
|
||||||
<div class="input-group mt-2">
|
|
||||||
<input type="text" class="form-control" id="apiKey" value="{{ masked_api_key }}" readonly>
|
|
||||||
<button class="btn btn-outline-secondary" type="button"
|
|
||||||
onclick="copyToClipboard('apiKey')">
|
|
||||||
<i class="fas fa-copy"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<strong>{% trans "API Secret" %}:</strong>
|
|
||||||
<div class="input-group mt-2">
|
|
||||||
<input type="text" class="form-control" id="apiSecret" value="{{ masked_api_secret }}" readonly>
|
|
||||||
<button class="btn btn-outline-secondary" type="button"
|
|
||||||
onclick="copyToClipboard('apiSecret')">
|
|
||||||
<i class="fas fa-copy"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Integration Logs -->
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card shadow">
|
|
||||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
|
||||||
<i class="fas fa-history me-2"></i>
|
|
||||||
{% trans "Recent Integration Logs" %}
|
|
||||||
</h6>
|
|
||||||
<a href="{% url 'source_list' %}" class="btn btn-sm btn-secondary">
|
|
||||||
<i class="fas fa-arrow-left me-1"></i>
|
|
||||||
{% trans "Back to List" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if recent_logs %}
|
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-bordered">
|
<table class="table table-sm">
|
||||||
<thead>
|
<thead class="table-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans "Time" %}</th>
|
<th>Timestamp</th>
|
||||||
<th>{% trans "Action" %}</th>
|
<th>Method</th>
|
||||||
<th>{% trans "Endpoint" %}</th>
|
<th>Status</th>
|
||||||
<th>{% trans "Method" %}</th>
|
<th>Response Time</th>
|
||||||
<th>{% trans "Status" %}</th>
|
<th>Details</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for log in recent_logs %}
|
{% for log in integration_logs %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ log.created_at|date:"M d, Y H:i:s" }}</td>
|
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-info">{{ log.get_action_display }}</span>
|
<small>{{ log.created_at|date:"M d, Y H:i:s" }}</small>
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<code class="text-muted">{{ log.endpoint }}</code>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<span class="badge bg-secondary">{{ log.method }}</span>
|
<span class="badge bg-secondary">{{ log.method }}</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if log.success %}
|
{% if log.status_code >= 200 and log.status_code < 300 %}
|
||||||
<span class="badge bg-success">Success</span>
|
<span class="badge bg-success">{{ log.status_code }}</span>
|
||||||
|
{% elif log.status_code >= 400 %}
|
||||||
|
<span class="badge bg-danger">{{ log.status_code }}</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-danger">Failed</span>
|
<span class="badge bg-warning">{{ log.status_code }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if log.response_time_ms %}
|
||||||
|
<small>{{ log.response_time_ms }}ms</small>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if log.request_data %}
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-info"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#logDetailModal{{ log.id }}"
|
||||||
|
title="View details">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">No data</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -230,20 +289,10 @@
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{% if recent_logs.has_previous %}
|
|
||||||
<div class="text-center mt-3">
|
|
||||||
<a href="?page={{ recent_logs.previous_page_number }}" class="btn btn-sm btn-secondary">
|
|
||||||
<i class="fas fa-chevron-left"></i> {% trans "Previous" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="text-center py-5">
|
<div class="text-center py-4">
|
||||||
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
<i class="fas fa-clipboard-list fa-2x text-muted mb-3"></i>
|
||||||
<h5 class="text-muted">{% trans "No integration logs found" %}</h5>
|
<p class="text-muted">No integration logs found</p>
|
||||||
<p class="text-muted">
|
|
||||||
{% trans "Integration logs will appear here when this source is used for external integrations." %}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@ -251,35 +300,119 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Log Detail Modals -->
|
||||||
|
{% for log in integration_logs %}
|
||||||
|
{% if log.request_data %}
|
||||||
|
<div class="modal fade" id="logDetailModal{{ log.id }}" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Integration Log Details</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Timestamp:</strong><br>
|
||||||
|
{{ log.created_at|date:"M d, Y H:i:s" }}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Method:</strong><br>
|
||||||
|
<span class="badge bg-secondary">{{ log.method }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Status Code:</strong><br>
|
||||||
|
{% if log.status_code >= 200 and log.status_code < 300 %}
|
||||||
|
<span class="badge bg-success">{{ log.status_code }}</span>
|
||||||
|
{% elif log.status_code >= 400 %}
|
||||||
|
<span class="badge bg-danger">{{ log.status_code }}</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-warning">{{ log.status_code }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<strong>Response Time:</strong><br>
|
||||||
|
{% if log.response_time_ms %}
|
||||||
|
{{ log.response_time_ms }}ms
|
||||||
|
{% else %}
|
||||||
|
-
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Request Data:</strong>
|
||||||
|
<pre class="bg-light p-2 rounded"><code>{{ log.request_data|pprint }}</code></pre>
|
||||||
|
</div>
|
||||||
|
{% if log.response_data %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Response Data:</strong>
|
||||||
|
<pre class="bg-light p-2 rounded"><code>{{ log.response_data|pprint }}</code></pre>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if log.error_message %}
|
||||||
|
<div class="mb-3">
|
||||||
|
<strong>Error Message:</strong>
|
||||||
|
<div class="alert alert-danger">{{ log.error_message }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
// Make function available globally
|
function toggleSecretVisibility() {
|
||||||
window.copyToClipboard = function(elementId) {
|
const secretInput = document.getElementById('api-secret');
|
||||||
const element = document.getElementById(elementId);
|
const toggleIcon = document.getElementById('secret-toggle-icon');
|
||||||
if (element) {
|
|
||||||
const text = element.value;
|
|
||||||
navigator.clipboard.writeText(text).then(function() {
|
|
||||||
// Show success message
|
|
||||||
const button = event.target.closest('button');
|
|
||||||
const originalContent = button.innerHTML;
|
|
||||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
|
||||||
button.classList.add('btn-success');
|
|
||||||
button.classList.remove('btn-outline-secondary');
|
|
||||||
|
|
||||||
setTimeout(function() {
|
if (secretInput.type === 'password') {
|
||||||
button.innerHTML = originalContent;
|
secretInput.type = 'text';
|
||||||
button.classList.remove('btn-success');
|
toggleIcon.classList.remove('fa-eye');
|
||||||
button.classList.add('btn-outline-secondary');
|
toggleIcon.classList.add('fa-eye-slash');
|
||||||
}, 2000);
|
|
||||||
}).catch(function(err) {
|
|
||||||
console.error('Failed to copy text: ', err);
|
|
||||||
alert('{% trans "Failed to copy to clipboard" %}');
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Element not found:', elementId);
|
secretInput.type = 'password';
|
||||||
|
toggleIcon.classList.remove('fa-eye-slash');
|
||||||
|
toggleIcon.classList.add('fa-eye');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle HTMX copy to clipboard feedback
|
||||||
|
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||||
|
if (evt.detail.successful && evt.detail.target.matches('[hx-post*="copy_to_clipboard"]')) {
|
||||||
|
const button = evt.detail.target;
|
||||||
|
const originalIcon = button.innerHTML;
|
||||||
|
button.innerHTML = '<i class="fas fa-check"></i>';
|
||||||
|
button.classList.remove('btn-outline-secondary');
|
||||||
|
button.classList.add('btn-success');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
button.innerHTML = originalIcon;
|
||||||
|
button.classList.remove('btn-success');
|
||||||
|
button.classList.add('btn-outline-secondary');
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Auto-refresh after status toggle
|
||||||
|
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||||
|
if (evt.detail.successful && evt.detail.target.matches('[hx-post*="toggle_source_status"]')) {
|
||||||
|
// Reload the page after a short delay to show updated status
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -1,293 +1,177 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
{% load i18n %}
|
{% load static %}
|
||||||
|
{% load widget_tweaks %}
|
||||||
|
|
||||||
{% block title %}
|
{% block title %}{{ title }}{% endblock %}
|
||||||
{% if title %}{{ title }} | {% endif %}{% trans "Source" %} | {% trans "Recruitment System" %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
<!-- Script to define functions globally before buttons are rendered -->
|
|
||||||
<script>
|
|
||||||
function copyToClipboard(elementId) {
|
|
||||||
const element = document.getElementById(elementId);
|
|
||||||
if (element) {
|
|
||||||
const text = element.value;
|
|
||||||
navigator.clipboard.writeText(text).then(function() {
|
|
||||||
const button = event.target.closest('button');
|
|
||||||
if (button) {
|
|
||||||
const orig = button.innerHTML;
|
|
||||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
|
||||||
button.classList.add('btn-success');
|
|
||||||
button.classList.remove('btn-outline-secondary');
|
|
||||||
setTimeout(function() {
|
|
||||||
button.innerHTML = orig;
|
|
||||||
button.classList.remove('btn-success');
|
|
||||||
button.classList.add('btn-outline-secondary');
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}).catch(function(err) {
|
|
||||||
console.error('Failed to copy text: ', err);
|
|
||||||
alert('{% trans "Failed to copy to clipboard" %}');
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('Element not found:', elementId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateRandomKey(elementId, length) {
|
|
||||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
|
||||||
let result = '';
|
|
||||||
for (let i = 0; i < length; i++) {
|
|
||||||
result += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
|
|
||||||
}
|
|
||||||
|
|
||||||
const element = document.getElementById(elementId);
|
|
||||||
if (element) {
|
|
||||||
element.value = result;
|
|
||||||
|
|
||||||
const button = event.target.closest('button');
|
|
||||||
if (button) {
|
|
||||||
const orig = button.innerHTML;
|
|
||||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
|
||||||
button.classList.add('btn-success');
|
|
||||||
button.classList.remove('btn-outline-secondary');
|
|
||||||
setTimeout(function() {
|
|
||||||
button.innerHTML = orig;
|
|
||||||
button.classList.remove('btn-success');
|
|
||||||
button.classList.add('btn-outline-secondary');
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('Element not found:', elementId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make functions globally available
|
|
||||||
window.copyToClipboard = copyToClipboard;
|
|
||||||
window.generateRandomKey = generateRandomKey;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="container-fluid py-4">
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card shadow">
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
<div class="card-header py-3">
|
<h1 class="h3 mb-0">{{ title }}</h1>
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
||||||
{% if title %}{{ title }}{% else %}{% trans "Create New Source" %}{% endif %}
|
<i class="fas fa-arrow-left"></i> Back to Sources
|
||||||
</h6>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="post" id="sourceForm">
|
<form method="post" novalidate>
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<!-- Form Messages -->
|
{% if form.non_field_errors %}
|
||||||
{% if form.errors %}
|
|
||||||
<div class="alert alert-danger">
|
<div class="alert alert-danger">
|
||||||
<h5 class="alert-heading">{% trans "Please correct the errors below:" %}</h5>
|
{% for error in form.non_field_errors %}
|
||||||
{% for field in form %}
|
{{ error }}
|
||||||
{% if field.errors %}
|
|
||||||
<p class="mb-0">{{ field.label }}: {{ field.errors|join:", " }}</p>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if messages %}
|
<div class="row">
|
||||||
{% for message in messages %}
|
<div class="col-md-6">
|
||||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
<div class="mb-3">
|
||||||
{{ message }}
|
<label for="{{ form.name.id_for_label }}" class="form-label">
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
{{ form.name.label }} <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ form.name|add_class:"form-control" }}
|
||||||
|
{% if form.name.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.name.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-text">{{ form.name.help_text }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Basic Information -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5 class="mb-3">{% trans "Basic Information" %}</h5>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
<div class="col-md-6 mb-3">
|
<div class="mb-3">
|
||||||
{{ form.name.label_tag }}
|
<label for="{{ form.source_type.id_for_label }}" class="form-label">
|
||||||
{{ form.name }}
|
{{ form.source_type.label }} <span class="text-danger">*</span>
|
||||||
{% if form.name.help_text %}
|
</label>
|
||||||
<small class="form-text text-muted">{{ form.name.help_text }}</small>
|
{{ form.source_type|add_class:"form-select" }}
|
||||||
{% endif %}
|
{% if form.source_type.errors %}
|
||||||
{% if form.name.errors %}
|
<div class="invalid-feedback d-block">
|
||||||
<div class="invalid-feedback d-block">{{ form.name.errors }}</div>
|
{% for error in form.source_type.errors %}
|
||||||
{% endif %}
|
{{ error }}
|
||||||
</div>
|
{% endfor %}
|
||||||
|
</div>
|
||||||
<div class="col-md-6 mb-3">
|
{% endif %}
|
||||||
{{ form.source_type.label_tag }}
|
<div class="form-text">{{ form.source_type.help_text }}</div>
|
||||||
{{ form.source_type }}
|
|
||||||
{% if form.source_type.help_text %}
|
|
||||||
<small class="form-text text-muted">{{ form.source_type.help_text }}</small>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.source_type.errors %}
|
|
||||||
<div class="invalid-feedback d-block">{{ form.source_type.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-12 mb-3">
|
|
||||||
{{ form.description.label_tag }}
|
|
||||||
{{ form.description }}
|
|
||||||
{% if form.description.help_text %}
|
|
||||||
<small class="form-text text-muted">{{ form.description.help_text }}</small>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.description.errors %}
|
|
||||||
<div class="invalid-feedback d-block">{{ form.description.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Network Configuration -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5 class="mb-3">{% trans "Network Configuration" %}</h5>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
{{ form.ip_address.label_tag }}
|
|
||||||
{{ form.ip_address }}
|
|
||||||
{% if form.ip_address.help_text %}
|
|
||||||
<small class="form-text text-muted">{{ form.ip_address.help_text }}</small>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.ip_address.errors %}
|
|
||||||
<div class="invalid-feedback d-block">{{ form.ip_address.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
{{ form.trusted_ips.label_tag }}
|
|
||||||
{{ form.trusted_ips }}
|
|
||||||
{% if form.trusted_ips.help_text %}
|
|
||||||
<small class="form-text text-muted">{{ form.trusted_ips.help_text }}</small>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.trusted_ips.errors %}
|
|
||||||
<div class="invalid-feedback d-block">{{ form.trusted_ips.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Settings -->
|
|
||||||
<div class="row mb-4">
|
|
||||||
<div class="col-12">
|
|
||||||
<h5 class="mb-3">{% trans "Settings" %}</h5>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
{{ form.integration_version.label_tag }}
|
|
||||||
{{ form.integration_version }}
|
|
||||||
{% if form.integration_version.help_text %}
|
|
||||||
<small class="form-text text-muted">{{ form.integration_version.help_text }}</small>
|
|
||||||
{% endif %}
|
|
||||||
{% if form.integration_version.errors %}
|
|
||||||
<div class="invalid-feedback d-block">{{ form.integration_version.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<div class="form-check form-switch">
|
|
||||||
{{ form.is_active }}
|
|
||||||
{{ form.is_active.label_tag }}
|
|
||||||
</div>
|
</div>
|
||||||
{% if form.is_active.help_text %}
|
|
||||||
<small class="form-text text-muted">{{ form.is_active.help_text }}</small>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- API Configuration -->
|
<div class="mb-3">
|
||||||
<div class="row mb-4">
|
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||||
<div class="col-12">
|
{{ form.description.label }}
|
||||||
<h5 class="mb-3">{% trans "API Configuration" %}</h5>
|
</label>
|
||||||
</div>
|
{{ form.description|add_class:"form-control" }}
|
||||||
|
{% if form.description.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.description.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-text">{{ form.description.help_text }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-12 mb-3">
|
<div class="row">
|
||||||
<div class="card">
|
<div class="col-md-6">
|
||||||
<div class="card-body">
|
<div class="mb-3">
|
||||||
<div class="row align-items-center">
|
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
|
||||||
<div class="col-md-8">
|
{{ form.ip_address.label }}
|
||||||
<h6 class="mb-0">{% trans "API Keys" %}</h6>
|
</label>
|
||||||
<small class="text-muted">{% trans "Generate secure API keys for external integrations" %}</small>
|
{{ 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">
|
||||||
|
{{ form.is_active|add_class:"form-check-input" }}
|
||||||
|
<label for="{{ form.is_active.id_for_label }}" class="form-check-label">
|
||||||
|
{{ form.is_active.label }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% if form.is_active.errors %}
|
||||||
|
<div class="invalid-feedback d-block">
|
||||||
|
{% for error in form.is_active.errors %}
|
||||||
|
{{ error }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="form-text">{{ form.is_active.help_text }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- API Credentials Section -->
|
||||||
|
{% if source %}
|
||||||
|
<div class="card bg-light mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">API Credentials</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">API Key</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="text" class="form-control" value="{{ source.api_key }}" readonly>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
hx-post="{% url 'copy_to_clipboard' %}"
|
||||||
|
hx-vals='{"text": "{{ source.api_key }}"}'
|
||||||
|
title="Copy to clipboard">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-end">
|
</div>
|
||||||
<div class="form-check form-switch">
|
<div class="col-md-6">
|
||||||
{{ form.generate_keys }}
|
<div class="mb-3">
|
||||||
{{ form.generate_keys.label_tag }}
|
<label class="form-label">API Secret</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="password" class="form-control" value="{{ source.api_secret }}" readonly id="api-secret">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="toggleSecretVisibility()">
|
||||||
|
<i class="fas fa-eye" id="secret-toggle-icon"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
hx-post="{% url 'copy_to_clipboard' %}"
|
||||||
|
hx-vals='{"text": "{{ source.api_secret }}"}'
|
||||||
|
title="Copy to clipboard">
|
||||||
|
<i class="fas fa-copy"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="text-end">
|
||||||
</div>
|
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
|
||||||
|
<i class="fas fa-key"></i> Generate New Keys
|
||||||
<!-- Generated API Key -->
|
</a>
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">{% trans "API Key" %}</label>
|
|
||||||
<div class="input-group">
|
|
||||||
{{ form.api_key_generated }}
|
|
||||||
<button class="btn btn-outline-secondary" type="button"
|
|
||||||
id="generateApiKey"
|
|
||||||
onclick="generateRandomKey('id_api_key_generated', 32)"
|
|
||||||
title="{% trans 'Generate random API key' %}">
|
|
||||||
<i class="fas fa-sync-alt"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary" type="button"
|
|
||||||
id="copyApiKey"
|
|
||||||
onclick="copyToClipboard('id_api_key_generated')"
|
|
||||||
title="{% trans 'Copy to clipboard' %}">
|
|
||||||
<i class="fas fa-copy"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% if form.api_key_generated.errors %}
|
|
||||||
<div class="invalid-feedback d-block">{{ form.api_key_generated.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Generated API Secret -->
|
|
||||||
<div class="col-md-6 mb-3">
|
|
||||||
<label class="form-label">{% trans "API Secret" %}</label>
|
|
||||||
<div class="input-group">
|
|
||||||
{{ form.api_secret_generated }}
|
|
||||||
<button class="btn btn-outline-secondary" type="button"
|
|
||||||
id="generateApiSecret"
|
|
||||||
onclick="generateRandomKey('id_api_secret_generated', 64)"
|
|
||||||
title="{% trans 'Generate random API secret' %}">
|
|
||||||
<i class="fas fa-sync-alt"></i>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-outline-secondary" type="button"
|
|
||||||
id="copyApiSecret"
|
|
||||||
onclick="copyToClipboard('id_api_secret_generated')"
|
|
||||||
title="{% trans 'Copy to clipboard' %}">
|
|
||||||
<i class="fas fa-copy"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{% if form.api_secret_generated.errors %}
|
|
||||||
<div class="invalid-feedback d-block">{{ form.api_secret_generated.errors }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Form Actions -->
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-12">
|
|
||||||
<hr>
|
|
||||||
<div class="d-flex justify-content-between">
|
|
||||||
<a href="{% url 'source_list' %}" class="btn btn-secondary">
|
|
||||||
<i class="fas fa-times me-2"></i>
|
|
||||||
{% trans "Cancel" %}
|
|
||||||
</a>
|
|
||||||
<div>
|
|
||||||
<button type="submit" class="btn btn-primary">
|
|
||||||
<i class="fas fa-save me-2"></i>
|
|
||||||
{% trans "Save Source" %}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
||||||
|
<i class="fas fa-times"></i> Cancel
|
||||||
|
</a>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="fas fa-save"></i> {{ button_text }}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
@ -297,79 +181,37 @@ window.generateRandomKey = generateRandomKey;
|
|||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block customJS %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
// Function to copy text to clipboard
|
function toggleSecretVisibility() {
|
||||||
function copyToClipboard(elementId) {
|
const secretInput = document.getElementById('api-secret');
|
||||||
const element = document.getElementById(elementId);
|
const toggleIcon = document.getElementById('secret-toggle-icon');
|
||||||
if (element) {
|
|
||||||
const text = element.value;
|
|
||||||
navigator.clipboard.writeText(text).then(function() {
|
|
||||||
// Show success message
|
|
||||||
const button = event.target.closest('button');
|
|
||||||
if (button) {
|
|
||||||
const originalContent = button.innerHTML;
|
|
||||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
|
||||||
button.classList.add('btn-success');
|
|
||||||
button.classList.remove('btn-outline-secondary');
|
|
||||||
|
|
||||||
setTimeout(function() {
|
if (secretInput.type === 'password') {
|
||||||
button.innerHTML = originalContent;
|
secretInput.type = 'text';
|
||||||
button.classList.remove('btn-success');
|
toggleIcon.classList.remove('fa-eye');
|
||||||
button.classList.add('btn-outline-secondary');
|
toggleIcon.classList.add('fa-eye-slash');
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
}).catch(function(err) {
|
|
||||||
console.error('Failed to copy text: ', err);
|
|
||||||
alert('{% trans "Failed to copy to clipboard" %}');
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Element not found:', elementId);
|
secretInput.type = 'password';
|
||||||
|
toggleIcon.classList.remove('fa-eye-slash');
|
||||||
|
toggleIcon.classList.add('fa-eye');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to generate random key
|
// Handle HTMX copy to clipboard feedback
|
||||||
function generateRandomKey(elementId, length) {
|
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~';
|
if (evt.detail.successful && evt.detail.target.matches('[hx-post*="copy_to_clipboard"]')) {
|
||||||
let result = '';
|
const button = evt.detail.target;
|
||||||
for (let i = 0; i < length; i++) {
|
const originalIcon = button.innerHTML;
|
||||||
result += alphabet.charAt(Math.floor(Math.random() * alphabet.length));
|
button.innerHTML = '<i class="fas fa-check"></i>';
|
||||||
}
|
button.classList.remove('btn-outline-secondary');
|
||||||
console.log(elementId);
|
button.classList.add('btn-success');
|
||||||
const element = document.getElementById(elementId);
|
|
||||||
if (element) {
|
|
||||||
element.value = result;
|
|
||||||
|
|
||||||
// Show success animation on the generate button
|
setTimeout(() => {
|
||||||
const button = event.target.closest('button');
|
button.innerHTML = originalIcon;
|
||||||
if (button) {
|
button.classList.remove('btn-success');
|
||||||
const originalContent = button.innerHTML;
|
button.classList.add('btn-outline-secondary');
|
||||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
}, 2000);
|
||||||
button.classList.add('btn-success');
|
|
||||||
button.classList.remove('btn-outline-secondary');
|
|
||||||
|
|
||||||
setTimeout(function() {
|
|
||||||
button.innerHTML = originalContent;
|
|
||||||
button.classList.remove('btn-success');
|
|
||||||
button.classList.add('btn-outline-secondary');
|
|
||||||
}, 1500);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.error('Element not found:', elementId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize after DOM is fully loaded
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
|
||||||
const generateKeysCheckbox = document.getElementById('id_generate_keys');
|
|
||||||
if (generateKeysCheckbox) {
|
|
||||||
// If API keys are already generated, show them
|
|
||||||
const apiKeyField = document.getElementById('id_api_key_generated');
|
|
||||||
const apiSecretField = document.getElementById('id_api_secret_generated');
|
|
||||||
|
|
||||||
if (apiKeyField && apiSecretField && (apiKeyField.value || apiSecretField.value)) {
|
|
||||||
generateKeysCheckbox.checked = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,200 +1,187 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends "base.html" %}
|
||||||
{% load i18n %}
|
{% load static %}
|
||||||
|
|
||||||
{% block title %}{% trans "Sources" %} | {% trans "Recruitment System" %}{% endblock %}
|
{% block title %}Sources{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="container-fluid py-4">
|
<div class="container-fluid">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="row">
|
||||||
<h1 class="h3 mb-0 text-gray-800">
|
<div class="col-12">
|
||||||
<i class="fas fa-database me-2"></i>
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||||
{% trans "Data Sources" %}
|
<h1 class="h3 mb-0">Sources</h1>
|
||||||
</h1>
|
<a href="{% url 'source_create' %}" class="btn btn-primary">
|
||||||
<a href="{% url 'source_create' %}" class="btn btn-primary">
|
<i class="fas fa-plus"></i> Create Source
|
||||||
<i class="fas fa-plus me-2"></i>
|
</a>
|
||||||
{% trans "Create New Source" %}
|
</div>
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Search Bar -->
|
<!-- Search and Filters -->
|
||||||
<div class="card shadow mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form method="get" class="row g-3">
|
<form method="get" class="row g-3">
|
||||||
<div class="col-md-10">
|
<div class="col-md-8">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<span class="input-group-text">
|
<span class="input-group-text">
|
||||||
<i class="fas fa-search"></i>
|
<i class="fas fa-search"></i>
|
||||||
</span>
|
</span>
|
||||||
<input type="text" name="search" class="form-control"
|
<input type="text" class="form-control" name="q"
|
||||||
placeholder="{% trans 'Search by name, type, or description...' %}"
|
placeholder="Search sources..." value="{{ search_query }}">
|
||||||
value="{{ search_query }}">
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="col-md-4">
|
||||||
<div class="col-md-2">
|
<button type="submit" class="btn btn-outline-primary">
|
||||||
<button type="submit" class="btn btn-secondary w-100">
|
<i class="fas fa-search"></i> Search
|
||||||
<i class="fas fa-filter me-2"></i>
|
</button>
|
||||||
{% trans "Filter" %}
|
{% if search_query %}
|
||||||
</button>
|
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
||||||
</div>
|
<i class="fas fa-times"></i> Clear
|
||||||
</form>
|
</a>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sources List -->
|
|
||||||
<div class="card shadow">
|
|
||||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
|
||||||
<h6 class="m-0 font-weight-bold text-primary">
|
|
||||||
{% trans "Available Sources" %}
|
|
||||||
</h6>
|
|
||||||
<span class="badge bg-secondary">
|
|
||||||
{{ page_obj.paginator.count }} {% trans "sources" %}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
{% if sources %}
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-bordered" id="dataTable" width="100%" cellspacing="0">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{% trans "Name" %}</th>
|
|
||||||
<th>{% trans "Type" %}</th>
|
|
||||||
<th>{% trans "Status" %}</th>
|
|
||||||
<th>{% trans "API Key" %}</th>
|
|
||||||
<th>{% trans "Created By" %}</th>
|
|
||||||
<th>{% trans "Created At" %}</th>
|
|
||||||
<th>{% trans "Actions" %}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for source in sources %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="me-3">
|
|
||||||
<div class="icon-circle bg-primary text-white">
|
|
||||||
<i class="fas fa-server"></i>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div class="fw-bold">{{ source.name }}</div>
|
|
||||||
<small class="text-muted">{{ source.description|truncatewords:10 }}</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<span class="badge bg-info">{{ source.source_type }}</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if source.is_active %}
|
|
||||||
<span class="badge bg-success">
|
|
||||||
<i class="fas fa-check-circle me-1"></i>
|
|
||||||
{% trans "Active" %}
|
|
||||||
</span>
|
|
||||||
{% else %}
|
|
||||||
<span class="badge bg-danger">
|
|
||||||
<i class="fas fa-times-circle me-1"></i>
|
|
||||||
{% trans "Inactive" %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if source.api_key %}
|
|
||||||
<code class="text-muted">{{ source.api_key|slice:":8" }}...</code>
|
|
||||||
{% else %}
|
|
||||||
<span class="text-muted">
|
|
||||||
<i class="fas fa-key me-1"></i>
|
|
||||||
{% trans "Not generated" %}
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ source.created_by }}</td>
|
|
||||||
<td>{{ source.created_at|date:"M d, Y" }}</td>
|
|
||||||
<td>
|
|
||||||
<div class="btn-group" role="group">
|
|
||||||
<a href="{% url 'source_detail' source.pk %}"
|
|
||||||
class="btn btn-sm btn-outline-primary"
|
|
||||||
title="{% trans 'View Details' %}">
|
|
||||||
<i class="fas fa-eye"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'source_update' source.pk %}"
|
|
||||||
class="btn btn-sm btn-outline-secondary"
|
|
||||||
title="{% trans 'Edit' %}">
|
|
||||||
<i class="fas fa-edit"></i>
|
|
||||||
</a>
|
|
||||||
<a href="{% url 'source_delete' source.pk %}"
|
|
||||||
class="btn btn-sm btn-outline-danger"
|
|
||||||
title="{% trans 'Delete' %}">
|
|
||||||
<i class="fas fa-trash"></i>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Pagination -->
|
|
||||||
{% if is_paginated %}
|
|
||||||
<nav aria-label="Page navigation">
|
|
||||||
<ul class="pagination justify-content-center">
|
|
||||||
{% if page_obj.has_previous %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page=1{% if search_query %}&search={{ search_query }}{% endif %}"
|
|
||||||
aria-label="{% trans 'First' %}">
|
|
||||||
<span aria-hidden="true">««</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}"
|
|
||||||
aria-label="{% trans 'Previous' %}">
|
|
||||||
<span aria-hidden="true">«</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% for num in page_obj.paginator.page_range %}
|
<!-- Results Summary -->
|
||||||
{% if page_obj.number == num %}
|
{% if search_query %}
|
||||||
<li class="page-item active">
|
<div class="alert alert-info">
|
||||||
<span class="page-link">{{ num }}</span>
|
Found {{ total_sources }} source{{ total_sources|pluralize }} matching "{{ search_query }}"
|
||||||
</li>
|
|
||||||
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ num }}{% if search_query %}&search={{ search_query }}{% endif %}">{{ num }}</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if page_obj.has_next %}
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&search={{ search_query }}{% endif %}"
|
|
||||||
aria-label="{% trans 'Next' %}">
|
|
||||||
<span aria-hidden="true">»</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li class="page-item">
|
|
||||||
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search_query %}&search={{ search_query }}{% endif %}"
|
|
||||||
aria-label="{% trans 'Last' %}">
|
|
||||||
<span aria-hidden="true">»»</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
<div class="text-center py-5">
|
|
||||||
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
|
||||||
<h5 class="text-muted">{% trans "No sources found" %}</h5>
|
|
||||||
<p class="text-muted mb-4">
|
|
||||||
{% trans "Get started by creating your first data source." %}
|
|
||||||
</p>
|
|
||||||
<a href="{% url 'source_create' %}" class="btn btn-primary">
|
|
||||||
<i class="fas fa-plus me-2"></i>
|
|
||||||
{% trans "Create Source" %}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Sources Table -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
{% if page_obj %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead class="table-light">
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Type</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>API Key</th>
|
||||||
|
<th>Created</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for source in page_obj %}
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<a href="{% url 'source_detail' source.pk %}" class="text-decoration-none">
|
||||||
|
<strong>{{ source.name }}</strong>
|
||||||
|
</a>
|
||||||
|
{% if source.description %}
|
||||||
|
<br><small class="text-muted">{{ source.description|truncatechars:50 }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="badge bg-info">{{ source.get_source_type_display }}</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if source.is_active %}
|
||||||
|
<span class="badge bg-success">Active</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge bg-secondary">Inactive</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<code class="small">{{ source.api_key|truncatechars:20 }}</code>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted">{{ source.created_at|date:"M d, Y" }}</small>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<a href="{% url 'source_detail' source.pk %}"
|
||||||
|
class="btn btn-sm btn-outline-primary" title="View">
|
||||||
|
<i class="fas fa-eye"></i>
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'source_update' source.pk %}"
|
||||||
|
class="btn btn-sm btn-outline-secondary" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</a>
|
||||||
|
<button type="button"
|
||||||
|
class="btn btn-sm btn-outline-warning"
|
||||||
|
hx-post="{% url 'toggle_source_status' source.pk %}"
|
||||||
|
hx-confirm="Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?"
|
||||||
|
title="{{ source.is_active|yesno:'Deactivate,Activate' }}">
|
||||||
|
<i class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i>
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'source_delete' source.pk %}"
|
||||||
|
class="btn btn-sm btn-outline-danger" title="Delete">
|
||||||
|
<i class="fas fa-trash"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
{% if page_obj.has_other_pages %}
|
||||||
|
<nav aria-label="Sources pagination">
|
||||||
|
<ul class="pagination justify-content-center">
|
||||||
|
{% if page_obj.has_previous %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page=1{% if search_query %}&q={{ search_query }}{% endif %}">
|
||||||
|
<i class="fas fa-angle-double-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}">
|
||||||
|
<i class="fas fa-angle-left"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% for num in page_obj.paginator.page_range %}
|
||||||
|
{% if page_obj.number == num %}
|
||||||
|
<li class="page-item active">
|
||||||
|
<span class="page-link">{{ num }}</span>
|
||||||
|
</li>
|
||||||
|
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ num }}{% if search_query %}&q={{ search_query }}{% endif %}">{{ num }}</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if page_obj.has_next %}
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search_query %}&q={{ search_query }}{% endif %}">
|
||||||
|
<i class="fas fa-angle-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li class="page-item">
|
||||||
|
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}{% if search_query %}&q={{ search_query }}{% endif %}">
|
||||||
|
<i class="fas fa-angle-double-right"></i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
<div class="text-center py-5">
|
||||||
|
<i class="fas fa-database fa-3x text-muted mb-3"></i>
|
||||||
|
<h5 class="text-muted">No sources found</h5>
|
||||||
|
<p class="text-muted">
|
||||||
|
{% if search_query %}
|
||||||
|
No sources match your search criteria.
|
||||||
|
{% else %}
|
||||||
|
Get started by creating your first source.
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
<a href="{% url 'source_create' %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-plus"></i> Create Source
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -202,8 +189,14 @@
|
|||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
// Auto-refresh after status toggle
|
||||||
// Add any DataTables initialization or other JavaScript here
|
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||||
|
if (evt.detail.successful) {
|
||||||
|
// Reload the page after a short delay to show updated status
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.reload();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@ -146,7 +146,7 @@
|
|||||||
<div class="row px-lg-4">
|
<div class="row px-lg-4">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h1 class="h3 fw-bold dashboard-header">
|
<h1 class="h3 fw-bold dashboard-header">
|
||||||
<i class="fas fa-cogs me-3 text-accent"></i>{% trans "Admin Settings Dashboard" %}
|
<i class="fas fa-cogs me-3 text-accent"></i>{% trans "Staff Management Dashboard" %}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -165,6 +165,7 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="col-12 table-card">
|
<div class="col-12 table-card">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover align-middle mb-0">
|
<table class="table table-striped table-hover align-middle mb-0">
|
||||||
@ -180,6 +181,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|
||||||
{% for user in staffs %}
|
{% for user in staffs %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ user.pk }}</td>
|
<td>{{ user.pk }}</td>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user