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
|
||||
*.pot
|
||||
*.sqlite3
|
||||
local_settings.py
|
||||
settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Virtual environment
|
||||
@ -95,7 +95,7 @@ coverage.xml
|
||||
# Django stuff:
|
||||
|
||||
# Local settings
|
||||
local_settings.py
|
||||
settings.py
|
||||
|
||||
# Database sqlite files:
|
||||
# The base directory for relative paths in .gitignore
|
||||
|
||||
Binary file not shown.
@ -66,7 +66,7 @@ INSTALLED_APPS = [
|
||||
SITE_ID = 1
|
||||
|
||||
|
||||
LOGIN_REDIRECT_URL = '/'
|
||||
LOGIN_REDIRECT_URL = 'dashboard'
|
||||
|
||||
|
||||
ACCOUNT_LOGOUT_REDIRECT_URL = '/'
|
||||
@ -135,9 +135,9 @@ WSGI_APPLICATION = 'NorahUniversity.wsgi.application'
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql_psycopg2',
|
||||
'NAME': 'norahuniversity',
|
||||
'USER': 'norahuniversity',
|
||||
'PASSWORD': 'norahuniversity',
|
||||
'NAME': 'haikal_db',
|
||||
'USER': 'faheed',
|
||||
'PASSWORD': 'Faheed@215',
|
||||
'HOST': '127.0.0.1',
|
||||
'PORT': '5432',
|
||||
}
|
||||
@ -183,32 +183,19 @@ ACCOUNT_LOGIN_METHODS = ['email']
|
||||
ACCOUNT_SIGNUP_FIELDS = ['email*', 'password1*', 'password2*']
|
||||
|
||||
ACCOUNT_UNIQUE_EMAIL = True
|
||||
ACCOUNT_EMAIL_VERIFICATION = 'none'
|
||||
ACCOUNT_USER_MODEL_USERNAME_FIELD = None
|
||||
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
|
||||
ACCOUNT_EMAIL_VERIFICATION = "mandatory"
|
||||
ACCOUNT_LOGIN_ON_EMAIL_CONFIRMATION = True
|
||||
|
||||
|
||||
ACCOUNT_FORMS = {'signup': 'recruitment.forms.StaffSignupForm'}
|
||||
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.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'
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
|
||||
|
||||
# Crispy Forms Configuration
|
||||
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
|
||||
CRISPY_TEMPLATE_PACK = "bootstrap5"
|
||||
CRISPY_TEMPLATE_PACK = "bootstrapconsole5"
|
||||
|
||||
# Bootstrap 5 Configuration
|
||||
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,10 +162,17 @@ def send_interview_invitation_email(candidate, job, meeting_details=None, recipi
|
||||
try:
|
||||
# Prepare recipient list
|
||||
recipients = []
|
||||
if candidate.email:
|
||||
if candidate.hiring_source == "Agency":
|
||||
try:
|
||||
recipients.append(candidate.hiring_agency.email)
|
||||
except :
|
||||
pass
|
||||
else:
|
||||
recipients.append(candidate.email)
|
||||
|
||||
if recipient_list:
|
||||
recipients.extend(recipient_list)
|
||||
|
||||
|
||||
if not recipients:
|
||||
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))
|
||||
|
||||
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
|
||||
generate_keys = forms.CharField(
|
||||
|
||||
@ -682,7 +682,30 @@ class Candidate(Base):
|
||||
@property
|
||||
def scoring_timeout(self):
|
||||
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):
|
||||
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
||||
|
||||
@ -5,7 +5,7 @@ from . import views_integration
|
||||
from . import views_source
|
||||
|
||||
urlpatterns = [
|
||||
path('', views_frontend.dashboard_view, name='dashboard'),
|
||||
path('dashboard/', views_frontend.dashboard_view, name='dashboard'),
|
||||
|
||||
# Job URLs (using JobPosting model)
|
||||
path('jobs/', views_frontend.JobListView.as_view(), name='job_list'),
|
||||
@ -62,6 +62,7 @@ urlpatterns = [
|
||||
|
||||
# Form Preview URLs
|
||||
# path('forms/', views.form_list, name='form_list'),
|
||||
|
||||
path('forms/builder/', 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'),
|
||||
@ -140,7 +141,8 @@ urlpatterns = [
|
||||
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>/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'),
|
||||
|
||||
|
||||
|
||||
@ -449,7 +449,7 @@ def schedule_interviews(schedule):
|
||||
interview_date=slot['date'],
|
||||
interview_time=slot['time']
|
||||
)
|
||||
|
||||
candidate.interview_date=interview_datetime
|
||||
# Send email to candidate
|
||||
send_interview_email(scheduled_interview)
|
||||
|
||||
|
||||
@ -42,7 +42,8 @@ from .forms import (
|
||||
AgencyJobAssignmentForm,
|
||||
LinkedPostContentForm,
|
||||
ParticipantsSelectForm,
|
||||
CandidateEmailForm
|
||||
CandidateEmailForm,
|
||||
SourceForm
|
||||
)
|
||||
from easyaudit.models import CRUDEvent, LoginEvent, RequestEvent
|
||||
from rest_framework import viewsets
|
||||
@ -80,7 +81,8 @@ from .models import (
|
||||
Profile,MeetingComment,HiringAgency,
|
||||
AgencyJobAssignment,
|
||||
AgencyAccessLink,
|
||||
Notification
|
||||
Notification,
|
||||
Source
|
||||
)
|
||||
import logging
|
||||
from datastar_py.django import (
|
||||
@ -858,13 +860,13 @@ def application_submit_form(request, template_slug):
|
||||
if is_limit_exceeded:
|
||||
messages.error(
|
||||
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)
|
||||
if job.is_expired:
|
||||
messages.error(
|
||||
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)
|
||||
|
||||
@ -1422,10 +1424,26 @@ def candidate_set_exam_date(request, slug):
|
||||
def candidate_update_status(request, slug):
|
||||
job = get_object_or_404(JobPosting, slug=slug)
|
||||
mark_as = request.POST.get('mark_as')
|
||||
|
||||
if mark_as != '----------':
|
||||
candidate_ids = request.POST.getlist("candidate_ids")
|
||||
print(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")
|
||||
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}!')
|
||||
return redirect('agency_assignment_detail', slug=assignment.slug)
|
||||
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:
|
||||
form = AgencyJobAssignmentForm()
|
||||
try:
|
||||
from django.forms import HiddenInput
|
||||
# from django.forms import HiddenInput
|
||||
form.initial['agency'] = agency
|
||||
# form.fields['agency'].widget = HiddenInput()
|
||||
except HiringAgency.DoesNotExist:
|
||||
@ -3084,6 +3103,7 @@ def agency_access_link_detail(request, slug):
|
||||
AgencyAccessLink.objects.select_related('assignment__agency', 'assignment__job'),
|
||||
slug=slug
|
||||
)
|
||||
|
||||
|
||||
context = {
|
||||
'access_link': access_link,
|
||||
@ -3800,3 +3820,170 @@ def compose_candidate_email(request, job_slug, candidate_slug):
|
||||
'job': job,
|
||||
'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:
|
||||
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_to_markdown_table([parsed])
|
||||
return render(request, 'recruitment/candidate_detail.html', {
|
||||
@ -458,19 +459,25 @@ def dashboard_view(request):
|
||||
|
||||
# B. Efficiency & Conversion Metrics (Scoped)
|
||||
hired_candidates = candidate_queryset.filter(
|
||||
Q(offer_status="Accepted") | Q(stage='HIRED'),
|
||||
join_date__isnull=False
|
||||
stage='Hired'
|
||||
)
|
||||
print(hired_candidates)
|
||||
lst=[c.time_to_hire_days for c in hired_candidates]
|
||||
print(lst)
|
||||
time_to_hire_query = hired_candidates.annotate(
|
||||
time_diff=ExpressionWrapper(
|
||||
F('join_date') - F('created_at__date'),
|
||||
F('hired_date') - F('created_at__date'),
|
||||
output_field=fields.DurationField()
|
||||
)
|
||||
).aggregate(avg_time_to_hire=Avg('time_diff'))
|
||||
|
||||
print(time_to_hire_query)
|
||||
|
||||
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
|
||||
)
|
||||
print(avg_time_to_hire_days)
|
||||
|
||||
applied_count = candidate_queryset.filter(stage='Applied').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!')
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
def generate_api_keys_view(request):
|
||||
"""API endpoint to generate API keys"""
|
||||
def generate_api_keys_view(request, pk):
|
||||
"""Generate new API keys for a specific 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':
|
||||
api_key = generate_api_key()
|
||||
api_secret = generate_api_secret()
|
||||
# Generate new API keys
|
||||
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({
|
||||
'success': True,
|
||||
'api_key': api_key,
|
||||
'api_secret': api_secret,
|
||||
'message': 'API keys generated successfully'
|
||||
'api_key': new_api_key,
|
||||
'api_secret': new_api_secret,
|
||||
'message': 'API keys regenerated successfully'
|
||||
})
|
||||
|
||||
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):
|
||||
"""HTMX endpoint to copy text to clipboard"""
|
||||
if request.method == 'POST':
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<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 rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||
<style>
|
||||
@ -166,25 +166,25 @@
|
||||
</div>
|
||||
|
||||
<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 #}
|
||||
<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>{% translate "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 "Position" %}:</strong> <span class="fw-bold">{{ job.title }}</span></p>
|
||||
<p class="mb-2"><strong>{% trans "Job ID" %}:</strong> {{ job.internal_job_id }}</p>
|
||||
<p class="mb-2"><strong>{% trans "Department" %}:</strong> {{ job.department|default:"Not specified" }}</p>
|
||||
{% 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 %}
|
||||
</div> {% endcomment %}
|
||||
|
||||
<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>
|
||||
|
||||
<div style="margin-top: 30px;">
|
||||
<a href="https://kaauh.edu.sa/career" class="btn btn-main-action btn-lg">
|
||||
<i class="fas fa-arrow-left me-2"></i> {% translate "Return to Job Listings" %}
|
||||
<a href="{% url 'kaauh_career' %}" class="btn btn-main-action btn-lg">
|
||||
<i class="fas fa-arrow-left me-2"></i> {% trans "Return to Job Listings" %}
|
||||
</a>
|
||||
{# 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 %}
|
||||
|
||||
@ -251,25 +251,25 @@
|
||||
</div>
|
||||
|
||||
{# Description Blocks (Main Content) #}
|
||||
{% if job.description %}
|
||||
{% if job.has_description_content %}
|
||||
<div class="mb-4">
|
||||
<h5>{% trans "Job Description" %}</h5>
|
||||
<div class="text-secondary">{{ job.description|safe }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if job.qualifications %}
|
||||
{% if job.has_qualifications_content %}
|
||||
<div class="mb-4">
|
||||
<h5>{% trans "Required Qualifications" %}</h5>
|
||||
<div class="text-secondary">{{ job.qualifications|safe }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if job.benefits %}
|
||||
{% if job.has_benefits_content %}
|
||||
<div class="mb-4">
|
||||
<h5>{% trans "Benefits" %}</h5>
|
||||
<div class="text-secondary">{{ job.benefits|safe}}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if job.application_instructions %}
|
||||
{% if job.has_application_instructions_content %}
|
||||
<div class="mb-4">
|
||||
<h5>{% trans "Application Instructions" %}</h5>
|
||||
<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" %}
|
||||
</a>
|
||||
{% else %}
|
||||
{% if job.form_template.is_active %}
|
||||
<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" %}
|
||||
</a>
|
||||
{% else %}
|
||||
{% if job.form_template.is_active %}
|
||||
<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" %}
|
||||
</a>
|
||||
<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>
|
||||
{% endif %}
|
||||
|
||||
@ -446,7 +449,7 @@
|
||||
<div class="row g-3 stats-grid">
|
||||
|
||||
{# 1. Job Avg. Score #}
|
||||
<div class="col-6">
|
||||
<div class="col-4">
|
||||
<div class="card text-center h-100 kpi-card">
|
||||
<div class="card-body p-2">
|
||||
<i class="fas fa-star text-primary mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||
@ -457,7 +460,7 @@
|
||||
</div>
|
||||
|
||||
{# 2. High Potential Count #}
|
||||
<div class="col-6">
|
||||
<div class="col-4">
|
||||
<div class="card text-center h-100">
|
||||
<div class="card-body p-2">
|
||||
<i class="fas fa-trophy text-success mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||
@ -468,7 +471,7 @@
|
||||
</div>
|
||||
|
||||
{# 3. Avg. Time to Interview #}
|
||||
<div class="col-6">
|
||||
{% comment %} <div class="col-6">
|
||||
<div class="card text-center h-100">
|
||||
<div class="card-body p-2">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
<!--Vacancy fill rate-->
|
||||
<div class="col-6">
|
||||
<div class="col-4">
|
||||
<div class="card text-center h-100">
|
||||
<div class="card-body p-2">
|
||||
<i class="fas fa-trophy text-secondary mb-1 d-block" style="font-size: 1.2rem;"></i>
|
||||
|
||||
@ -325,7 +325,7 @@
|
||||
</td>
|
||||
|
||||
{# 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-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>
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
{% block content %}
|
||||
<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>
|
||||
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
|
||||
<i class="fas fa-link me-2"></i>
|
||||
@ -20,7 +20,7 @@
|
||||
|
||||
<div class="row">
|
||||
<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="d-flex justify-content-between align-items-start mb-3">
|
||||
<h5 class="card-title mb-0">
|
||||
@ -77,7 +77,7 @@
|
||||
</div>
|
||||
|
||||
<div class="kaauh-card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="card-body px-3 py-3">
|
||||
<h5 class="card-title mb-3">
|
||||
<i class="fas fa-key me-2 text-warning"></i>
|
||||
{% trans "Access Credentials" %}
|
||||
@ -125,7 +125,7 @@
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<h5 class="card-title mb-3">
|
||||
<i class="fas fa-chart-line me-2 text-info"></i>
|
||||
@ -161,7 +161,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kaauh-card shadow-sm">
|
||||
<div class="kaauh-card shadow-sm px-3 py-3">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title mb-3">
|
||||
<i class="fas fa-cog me-2 text-secondary"></i>
|
||||
|
||||
@ -473,7 +473,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
|
||||
<i class="fas fa-bolt me-2"></i>
|
||||
@ -503,7 +503,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
|
||||
<!-- Agency Information -->
|
||||
<div class="card kaauh-card">
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
--kaauh-info: #17a2b8;
|
||||
--kaauh-danger: #dc3545;
|
||||
--kaauh-warning: #ffc107;
|
||||
--kaauh-gray-light: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Primary Color Overrides */
|
||||
@ -53,6 +54,17 @@
|
||||
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 {
|
||||
background-color: #f8f9fa;
|
||||
@ -71,6 +83,41 @@
|
||||
font-size: 0.875rem;
|
||||
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>
|
||||
{% endblock %}
|
||||
|
||||
@ -115,7 +162,7 @@
|
||||
value="{{ search_query }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<div class="col-md-1">
|
||||
<button type="submit" class="btn btn-main-action w-100">
|
||||
<i class="fas fa-search me-1"></i> {% trans "Search" %}
|
||||
</button>
|
||||
@ -125,78 +172,168 @@
|
||||
|
||||
<!-- Agencies List -->
|
||||
{% if page_obj %}
|
||||
<div class="row">
|
||||
{% for agency in page_obj %}
|
||||
<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>
|
||||
<div id="agency-list">
|
||||
{% include "includes/_list_view_switcher.html" with list_id="agency-list" %}
|
||||
|
||||
<!-- Contact Information -->
|
||||
{% if agency.contact_person %}
|
||||
<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 %}
|
||||
<!-- Table View -->
|
||||
<div class="table-view">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover align-middle mb-0">
|
||||
<thead>
|
||||
<tr>
|
||||
<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 %}
|
||||
<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" %}
|
||||
<!-- Card View -->
|
||||
<div class="card-view row g-4">
|
||||
{% for agency in page_obj %}
|
||||
<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>
|
||||
<div>
|
||||
<span class="stats-badge">
|
||||
{% trans "Created" %} {{ agency.created_at|date:"M d, Y" }}
|
||||
</span>
|
||||
|
||||
<!-- Contact Information -->
|
||||
{% if agency.contact_person %}
|
||||
<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>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
|
||||
@ -179,7 +179,7 @@
|
||||
.timeline-bg-offer { background-color: #28a745 !important; }
|
||||
.timeline-bg-rejected { background-color: #dc3545 !important; }
|
||||
|
||||
|
||||
|
||||
|
||||
/* ------------------------------------------- */
|
||||
/* 1. Base Spinner Styling */
|
||||
@ -272,7 +272,7 @@
|
||||
<li class="breadcrumb-item"><a href="{% url 'job_detail' candidate.job.slug %}" class="text-secondary">Job:({{candidate.job.title}})</a></li>
|
||||
<li class="breadcrumb-item active" aria-current="page" class="text-secondary" style="
|
||||
color: #F43B5E; /* Rosy Accent Color */
|
||||
font-weight: 600;
|
||||
font-weight: 600;
|
||||
">Applicant Detail</li>
|
||||
</ol>
|
||||
</nav>
|
||||
@ -313,14 +313,14 @@
|
||||
<i class="fas fa-id-card me-1"></i> {% trans "Contact & Job" %}
|
||||
</button>
|
||||
</li>
|
||||
|
||||
|
||||
<li class="nav-item" role="presentation">
|
||||
{# NEW TAB ADDED HERE #}
|
||||
<button class="nav-link" id="timeline-tab" data-bs-toggle="tab" data-bs-target="#timeline-pane" type="button" role="tab" aria-controls="timeline-pane" aria-selected="false">
|
||||
<i class="fas fa-route me-1"></i> {% trans "Journey Timeline" %}
|
||||
</button>
|
||||
</li>
|
||||
|
||||
|
||||
</ul>
|
||||
|
||||
<div class="card-body">
|
||||
@ -367,7 +367,7 @@
|
||||
</div>
|
||||
|
||||
{# TAB 2 CONTENT: RESUME #}
|
||||
|
||||
|
||||
|
||||
{# NEW TAB 3 CONTENT: CANDIDATE JOURNEY TIMELINE #}
|
||||
<div class="tab-pane fade" id="timeline-pane" role="tabpanel" aria-labelledby="timeline-tab">
|
||||
@ -417,15 +417,16 @@
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if candidate.interview_date %}
|
||||
|
||||
{% if candidate.get_interview_date %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-icon timeline-bg-applied"><i class="fas fas fa-comments"></i></div>
|
||||
<div class="timeline-content">
|
||||
<p class="timeline-stage fw-bold mb-0 ms-2">{% trans "Interview" %}</p>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
@ -439,8 +440,21 @@
|
||||
<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.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>
|
||||
</div>
|
||||
|
||||
@ -615,9 +629,9 @@
|
||||
|
||||
{# RIGHT COLUMN: ACTIONS AND CANDIDATE TIMELINE #}
|
||||
<div class="col-lg-4">
|
||||
|
||||
|
||||
{# ACTIONS CARD #}
|
||||
|
||||
|
||||
<div class="card shadow-sm mb-2 p-2">
|
||||
<h5 class="text-muted mb-3"><i class="fas fa-cog me-2"></i>{% trans "Management Actions" %}</h5>
|
||||
<div class="d-grid gap-2">
|
||||
@ -631,7 +645,7 @@
|
||||
<i class="fas fa-arrow-left"></i> {% trans "Back to List" %}
|
||||
</a>
|
||||
{% if candidate.resume %}
|
||||
|
||||
|
||||
<a href="{{ candidate.resume.url }}" target="_blank" class="btn btn-outline-primary">
|
||||
<i class="fas fa-eye me-1"></i>
|
||||
{% trans "View Actual Resume" %}
|
||||
@ -640,22 +654,31 @@
|
||||
<i class="fas fa-download me-1"></i>
|
||||
{% trans "Download Resume" %}
|
||||
</a>
|
||||
|
||||
|
||||
<a href="{% url 'candidate_resume_template' candidate.slug %}" class="btn btn-outline-info">
|
||||
<i class="fas fa-file-alt me-1"></i>
|
||||
{% trans "View Resume AI Overview" %}
|
||||
</a>
|
||||
|
||||
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
@ -668,17 +691,18 @@
|
||||
{% if candidate.scoring_timeout %}
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
|
||||
<div class="ai-loading-container">
|
||||
<i class="fas fa-robot ai-robot-icon"></i>
|
||||
<i class="fas fa-robot ai-robot-icon"></i>
|
||||
<span>Resume is been Scoring...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<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>`">
|
||||
{% trans "Retry AI Scoring" %}
|
||||
</button>
|
||||
<i class="fas fa-redo-alt me-1"></i>
|
||||
{% trans "Unable to Parse Resume , click to retry" %}
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@ -265,7 +265,7 @@
|
||||
<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: 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>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -276,11 +276,20 @@
|
||||
<div class="form-check">
|
||||
<input name="candidate_ids" value="{{ candidate.id }}" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
|
||||
</div>
|
||||
|
||||
</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 }}
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
</td>
|
||||
<td>
|
||||
<div class="candidate-details">
|
||||
@ -365,14 +374,14 @@
|
||||
{% endif %}
|
||||
</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-target="#candidateviewModal"
|
||||
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
|
||||
hx-target="#candidateviewModalBody"
|
||||
title="View Profile">
|
||||
<i class="fas fa-eye"></i>
|
||||
</button>
|
||||
</button> {% endcomment %}
|
||||
<button type="button" class="btn btn-outline-info btn-sm"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#emailModal"
|
||||
@ -457,17 +466,14 @@
|
||||
|
||||
<form method="post" action="{% url 'candidate_interview_view' job.slug %}">
|
||||
{% csrf_token %}
|
||||
<<<<<<< HEAD
|
||||
|
||||
|
||||
<div class="modal-body table-responsive">
|
||||
=======
|
||||
|
||||
<div class="modal-body">
|
||||
>>>>>>> c6fcb276135dc7e87bb0d065a93ff89091ff0207
|
||||
{{ job.internal_job_id }} {{ job.title}}
|
||||
|
||||
<hr>
|
||||
<<<<<<< HEAD
|
||||
|
||||
|
||||
|
||||
<table class="table tab table-bordered mt-3">
|
||||
@ -490,18 +496,7 @@
|
||||
|
||||
|
||||
</table>
|
||||
=======
|
||||
|
||||
<h3>👥 {% trans "Participants" %}</h3>
|
||||
{{ form.participants.errors }}
|
||||
{{ form.participants }}
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>🧑💼 {% trans "Users" %}</h3>
|
||||
{{ form.users.errors }}
|
||||
{{ form.users }}
|
||||
>>>>>>> c6fcb276135dc7e87bb0d065a93ff89091ff0207
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
|
||||
@ -295,11 +295,9 @@
|
||||
<a href="{% url 'candidate_list' %}" class="text-decoration-none d-flex align-items-center gap-2">
|
||||
<svg class="kaats-spinner" viewBox="0 0 50 50" style="width: 25px; height: 25px;">
|
||||
<circle cx="25" cy="25" r="20"></circle>
|
||||
<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>
|
||||
</svg>
|
||||
{# CRITICAL: Remove the DIV and the text-nowrap class #}
|
||||
<span class="text-teal-primary">{% trans "AI Scoring..." %}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
@ -30,54 +30,113 @@
|
||||
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 {
|
||||
font-weight: 600;
|
||||
padding: 1.25rem;
|
||||
/* Consistent, reduced padding for compact look */
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
background-color: #f8f9fa;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
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;
|
||||
align-items: center;
|
||||
color: var(--kaauh-primary-text);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
|
||||
/* Font size for MAIN card titles (e.g., "Data Scope: All Jobs") */
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* 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 {
|
||||
color: var(--kaauh-teal);
|
||||
font-size: 1.75rem;
|
||||
margin-right: 0.75rem;
|
||||
font-size: 0.8rem; /* Small icon size */
|
||||
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 {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 3rem;
|
||||
/* Force 9 columns */
|
||||
grid-template-columns: repeat(9, 1fr);
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Stat Card Specific Styling */
|
||||
.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;
|
||||
color: var(--kaauh-teal-dark);
|
||||
font-weight: 700;
|
||||
padding: 1rem 1rem 0.5rem;
|
||||
font-weight: 700;
|
||||
padding: 0.5rem 0.25rem 0.1rem; /* Very little bottom padding */
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.stat-caption {
|
||||
font-size: 0.9rem;
|
||||
/* Smallest text for the label below the value */
|
||||
font-size: 0.7rem; /* Minimized size */
|
||||
text-align: center;
|
||||
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 */
|
||||
@ -130,7 +189,7 @@
|
||||
<h2>
|
||||
<i class="fas fa-search stat-icon"></i>
|
||||
{% if current_job %}
|
||||
{% trans "Data Scope: " %} **{{ current_job.title }}**
|
||||
{% trans "Data Scope: " %}{{ current_job.title }}
|
||||
{% else %}
|
||||
{% trans "Data Scope: All Jobs" %}
|
||||
{% endif %}
|
||||
@ -153,9 +212,7 @@
|
||||
{# STATS CARDS SECTION (12 KPIs) #}
|
||||
{# -------------------------------------------------------------------------- #}
|
||||
{% include 'recruitment/partials/stats_cards.html' %}
|
||||
|
||||
|
||||
|
||||
{# Note: The content of 'recruitment/partials/stats_cards.html' uses h3 which is styled correctly here #}
|
||||
|
||||
{# -------------------------------------------------------------------------- #}
|
||||
{# CHARTS SECTION #}
|
||||
@ -197,7 +254,7 @@
|
||||
<h2>
|
||||
<i class="fas fa-funnel-dollar stat-icon"></i>
|
||||
{% if current_job %}
|
||||
{% trans "Pipeline Funnel: " %} **{{ current_job.title }}**
|
||||
{% trans "Pipeline Funnel: " %}{{ current_job.title }}
|
||||
{% else %}
|
||||
{% trans "Total Pipeline Funnel (All Jobs)" %}
|
||||
{% endif %}
|
||||
@ -223,28 +280,13 @@
|
||||
</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>
|
||||
|
||||
<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/chartjs-adapter-luxon@1.3.1"></script>
|
||||
|
||||
|
||||
<script>
|
||||
// Pass context data safely to JavaScript
|
||||
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>
|
||||
|
||||
{% endblock %}
|
||||
@ -10,7 +10,7 @@
|
||||
<h3><i class="fas fa-list stat-icon"></i> {% trans "Total Jobs" %}</h3>
|
||||
</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>
|
||||
|
||||
{# SCOPED - 2. Total Active Jobs #}
|
||||
@ -19,7 +19,7 @@
|
||||
<h3><i class="fas fa-briefcase stat-icon"></i> {% trans "Active Jobs" %}</h3>
|
||||
</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>
|
||||
|
||||
{# SCOPED - 3. Total Candidates #}
|
||||
@ -28,7 +28,7 @@
|
||||
<h3><i class="fas fa-users stat-icon"></i> {% trans "Total Candidates" %}</h3>
|
||||
</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>
|
||||
|
||||
{# SCOPED - 4. Open Positions #}
|
||||
@ -37,7 +37,7 @@
|
||||
<h3><i class="fas fa-th-list stat-icon"></i> {% trans "Open Positions" %}</h3>
|
||||
</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>
|
||||
|
||||
{# GLOBAL - 5. Total Participants #}
|
||||
@ -46,11 +46,11 @@
|
||||
<h3><i class="fas fa-address-book stat-icon"></i> {% trans "Total Participants" %}</h3>
|
||||
</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>
|
||||
|
||||
{# GLOBAL - 6. Total LinkedIn Posts #}
|
||||
<div class="card">
|
||||
{% comment %} <div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fab fa-linkedin stat-icon"></i> {% trans "LinkedIn Posts" %}</h3>
|
||||
</div>
|
||||
@ -65,13 +65,13 @@
|
||||
<div class="stat-value">{{ new_candidates_7days }}</div>
|
||||
<div class="stat-caption">{% trans "Incoming applications last week" %}</div>
|
||||
</div>
|
||||
|
||||
{% endcomment %}
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-cogs stat-icon"></i> {% trans "Avg. Apps per Job" %}</h3>
|
||||
</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>
|
||||
|
||||
{# --- Efficiency & Quality Metrics --- #}
|
||||
@ -81,7 +81,7 @@
|
||||
<h3><i class="fas fa-clock stat-icon"></i> {% trans "Time-to-Hire" %}</h3>
|
||||
</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 class="card">
|
||||
@ -89,7 +89,7 @@
|
||||
<h3><i class="fas fa-star stat-icon"></i> {% trans "Avg. Match Score" %}</h3>
|
||||
</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 class="card">
|
||||
@ -100,13 +100,13 @@
|
||||
<div class="stat-caption">{% trans "Score ≥ 75% Profiles" %} ({{ high_potential_ratio|floatformat:1 }}%)</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
{% comment %} <div class="card">
|
||||
<div class="card-header">
|
||||
<h3><i class="fas fa-calendar-alt stat-icon"></i> {% trans "Meetings This Week" %}</h3>
|
||||
</div>
|
||||
<div class="stat-value">{{ meetings_scheduled_this_week }}</div>
|
||||
<div class="stat-caption">{% trans "Scheduled Interviews (Current Week)" %}</div>
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,106 +1,105 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
{% trans "Delete Source" %} | {% trans "Recruitment System" %}
|
||||
{% endblock %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-danger">
|
||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||
{% trans "Delete Source" %}
|
||||
</h6>
|
||||
</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>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0">{{ title }}</h1>
|
||||
<a href="{% url 'source_detail' source.slug %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Source
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Source Information -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h5>{% trans "Source Information" %}</h5>
|
||||
<div class="table-responsive">
|
||||
<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>
|
||||
<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 class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-warning d-flex align-items-center">
|
||||
<i class="fas fa-exclamation-triangle fa-2x me-3"></i>
|
||||
<div>
|
||||
<a href="{% url 'source_detail' source.pk %}" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left me-2"></i>
|
||||
{% trans "Cancel" %}
|
||||
</a>
|
||||
<strong>Warning:</strong> This action cannot be undone.
|
||||
Deleting this source will also remove all associated integration logs and API credentials.
|
||||
</div>
|
||||
<form method="post" action="">
|
||||
{% csrf_token %}
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<i class="fas fa-trash me-2"></i>
|
||||
{% trans "Delete Source" %}
|
||||
<i class="fas fa-trash"></i> Delete Source
|
||||
</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>
|
||||
@ -110,19 +109,3 @@
|
||||
</div>
|
||||
</div>
|
||||
{% 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' %}
|
||||
{% load i18n %}
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}
|
||||
{{ source.name }} | {% trans "Source Details" %} | {% trans "Recruitment System" %}
|
||||
{% endblock %}
|
||||
{% block title %}{{ source.name }} - Source Details{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<!-- 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="container-fluid">
|
||||
<div class="row">
|
||||
<!-- Network Configuration -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
<i class="fas fa-network-wired me-2"></i>
|
||||
{% trans "Network Configuration" %}
|
||||
</h6>
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0">{{ source.name }}</h1>
|
||||
<div class="btn-group">
|
||||
<a href="{% url 'source_update' source.pk %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-edit"></i> Edit
|
||||
</a>
|
||||
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
|
||||
<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 class="card-body">
|
||||
<div class="mb-3">
|
||||
<strong>{% trans "IP Address" %}:</strong>
|
||||
<code>{{ source.ip_address|default:"Not specified" }}</code>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<strong>{% trans "Trusted IPs" %}:</strong>
|
||||
<div class="mt-2">
|
||||
{% if source.trusted_ips %}
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
{% for ip in source.trusted_ips|split:"," %}
|
||||
<span class="badge bg-secondary">{{ ip|strip }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<!-- Source Information -->
|
||||
<div class="row">
|
||||
<div class="col-md-8">
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">Source Information</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<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>
|
||||
{% else %}
|
||||
<span class="text-muted">Not specified</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Configuration -->
|
||||
<div class="col-lg-6 mb-4">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
<i class="fas fa-key me-2"></i>
|
||||
{% trans "API Configuration" %}
|
||||
</h6>
|
||||
<!-- Integration Logs -->
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">Recent Integration Logs</h6>
|
||||
<small class="text-muted">Last 10 logs</small>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
<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 %}
|
||||
{% if integration_logs %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<table class="table table-sm">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>{% trans "Time" %}</th>
|
||||
<th>{% trans "Action" %}</th>
|
||||
<th>{% trans "Endpoint" %}</th>
|
||||
<th>{% trans "Method" %}</th>
|
||||
<th>{% trans "Status" %}</th>
|
||||
<th>Timestamp</th>
|
||||
<th>Method</th>
|
||||
<th>Status</th>
|
||||
<th>Response Time</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for log in recent_logs %}
|
||||
{% for log in integration_logs %}
|
||||
<tr>
|
||||
<td>{{ log.created_at|date:"M d, Y H:i:s" }}</td>
|
||||
<td>
|
||||
<span class="badge bg-info">{{ log.get_action_display }}</span>
|
||||
</td>
|
||||
<td>
|
||||
<code class="text-muted">{{ log.endpoint }}</code>
|
||||
<small>{{ log.created_at|date:"M d, Y H:i:s" }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge bg-secondary">{{ log.method }}</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if log.success %}
|
||||
<span class="badge bg-success">Success</span>
|
||||
{% 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-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 %}
|
||||
</td>
|
||||
</tr>
|
||||
@ -230,20 +289,10 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</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 %}
|
||||
<div class="text-center py-5">
|
||||
<i class="fas fa-inbox fa-3x text-muted mb-3"></i>
|
||||
<h5 class="text-muted">{% trans "No integration logs found" %}</h5>
|
||||
<p class="text-muted">
|
||||
{% trans "Integration logs will appear here when this source is used for external integrations." %}
|
||||
</p>
|
||||
<div class="text-center py-4">
|
||||
<i class="fas fa-clipboard-list fa-2x text-muted mb-3"></i>
|
||||
<p class="text-muted">No integration logs found</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@ -251,35 +300,119 @@
|
||||
</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 %}
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Make function available globally
|
||||
window.copyToClipboard = function(elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
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');
|
||||
function toggleSecretVisibility() {
|
||||
const secretInput = document.getElementById('api-secret');
|
||||
const toggleIcon = document.getElementById('secret-toggle-icon');
|
||||
|
||||
setTimeout(function() {
|
||||
button.innerHTML = originalContent;
|
||||
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" %}');
|
||||
});
|
||||
if (secretInput.type === 'password') {
|
||||
secretInput.type = 'text';
|
||||
toggleIcon.classList.remove('fa-eye');
|
||||
toggleIcon.classList.add('fa-eye-slash');
|
||||
} 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>
|
||||
{% endblock %}
|
||||
|
||||
@ -1,293 +1,177 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
{% load widget_tweaks %}
|
||||
|
||||
{% block title %}
|
||||
{% if title %}{{ title }} | {% endif %}{% trans "Source" %} | {% trans "Recruitment System" %}
|
||||
{% endblock %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- 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="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card shadow">
|
||||
<div class="card-header py-3">
|
||||
<h6 class="m-0 font-weight-bold text-primary">
|
||||
{% if title %}{{ title }}{% else %}{% trans "Create New Source" %}{% endif %}
|
||||
</h6>
|
||||
</div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0">{{ title }}</h1>
|
||||
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-arrow-left"></i> Back to Sources
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<form method="post" id="sourceForm">
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Form Messages -->
|
||||
{% if form.errors %}
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
<h5 class="alert-heading">{% trans "Please correct the errors below:" %}</h5>
|
||||
{% for field in form %}
|
||||
{% if field.errors %}
|
||||
<p class="mb-0">{{ field.label }}: {{ field.errors|join:", " }}</p>
|
||||
{% endif %}
|
||||
{% for error in form.non_field_errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.name.id_for_label }}" class="form-label">
|
||||
{{ 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>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<!-- Basic Information -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h5 class="mb-3">{% trans "Basic Information" %}</h5>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
{{ form.name.label_tag }}
|
||||
{{ form.name }}
|
||||
{% if form.name.help_text %}
|
||||
<small class="form-text text-muted">{{ form.name.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if form.name.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.name.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
{{ form.source_type.label_tag }}
|
||||
{{ 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 class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.source_type.id_for_label }}" class="form-label">
|
||||
{{ form.source_type.label }} <span class="text-danger">*</span>
|
||||
</label>
|
||||
{{ form.source_type|add_class:"form-select" }}
|
||||
{% if form.source_type.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.source_type.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">{{ form.source_type.help_text }}</div>
|
||||
</div>
|
||||
{% if form.is_active.help_text %}
|
||||
<small class="form-text text-muted">{{ form.is_active.help_text }}</small>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- API Configuration -->
|
||||
<div class="row mb-4">
|
||||
<div class="col-12">
|
||||
<h5 class="mb-3">{% trans "API Configuration" %}</h5>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.description.id_for_label }}" class="form-label">
|
||||
{{ form.description.label }}
|
||||
</label>
|
||||
{{ 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="card">
|
||||
<div class="card-body">
|
||||
<div class="row align-items-center">
|
||||
<div class="col-md-8">
|
||||
<h6 class="mb-0">{% trans "API Keys" %}</h6>
|
||||
<small class="text-muted">{% trans "Generate secure API keys for external integrations" %}</small>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.ip_address.id_for_label }}" class="form-label">
|
||||
{{ form.ip_address.label }}
|
||||
</label>
|
||||
{{ form.ip_address|add_class:"form-control" }}
|
||||
{% if form.ip_address.errors %}
|
||||
<div class="invalid-feedback d-block">
|
||||
{% for error in form.ip_address.errors %}
|
||||
{{ error }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-text">{{ form.ip_address.help_text }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<div class="form-check">
|
||||
{{ 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 class="col-md-4 text-end">
|
||||
<div class="form-check form-switch">
|
||||
{{ form.generate_keys }}
|
||||
{{ form.generate_keys.label_tag }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<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>
|
||||
|
||||
<!-- Generated API Key -->
|
||||
<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 class="text-end">
|
||||
<a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
|
||||
<i class="fas fa-key"></i> Generate New Keys
|
||||
</a>
|
||||
</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>
|
||||
</form>
|
||||
</div>
|
||||
@ -297,79 +181,37 @@ window.generateRandomKey = generateRandomKey;
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
// Function to copy text to clipboard
|
||||
function copyToClipboard(elementId) {
|
||||
const element = document.getElementById(elementId);
|
||||
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');
|
||||
function toggleSecretVisibility() {
|
||||
const secretInput = document.getElementById('api-secret');
|
||||
const toggleIcon = document.getElementById('secret-toggle-icon');
|
||||
|
||||
setTimeout(function() {
|
||||
button.innerHTML = originalContent;
|
||||
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" %}');
|
||||
});
|
||||
if (secretInput.type === 'password') {
|
||||
secretInput.type = 'text';
|
||||
toggleIcon.classList.remove('fa-eye');
|
||||
toggleIcon.classList.add('fa-eye-slash');
|
||||
} 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
|
||||
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));
|
||||
}
|
||||
console.log(elementId);
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
element.value = result;
|
||||
// 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');
|
||||
|
||||
// Show success animation on the generate button
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
setTimeout(() => {
|
||||
button.innerHTML = originalIcon;
|
||||
button.classList.remove('btn-success');
|
||||
button.classList.add('btn-outline-secondary');
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -1,200 +1,187 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n %}
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}{% trans "Sources" %} | {% trans "Recruitment System" %}{% endblock %}
|
||||
{% block title %}Sources{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container-fluid py-4">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0 text-gray-800">
|
||||
<i class="fas fa-database me-2"></i>
|
||||
{% trans "Data Sources" %}
|
||||
</h1>
|
||||
<a href="{% url 'source_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus me-2"></i>
|
||||
{% trans "Create New Source" %}
|
||||
</a>
|
||||
</div>
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h1 class="h3 mb-0">Sources</h1>
|
||||
<a href="{% url 'source_create' %}" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> Create Source
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="card shadow mb-4">
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-3">
|
||||
<div class="col-md-10">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="fas fa-search"></i>
|
||||
</span>
|
||||
<input type="text" name="search" class="form-control"
|
||||
placeholder="{% trans 'Search by name, type, or description...' %}"
|
||||
value="{{ search_query }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-2">
|
||||
<button type="submit" class="btn btn-secondary w-100">
|
||||
<i class="fas fa-filter me-2"></i>
|
||||
{% trans "Filter" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</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>
|
||||
<!-- Search and Filters -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-body">
|
||||
<form method="get" class="row g-3">
|
||||
<div class="col-md-8">
|
||||
<div class="input-group">
|
||||
<span class="input-group-text">
|
||||
<i class="fas fa-search"></i>
|
||||
</span>
|
||||
<input type="text" class="form-control" name="q"
|
||||
placeholder="Search sources..." value="{{ search_query }}">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<button type="submit" class="btn btn-outline-primary">
|
||||
<i class="fas fa-search"></i> Search
|
||||
</button>
|
||||
{% if search_query %}
|
||||
<a href="{% url 'source_list' %}" class="btn btn-outline-secondary">
|
||||
<i class="fas fa-times"></i> Clear
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% 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 %}&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>
|
||||
<!-- Results Summary -->
|
||||
{% if search_query %}
|
||||
<div class="alert alert-info">
|
||||
Found {{ total_sources }} source{{ total_sources|pluralize }} matching "{{ search_query }}"
|
||||
</div>
|
||||
{% 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>
|
||||
@ -202,8 +189,14 @@
|
||||
|
||||
{% block extra_js %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
// Add any DataTables initialization or other JavaScript here
|
||||
// Auto-refresh after status toggle
|
||||
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>
|
||||
{% endblock %}
|
||||
|
||||
@ -146,7 +146,7 @@
|
||||
<div class="row px-lg-4">
|
||||
<div class="col-12">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@ -165,6 +165,7 @@
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class="col-12 table-card">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-striped table-hover align-middle mb-0">
|
||||
@ -180,6 +181,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
{% for user in staffs %}
|
||||
<tr>
|
||||
<td>{{ user.pk }}</td>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user