after updating the stages

This commit is contained in:
Faheed 2026-02-01 19:47:32 +03:00
parent dba9af100a
commit b50e48f510
24 changed files with 2838 additions and 1340 deletions

1011
PRODUCTION_SETUP.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1047,7 +1047,11 @@ class Application(Base):
def get_meetings(self):
"""Legacy compatibility - get scheduled interviews for this application"""
return self.scheduled_interviews.all()
@property
def get_latest_meeting(self):
"""Legacy compatibility - get latest scheduled interview for this application"""
return self.scheduled_interviews.order_by('-interview_date', '-interview_time').first()
@property
def has_future_meeting(self):
"""Legacy compatibility - check for future meetings"""

View File

@ -6,74 +6,90 @@ register = template.Library()
@register.simple_tag
def logo_tenhal_ats(height="40", width=None):
"""
Renders the Tenhal ATS logo SVG.
Renders Tenhal ATS full logo SVG (icon + text).
Optimized for all screen sizes.
Usage: {% logo_tenhal_ats height="40" %}
"""
# Calculate width proportionally if not provided
if width is None:
width = str(int(float(height) * 3.57)) # Aspect ratio of 500/140
width = str(int(float(height) * 4.2)) # Adjusted aspect ratio
svg = f'''
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 140" height="{height}" width="{width}" class="tenhal-logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 100" height="{height}" width="{width}" class="tenhal-logo">
<defs>
<!-- Vibrant red gradient for icon -->
<linearGradient id="redGradient-{height}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#C41E3A;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8B1228;stop-opacity:1" />
<stop offset="0%" style="stop-color:#EF4444;stop-opacity:1" />
<stop offset="100%" style="stop-color:#DC2626;stop-opacity:1" />
</linearGradient>
<filter id="shadow-{height}" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="2"/>
<feOffset dx="0" dy="2" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.15"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
<!-- Subtle shadow -->
<filter id="shadow-{height}">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.3"/>
</filter>
</defs>
<g transform="translate(40, 35)" filter="url(#shadow-{height})">
<path d="M 30 5 L 50 15 L 50 35 L 30 45 L 10 35 L 10 15 Z"
<!-- Icon: Modern hexagonal badge -->
<g transform="translate(15, 15)" filter="url(#shadow-{height})">
<!-- Outer hexagon -->
<path d="M 35 5 L 60 20 L 60 50 L 35 65 L 10 50 L 10 20 Z"
fill="url(#redGradient-{height})"
stroke="none"/>
stroke="#EF4444"
stroke-width="1.5"/>
<path d="M 30 15 L 40 20 L 40 30 L 30 35 L 20 30 L 20 20 Z"
<!-- Inner hexagon outline -->
<path d="M 35 18 L 50 26 L 50 44 L 35 52 L 20 44 L 20 26 Z"
fill="none"
stroke="#ffffff"
stroke-width="2"
opacity="0.8"/>
stroke="#FFFFFF"
stroke-width="3"
opacity="0.9"/>
<circle cx="30" cy="23" r="3" fill="#ffffff"/>
<path d="M 24 30 Q 30 27 36 30"
<!-- Candidate icon -->
<circle cx="35" cy="30" r="4" fill="#FFFFFF"/>
<path d="M 27 42 Q 35 38 43 42"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke="#FFFFFF"
stroke-width="3"
stroke-linecap="round"/>
<circle cx="30" cy="2" r="1.5" fill="#C41E3A" opacity="0.6"/>
<circle cx="54" cy="25" r="1.5" fill="#C41E3A" opacity="0.6"/>
<circle cx="6" cy="25" r="1.5" fill="#C41E3A" opacity="0.6"/>
<!-- Flow indicators (3 dots) -->
<circle cx="35" cy="0" r="2.5" fill="#FFFFFF" opacity="0.7"/>
<circle cx="65" cy="35" r="2.5" fill="#FFFFFF" opacity="0.7"/>
<circle cx="5" cy="35" r="2.5" fill="#FFFFFF" opacity="0.7"/>
</g>
<rect x="95" y="35" width="2" height="55" fill="#E0E0E0"/>
<!-- Vertical divider -->
<line x1="92" y1="25" x2="92" y2="75" stroke="#374151" stroke-width="2"/>
<text x="125" y="62"
font-family="'Helvetica Neue', Arial, sans-serif"
font-size="38"
font-weight="300"
fill="#f8f7f2"
letter-spacing="4">TENHAL</text>
<!-- Company name: TENHAL -->
<text x="110" y="52"
font-family="'Segoe UI', 'Helvetica Neue', Arial, sans-serif"
font-size="32"
font-weight="700"
fill="#FFFFFF"
letter-spacing="3">TENHAL</text>
<rect x="125" y="72" width="55" height="2.5" fill="#C41E3A"/>
<!-- Product badge: ATS -->
<g transform="translate(245, 35)">
<rect x="0" y="0" width="65" height="28" rx="14"
fill="#DC2626"
opacity="0.9"/>
<text x="32.5" y="19"
font-family="'Segoe UI', 'Helvetica Neue', Arial, sans-serif"
font-size="14"
font-weight="700"
fill="#FFFFFF"
text-anchor="middle"
letter-spacing="2">ATS</text>
</g>
<text x="125" y="92"
font-family="'Helvetica Neue', Arial, sans-serif"
font-size="13"
font-weight="400"
fill="#f8f7f2"
letter-spacing="2.5">APPLICANT TRACKING SYSTEM</text>
<!-- Tagline -->
<text x="110" y="72"
font-family="'Segoe UI', 'Helvetica Neue', Arial, sans-serif"
font-size="14"
font-weight="500"
fill="#FFFFFF"
letter-spacing="1.5">Applicant Tracking System</text>
</svg>
'''
@ -82,60 +98,119 @@ def logo_tenhal_ats(height="40", width=None):
@register.simple_tag
def logo_ats(height="40", width=None):
"""
Renders the Tenhal ATS logo SVG.
Usage: {% logo_tenhal_ats height="40" %}
Renders compact Tenhal logo with ATS badge (icon + minimal text).
Perfect for collapsed sidebars and mobile.
Usage: {% logo_ats height="40" %}
"""
# Calculate width proportionally if not provided
# Calculate width proportionally
if width is None:
width = height # Aspect ratio of 500/140
width = str(int(float(height) * 2.5))
svg = f'''
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 140" height="{height}" width="{width}" class="tenhal-logo">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 80" height="{height}" width="{width}" class="tenhal-logo-compact">
<defs>
<linearGradient id="redGradient-{height}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#C41E3A;stop-opacity:1" />
<stop offset="100%" style="stop-color:#8B1228;stop-opacity:1" />
<!-- Vibrant red gradient -->
<linearGradient id="redGradient-compact-{height}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#EF4444;stop-opacity:1" />
<stop offset="100%" style="stop-color:#DC2626;stop-opacity:1" />
</linearGradient>
<filter id="shadow-{height}" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="2"/>
<feOffset dx="0" dy="2" result="offsetblur"/>
<feComponentTransfer>
<feFuncA type="linear" slope="0.15"/>
</feComponentTransfer>
<feMerge>
<feMergeNode/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
<filter id="shadow-compact-{height}">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.3"/>
</filter>
</defs>
<g transform="translate(40, 35)" filter="url(#shadow-{height})">
<path d="M 30 5 L 50 15 L 50 35 L 30 45 L 10 35 L 10 15 Z"
fill="url(#redGradient-{height})"
stroke="none"/>
<!-- Icon -->
<g transform="translate(10, 10)" filter="url(#shadow-compact-{height})">
<path d="M 30 5 L 50 17 L 50 43 L 30 55 L 10 43 L 10 17 Z"
fill="url(#redGradient-compact-{height})"
stroke="#EF4444"
stroke-width="1.5"/>
<path d="M 30 15 L 40 20 L 40 30 L 30 35 L 20 30 L 20 20 Z"
<path d="M 30 16 L 42 23 L 42 37 L 30 44 L 18 37 L 18 23 Z"
fill="none"
stroke="#ffffff"
stroke-width="2"
opacity="0.8"/>
stroke="#FFFFFF"
stroke-width="2.5"
opacity="0.9"/>
<circle cx="30" cy="23" r="3" fill="#ffffff"/>
<path d="M 24 30 Q 30 27 36 30"
<circle cx="30" cy="26" r="3.5" fill="#FFFFFF"/>
<path d="M 24 36 Q 30 33 36 36"
fill="none"
stroke="#ffffff"
stroke-width="2"
stroke="#FFFFFF"
stroke-width="2.5"
stroke-linecap="round"/>
<circle cx="30" cy="2" r="1.5" fill="#C41E3A" opacity="0.6"/>
<circle cx="54" cy="25" r="1.5" fill="#C41E3A" opacity="0.6"/>
<circle cx="6" cy="25" r="1.5" fill="#C41E3A" opacity="0.6"/>
</g>
<!-- Text: T + ATS badge -->
<text x="75" y="48"
font-family="'Segoe UI', 'Helvetica Neue', Arial, sans-serif"
font-size="28"
font-weight="700"
fill="#FFFFFF"
letter-spacing="2">T</text>
<g transform="translate(105, 25)">
<rect x="0" y="0" width="55" height="24" rx="12"
fill="#DC2626"
opacity="0.9"/>
<text x="27.5" y="16.5"
font-family="'Segoe UI', 'Helvetica Neue', Arial, sans-serif"
font-size="12"
font-weight="700"
fill="#FFFFFF"
text-anchor="middle"
letter-spacing="1.5">ATS</text>
</g>
</svg>
'''
return mark_safe(svg)
@register.simple_tag
def logo_icon_only(height="40", width=None):
"""
Renders icon only (no text) - perfect for very small spaces.
Usage: {% logo_icon_only height="40" %}
"""
if width is None:
width = height
svg = f'''
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 70 70" height="{height}" width="{width}" class="tenhal-icon">
<defs>
<linearGradient id="redGradient-icon-{height}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#EF4444;stop-opacity:1" />
<stop offset="100%" style="stop-color:#DC2626;stop-opacity:1" />
</linearGradient>
<filter id="shadow-icon-{height}">
<feDropShadow dx="0" dy="2" stdDeviation="3" flood-opacity="0.3"/>
</filter>
</defs>
<g transform="translate(5, 5)" filter="url(#shadow-icon-{height})">
<path d="M 30 5 L 50 17 L 50 43 L 30 55 L 10 43 L 10 17 Z"
fill="url(#redGradient-icon-{height})"
stroke="#EF4444"
stroke-width="1.5"/>
<path d="M 30 16 L 42 23 L 42 37 L 30 44 L 18 37 L 18 23 Z"
fill="none"
stroke="#FFFFFF"
stroke-width="2.5"
opacity="0.9"/>
<circle cx="30" cy="26" r="3.5" fill="#FFFFFF"/>
<path d="M 24 36 Q 30 33 36 36"
fill="none"
stroke="#FFFFFF"
stroke-width="2.5"
stroke-linecap="round"/>
<circle cx="30" cy="0" r="2" fill="#FFFFFF" opacity="0.7"/>
<circle cx="55" cy="30" r="2" fill="#FFFFFF" opacity="0.7"/>
<circle cx="5" cy="30" r="2" fill="#FFFFFF" opacity="0.7"/>
</g>
</svg>
'''

View File

@ -1,56 +1,65 @@
{% load i18n %}
{% load account %}
{% extends "base.html" %}
{% load i18n account %}
{% get_current_language_bidi as LANGUAGE_BIDI %}
{% get_current_language as LANGUAGE_CODE %}
<!DOCTYPE html>
<html lang="{{LANGUAGE_CODE}}" dir="{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans "Email Addresses" %}</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'temple-red': '#9d2235',
'temple-dark': '#1a1a1a',
'temple-cream': '#f8f7f2',
}
}
}
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');
body { font-family: 'Inter', sans-serif; }
</style>
</head>
<body class="bg-temple-cream min-h-screen py-8 px-4">
<div class="max-w-2xl mx-auto">
<div class="bg-white rounded-2xl shadow-xl border border-gray-100 p-8">
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-temple-red/10 mb-4">
<i data-lucide="mail" class="w-8 h-8 text-temple-red"></i>
</div>
<h1 class="text-2xl font-bold text-gray-900 mb-2">{% trans "Email Addresses" %}</h1>
<p class="text-sm text-gray-500">
{% trans "These email addresses are linked to your account. You can set primary address, resend verification, or remove an address." %}
</p>
{% block title %}{% trans "Email Addresses" %} - {{ block.super }}{% endblock %}
{% block content %}
<div class="max-w-4xl mx-auto py-6 px-4">
<!-- Breadcrumb -->
<nav class="mb-6" aria-label="breadcrumb">
<ol class="flex items-center gap-2 text-sm flex-wrap">
<li><a href="{% url 'account_email' %}" class="text-gray-500 hover:underline transition flex items-center gap-1">
<i data-lucide="mail" class="w-4 h-4"></i> {% trans "Account" %}
</a></li>
<li class="text-gray-400">/</li>
<li class="font-semibold" style="color: #9d2235;">{% trans "Email Addresses" %}</li>
</ol>
</nav>
<!-- Header -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
<h1 class="text-2xl sm:text-3xl font-bold flex items-center gap-3">
<div class="w-12 h-12 rounded-xl flex items-center justify-center" style="background-color: rgba(157, 34, 53, 0.1);">
<i data-lucide="mail" class="w-6 h-6" style="color: #9d2235;"></i>
</div>
{% trans "Email Addresses" %}
</h1>
</div>
<!-- Info Card -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6 p-6">
<div class="rounded-lg p-4 border-l-4" style="background-color: #eff6ff; border-color: #9d2235;">
<h6 class="font-bold mb-2 flex items-center gap-2" style="color: #9d2235;">
<i data-lucide="info" class="w-4 h-4"></i>
{% trans "About Email Addresses" %}
</h6>
<p class="text-gray-600 text-sm">
{% trans "These email addresses are linked to your account. You can set primary address, resend verification, or remove an address." %}
</p>
</div>
</div>
<!-- Email Addresses Card -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6">
<div class="px-6 py-4 border-b border-gray-100" style="background-color: #f8f9fa;">
<h2 class="text-lg font-bold flex items-center gap-2">
<i data-lucide="mail" class="w-5 h-5" style="color: #9d2235;"></i>
{% trans "Linked Email Addresses" %}
</h2>
</div>
<div class="p-6">
<!-- Django Messages -->
{% if messages %}
{% for message in messages %}
<div class="mb-4 p-4 rounded-xl {% if message.tags == 'error' %}bg-red-50 text-red-700{% else %}bg-green-50 text-green-700{% endif %}" role="alert">
{{ message }}
<div class="mb-4 p-4 rounded-lg {% if message.tags == 'error' %}bg-red-50 text-red-700{% else %}bg-green-50 text-green-700{% endif %}" role="alert">
<div class="flex items-start gap-3">
<i data-lucide="{% if message.tags == 'error' %}alert-triangle{% else %}check-circle{% endif %}" class="w-5 h-5 flex-shrink-0 mt-0.5"></i>
<span>{{ message }}</span>
</div>
</div>
{% endfor %}
{% endif %}
@ -58,105 +67,170 @@
{% if emailaddresses %}
<div class="space-y-4">
{% for emailaddress in emailaddresses %}
<div class="flex flex-col sm:flex-row sm:items-center justify-between py-4 {% if not forloop.last %}border-b border-gray-200{% endif %} gap-4">
<div class="flex flex-col sm:flex-row sm:items-center justify-between p-4 rounded-lg {% if not forloop.last %}border-b border-gray-200{% endif %} gap-4 bg-white">
<div class="flex-1">
<span class="font-semibold text-temple-dark">{{ emailaddress.email }}</span>
<!-- Status Badges -->
<div class="flex-1">
<span class="font-semibold text-gray-900 block">{{ emailaddress.email }}</span>
<!-- Status Badges -->
<div class="flex flex-wrap gap-2 mt-2">
{% if emailaddress.primary %}
<span class="inline-block ml-2 px-3 py-1 text-xs font-medium bg-blue-100 text-blue-700 rounded-full">{% trans "Primary" %}</span>
<span class="inline-flex items-center gap-1 px-3 py-1 text-xs font-medium bg-blue-100 text-blue-700 rounded-full">
<i data-lucide="star" class="w-3 h-3"></i>
{% trans "Primary" %}
</span>
{% endif %}
{% if emailaddress.verified %}
<span class="inline-block ml-2 px-3 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full">{% trans "Verified" %}</span>
<span class="inline-flex items-center gap-1 px-3 py-1 text-xs font-medium bg-green-100 text-green-700 rounded-full">
<i data-lucide="check" class="w-3 h-3"></i>
{% trans "Verified" %}
</span>
{% else %}
<span class="inline-block ml-2 px-3 py-1 text-xs font-medium bg-yellow-100 text-yellow-700 rounded-full">{% trans "Unverified" %}</span>
{% endif %}
</div>
<div class="flex flex-wrap gap-2">
<!-- Make Primary -->
{% if not emailaddress.primary %}
<form method="post" action="{% url 'account_email' %}" class="inline">
{% csrf_token %}
<input type="hidden" name="email" value="{{ emailaddress.email }}" />
<button type="submit" name="action_primary"
class="px-4 py-2 text-sm font-medium text-temple-red border border-temple-red rounded-lg hover:bg-red-50 transition">
{% trans "Make Primary" %}
</button>
</form>
{% endif %}
<!-- Resend Verification -->
{% if not emailaddress.verified %}
<form method="post" action="{% url 'account_email' %}" class="inline">
{% csrf_token %}
<input type="hidden" name="email" value="{{ emailaddress.email }}" />
<button type="submit" name="action_send"
class="px-4 py-2 text-sm font-medium text-yellow-600 border border-yellow-600 rounded-lg hover:bg-yellow-50 transition">
{% trans "Re-send Verification" %}
</button>
</form>
{% endif %}
<!-- Remove -->
{% if not emailaddress.primary %}
<form method="post" action="{% url 'account_email' %}" class="inline">
{% csrf_token %}
<input type="hidden" name="email" value="{{ emailaddress.email }}" />
<button type="submit" name="action_remove"
class="px-4 py-2 text-sm font-medium text-red-600 border border-red-600 rounded-lg hover:bg-red-50 transition">
{% trans "Remove" %}
</button>
</form>
<span class="inline-flex items-center gap-1 px-3 py-1 text-xs font-medium bg-yellow-100 text-yellow-700 rounded-full">
<i data-lucide="alert-circle" class="w-3 h-3"></i>
{% trans "Unverified" %}
</span>
{% endif %}
</div>
</div>
<div class="flex flex-wrap gap-2">
<!-- Make Primary -->
{% if not emailaddress.primary %}
<form method="post" action="{% url 'account_email' %}" class="inline">
{% csrf_token %}
<input type="hidden" name="email" value="{{ emailaddress.email }}" />
<button type="submit" name="action_primary"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-white rounded-lg transition-all duration-200"
style="background-color: #9d2235;"
onmouseover="this.style.backgroundColor='#7a1a29'"
onmouseout="this.style.backgroundColor='#9d2235'">
<i data-lucide="star" class="w-4 h-4"></i>
{% trans "Make Primary" %}
</button>
</form>
{% endif %}
<!-- Resend Verification -->
{% if not emailaddress.verified %}
<form method="post" action="{% url 'account_email' %}" class="inline">
{% csrf_token %}
<input type="hidden" name="email" value="{{ emailaddress.email }}" />
<button type="submit" name="action_send"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium border-2 border-yellow-600 text-yellow-600 rounded-lg hover:bg-yellow-50 transition-all duration-200">
<i data-lucide="send" class="w-4 h-4"></i>
{% trans "Re-send Verification" %}
</button>
</form>
{% endif %}
<!-- Remove -->
{% if not emailaddress.primary %}
<form method="post" action="{% url 'account_email' %}" class="inline">
{% csrf_token %}
<input type="hidden" name="email" value="{{ emailaddress.email }}" />
<button type="submit" name="action_remove"
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium border-2 border-red-600 text-red-600 rounded-lg hover:bg-red-50 transition-all duration-200">
<i data-lucide="trash-2" class="w-4 h-4"></i>
{% trans "Remove" %}
</button>
</form>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="mb-6 p-4 bg-blue-50 text-blue-700 rounded-xl">{% trans "No email addresses found." %}</p>
<p class="p-4 bg-blue-50 text-blue-700 rounded-xl flex items-center gap-3">
<i data-lucide="inbox" class="w-5 h-5"></i>
{% trans "No email addresses found." %}
</p>
{% endif %}
</div>
</div>
<div class="my-8 border-t border-gray-200"></div>
<!-- Add Email Form -->
{% if can_add_email %}
<h2 class="text-lg font-bold text-temple-dark mb-4">{% trans "Add Email Address" %}</h2>
<form method="post" action="{% url 'account_email' %}" class="space-y-4">
<!-- Add Email Card -->
{% if can_add_email %}
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-100" style="background-color: #f8f9fa;">
<h2 class="text-lg font-bold flex items-center gap-2">
<i data-lucide="plus-circle" class="w-5 h-5" style="color: #9d2235;"></i>
{% trans "Add Email Address" %}
</h2>
</div>
<div class="p-6">
<form method="post" action="{% url 'account_email' %}" class="space-y-6" id="add-email-form">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="p-4 bg-red-50 text-red-700 rounded-xl" role="alert">
{{ form.non_field_errors }}
<div class="mb-6 p-4 rounded-lg bg-red-50 border border-red-200" role="alert">
<div class="flex items-start gap-3">
<i data-lucide="alert-triangle" class="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5"></i>
<div>
<h5 class="font-semibold text-red-800 mb-1">{% trans "Error" %}</h5>
<p class="text-red-700 mb-0">{{ form.non_field_errors }}</p>
</div>
</div>
</div>
{% endif %}
<div>
<label for="id_email" class="block text-sm font-semibold text-gray-700 mb-2">
{% trans "Email Address" %}
{% trans "Email Address" %} <span class="text-red-500">*</span>
</label>
<input type="email"
name="email"
id="id_email"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition bg-white"
placeholder="{% trans 'Enter your email' %}"
required>
</div>
<button type="submit" name="action_add"
class="w-full bg-temple-red hover:bg-[#7a1a29] text-white font-semibold py-4 rounded-xl text-sm transition shadow-md hover:shadow-lg flex items-center justify-center gap-2">
class="w-full inline-flex items-center justify-center gap-2 px-8 py-3 rounded-lg font-medium text-white transition-all duration-200"
style="background-color: #9d2235;"
onmouseover="this.style.backgroundColor='#7a1a29'"
onmouseout="this.style.backgroundColor='#9d2235'">
<i data-lucide="plus" class="w-5 h-5"></i>
{% trans "Add Email" %}
</button>
</form>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
<script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
</script>
</body>
</html>
}
// Form validation
const form = document.getElementById('add-email-form');
if (form) {
form.addEventListener('submit', function(e) {
const emailInput = document.getElementById('id_email');
const emailValue = emailInput.value.trim();
if (!emailValue) {
e.preventDefault();
alert("{% trans 'Please enter an email address.' %}");
emailInput.focus();
return false;
}
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(emailValue)) {
e.preventDefault();
alert("{% trans 'Please enter a valid email address.' %}");
emailInput.focus();
return false;
}
});
}
});
</script>
{% endblock %}

View File

@ -1,98 +1,137 @@
{% load static %}
{% extends "base.html" %}
{% load i18n %}
{% get_current_language_bidi as LANGUAGE_BIDI %}
{% get_current_language as LANGUAGE_CODE %}
<!DOCTYPE html>
<html lang="{{LANGUAGE_CODE}}" dir="{% if LANGUAGE_BIDI %}rtl{% else %}ltr{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% trans "Change Password" %} - KAAUH ATS</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
'temple-red': '#9d2235',
'temple-dark': '#1a1a1a',
'temple-cream': '#f8f7f2',
}
}
}
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap');
body { font-family: 'Inter', sans-serif; }
</style>
</head>
<body class="bg-temple-cream min-h-screen flex items-center justify-center p-6">
<div class="w-full max-w-md">
<div class="bg-white rounded-2xl shadow-xl border border-gray-100 p-8">
<div class="text-center mb-8">
<div class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-temple-red/10 mb-4">
<i data-lucide="lock" class="w-10 h-10 text-temple-red"></i>
{% block title %}{% trans "Change Password" %} - {{ block.super }}{% endblock %}
{% block content %}
<div class="max-w-lg mx-auto py-6 px-4">
<!-- Breadcrumb -->
<nav class="mb-6" aria-label="breadcrumb">
<ol class="flex items-center gap-2 text-sm flex-wrap">
<li><a href="{% url 'user_detail' request.user.pk %}" class="text-gray-500 hover:underline transition flex items-center gap-1">
<i data-lucide="user" class="w-4 h-4"></i> {% trans "Profile" %}
</a></li>
<li class="text-gray-400">/</li>
<li class="font-semibold" style="color: #9d2235;">{% trans "Change Password" %}</li>
</ol>
</nav>
<!-- Password Change Card -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-100" style="background-color: #f8f9fa;">
<h2 class="text-lg font-bold flex items-center gap-2">
<i data-lucide="lock" class="w-5 h-5" style="color: #9d2235;"></i>
{% trans "Change Password" %}
</h2>
</div>
<div class="p-6">
<div class="text-center mb-6">
<div class="inline-flex items-center justify-center w-16 h-16 rounded-full mb-4" style="background-color: rgba(157, 34, 53, 0.1);">
<i data-lucide="lock" class="w-8 h-8" style="color: #9d2235;"></i>
</div>
<h1 class="text-2xl font-bold text-gray-900">{% trans "Change Password" %}</h1>
<p class="text-sm text-gray-500 mt-2">
<p class="text-sm text-gray-600">
{% trans "Please enter your current password and a new password to secure your account." %}
</p>
</div>
<form method="post" action="{% url 'account_change_password' %}" class="space-y-6">
<form method="post" action="{% url 'account_change_password' %}" class="space-y-6" id="password-change-form">
{% csrf_token %}
{% if form.non_field_errors %}
<div class="p-4 bg-red-50 text-red-700 rounded-xl" role="alert">
{% for error in form.non_field_errors %}{{ error }}{% endfor %}
<div class="mb-6 p-4 rounded-lg bg-red-50 border border-red-200" role="alert">
<div class="flex items-start gap-3">
<i data-lucide="alert-triangle" class="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5"></i>
<div>
<h5 class="font-semibold text-red-800 mb-1">{% trans "Error" %}</h5>
{% for error in form.non_field_errors %}
<p class="text-red-700 mb-0">{{ error }}</p>
{% endfor %}
</div>
</div>
</div>
{% endif %}
{% for field in form %}
<div>
<label for="id_{{ field.name }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ field.label }}
{{ field.label }} <span class="text-red-500">*</span>
</label>
<div class="relative">
<i data-lucide="lock" class="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400"></i>
{{ field }}
</div>
{% if field.errors %}
<p class="mt-1 text-xs text-red-600">{{ field.errors.0 }}</p>
<p class="mt-1 text-xs text-red-600">{{ field.errors.0 }}</p>
{% endif %}
{% if field.help_text %}
<p class="mt-1 text-xs text-gray-500">{{ field.help_text }}</p>
{% endif %}
</div>
{% endfor %}
<button type="submit"
class="w-full bg-temple-red hover:bg-[#7a1a29] text-white font-semibold py-4 rounded-xl text-sm transition shadow-md hover:shadow-lg flex items-center justify-center gap-2">
<i data-lucide="lock" class="w-5 h-5"></i>
{% trans "Change Password" %}
</button>
<div class="flex flex-col sm:flex-row justify-between items-center gap-4 pt-4 border-t border-gray-200">
<a href="{% url 'user_detail' request.user.pk %}"
class="inline-flex items-center gap-2 px-6 py-3 rounded-lg font-medium border-2 border-gray-200 text-gray-700 hover:bg-gray-50 transition-all duration-200">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
{% trans "Cancel" %}
</a>
<button type="submit"
class="inline-flex items-center justify-center gap-2 px-8 py-3 rounded-lg font-medium text-white transition-all duration-200"
style="background-color: #9d2235;"
onmouseover="this.style.backgroundColor='#7a1a29'"
onmouseout="this.style.backgroundColor='#9d2235';">
<i data-lucide="lock" class="w-5 h-5"></i>
{% trans "Change Password" %}
</button>
</div>
</form>
<div class="mt-6 pt-6 text-center border-t border-gray-200">
<p class="text-gray-600 text-sm">
<i data-lucide="arrow-left" class="w-4 h-4 inline-block mr-1 align-text-bottom"></i>
<a href="{% url 'user_detail' request.user.pk %}" class="text-temple-red hover:text-temple-red/80 transition font-medium">{% trans "Return to Profile" %}</a>
</p>
</div>
</div>
</div>
<script>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
// Add Tailwind classes to password inputs
document.querySelectorAll('input[type="password"]').forEach(input => {
input.classList.add('w-full', 'pl-10', 'pr-4', 'py-3.5', 'border', 'border-gray-200', 'rounded-xl', 'text-sm', 'focus:ring-2', 'focus:ring-temple-red/20', 'focus:border-temple-red', 'outline-none', 'transition');
}
// Add Tailwind classes to password inputs
document.querySelectorAll('input[type="password"]').forEach(input => {
input.classList.add('w-full', 'pl-10', 'pr-4', 'py-3', 'border', 'border-gray-200', 'rounded-xl', 'text-sm', 'focus:ring-2', 'focus:ring-temple-red/20', 'focus:border-temple-red', 'outline-none', 'transition', 'bg-white');
});
// Form validation
const form = document.getElementById('password-change-form');
if (form) {
form.addEventListener('submit', function(e) {
const passwordInputs = form.querySelectorAll('input[type="password"]');
const oldPassword = passwordInputs[0].value;
const newPassword = passwordInputs[1].value;
const confirmPassword = passwordInputs[2].value;
if (!oldPassword || !newPassword || !confirmPassword) {
e.preventDefault();
alert("{% trans 'Please fill in all password fields.' %}");
return false;
}
if (newPassword !== confirmPassword) {
e.preventDefault();
alert("{% trans 'New password and confirmation do not match.' %}");
return false;
}
if (newPassword.length < 8) {
e.preventDefault();
alert("{% trans 'Password must be at least 8 characters long.' %}");
return false;
}
});
</script>
</body>
</html>
}
});
</script>
{% endblock %}

View File

@ -273,12 +273,12 @@
<div class="flex flex-col h-full">
<!-- Sidebar Header -->
<div class="p-4 sm:p-6 flex items-center gap-3 text-white sidebar-header safe-area-padding-top">
{% comment %} <div class="bg-temple-red p-2 rounded-lg shrink-0">
<i data-lucide="shield-check" class="w-5 h-5 sm:w-6 sm:h-6 text-white"></i>
<div class="sidebar-expanded-logo">
{% logo_tenhal_ats height="42" %}
</div>
<div class="sidebar-collapsed-logo hidden">
{% logo_icon_only height="40" %}
</div>
<span class="text-lg sm:text-xl font-bold tracking-tight sidebar-text">{% trans "ATS" %}</span> {% endcomment %}
{% logo_ats height="40" %}
{% logo_tenhal_ats height="40" %}
<button onclick="closeMobileSidebar()"
class="lg:hidden ml-auto text-gray-400 hover:text-white transition p-2 -mr-2"
aria-label="{% trans 'Close menu' %}">
@ -447,10 +447,7 @@
<i data-lucide="panel-left" class="w-5 h-5"></i>
</button>
<!-- Logo/Title -->
<div class="truncate">
<span class="text-temple-red font-bold text-sm sm:text-base lg:text-lg">{% trans "KAAUH ATS" %}</span>
</div>
</div>
<!-- Header Actions -->
@ -571,7 +568,7 @@
<!-- Footer -->
<footer class="mt-auto p-4 sm:p-6 text-xs text-gray-400 text-center border-t border-gray-200 bg-white safe-area-padding-bottom">
&copy; {% now "Y" %} KAAUH ATS. {% trans "All rights reserved." %}
&copy; {% now "Y" %} Tenhal. {% trans "All rights reserved." %}
</footer>
</main>
@ -693,6 +690,16 @@
mainContent.style.marginLeft = '80px';
sidebar.classList.add('sidebar-collapsed');
// Hide expanded logo, show collapsed logo
document.querySelectorAll('.sidebar-expanded-logo').forEach(el => {
el.style.display = 'none';
});
document.querySelectorAll('.sidebar-collapsed-logo').forEach(el => {
el.style.display = 'flex';
el.classList.remove('hidden');
el.classList.add('flex', 'justify-center', 'items-center');
});
// Hide all text elements
document.querySelectorAll('.sidebar-text, .nav-text, .section-header').forEach(el => {
el.style.display = 'none';
@ -729,6 +736,16 @@
mainContent.style.marginLeft = '256px';
sidebar.classList.remove('sidebar-collapsed');
// Show expanded logo, hide collapsed logo
document.querySelectorAll('.sidebar-expanded-logo').forEach(el => {
el.style.display = 'flex';
el.classList.remove('hidden');
el.classList.add('flex', 'items-center', 'gap-3');
});
document.querySelectorAll('.sidebar-collapsed-logo').forEach(el => {
el.style.display = 'none';
});
// Show all text elements
document.querySelectorAll('.sidebar-text, .nav-text, .section-header').forEach(el => {
el.style.display = '';

View File

@ -1,6 +1,6 @@
{% load i18n %}
<form id="exam-update-form" hx-post="{% url 'update_application_status' job.slug application.slug 'exam' 'Failed' %}" hx-swap='outerHTML' hx-target="#status-result-{{ application.pk }}"
hx-on::after-request="const modal = bootstrap.Modal.getInstance(document.getElementById('candidateviewModal')); if (modal) { modal.hide(); }">
<form id="exam-update-form" hx-post="{% url 'update_application_status' job.slug application.slug 'exam' 'Failed' %}" hx-swap="outerHTML" hx-target="#status-result-{{ application.pk }}"
hx-on::after-request="if (typeof closeCandidateModal === 'function') { closeCandidateModal(); }">
<div class="flex justify-center items-center gap-4 mb-4">
<label class="flex items-center gap-2 cursor-pointer">
<input type="radio" name="exam_status" id="exam_passed" value="Passed" {% if application.exam_status == 'Passed' %}checked{% endif %} class="w-4 h-4 text-temple-red border-gray-300 focus:ring-temple-red/20">

View File

@ -1,7 +1,7 @@
{% load i18n %}
<div class="flex justify-center items-center gap-3" hx-swap='outerHTML' hx-target="#interview-result-{{ application.pk }}"
hx-on::after-request="const modal = bootstrap.Modal.getInstance(document.getElementById('candidateviewModal')); if (modal) { modal.hide(); }">
<a hx-post="{% url 'update_application_status' job.slug application.slug 'interview' 'Passed' %}" class="inline-flex items-center gap-2 px-4 py-2 border-2 border-gray-300 text-gray-700 hover:border-temple-red hover:text-temple-red rounded-lg text-sm font-medium transition">
<div class="flex justify-center items-center gap-3" hx-swap="outerHTML" hx-target="#interview-result-{{ application.pk }}"
hx-on::after-request="if (typeof closeCandidateModal === 'function') { closeCandidateModal(); }">
<a hx-post="{% url 'update_application_status' job.slug application.slug 'interview' 'Passed' %}" class="inline-flex items-center gap-2 px-4 py-2 border-2 border-gray-300 text-gray-700 hover:border-green-500 hover:text-green-600 rounded-lg text-sm font-medium transition">
<i data-lucide="check" class="w-4 h-4"></i> {% trans "Passed" %}
</a>
<a hx-post="{% url 'update_application_status' job.slug application.slug 'interview' 'Failed' %}" class="inline-flex items-center gap-2 px-4 py-2 bg-red-500 hover:bg-red-600 text-white rounded-lg text-sm font-medium transition shadow-sm hover:shadow-md">
@ -11,4 +11,4 @@
<script>
lucide.createIcons();
</script>
</script>

View File

@ -1,6 +1,6 @@
{% load i18n %}
<div class="flex justify-center items-center gap-3" hx-swap='outerHTML' hx-target="#status-result-{{ application.pk }}"
hx-on::after-request="const modal = bootstrap.Modal.getInstance(document.getElementById('candidateviewModal')); if (modal) { modal.hide(); }">
<div class="flex justify-center items-center gap-3" hx-swap="outerHTML" hx-target="#status-result-{{ application.pk }}"
hx-on::after-request="if (typeof closeCandidateModal === 'function') { closeCandidateModal(); }">
<a hx-post="{% url 'update_application_status' job.slug application.slug 'offer' 'Accepted' %}" class="inline-flex items-center gap-2 px-4 py-2 border-2 border-gray-300 text-gray-700 hover:border-temple-red hover:text-temple-red rounded-lg text-sm font-medium transition">
<i data-lucide="check" class="w-4 h-4"></i> {% trans "Accepted" %}
</a>

View File

@ -16,9 +16,9 @@
</h1>
<p class="text-gray-600 mt-1">{% trans "for" %} {{ application.name }} - {{ job.title }}</p>
</div>
<a href="{% url 'interview_create_type_selection' application.slug %}"
<a href="{% url 'applications_interview_view' job.slug %}"
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium border-2 border-gray-300 text-gray-700 hover:bg-gray-50 transition-all duration-200">
<i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Back to Type Selection" %}
<i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Back to application interview list" %}
</a>
</div>
@ -60,7 +60,7 @@
<label for="{{ form.topic.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
<i data-lucide="tag" class="w-4 h-4 inline mr-1 text-temple-red"></i> {% trans "Topic" %}
</label>
{{ form.topic|add_class:"w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition"|attr:"placeholder" }}
{{ form.topic|add_class:"w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
{% if form.topic.errors %}
<div class="mt-1 text-sm text-red-600">{{ form.topic.errors }}</div>
{% endif %}
@ -96,7 +96,7 @@
<label for="{{ form.duration.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
<i data-lucide="timer" class="w-4 h-4 inline mr-1 text-temple-red"></i> {% trans "Duration (minutes)" %}
</label>
{{ form.duration|add_class:"w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition"|attr:"placeholder" }}
{{ form.duration|add_class:"w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
{% if form.duration.errors %}
<div class="mt-1 text-sm text-red-600">{{ form.duration.errors }}</div>
{% endif %}
@ -107,7 +107,7 @@
<label for="{{ form.room_number.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
<i data-lucide="door-open" class="w-4 h-4 inline mr-1 text-temple-red"></i> {% trans "Room Number" %}
</label>
{{ form.room_number|add_class:"w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition"|attr:"placeholder" }}
{{ form.room_number|add_class:"w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
{% if form.room_number.errors %}
<div class="mt-1 text-sm text-red-600">{{ form.room_number.errors }}</div>
{% endif %}
@ -119,7 +119,7 @@
<label for="{{ form.physical_address.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
<i data-lucide="map-pin" class="w-4 h-4 inline mr-1 text-temple-red"></i> {% trans "Physical Address" %}
</label>
{{ form.physical_address|add_class:"w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition"|attr:"placeholder" }}
{{ form.physical_address|add_class:"w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
{% if form.physical_address.errors %}
<div class="mt-1 text-sm text-red-600">{{ form.physical_address.errors }}</div>
{% endif %}

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% load i18n %}
{% load i18n widget_tweaks %}
{% block title %}{% trans "Create Remote Interview" %}{% endblock %}
@ -16,10 +16,11 @@
</h1>
<p class="text-gray-600 mt-1">{% trans "for" %} {{ application.name }} - {{ job.title }}</p>
</div>
<a href="{% url 'interview_create_type_selection' application.slug %}"
<a href="{% url 'applications_interview_view' job.slug %}"
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium border-2 border-gray-300 text-gray-700 hover:bg-gray-50 transition-all duration-200">
<i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Back to Type Selection" %}
<i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Back to application interview list" %}
</a>
</div>
<!-- Form Card -->
@ -76,7 +77,7 @@
{% endif %}
{{ field.label }}
</label>
{{ field|attr:"class: w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
{{ field|add_class:"w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition" }}
{% if field.errors %}
<div class="mt-1 text-sm text-red-600">{{ field.errors }}</div>
{% endif %}

View File

@ -1,10 +1,6 @@
{% extends "base.html" %}
{% load i18n %}
{% load static i18n %}
{% block title %}{% trans "Create Interview - Select Type" %}{% endblock %}
{% block content %}
<div class="space-y-6">
<div class="space-y-6" id="interview-type-selection-content">
<!-- Header -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div>
@ -16,10 +12,6 @@
</h1>
<p class="text-gray-600 mt-1">{% trans "for" %} {{ application.name }} - {{ job.title }}</p>
</div>
<a href="{% url 'job_detail' job.slug %}"
class="inline-flex items-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium border-2 border-gray-300 text-gray-700 hover:bg-gray-50 transition-all duration-200">
<i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Back to Job" %}
</a>
</div>
<!-- Selection Cards -->
@ -63,10 +55,8 @@
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
});
</script>
{% endblock %}
// Reinitialize Lucide icons after content loads
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
</script>

View File

@ -1,245 +1,265 @@
{% extends 'base.html' %}
{% load static i18n widget_tweaks %}
{% extends "base.html" %}
{% load static i18n %}
{% block title %}{{ title }} - ATS{% endblock %}
{% block customCSS %}
<style>
/* KAAT-S UI Variables */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-light: #e0f7f9; /* Added for contrast */
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-success: #28a745;
--kaauh-info: #17a2b8;
--kaauh-danger: #dc3545;
--kaauh-warning: #ffc107;
}
body {
background-color: #f8f9fa; /* Light background for better contrast */
}
.kaauh-card {
padding: 2.5rem; /* Increased padding */
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
}
/* Main Action Button (Teal Fill) */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
padding: 0.6rem 1.25rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Secondary Action Button (Teal Outline) */
.btn-outline-primary-teal {
color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
font-weight: 600;
padding: 0.6rem 1.25rem;
border-radius: 0.5rem;
}
.btn-outline-primary-teal:hover {
background-color: var(--kaauh-teal);
color: white;
}
/* Form Consistency */
.form-control:focus, .form-select:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
.form-label {
font-weight: 600;
color: var(--kaauh-primary-text);
}
/* Applying Bootstrap classes to Django fields if not done in the form definition */
.kaauh-field-control > input,
.kaauh-field-control > textarea,
.kaauh-field-control > select {
display: block;
width: 100%;
padding: 0.5rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: var(--kaauh-primary-text);
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
appearance: none;
border-radius: 0.5rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
/* Specific overrides for different types */
.kaauh-field-control > select {
padding-right: 2.5rem; /* Space for the caret */
}
</style>
{% endblock %}
{% block title %}{{ title }} - {{ block.super }}{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-tasks me-2"></i>
{% trans "Create New Assignment" %}
</h1>
<p class="text-muted mb-0">
{% trans "Assign a job to an external hiring agency" %}
</p>
</div>
<a href="{% url 'agency_assignment_list' %}" class="btn btn-outline-primary-teal">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Assignments" %}
</a>
<div class="max-w-4xl mx-auto py-6 px-4">
<!-- Breadcrumb -->
<nav class="mb-6" aria-label="breadcrumb">
<ol class="flex items-center gap-2 text-sm flex-wrap">
<li><a href="{% url 'agency_assignment_list' %}" class="text-gray-500 hover:underline transition flex items-center gap-1">
<i data-lucide="briefcase" class="w-4 h-4"></i> {% trans "Assignments" %}
</a></li>
<li class="text-gray-400">/</li>
<li class="font-semibold" style="color: #9d2235;">{{ title }}</li>
</ol>
</nav>
<!-- Header -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
<h1 class="text-2xl sm:text-3xl font-bold flex items-center gap-3">
<div class="w-12 h-12 rounded-xl flex items-center justify-center" style="background-color: rgba(157, 34, 53, 0.1);">
<i data-lucide="briefcase" class="w-6 h-6" style="color: #9d2235;"></i>
</div>
{{ title }}
</h1>
<a href="{% url 'agency_assignment_list' %}"
class="inline-flex items-center gap-2 px-6 py-3 rounded-lg font-medium border-2 border-gray-200 text-gray-700 hover:bg-gray-50 transition-all duration-200">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
<span class="hidden sm:inline">{% trans "Back to List" %}</span>
</a>
</div>
<div class="kaauh-card">
<form method="post" novalidate>
{% csrf_token %}
<div class="row g-3 mb-4">
<div class="col-md-6">
<label for="{{ form.agency.id_for_label }}" class="form-label">
{{ form.agency.label }} <span class="text-danger">*</span>
</label>
{# Wrapper Div for styling consistency (Assumes agency is a SELECT field) #}
<div class="kaauh-field-control">
{{ form.agency|attr:'class:form-select' }}
</div>
{% if form.agency.errors %}
<div class="text-danger small mt-1">
{% for error in form.agency.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
<div class="col-md-6">
<label for="{{ form.job.id_for_label }}" class="form-label">
{{ form.job.label }} <span class="text-danger">*</span>
</label>
{# Wrapper Div for styling consistency (Assumes job is a SELECT field) #}
<div class="kaauh-field-control">
{{ form.job|attr:'class:form-select' }}
</div>
{% if form.job.errors %}
<div class="text-danger small mt-1">
{% for error in form.job.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-md-6">
<label for="{{ form.max_candidates.id_for_label }}" class="form-label">
{{ form.max_candidates.label }} <span class="text-danger">*</span>
</label>
{# Wrapper Div for styling consistency (Assumes max_candidates is an INPUT field) #}
<div class="kaauh-field-control">
{{ form.max_candidates|attr:'class:form-control' }}
</div>
{% if form.max_candidates.errors %}
<div class="text-danger small mt-1">
{% for error in form.max_candidates.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">
{% trans "Maximum number of candidates the agency can submit" %}
</small>
</div>
<div class="col-md-6">
<label for="{{ form.deadline_date.id_for_label }}" class="form-label">
{{ form.deadline_date.label }} <span class="text-danger">*</span>
</label>
{# Wrapper Div for styling consistency (Assumes deadline_date is an INPUT field) #}
<div class="kaauh-field-control">
{{ form.deadline_date|attr:'class:form-control' }}
</div>
{% if form.deadline_date.errors %}
<div class="text-danger small mt-1">
{% for error in form.deadline_date.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">
{% trans "Date and time when submission period ends" %}
</small>
</div>
</div>
<div class="mb-4">
<label for="{{ form.admin_notes.id_for_label }}" class="form-label">
{{ form.admin_notes.label }}
</label>
{# Wrapper Div for styling consistency (Assumes admin_notes is a TEXTAREA field) #}
<div class="kaauh-field-control">
{{ form.admin_notes|attr:'class:form-control' }}
</div>
{% if form.admin_notes.errors %}
<div class="text-danger small mt-1">
{% for error in form.admin_notes.errors %}{{ error }}{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">
{% trans "Internal notes about this assignment (not visible to agency)" %}
</small>
</div>
<div class="d-flex justify-content-between align-items-center pt-3 border-top">
<a href="{% url 'agency_assignment_list' %}" class="btn btn-outline-primary-teal">
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
</a>
<button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i> {% trans "Create Assignment" %}
</button>
</div>
</form>
</div>
<!-- Info Card -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6 p-6">
<div class="rounded-lg p-4 border-l-4" style="background-color: #eff6ff; border-color: #9d2235;">
<h6 class="font-bold mb-2 flex items-center gap-2" style="color: #9d2235;">
<i data-lucide="info" class="w-4 h-4"></i>
{% trans "About Assignments" %}
</h6>
<p class="text-gray-600 text-sm">
{% trans "Assign a job to an external hiring agency. The agency will be able to submit candidates for this position until the deadline." %}
</p>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<!-- Form Card -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-100" style="background-color: #f8f9fa;">
<h2 class="text-lg font-bold flex items-center gap-2">
<i data-lucide="briefcase" class="w-5 h-5" style="color: #9d2235;"></i>
{% trans "Assignment Details" %}
</h2>
</div>
<div class="p-6">
{% if form.non_field_errors %}
<div class="mb-6 p-4 rounded-lg bg-red-50 border border-red-200" role="alert">
<div class="flex items-start gap-3">
<i data-lucide="alert-triangle" class="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5"></i>
<div>
<h5 class="font-semibold text-red-800 mb-1">{% trans "Error" %}</h5>
{% for error in form.non_field_errors %}
<p class="text-red-700 mb-0">{{ error }}</p>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<form method="post" novalidate id="assignment-form" class="space-y-6">
{% csrf_token %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Agency -->
<div>
<label for="{{ form.agency.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.agency.label }} <span class="text-red-500">*</span>
</label>
<select name="agency" id="{{ form.agency.id_for_label }}"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition bg-white">
<option value="">{% trans "Select Agency" %}</option>
{% for choice in form.agency.field.queryset %}
<option value="{{ choice.pk }}" {% if form.agency.value == choice.pk|stringformat:"s" %}selected{% endif %}>
{{ choice.name }}
</option>
{% endfor %}
</select>
{% if form.agency.errors %}
<div class="text-red-600 text-sm mt-1">{{ form.agency.errors.0 }}</div>
{% endif %}
{% if form.agency.help_text %}
<p class="text-xs text-gray-500 mt-1">{{ form.agency.help_text }}</p>
{% endif %}
</div>
<!-- Job -->
<div>
<label for="{{ form.job.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.job.label }} <span class="text-red-500">*</span>
</label>
<select name="job" id="{{ form.job.id_for_label }}"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition bg-white">
<option value="">{% trans "Select Job" %}</option>
{% for choice in form.job.field.queryset %}
<option value="{{ choice.pk }}" {% if form.job.value == choice.pk|stringformat:"s" %}selected{% endif %}>
{{ choice.title }}
</option>
{% endfor %}
</select>
{% if form.job.errors %}
<div class="text-red-600 text-sm mt-1">{{ form.job.errors.0 }}</div>
{% endif %}
{% if form.job.help_text %}
<p class="text-xs text-gray-500 mt-1">{{ form.job.help_text }}</p>
{% endif %}
</div>
<!-- Max Candidates -->
<div>
<label for="{{ form.max_candidates.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.max_candidates.label }} <span class="text-red-500">*</span>
</label>
<input type="number"
name="max_candidates"
id="{{ form.max_candidates.id_for_label }}"
value="{{ form.max_candidates.value|default:'' }}"
min="1"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition"
placeholder="{% trans 'Maximum candidates' %}">
{% if form.max_candidates.errors %}
<div class="text-red-600 text-sm mt-1">{{ form.max_candidates.errors.0 }}</div>
{% endif %}
{% if form.max_candidates.help_text %}
<p class="text-xs text-gray-500 mt-1">{{ form.max_candidates.help_text }}</p>
{% else %}
<p class="text-xs text-gray-500 mt-1">{% trans "Maximum number of candidates agency can submit" %}</p>
{% endif %}
</div>
<!-- Deadline Date -->
<div>
<label for="{{ form.deadline_date.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.deadline_date.label }} <span class="text-red-500">*</span>
</label>
<input type="datetime-local"
name="deadline_date"
id="{{ form.deadline_date.id_for_label }}"
value="{{ form.deadline_date.value|default:'' }}"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition">
{% if form.deadline_date.errors %}
<div class="text-red-600 text-sm mt-1">{{ form.deadline_date.errors.0 }}</div>
{% endif %}
{% if form.deadline_date.help_text %}
<p class="text-xs text-gray-500 mt-1">{{ form.deadline_date.help_text }}</p>
{% else %}
<p class="text-xs text-gray-500 mt-1">{% trans "Date and time when submission period ends" %}</p>
{% endif %}
</div>
</div>
<!-- Admin Notes (Full Width) -->
<div>
<label for="{{ form.admin_notes.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.admin_notes.label }}
</label>
<textarea
name="admin_notes"
id="{{ form.admin_notes.id_for_label }}"
rows="4"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition resize-vertical"
placeholder="{% trans 'Internal notes about this assignment (not visible to agency)' %}">{{ form.admin_notes.value|default:'' }}</textarea>
{% if form.admin_notes.errors %}
<div class="text-red-600 text-sm mt-1">{{ form.admin_notes.errors.0 }}</div>
{% endif %}
{% if form.admin_notes.help_text %}
<p class="text-xs text-gray-500 mt-1">{{ form.admin_notes.help_text }}</p>
{% endif %}
</div>
<!-- Form Actions -->
<div class="flex flex-col sm:flex-row justify-between items-center gap-4 pt-4 border-t border-gray-200">
<a href="{% url 'agency_assignment_list' %}"
class="inline-flex items-center gap-2 px-6 py-3 rounded-lg font-medium border-2 border-gray-200 text-gray-700 hover:bg-gray-50 transition-all duration-200">
<i data-lucide="x" class="w-4 h-4"></i>
{% trans "Cancel" %}
</a>
<button type="submit"
class="inline-flex items-center gap-2 px-8 py-3 rounded-lg font-medium text-white transition-all duration-200"
style="background-color: #9d2235;"
onmouseover="this.style.backgroundColor='#7a1a29'"
onmouseout="this.style.backgroundColor='#9d2235'">
<i data-lucide="save" class="w-4 h-4"></i>
{{ button_text|default:"{% trans 'Create Assignment' %}" }}
</button>
</div>
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// --- Consistency Check: Ensure Django widgets have the Bootstrap classes ---
// If your form fields are NOT already adding classes via widget attrs in the Django form,
// you MUST add the following utility filter to your project to make this template work:
// `|attr:'class:form-control'`
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Auto-populate agency field when job is selected
const jobSelect = document.getElementById('{{ form.job.id_for_label }}');
const agencySelect = document.getElementById('{{ form.agency.id_for_label }}');
// Form Validation
const form = document.getElementById('assignment-form');
if (form) {
form.addEventListener('submit', function(e) {
const agencySelect = document.getElementById('{{ form.agency.id_for_label }}');
const jobSelect = document.getElementById('{{ form.job.id_for_label }}');
const maxCandidatesInput = document.getElementById('{{ form.max_candidates.id_for_label }}');
const deadlineInput = document.getElementById('{{ form.deadline_date.id_for_label }}');
// Agency validation
if (agencySelect && !agencySelect.value) {
e.preventDefault();
alert("{% trans 'Please select an agency.' %}");
agencySelect.focus();
return false;
}
if (jobSelect && agencySelect) {
jobSelect.addEventListener('change', function() {
// You could add logic here to filter agencies based on job requirements
// For now, just log the selection
console.log('Job selected:', this.value);
// Job validation
if (jobSelect && !jobSelect.value) {
e.preventDefault();
alert("{% trans 'Please select a job.' %}");
jobSelect.focus();
return false;
}
// Max candidates validation
if (maxCandidatesInput && (!maxCandidatesInput.value || parseInt(maxCandidatesInput.value) < 1)) {
e.preventDefault();
alert("{% trans 'Please enter a valid number of candidates (minimum 1).' %}");
maxCandidatesInput.focus();
return false;
}
// Deadline validation
if (deadlineInput && !deadlineInput.value) {
e.preventDefault();
alert("{% trans 'Please select a deadline date and time.' %}");
deadlineInput.focus();
return false;
}
// Check deadline is in future
if (deadlineInput && deadlineInput.value) {
const deadline = new Date(deadlineInput.value);
const now = new Date();
if (deadline <= now) {
e.preventDefault();
alert("{% trans 'Deadline must be in the future.' %}");
deadlineInput.focus();
return false;
}
}
});
}
@ -252,6 +272,18 @@ document.addEventListener('DOMContentLoaded', function() {
.slice(0, 16);
deadlineInput.min = localDateTime;
}
// Auto-populate or log job selection
const jobSelect = document.getElementById('{{ form.job.id_for_label }}');
const agencySelect = document.getElementById('{{ form.agency.id_for_label }}');
if (jobSelect && agencySelect) {
jobSelect.addEventListener('change', function() {
// You could add logic here to filter agencies based on job requirements
// For now, just log the selection
console.log('Job selected:', this.value);
});
}
});
</script>
{% endblock %}

View File

@ -4,51 +4,56 @@
{% block title %}{{ title }} - {{ block.super }}{% endblock %}
{% block content %}
<div class="space-y-6">
<div class="max-w-4xl mx-auto py-6 px-4">
<!-- Breadcrumb -->
<nav class="mb-6" aria-label="breadcrumb">
<ol class="flex items-center gap-2 text-sm flex-wrap">
<li><a href="{% url 'agency_list' %}" class="text-gray-500 hover:underline transition flex items-center gap-1">
<i data-lucide="building-2" class="w-4 h-4"></i> {% trans "Agencies" %}
</a></li>
<li class="text-gray-400">/</li>
<li class="font-semibold" style="color: #9d2235;">{{ title }}</li>
</ol>
</nav>
<!-- Header Card with Gradient Background -->
<div class="bg-gradient-to-r from-temple-red to-[#7a1a29] rounded-2xl shadow-lg p-6 md:p-8 text-white">
<div class="flex flex-col md:flex-row md:justify-between md:items-start gap-4">
<div class="flex-1">
<h1 class="text-2xl md:text-3xl font-bold mb-2 flex items-center gap-3">
<i data-lucide="building" class="w-8 h-8"></i>
{{ title }}
</h1>
<p class="text-white/80 text-sm md:text-base">
{% if agency %}
{% trans "Update agency information" %}
{% else %}
{% trans "Enter details to create a new agency." %}
{% endif %}
</p>
<!-- Header -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
<h1 class="text-2xl sm:text-3xl font-bold flex items-center gap-3">
<div class="w-12 h-12 rounded-xl flex items-center justify-center" style="background-color: rgba(157, 34, 53, 0.1);">
<i data-lucide="building" class="w-6 h-6" style="color: #9d2235;"></i>
</div>
<div class="flex flex-wrap gap-2">
{% if agency %}
<a href="{% url 'agency_detail' agency.slug %}"
class="inline-flex items-center gap-2 px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-white font-medium text-sm transition backdrop-blur-sm touch-target">
<i data-lucide="eye" class="w-4 h-4"></i>
<span class="hidden sm:inline">{% trans "View Details" %}</span>
</a>
<a href="{% url 'agency_delete' agency.slug %}"
class="inline-flex items-center gap-2 px-4 py-2 bg-red-600 hover:bg-red-700 rounded-lg text-white font-medium text-sm transition touch-target">
<i data-lucide="trash-2" class="w-4 h-4"></i>
<span class="hidden sm:inline">{% trans "Delete" %}</span>
</a>
{% endif %}
<a href="{% url 'agency_list' %}"
class="inline-flex items-center gap-2 px-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-white font-medium text-sm transition backdrop-blur-sm touch-target">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
<span class="hidden sm:inline">{% trans "Back to List" %}</span>
{{ title }}
</h1>
<div class="flex gap-2">
{% if agency %}
<a href="{% url 'agency_detail' agency.slug %}"
class="inline-flex items-center gap-2 px-4 py-3 rounded-lg font-medium border-2 border-gray-200 text-gray-700 hover:bg-gray-50 transition-all duration-200">
<i data-lucide="eye" class="w-4 h-4"></i>
<span class="hidden sm:inline">{% trans "View Details" %}</span>
</a>
</div>
<a href="{% url 'agency_delete' agency.slug %}"
class="inline-flex items-center gap-2 px-4 py-3 rounded-lg font-medium text-white transition-all duration-200"
style="background-color: #dc2626;"
onmouseover="this.style.backgroundColor='#b91c1c'"
onmouseout="this.style.backgroundColor='#dc2626'">
<i data-lucide="trash-2" class="w-4 h-4"></i>
<span class="hidden sm:inline">{% trans "Delete" %}</span>
</a>
{% endif %}
<a href="{% url 'agency_list' %}"
class="inline-flex items-center gap-2 px-6 py-3 rounded-lg font-medium border-2 border-gray-200 text-gray-700 hover:bg-gray-50 transition-all duration-200">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
<span class="hidden sm:inline">{% trans "Back to List" %}</span>
</a>
</div>
</div>
{% if agency %}
<!-- Current Agency Info Card -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<div class="bg-temple-cream rounded-lg p-4 border-l-4 border-temple-red">
<h6 class="text-temple-dark font-bold mb-3 flex items-center gap-2">
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6 p-6">
<div class="rounded-lg p-4 border-l-4" style="background-color: #fef2f2; border-color: #9d2235;">
<h6 class="font-bold mb-3 flex items-center gap-2" style="color: #9d2235;">
<i data-lucide="info" class="w-4 h-4"></i>
{% trans "Currently Editing" %}
</h6>
@ -71,22 +76,22 @@
<!-- Form Card -->
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="border-b border-gray-200 px-6 py-4 bg-gray-50 rounded-t-xl">
<h2 class="text-lg font-bold text-temple-red flex items-center gap-2">
<i data-lucide="file-text" class="w-5 h-5"></i>
<div class="px-6 py-4 border-b border-gray-100" style="background-color: #f8f9fa;">
<h2 class="text-lg font-bold flex items-center gap-2">
<i data-lucide="building-2" class="w-5 h-5" style="color: #9d2235;"></i>
{% trans "Agency Information" %}
</h2>
</div>
<div class="p-6 md:p-8">
<div class="p-6">
{% if form.non_field_errors %}
<div class="bg-red-50 border border-red-200 rounded-xl p-4 mb-6" role="alert">
<div class="mb-6 p-4 rounded-lg bg-red-50 border border-red-200" role="alert">
<div class="flex items-start gap-3">
<i data-lucide="alert-circle" class="w-5 h-5 text-red-600 shrink-0 mt-0.5"></i>
<i data-lucide="alert-triangle" class="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5"></i>
<div>
<h5 class="font-bold text-red-800 mb-1">{% trans "Error" %}</h5>
<h5 class="font-semibold text-red-800 mb-1">{% trans "Error" %}</h5>
{% for error in form.non_field_errors %}
<p class="text-red-700 text-sm">{{ error }}</p>
<p class="text-red-700 mb-0">{{ error }}</p>
{% endfor %}
</div>
</div>
@ -96,231 +101,193 @@
<form method="post" novalidate id="agency-form" class="space-y-6">
{% csrf_token %}
<!-- Two Column Form Layout -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Name -->
<div class="space-y-2">
<label for="{{ form.name.id_for_label }}" class="block text-sm font-semibold text-gray-700">
{{ form.name.label }} <span class="text-red-600">*</span>
<div>
<label for="{{ form.name.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.name.label }} <span class="text-red-500">*</span>
</label>
<input type="text"
name="name"
id="{{ form.name.id_for_label }}"
value="{{ form.name.value|default:'' }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition text-sm"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition"
placeholder="{% trans 'Agency Name' %}"
{% if form.name.field.required %}required{% endif %}>
{% if form.name.errors %}
{% for error in form.name.errors %}
<p class="text-red-600 text-xs mt-1 flex items-center gap-1">
<i data-lucide="alert-circle" class="w-3 h-3"></i>
{{ error }}
</p>
{% endfor %}
<div class="text-red-600 text-sm mt-1">{{ form.name.errors.0 }}</div>
{% endif %}
{% if form.name.help_text %}
<p class="text-gray-500 text-xs">{{ form.name.help_text }}</p>
<p class="text-xs text-gray-500 mt-1">{{ form.name.help_text }}</p>
{% endif %}
</div>
<!-- Contact Person -->
<div class="space-y-2">
<label for="{{ form.contact_person.id_for_label }}" class="block text-sm font-semibold text-gray-700">
<div>
<label for="{{ form.contact_person.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.contact_person.label }}
</label>
<input type="text"
name="contact_person"
id="{{ form.contact_person.id_for_label }}"
value="{{ form.contact_person.value|default:'' }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition text-sm"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition"
placeholder="{% trans 'Contact Person' %}">
{% if form.contact_person.errors %}
{% for error in form.contact_person.errors %}
<p class="text-red-600 text-xs mt-1 flex items-center gap-1">
<i data-lucide="alert-circle" class="w-3 h-3"></i>
{{ error }}
</p>
{% endfor %}
<div class="text-red-600 text-sm mt-1">{{ form.contact_person.errors.0 }}</div>
{% endif %}
{% if form.contact_person.help_text %}
<p class="text-gray-500 text-xs">{{ form.contact_person.help_text }}</p>
<p class="text-xs text-gray-500 mt-1">{{ form.contact_person.help_text }}</p>
{% endif %}
</div>
<!-- Phone -->
<div class="space-y-2">
<label for="{{ form.phone.id_for_label }}" class="block text-sm font-semibold text-gray-700">
<div>
<label for="{{ form.phone.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.phone.label }}
</label>
<input type="tel"
name="phone"
id="{{ form.phone.id_for_label }}"
value="{{ form.phone.value|default:'' }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition text-sm"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition"
placeholder="{% trans 'Phone Number' %}">
{% if form.phone.errors %}
{% for error in form.phone.errors %}
<p class="text-red-600 text-xs mt-1 flex items-center gap-1">
<i data-lucide="alert-circle" class="w-3 h-3"></i>
{{ error }}
</p>
{% endfor %}
<div class="text-red-600 text-sm mt-1">{{ form.phone.errors.0 }}</div>
{% endif %}
{% if form.phone.help_text %}
<p class="text-gray-500 text-xs">{{ form.phone.help_text }}</p>
<p class="text-xs text-gray-500 mt-1">{{ form.phone.help_text }}</p>
{% endif %}
</div>
<!-- Email -->
<div class="space-y-2">
<label for="{{ form.email.id_for_label }}" class="block text-sm font-semibold text-gray-700">
{{ form.email.label }} <span class="text-red-600">*</span>
<div>
<label for="{{ form.email.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.email.label }} <span class="text-red-500">*</span>
</label>
<input type="email"
name="email"
id="{{ form.email.id_for_label }}"
value="{{ form.email.value|default:'' }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition text-sm"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition"
placeholder="{% trans 'Email Address' %}"
{% if form.email.field.required %}required{% endif %}>
{% if form.email.errors %}
{% for error in form.email.errors %}
<p class="text-red-600 text-xs mt-1 flex items-center gap-1">
<i data-lucide="alert-circle" class="w-3 h-3"></i>
{{ error }}
</p>
{% endfor %}
<div class="text-red-600 text-sm mt-1">{{ form.email.errors.0 }}</div>
{% endif %}
{% if form.email.help_text %}
<p class="text-gray-500 text-xs">{{ form.email.help_text }}</p>
<p class="text-xs text-gray-500 mt-1">{{ form.email.help_text }}</p>
{% endif %}
</div>
<!-- Website -->
<div class="space-y-2">
<label for="{{ form.website.id_for_label }}" class="block text-sm font-semibold text-gray-700">
<div>
<label for="{{ form.website.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.website.label }}
</label>
<input type="url"
name="website"
id="{{ form.website.id_for_label }}"
value="{{ form.website.value|default:'' }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition text-sm"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition"
placeholder="{% trans 'Website URL' %}">
{% if form.website.errors %}
{% for error in form.website.errors %}
<p class="text-red-600 text-xs mt-1 flex items-center gap-1">
<i data-lucide="alert-circle" class="w-3 h-3"></i>
{{ error }}
</p>
{% endfor %}
<div class="text-red-600 text-sm mt-1">{{ form.website.errors.0 }}</div>
{% endif %}
{% if form.website.help_text %}
<p class="text-gray-500 text-xs">{{ form.website.help_text }}</p>
<p class="text-xs text-gray-500 mt-1">{{ form.website.help_text }}</p>
{% endif %}
</div>
<!-- Country -->
<div class="space-y-2">
<label for="{{ form.country.id_for_label }}" class="block text-sm font-semibold text-gray-700">
<div>
<label for="{{ form.country.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.country.label }}
</label>
<input type="text"
name="country"
id="{{ form.country.id_for_label }}"
value="{{ form.country.value|default:'' }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition text-sm"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition"
placeholder="{% trans 'Country' %}">
{% if form.country.errors %}
{% for error in form.country.errors %}
<p class="text-red-600 text-xs mt-1 flex items-center gap-1">
<i data-lucide="alert-circle" class="w-3 h-3"></i>
{{ error }}
</p>
{% endfor %}
<div class="text-red-600 text-sm mt-1">{{ form.country.errors.0 }}</div>
{% endif %}
{% if form.country.help_text %}
<p class="text-gray-500 text-xs">{{ form.country.help_text }}</p>
<p class="text-xs text-gray-500 mt-1">{{ form.country.help_text }}</p>
{% endif %}
</div>
<!-- City -->
<div class="space-y-2">
<label for="{{ form.city.id_for_label }}" class="block text-sm font-semibold text-gray-700">
<div>
<label for="{{ form.city.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.city.label }}
</label>
<input type="text"
name="city"
id="{{ form.city.id_for_label }}"
value="{{ form.city.value|default:'' }}"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition text-sm"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition"
placeholder="{% trans 'City' %}">
{% if form.city.errors %}
{% for error in form.city.errors %}
<p class="text-red-600 text-xs mt-1 flex items-center gap-1">
<i data-lucide="alert-circle" class="w-3 h-3"></i>
{{ error }}
</p>
{% endfor %}
<div class="text-red-600 text-sm mt-1">{{ form.city.errors.0 }}</div>
{% endif %}
{% if form.city.help_text %}
<p class="text-gray-500 text-xs">{{ form.city.help_text }}</p>
<p class="text-xs text-gray-500 mt-1">{{ form.city.help_text }}</p>
{% endif %}
</div>
</div>
<!-- Address (Full Width) -->
<div class="space-y-2">
<label for="{{ form.address.id_for_label }}" class="block text-sm font-semibold text-gray-700">
<div>
<label for="{{ form.address.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.address.label }}
</label>
<textarea
name="address"
id="{{ form.address.id_for_label }}"
rows="3"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition text-sm resize-none"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition resize-vertical"
placeholder="{% trans 'Street Address' %}">{{ form.address.value|default:'' }}</textarea>
{% if form.address.errors %}
{% for error in form.address.errors %}
<p class="text-red-600 text-xs mt-1 flex items-center gap-1">
<i data-lucide="alert-circle" class="w-3 h-3"></i>
{{ error }}
</p>
{% endfor %}
<div class="text-red-600 text-sm mt-1">{{ form.address.errors.0 }}</div>
{% endif %}
{% if form.address.help_text %}
<p class="text-gray-500 text-xs">{{ form.address.help_text }}</p>
<p class="text-xs text-gray-500 mt-1">{{ form.address.help_text }}</p>
{% endif %}
</div>
<!-- Description (Full Width) -->
<div class="space-y-2">
<label for="{{ form.description.id_for_label }}" class="block text-sm font-semibold text-gray-700">
<div>
<label for="{{ form.description.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.description.label }}
</label>
<textarea
name="description"
id="{{ form.description.id_for_label }}"
rows="4"
class="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition text-sm resize-none"
class="w-full px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition resize-vertical"
placeholder="{% trans 'Agency Description' %}">{{ form.description.value|default:'' }}</textarea>
{% if form.description.errors %}
{% for error in form.description.errors %}
<p class="text-red-600 text-xs mt-1 flex items-center gap-1">
<i data-lucide="alert-circle" class="w-3 h-3"></i>
{{ error }}
</p>
{% endfor %}
<div class="text-red-600 text-sm mt-1">{{ form.description.errors.0 }}</div>
{% endif %}
{% if form.description.help_text %}
<p class="text-gray-500 text-xs">{{ form.description.help_text }}</p>
<p class="text-xs text-gray-500 mt-1">{{ form.description.help_text }}</p>
{% endif %}
</div>
<!-- Submit Button -->
<div class="pt-4 border-t border-gray-200">
<button type="submit"
class="w-full sm:w-auto inline-flex items-center justify-center gap-2 px-8 py-3 bg-temple-red hover:bg-[#7a1a29] text-white font-semibold rounded-lg text-sm transition shadow-md hover:shadow-lg transform hover:-translate-y-0.5 touch-target">
<!-- Form Actions -->
<div class="flex flex-col sm:flex-row justify-between items-center gap-4 pt-4 border-t border-gray-200">
<a href="{% url 'agency_list' %}"
class="inline-flex items-center gap-2 px-6 py-3 rounded-lg font-medium border-2 border-gray-200 text-gray-700 hover:bg-gray-50 transition-all duration-200">
<i data-lucide="x" class="w-4 h-4"></i>
{% trans "Cancel" %}
</a>
<button type="submit"
class="inline-flex items-center gap-2 px-8 py-3 rounded-lg font-medium text-white transition-all duration-200"
style="background-color: #9d2235;"
onmouseover="this.style.backgroundColor='#7a1a29'"
onmouseout="this.style.backgroundColor='#9d2235'">
<i data-lucide="save" class="w-4 h-4"></i>
{{ button_text }}
</button>
@ -328,103 +295,56 @@
</form>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Reinitialize Lucide icons for dynamically added content
lucide.createIcons();
// Initialize Lucide icons
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Form Validation
const form = document.getElementById('agency-form');
if (form) {
form.addEventListener('submit', function(e) {
const submitBtn = form.querySelector('button[type="submit"]');
submitBtn.classList.add('opacity-75', 'cursor-not-allowed');
submitBtn.disabled = true;
submitBtn.innerHTML = `
<svg class="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{% trans 'Saving...' %}
`;
// Basic validation
const name = document.getElementById('{{ form.name.id_for_label }}');
if (name && !name.value.trim()) {
const nameInput = document.getElementById('{{ form.name.id_for_label }}');
const emailInput = document.getElementById('{{ form.email.id_for_label }}');
const websiteInput = document.getElementById('{{ form.website.id_for_label }}');
// Name validation
if (nameInput && !nameInput.value.trim()) {
e.preventDefault();
submitBtn.classList.remove('opacity-75', 'cursor-not-allowed');
submitBtn.disabled = false;
submitBtn.innerHTML = `<i data-lucide="save" class="w-4 h-4"></i> {{ button_text }}`;
lucide.createIcons();
alert('{% trans "Agency name is required." %}');
return;
alert("{% trans 'Agency name is required.' %}");
nameInput.focus();
return false;
}
const email = document.getElementById('{{ form.email.id_for_label }}');
if (email && email.value.trim() && !isValidEmail(email.value.trim())) {
e.preventDefault();
submitBtn.classList.remove('opacity-75', 'cursor-not-allowed');
submitBtn.disabled = false;
submitBtn.innerHTML = `<i data-lucide="save" class="w-4 h-4"></i> {{ button_text }}`;
lucide.createIcons();
alert('{% trans "Please enter a valid email address." %}');
return;
// Email validation
if (emailInput && emailInput.value.trim()) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(emailInput.value.trim())) {
e.preventDefault();
alert("{% trans 'Please enter a valid email address.' %}");
emailInput.focus();
return false;
}
}
const website = document.getElementById('{{ form.website.id_for_label }}');
if (website && website.value.trim() && !isValidURL(website.value.trim())) {
e.preventDefault();
submitBtn.classList.remove('opacity-75', 'cursor-not-allowed');
submitBtn.disabled = false;
submitBtn.innerHTML = `<i data-lucide="save" class="w-4 h-4"></i> {{ button_text }}`;
lucide.createIcons();
alert('{% trans "Please enter a valid website URL." %}');
return;
// Website validation
if (websiteInput && websiteInput.value.trim()) {
try {
new URL(websiteInput.value.trim());
} catch (_) {
e.preventDefault();
alert("{% trans 'Please enter a valid website URL.' %}");
websiteInput.focus();
return false;
}
}
});
}
// Email validation helper
function isValidEmail(email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
// URL validation helper
function isValidURL(url) {
try {
new URL(url);
return true;
} catch (_) {
return false;
}
}
// Warn before leaving if changes are made
let formChanged = false;
const formInputs = form ? form.querySelectorAll('input, select, textarea') : [];
formInputs.forEach(input => {
input.addEventListener('change', function() {
formChanged = true;
});
});
window.addEventListener('beforeunload', function(e) {
if (formChanged) {
e.preventDefault();
e.returnValue = '{% trans "You have unsaved changes. Are you sure you want to leave?" %}';
return e.returnValue;
}
});
if (form) {
form.addEventListener('submit', function() {
formChanged = false;
});
}
});
</script>
{% endblock %}

View File

@ -1,92 +1,159 @@
{% extends "base.html" %}
{% load static i18n crispy_forms_tags %}
{% load static i18n %}
{% block title %}{% trans "Create Application" %} - {{ block.super }}{% endblock %}
{% block content %}
<div class="px-4 py-6">
<!-- Header Card -->
<div class="bg-gradient-to-br from-temple-red to-red-800 rounded-xl shadow-xl p-6 mb-6 text-white">
<div class="flex flex-col md:flex-row md:justify-between md:items-start gap-4">
<div class="flex-1">
<h1 class="text-3xl font-bold mb-2 flex items-center gap-2">
<i data-lucide="user-plus" class="w-8 h-8"></i>
{% trans "Create New Application" %}
</h1>
<p class="text-red-100 text-lg">{% trans "Enter details to create a new application record." %}</p>
</div>
<div class="flex gap-2">
<button type="button" class="modal-trigger bg-white/20 hover:bg-white/30 backdrop-blur-sm text-white px-4 py-2 rounded-lg text-sm font-medium transition flex items-center gap-2" data-modal="personModal">
<i data-lucide="user-plus" class="w-4 h-4"></i>
<span class="hidden sm:inline">{% trans "Create New Applicant" %}</span>
</button>
<a href="{% url 'application_list' %}" class="bg-white/20 hover:bg-white/30 backdrop-blur-sm text-white px-4 py-2 rounded-lg text-sm font-medium transition flex items-center gap-2" title="{% trans 'Back to List' %}">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
<span class="hidden sm:inline">{% trans "Back to List" %}</span>
</a>
<div class="max-w-4xl mx-auto py-6 px-4">
<!-- Breadcrumb -->
<nav class="mb-6" aria-label="breadcrumb">
<ol class="flex items-center gap-2 text-sm flex-wrap">
<li><a href="{% url 'application_list' %}" class="text-gray-500 hover:underline transition flex items-center gap-1">
<i data-lucide="file-text" class="w-4 h-4"></i> {% trans "Applications" %}
</a></li>
<li class="text-gray-400">/</li>
<li class="font-semibold" style="color: #9d2235;">{% trans "Create Application" %}</li>
</ol>
</nav>
<!-- Header -->
<div class="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4 mb-6">
<h1 class="text-2xl sm:text-3xl font-bold flex items-center gap-3">
<div class="w-12 h-12 rounded-xl flex items-center justify-center" style="background-color: rgba(157, 34, 53, 0.1);">
<i data-lucide="file-plus" class="w-6 h-6" style="color: #9d2235;"></i>
</div>
{% trans "Create New Application" %}
</h1>
<div class="flex gap-2">
<button type="button" id="openPersonModal"
class="inline-flex items-center gap-2 px-4 py-3 rounded-lg font-medium border-2 text-white transition-all duration-200"
style="background-color: #9d2235;"
onmouseover="this.style.backgroundColor='#7a1a29'"
onmouseout="this.style.backgroundColor='#9d2235'">
<i data-lucide="user-plus" class="w-4 h-4"></i>
<span class="hidden sm:inline">{% trans "Create New Applicant" %}</span>
</button>
<a href="{% url 'application_list' %}"
class="inline-flex items-center gap-2 px-6 py-3 rounded-lg font-medium border-2 border-gray-200 text-gray-700 hover:bg-gray-50 transition-all duration-200">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
<span class="hidden sm:inline">{% trans "Back to List" %}</span>
</a>
</div>
</div>
<!-- Form Card -->
<div class="bg-white rounded-xl shadow-md overflow-hidden border border-gray-200">
<div class="px-6 py-4 border-b border-gray-200 bg-gray-50">
<h2 class="text-xl font-semibold text-temple-dark flex items-center gap-2">
<i data-lucide="file-text" class="w-5 h-5"></i>
<div class="bg-white rounded-xl shadow-sm border border-gray-200">
<div class="px-6 py-4 border-b border-gray-100" style="background-color: #f8f9fa;">
<h2 class="text-lg font-bold flex items-center gap-2">
<i data-lucide="file-text" class="w-5 h-5" style="color: #9d2235;"></i>
{% trans "Application Information" %}
</h2>
</div>
<div class="p-6">
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
{% for field in form %}
{% if form.non_field_errors %}
<div class="mb-6 p-4 rounded-lg bg-red-50 border border-red-200" role="alert">
<div class="flex items-start gap-3">
<i data-lucide="alert-triangle" class="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5"></i>
<div>
<label for="{{ field.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ field.label }}
{% if field.field.required %}<span class="text-red-500">*</span>{% endif %}
</label>
{{ field }}
{% if field.help_text %}
<p class="text-sm text-gray-500 mt-1">{{ field.help_text }}</p>
{% endif %}
{% for error in field.errors %}
<p class="text-sm text-red-500 mt-1">{{ error }}</p>
<h5 class="font-semibold text-red-800 mb-1">{% trans "Error" %}</h5>
{% for error in form.non_field_errors %}
<p class="text-red-700 mb-0">{{ error }}</p>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<form method="post" enctype="multipart/form-data" id="application-form" class="space-y-6">
{% csrf_token %}
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
{% for field in form %}
{% if field.name != 'person' %}
<div>
<label for="{{ field.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ field.label }}
{% if field.field.required %}<span class="text-red-500">*</span>{% endif %}
</label>
{{ field }}
{% if field.help_text %}
<p class="text-xs text-gray-500 mt-1">{{ field.help_text }}</p>
{% endif %}
{% for error in field.errors %}
<div class="text-red-600 text-sm mt-1">{{ error }}</div>
{% endfor %}
</div>
{% endif %}
{% endfor %}
</div>
<div class="border-t border-gray-200 mt-6 pt-6">
<button type="submit" class="bg-temple-red hover:bg-red-800 text-white font-semibold px-8 py-3 rounded-xl transition shadow-md hover:shadow-lg flex items-center gap-2">
<i data-lucide="save" class="w-5 h-5"></i>
<!-- Person Selection -->
<div>
<label for="{{ form.person.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ form.person.label }} <span class="text-red-500">*</span>
</label>
<div class="flex gap-2">
<select name="person" id="{{ form.person.id_for_label }}"
class="flex-1 px-4 py-3 border border-gray-200 rounded-xl text-sm focus:ring-2 focus:ring-temple-red/20 focus:border-temple-red outline-none transition bg-white">
<option value="">{% trans "Select Applicant" %}</option>
{% for choice in form.person.field.queryset %}
<option value="{{ choice.pk }}">{{ choice.get_full_name|default:choice.email }}</option>
{% endfor %}
</select>
<button type="button" id="openPersonModal"
class="inline-flex items-center justify-center px-4 py-3 rounded-xl border-2 text-white transition-all duration-200 whitespace-nowrap"
style="background-color: #9d2235;"
onmouseover="this.style.backgroundColor='#7a1a29'"
onmouseout="this.style.backgroundColor='#9d2235'">
<i data-lucide="user-plus" class="w-4 h-4 mr-2"></i>
{% trans "New" %}
</button>
</div>
{% for error in form.person.errors %}
<div class="text-red-600 text-sm mt-1">{{ error }}</div>
{% endfor %}
</div>
<!-- Form Actions -->
<div class="flex flex-col sm:flex-row justify-between items-center gap-4 pt-4 border-t border-gray-200">
<a href="{% url 'application_list' %}"
class="inline-flex items-center gap-2 px-6 py-3 rounded-lg font-medium border-2 border-gray-200 text-gray-700 hover:bg-gray-50 transition-all duration-200">
<i data-lucide="x" class="w-4 h-4"></i>
{% trans "Cancel" %}
</a>
<button type="submit"
class="inline-flex items-center gap-2 px-8 py-3 rounded-lg font-medium text-white transition-all duration-200"
style="background-color: #9d2235;"
onmouseover="this.style.backgroundColor='#7a1a29'"
onmouseout="this.style.backgroundColor='#9d2235'">
<i data-lucide="save" class="w-4 h-4"></i>
{% trans "Create Application" %}
</button>
</div>
</form>
</div>
</div>
</div>
<!-- Modal -->
<!-- Person Modal -->
<div class="hidden fixed inset-0 z-50 overflow-y-auto" id="personModal" role="dialog" aria-labelledby="personModalLabel">
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:block sm:p-0">
<div class="fixed inset-0 bg-black/50 transition-opacity" aria-hidden="true"></div>
<span class="hidden sm:inline-block sm:align-middle sm:h-screen sm:align-middle" aria-hidden="true">&#8203;</span>
<div class="inline-block align-bottom bg-white rounded-2xl text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-3xl sm:w-full">
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 border-b border-gray-200 flex justify-between items-center">
<h3 class="text-lg font-semibold text-gray-900 flex items-center gap-2" id="personModalLabel">
<i data-lucide="help-circle" class="w-5 h-5 text-temple-red"></i>
<div class="px-6 py-4 border-b border-gray-100" style="background-color: #f8f9fa;">
<h3 class="text-lg font-bold flex items-center gap-2" id="personModalLabel">
<i data-lucide="user-plus" class="w-5 h-5" style="color: #9d2235;"></i>
{% trans "Create New Applicant" %}
</h3>
<button type="button" class="modal-close-btn text-gray-400 hover:text-gray-600 transition">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="px-4 pt-5 pb-4 sm:p-6">
<div class="p-6">
<form id="person_form" hx-post="{% url 'person_create' %}" hx-vals='{"view":"job"}' hx-target="#div_id_person" hx-select="#div_id_person" hx-swap="outerHTML">
{% csrf_token %}
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="{{ person_form.first_name.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
@ -94,7 +161,7 @@
</label>
{{ person_form.first_name }}
{% for error in person_form.first_name.errors %}
<p class="text-sm text-red-500 mt-1">{{ error }}</p>
<div class="text-red-600 text-sm mt-1">{{ error }}</div>
{% endfor %}
</div>
<div>
@ -103,7 +170,7 @@
</label>
{{ person_form.middle_name }}
{% for error in person_form.middle_name.errors %}
<p class="text-sm text-red-500 mt-1">{{ error }}</p>
<div class="text-red-600 text-sm mt-1">{{ error }}</div>
{% endfor %}
</div>
<div>
@ -112,10 +179,11 @@
</label>
{{ person_form.last_name }}
{% for error in person_form.last_name.errors %}
<p class="text-sm text-red-500 mt-1">{{ error }}</p>
<div class="text-red-600 text-sm mt-1">{{ error }}</div>
{% endfor %}
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<label for="{{ person_form.email.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
@ -123,7 +191,7 @@
</label>
{{ person_form.email }}
{% for error in person_form.email.errors %}
<p class="text-sm text-red-500 mt-1">{{ error }}</p>
<div class="text-red-600 text-sm mt-1">{{ error }}</div>
{% endfor %}
</div>
<div>
@ -132,10 +200,11 @@
</label>
{{ person_form.phone }}
{% for error in person_form.phone.errors %}
<p class="text-sm text-red-500 mt-1">{{ error }}</p>
<div class="text-red-600 text-sm mt-1">{{ error }}</div>
{% endfor %}
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<label for="{{ person_form.gpa.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
@ -143,7 +212,7 @@
</label>
{{ person_form.gpa }}
{% for error in person_form.gpa.errors %}
<p class="text-sm text-red-500 mt-1">{{ error }}</p>
<div class="text-red-600 text-sm mt-1">{{ error }}</div>
{% endfor %}
</div>
<div>
@ -152,10 +221,11 @@
</label>
{{ person_form.national_id }}
{% for error in person_form.national_id.errors %}
<p class="text-sm text-red-500 mt-1">{{ error }}</p>
<div class="text-red-600 text-sm mt-1">{{ error }}</div>
{% endfor %}
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<label for="{{ person_form.date_of_birth.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
@ -163,7 +233,7 @@
</label>
{{ person_form.date_of_birth }}
{% for error in person_form.date_of_birth.errors %}
<p class="text-sm text-red-500 mt-1">{{ error }}</p>
<div class="text-red-600 text-sm mt-1">{{ error }}</div>
{% endfor %}
</div>
<div>
@ -172,61 +242,78 @@
</label>
{{ person_form.nationality }}
{% for error in person_form.nationality.errors %}
<p class="text-sm text-red-500 mt-1">{{ error }}</p>
<div class="text-red-600 text-sm mt-1">{{ error }}</div>
{% endfor %}
</div>
</div>
<div class="mt-4">
<label for="{{ person_form.address.id_for_label }}" class="block text-sm font-semibold text-gray-700 mb-2">
{{ person_form.address.label }}
</label>
{{ person_form.address }}
{% for error in person_form.address.errors %}
<p class="text-sm text-red-500 mt-1">{{ error }}</p>
<div class="text-red-600 text-sm mt-1">{{ error }}</div>
{% endfor %}
</div>
</form>
</div>
<div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
<button type="submit" class="modal-save-btn w-full inline-flex justify-center rounded-xl border border-transparent shadow-sm px-4 py-2 bg-temple-red text-base font-medium text-white hover:bg-red-800 focus:outline-none sm:ml-3 sm:w-auto sm:text-sm">
<i data-lucide="save" class="w-4 h-4 mr-2"></i>{% trans "Save" %}
</button>
<button type="button" class="modal-close-btn mt-3 w-full inline-flex justify-center rounded-xl border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm">
<i data-lucide="x" class="w-4 h-4 mr-2"></i>{% trans "Close" %}
</button>
<div class="px-6 py-4 border-t border-gray-100" style="background-color: #f8f9fa;">
<div class="flex flex-col sm:flex-row justify-end gap-3">
<button type="button" id="closePersonModal"
class="inline-flex items-center justify-center gap-2 px-6 py-3 rounded-lg font-medium border-2 border-gray-200 text-gray-700 hover:bg-gray-50 transition-all duration-200">
<i data-lucide="x" class="w-4 h-4"></i>
{% trans "Cancel" %}
</button>
<button type="submit" form="person_form"
class="inline-flex items-center justify-center gap-2 px-8 py-3 rounded-lg font-medium text-white transition-all duration-200"
style="background-color: #9d2235;"
onmouseover="this.style.backgroundColor='#7a1a29'"
onmouseout="this.style.backgroundColor='#9d2235'">
<i data-lucide="save" class="w-4 h-4"></i>
{% trans "Create Applicant" %}
</button>
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize Lucide icons
lucide.createIcons();
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
// Apply consistent form styling
const formInputs = document.querySelectorAll('#application-form input, #application-form select, #application-form textarea, #person_form input, #person_form select, #person_form textarea');
formInputs.forEach(input => {
if (!input.classList.contains('hidden')) {
input.classList.add('w-full', 'px-4', 'py-3', 'border', 'border-gray-200', 'rounded-xl', 'text-sm', 'focus:ring-2', 'focus:ring-temple-red/20', 'focus:border-temple-red', 'outline-none', 'transition', 'bg-white');
}
});
// Modal functionality
const modal = document.getElementById('personModal');
const openBtns = document.querySelectorAll('#openPersonModal');
const closeBtns = document.querySelectorAll('#closePersonModal');
let isModalOpen = false;
// Open modal buttons
const openModalBtns = document.querySelectorAll('.modal-trigger');
openModalBtns.forEach(btn => {
// Open modal
openBtns.forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (modal) {
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
isModalOpen = true;
setTimeout(() => lucide.createIcons(), 100);
}
});
});
// Close modal buttons
document.querySelectorAll('.modal-close-btn').forEach(btn => {
// Close modal
closeBtns.forEach(btn => {
btn.addEventListener('click', function(e) {
e.preventDefault();
if (modal && isModalOpen) {
@ -236,7 +323,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
});
// Close modal when clicking outside
if (modal) {
modal.addEventListener('click', function(e) {
@ -247,7 +334,7 @@ document.addEventListener('DOMContentLoaded', function() {
}
});
}
// Close modal on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && modal && isModalOpen) {
@ -256,22 +343,31 @@ document.addEventListener('DOMContentLoaded', function() {
isModalOpen = false;
}
});
// Add form styling
const formInputs = document.querySelectorAll('input, select, textarea');
formInputs.forEach(input => {
input.classList.add('w-full', 'px-3', 'py-2.5', 'border', 'border-gray-300', 'rounded-lg', 'focus:ring-2', 'focus:ring-temple-red', 'focus:border-transparent', 'transition');
});
// Form validation
const form = document.getElementById('application-form');
if (form) {
form.addEventListener('submit', function(e) {
const personSelect = document.getElementById('id_person');
if (!personSelect.value) {
e.preventDefault();
alert("{% trans 'Please select an applicant.' %}");
personSelect.focus();
return false;
}
});
}
// Reinitialize Lucide icons after HTMX updates
document.body.addEventListener('htmx:afterSwap', function(evt) {
lucide.createIcons();
// Re-apply form styling to new elements
const newFormInputs = evt.detail.xhr.response.querySelectorAll ?
evt.detail.xhr.response.querySelectorAll('input, select, textarea') : [];
const newFormInputs = document.querySelectorAll('#person_form input, #person_form select, #person_form textarea');
newFormInputs.forEach(input => {
input.classList.add('w-full', 'px-3', 'py-2.5', 'border', 'border-gray-300', 'rounded-lg', 'focus:ring-2', 'focus:ring-temple-red', 'focus:border-transparent', 'transition');
if (!input.classList.contains('hidden')) {
input.classList.add('w-full', 'px-4', 'py-3', 'border', 'border-gray-200', 'rounded-xl', 'text-sm', 'focus:ring-2', 'focus:ring-temple-red/20', 'focus:border-temple-red', 'outline-none', 'transition', 'bg-white');
}
});
});
});

View File

@ -95,7 +95,7 @@
</div>
<button id="changeStage" type="submit"
class="px-4 py-2 bg-temple-red text-white rounded-lg hover:bg-temple-red/90 transition text-sm font-medium flex items-center justify-center gap-2">
class="w-full sm:w-auto px-4 py-2 bg-temple-red text-white rounded-lg hover:bg-temple-red/90 transition text-sm font-medium flex items-center justify-center gap-2">
<i data-lucide="arrow-right" class="w-4 h-4"></i>
{% trans "Change Stage" %}
</button>
@ -105,7 +105,7 @@
<!-- Email Button -->
<button id="emailBotton" type="button"
class="flex-1 w-full sm:w-auto px-4 py-2 border-2 border-temple-red text-temple-red rounded-lg hover:bg-temple-red hover:text-white transition text-sm font-medium flex items-center justify-center gap-2"
class="w-full sm:w-auto px-4 py-2 border-2 border-temple-red text-temple-red rounded-lg hover:bg-temple-red hover:text-white transition text-sm font-medium flex items-center justify-center gap-2"
onclick="openEmailModal()"
title="{% trans 'Email Participants' %}">
<i data-lucide="mail" class="w-4 h-4"></i>

View File

@ -46,7 +46,7 @@
<i data-lucide="users" class="w-5 h-5"></i>
{% trans "Application List" %}
<span class="ml-2 px-2 py-1 bg-temple-red text-white text-xs rounded-full">
{{ applications|length }}
{{ applications|length }} / {{ total_candidates }} {% trans "Total" %}
</span>
</h2>
</div>
@ -56,54 +56,41 @@
<!-- Bulk Action Bar -->
{% if applications %}
<div class="p-3 sm:p-4 bg-gray-50 border-b border-gray-200">
<div class="flex flex-col sm:flex-row gap-3 sm:gap-4 items-end">
<!-- Stage Change Form -->
<form hx-boost="true" hx-include="#application-form"
action="{% url 'application_update_status' job.slug %}"
method="post"
class="flex items-end gap-2 flex-1 w-full sm:w-auto">
{% csrf_token %}
<div class="flex-1 w-full sm:w-auto">
<select name="mark_as" id="update_status"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-temple-red focus:border-transparent text-sm">
<option selected>----------</option>
<option value="Document Review">{% trans "To Document Review" %}</option>
<option value="Exam">{% trans "To Exam" %}</option>
</select>
</div>
<button id="changeStage" type="submit"
class="px-4 py-2 bg-temple-red text-white rounded-lg hover:bg-temple-red/90 transition text-sm font-medium flex items-center justify-center gap-2">
<i data-lucide="arrow-right" class="w-4 h-4"></i>
{% trans "Change Stage" %}
</button>
</form>
<div class="hidden sm:block w-px h-7 bg-gray-300"></div>
<!-- Bulk Schedule Form -->
<form hx-boost="true" hx-include="#application-form"
action="{% url 'schedule_interviews' job.slug %}"
method="get"
class="flex-1 w-full sm:w-auto">
<button id="scheduleInterview" type="submit"
class="w-full sm:w-auto px-4 py-2 bg-temple-red text-white rounded-lg hover:bg-temple-red/90 transition text-sm font-medium flex items-center justify-center gap-2">
<i data-lucide="calendar-plus" class="w-4 h-4"></i>
{% trans "Bulk Schedule Interviews" %}
</button>
</form>
<div class="hidden sm:block w-px h-7 bg-gray-300"></div>
<!-- Email Button -->
<form hx-boost="true" hx-include="#application-form"
action="{% url 'application_update_status' job.slug %}"
method="post"
class="flex flex-col sm:flex-row gap-3 sm:gap-4 items-end">
{% csrf_token %}
<div class="flex-1 w-full sm:w-auto">
<select name="mark_as" id="update_status"
class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-temple-red focus:border-transparent text-sm">
<option value="" selected>----------</option>
<option value="Document Review">{% trans "To Document Review" %}</option>
<option value="Exam">{% trans "To Exam" %}</option>
</select>
</div>
<button id="changeStage" type="submit"
class="w-full sm:w-auto px-4 py-2 bg-temple-red text-white rounded-lg hover:bg-temple-red/90 transition text-sm font-medium flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed">
<i data-lucide="arrow-right" class="w-4 h-4"></i>
{% trans "Change Stage" %}
</button>
<a href="{% url 'schedule_interviews' job.slug %}"
class="w-full sm:w-auto px-4 py-2 bg-temple-red text-white rounded-lg hover:bg-temple-red/90 transition text-sm font-medium flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
id="scheduleInterview">
<i data-lucide="calendar-plus" class="w-4 h-4"></i>
{% trans "Bulk Schedule Interviews" %}
</a>
<button id="emailBotton" type="button"
class="flex-1 w-full sm:w-auto px-4 py-2 border-2 border-temple-red text-temple-red rounded-lg hover:bg-temple-red hover:text-white transition text-sm font-medium flex items-center justify-center gap-2"
class="w-full sm:w-auto px-4 py-2 border-2 border-temple-red text-temple-red rounded-lg hover:bg-temple-red hover:text-white transition text-sm font-medium flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
onclick="openEmailModal()"
title="{% trans 'Email Participants' %}">
<i data-lucide="mail" class="w-4 h-4"></i>
</button>
</div>
</form>
</div>
{% endif %}
@ -192,47 +179,18 @@
<i data-lucide="plus" class="w-3 h-3"></i>
</button>
{% else %}
{% if application.interview_status %}
<button type="button"
class="px-3 py-1.5 {% if application.interview_status == 'Passed' %}bg-green-500 hover:bg-green-600 text-white{% else %}bg-red-500 hover:bg-red-600 text-white{% endif %} rounded-lg transition text-xs font-medium"
onclick="openCandidateModal('{% url 'update_application_status' job.slug application.slug 'interview' 'passed' %}')"
title="{% trans 'Update Result' %}">
{{ application.interview_status }}
</button>
{% endif %}
<button type="button"
class="px-3 py-1.5 {% if application.interview_status == 'Passed' %}bg-green-500 hover:bg-green-600 text-white{% else %}bg-red-500 hover:bg-red-600 text-white{% endif %} rounded-lg transition text-xs font-medium"
onclick="openCandidateModal('{% url 'update_application_status' job.slug application.slug 'interview' 'passed' %}')"
title="{% trans 'Update Result' %}">
{{ application.interview_status }}
</button>
{% endif %}
</td>
<td class="px-4 py-3 border-b border-gray-200 text-center">
<td class="px-4 py-3 text-center border-b border-gray-200">
<div class="flex items-center justify-center gap-1">
{% if application.get_latest_meeting %}
{% if application.get_latest_meeting.location_type == 'Remote' %}
<button type="button"
class="px-2 py-1.5 border-2 border-gray-300 text-gray-600 rounded-lg hover:border-temple-red hover:text-temple-red transition text-xs font-medium"
onclick="openCandidateModal('{% url 'reschedule_meeting_for_application' job.slug application.pk application.get_latest_meeting.pk %}')"
title="{% trans 'Reschedule' %}">
<i data-lucide="refresh-ccw" class="w-3 h-3"></i>
</button>
<button type="button"
class="px-2 py-1.5 border-2 border-red-300 text-red-600 rounded-lg hover:bg-red-500 hover:text-white transition text-xs font-medium"
onclick="openCandidateModal('{% url 'schedule_meeting_for_application' job.slug application.pk application.get_latest_meeting.pk %}')"
title="{% trans 'Delete Meeting' %}">
<i data-lucide="trash-2" class="w-3 h-3"></i>
</button>
{% else %}
<button type="button"
class="px-2 py-1.5 border-2 border-gray-300 text-gray-600 rounded-lg hover:border-temple-red hover:text-temple-red transition text-xs font-medium"
onclick="openCandidateModal('{% url 'reschedule_onsite_meeting' job.slug application.pk application.get_latest_meeting.pk %}')"
title="{% trans 'Reschedule' %}">
<i data-lucide="refresh-ccw" class="w-3 h-3"></i>
</button>
<button type="button"
class="px-2 py-1.5 border-2 border-red-300 text-red-600 rounded-lg hover:bg-red-500 hover:text-white transition text-xs font-medium"
onclick="openCandidateModal('{% url 'delete_onsite_meeting_for_application' job.slug application.pk application.get_latest_meeting.pk %}')"
title="{% trans 'Delete Meeting' %}">
<i data-lucide="trash-2" class="w-3 h-3"></i>
</button>
{% endif %}
{% else %}
<button type="button"
class="px-3 py-1.5 border-2 border-temple-red text-temple-red rounded-lg hover:bg-temple-red hover:text-white transition text-xs font-medium"
onclick="openCandidateModal('{% url 'interview_create_type_selection' application.slug %}')"
@ -240,7 +198,7 @@
<i data-lucide="calendar-plus" class="w-3 h-3 inline mr-1"></i>
{% trans "Schedule Interview" %}
</button>
{% endif %}
</div>
</td>
</tr>
@ -342,6 +300,9 @@
</div>
</div>
<!-- Include Note Modal -->
{% include "recruitment/partials/note_modal.html" %}
<!-- Stage Confirmation Modal -->
<div id="stageConfirmationModal" class="fixed inset-0 z-50 hidden" aria-labelledby="stageConfirmationModalLabel" role="dialog" aria-modal="true">
<!-- Backdrop -->
@ -540,12 +501,11 @@
const changeStageButton = document.getElementById('changeStage');
const emailButton = document.getElementById('emailBotton');
const updateStatus = document.getElementById('update_status');
const scheduleInterviewButton = document.getElementById('scheduleInterview');
const confirmStageChangeButton = document.getElementById('confirmStageChangeButton');
let isConfirmed = false;
if (selectAllCheckbox) {
// Function to safely update header checkbox state
// Function to safely update the header checkbox state
function updateSelectAllState() {
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
const totalCount = rowCheckboxes.length;
@ -556,21 +516,18 @@
if (changeStageButton) changeStageButton.disabled = true;
if (emailButton) emailButton.disabled = true;
if (updateStatus) updateStatus.disabled = true;
if (scheduleInterviewButton) scheduleInterviewButton.disabled = true;
} else if (checkedCount === totalCount) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
if (changeStageButton) changeStageButton.disabled = false;
if (emailButton) emailButton.disabled = false;
if (updateStatus) updateStatus.disabled = false;
if (scheduleInterviewButton) scheduleInterviewButton.disabled = false;
} else {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
if (changeStageButton) changeStageButton.disabled = false;
if (emailButton) emailButton.disabled = false;
if (updateStatus) updateStatus.disabled = false;
if (scheduleInterviewButton) scheduleInterviewButton.disabled = false;
}
}
@ -631,4 +588,4 @@
});
});
</script>
{% endblock %}
{% endblock %}

View File

@ -72,7 +72,7 @@
</div>
<button id="changeStage" type="submit"
class="px-4 py-2 bg-temple-red text-white rounded-lg hover:bg-temple-red/90 transition text-sm font-medium flex items-center justify-center gap-2">
class="w-full sm:w-auto px-4 py-2 bg-temple-red text-white rounded-lg hover:bg-temple-red/90 transition text-sm font-medium flex items-center justify-center gap-2">
<i data-lucide="arrow-right" class="w-4 h-4"></i>
{% trans "Change Stage" %}
</button>
@ -82,7 +82,7 @@
<!-- Email Button -->
<button id="emailBotton" type="button"
class="flex-1 w-full sm:w-auto px-4 py-2 border-2 border-temple-red text-temple-red rounded-lg hover:bg-temple-red hover:text-white transition text-sm font-medium flex items-center justify-center gap-2"
class="w-full sm:w-auto px-4 py-2 border-2 border-temple-red text-temple-red rounded-lg hover:bg-temple-red hover:text-white transition text-sm font-medium flex items-center justify-center gap-2"
onclick="openEmailModal()"
title="{% trans 'Email Participants' %}">
<i data-lucide="mail" class="w-4 h-4"></i>

View File

@ -1,18 +1,28 @@
<td class="text-center" id="status-result-{{ application.pk}}">
{% if application.exam_status %}
<button type="button" class="btn btn-{% if application.exam_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
data-bs-toggle="modal"
data-bs-target="#candidateviewModal"
hx-get="{% url 'update_application_status' job.slug application.slug 'exam' 'passed' %}"
hx-target="#candidateviewModalBody"
title="Pass Exam">
{{ application.exam_status }}
</button>
{% else %}
--
{% endif %}
{% load i18n %}
<td class="px-4 py-3 text-center border-b border-gray-200" id="status-result-{{ application.pk }}">
{% if application.exam_status %}
<button type="button"
class="px-3 py-1.5 {% if application.exam_status == 'Passed' %}bg-green-500 hover:bg-green-600 text-white{% else %}bg-red-500 hover:bg-red-600 text-white{% endif %} rounded-lg transition text-xs font-medium"
onclick="openCandidateModal('{% url 'update_application_status' job.slug application.slug 'exam' 'passed' %}')"
title="{% trans 'Update Exam Status' %}">
{{ application.exam_status }}
</button>
{% else %}
<button type="button"
class="px-3 py-1.5 bg-yellow-400 hover:bg-yellow-500 text-yellow-900 rounded-lg transition text-xs font-medium"
onclick="openCandidateModal('{% url 'update_application_status' job.slug application.slug 'exam' 'passed' %}')"
title="{% trans 'Add Exam Result' %}">
<i data-lucide="plus" class="w-3 h-3"></i>
</button>
{% endif %}
</td>
<td id="exam-score-{{ application.pk}}" hx-swap-oob="true">
{{application.exam_score|default:"--"}}
</td>
<td class="px-4 py-3 text-center border-b border-gray-200" id="exam-score-{{ application.pk }}" hx-swap-oob="true">
{{ application.exam_score|default:"--" }}
</td>
<script>
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
</script>

View File

@ -1,25 +1,24 @@
<td class="text-center" id="interview-result-{{ application.pk}}">
{% load i18n %}
<td class="px-4 py-3 text-center border-b border-gray-200" id="interview-result-{{ application.pk }}">
{% if not application.interview_status %}
<button type="button" class="btn btn-warning btn-sm"
data-bs-toggle="modal"
data-bs-target="#candidateviewModal"
hx-get="{% url 'update_application_status' job.slug application.slug 'interview' 'passed' %}"
hx-target="#candidateviewModalBody"
title="Pass Exam">
<i class="fas fa-plus"></i>
<button type="button"
class="px-3 py-1.5 bg-yellow-400 hover:bg-yellow-500 text-yellow-900 rounded-lg transition text-xs font-medium"
onclick="openCandidateModal('{% url 'update_application_status' job.slug application.slug 'interview' 'passed' %}')"
title="{% trans 'Add Result' %}">
<i data-lucide="plus" class="w-3 h-3"></i>
</button>
{% else %}
{% if application.interview_status %}
<button type="button" class="btn btn-{% if application.interview_status == 'Passed' %}success{% else %}danger{% endif %} btn-sm"
data-bs-toggle="modal"
data-bs-target="#candidateviewModal"
hx-get="{% url 'update_application_status' job.slug application.slug 'interview' 'passed' %}"
hx-target="#candidateviewModalBody"
title="Pass Exam">
{{ application.interview_status }}
</button>
{% else %}
--
{% endif %}
<button type="button"
class="px-3 py-1.5 {% if application.interview_status == 'Passed' %}bg-green-500 hover:bg-green-600 text-white{% else %}bg-red-500 hover:bg-red-600 text-white{% endif %} rounded-lg transition text-xs font-medium"
onclick="openCandidateModal('{% url 'update_application_status' job.slug application.slug 'interview' 'passed' %}')"
title="{% trans 'Update Result' %}">
{{ application.interview_status }}
</button>
{% endif %}
</td>
</td>
<script>
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
</script>

View File

@ -1,18 +1,24 @@
{% load static i18n %}
<div class="modal fade" id="noteModal" tabindex="-1" aria-labelledby="noteModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content bg-white border border-gray-200 rounded-xl shadow-lg">
<div class="modal-header flex justify-between items-center px-6 py-4 border-b border-gray-200">
<h5 class="modal-title text-lg font-bold text-temple-dark" id="noteModalLabel">
<div id="noteModal" class="fixed inset-0 z-50 hidden" role="dialog" aria-labelledby="noteModalLabel" aria-modal="true">
<!-- Backdrop -->
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm transition-opacity" onclick="closeModal('noteModal')"></div>
<!-- Modal Content -->
<div class="fixed inset-0 flex items-center justify-center p-4 pointer-events-none">
<div class="bg-white border border-gray-200 rounded-xl shadow-lg w-full max-w-4xl max-h-[90vh] overflow-hidden pointer-events-auto flex flex-col">
<!-- Header -->
<div class="flex justify-between items-center px-6 py-4 border-b border-gray-200">
<h5 class="text-lg font-bold text-temple-dark" id="noteModalLabel">
<i data-lucide="sticky-note" class="w-5 h-5 inline mr-2"></i>{% trans "Add Note" %}
</h5>
<button type="button" class="text-gray-400 hover:text-gray-600 transition p-1" data-bs-dismiss="modal" aria-label="Close">
<button type="button" class="text-gray-400 hover:text-gray-600 transition p-1" onclick="closeModal('noteModal')" aria-label="{% trans 'Close' %}">
<i data-lucide="x" class="w-5 h-5"></i>
</button>
</div>
<div class="modal-body notemodal p-6">
<!-- Body -->
<div class="notemodal flex-1 overflow-y-auto p-6">
<!-- Content will be loaded via HTMX -->
</div>
</div>
</div>
</div>
</div>

View File

@ -1,25 +1,24 @@
<td class="text-center" id="status-result-{{ application.pk}}">
{% load i18n %}
<td class="px-4 py-3 text-center border-b border-gray-200" id="status-result-{{ application.pk }}">
{% if not application.offer_status %}
<button type="button" class="btn btn-warning btn-sm"
data-bs-toggle="modal"
data-bs-target="#candidateviewModal"
hx-get="{% url 'update_application_status' job.slug application.slug 'offer' 'Accepted' %}"
hx-target="#candidateviewModalBody"
title="Pass Exam">
<i class="fas fa-plus"></i>
<button type="button"
class="px-3 py-1.5 bg-yellow-400 hover:bg-yellow-500 text-yellow-900 rounded-lg transition text-xs font-medium"
onclick="openCandidateModal('{% url 'update_application_status' job.slug application.slug 'offer' 'passed' %}')"
title="{% trans 'Add Offer Status' %}">
<i data-lucide="plus" class="w-3 h-3"></i>
</button>
{% else %}
{% if application.offer_status %}
<button type="button" class="btn btn-{% if application.offer_status == 'Accepted' %}success{% else %}danger{% endif %} btn-sm"
data-bs-toggle="modal"
data-bs-target="#candidateviewModal"
hx-get="{% url 'update_application_status' job.slug application.slug 'offer' 'Rejected' %}"
hx-target="#candidateviewModalBody"
title="Pass Exam">
<button type="button"
class="px-3 py-1.5 {% if application.offer_status == 'Accepted' %}bg-green-500 hover:bg-green-600 text-white{% else %}bg-red-500 hover:bg-red-600 text-white{% endif %} rounded-lg transition text-xs font-medium"
onclick="openCandidateModal('{% url 'update_application_status' job.slug application.slug 'offer' 'passed' %}')"
title="{% trans 'Update Offer Status' %}">
{{ application.offer_status }}
</button>
{% else %}
--
{% endif %}
</button>
{% endif %}
</td>
</td>
<script>
if (typeof lucide !== 'undefined') {
lucide.createIcons();
}
</script>

View File

@ -4,297 +4,295 @@
{% block title %}{{ source.name }} - {% trans "Source Details" %}{% endblock %}
{% block content %}
<div class="container-fluid">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'source_list' %}" class="text-decoration-none text-secondary">{% trans "Souce Settings" %}</a></li>
<li class="breadcrumb-item active" aria-current="page" style="
color: #F43B5E; /* Rosy Accent Color */
font-weight: 600;
">{% trans "Source Detail" %}</li>
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<!-- Breadcrumb -->
<nav aria-label="breadcrumb" class="mb-6">
<ol class="flex items-center gap-2 text-sm">
<li>
<a href="{% url 'source_list' %}"
class="text-gray-500 hover:text-temple-red transition-colors flex items-center gap-1">
<i data-lucide="database" class="w-4 h-4"></i>
{% trans "Source Settings" %}
</a>
</li>
<li class="text-gray-400">/</li>
<li class="text-temple-red font-semibold">{% trans "Source Detail" %}</li>
</ol>
</nav>
<div class="row">
<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> {% trans "Edit" %}
</a>
{% comment %} <a href="{% url 'generate_api_keys' source.pk %}" class="btn btn-warning">
<i class="fas fa-key"></i> Generate Keys
</a> {% endcomment %}
<button id="toggle-source-status"
type="button"
class="btn btn-outline-{{ source.is_active|yesno:'warning,success' }}"
hx-post="{% url 'toggle_source_status' source.pk %}"
hx-target="#toggle-source-status"
hx-select="#toggle-source-status"
hx-select-oob="#source-status"
hx-swap="outerHTML"
hx-confirm="{% blocktrans %}Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?{% endblocktrans %}">
<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> {% trans "Delete" %}
</a>
<!-- Page Header -->
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-6">
<div>
<h1 class="text-2xl sm:text-3xl font-bold text-gray-900 mb-2 flex items-center gap-3">
<div class="w-12 h-12 rounded-xl bg-temple-red/10 flex items-center justify-center">
<i data-lucide="database" class="w-6 h-6 text-temple-red"></i>
</div>
</div>
{{ source.name }}
</h1>
<p class="text-gray-500 text-sm sm:text-base">
{% trans "View and manage integration source details" %}
</p>
</div>
<div class="flex gap-3">
<button id="toggle-source-status"
type="button"
class="inline-flex items-center gap-2 px-5 py-3 border-2 {% if source.is_active %}border-yellow-500 text-yellow-600 hover:bg-yellow-50{% else %}border-emerald-500 text-emerald-600 hover:bg-emerald-50{% endif %} rounded-xl font-semibold transition"
hx-post="{% url 'toggle_source_status' source.pk %}"
hx-target="#toggle-source-status"
hx-select="#toggle-source-status"
hx-select-oob="#source-status"
hx-swap="outerHTML"
hx-confirm="{% blocktrans %}Are you sure you want to {{ source.is_active|yesno:'deactivate,activate' }} this source?{% endblocktrans %}">
<i data-lucide="{% if source.is_active %}pause{% else %}play{% endif %}" class="w-5 h-5"></i>
{% if source.is_active %}{% trans "Deactivate" %}{% else %}{% trans "Activate" %}{% endif %}
</button>
<a href="{% url 'source_update' source.pk %}"
class="inline-flex items-center gap-2 px-5 py-3 border-2 border-gray-200 text-gray-700 rounded-xl font-semibold hover:border-gray-300 transition">
<i data-lucide="edit-3" class="w-5 h-5"></i>
{% trans "Edit" %}
</a>
<a href="{% url 'source_delete' source.pk %}"
class="inline-flex items-center gap-2 px-5 py-3 bg-red-500 text-white rounded-xl font-semibold shadow-lg hover:shadow-xl transition">
<i data-lucide="trash-2" class="w-5 h-5"></i>
{% trans "Delete" %}
</a>
</div>
</div>
<!-- Source Information -->
<div class="row">
<div class="col-md-8">
<div class="card mb-4">
<div class="card-header">
<h6 class="mb-0">{% trans "Source Information" %}</h6>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Main Content Column -->
<div class="lg:col-span-2 space-y-6">
<!-- Source Information Card -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-temple-red/10 flex items-center justify-center">
<i data-lucide="info" class="w-5 h-5 text-temple-red"></i>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label text-muted">{% trans "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">{% trans "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 id="source-status">
{% 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">{% trans "Requires Authentication" %}</label>
<div>
{% if source.requires_auth %}
<span class="badge bg-warning">{% trans "Yes" %}</span>
{% else %}
<span class="badge bg-secondary">{% trans "No" %}</span>
{% endif %}
</div>
</div>
</div>
</div>
{% if source.webhook_url %}
<div class="mb-3">
<label class="form-label text-muted">{% trans "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>
<h2 class="text-lg font-bold text-gray-900">{% trans "Source Information" %}</h2>
<p class="text-sm text-gray-500">{% trans "Basic integration details" %}</p>
</div>
</div>
</div>
<div class="col-md-4">
<!-- API Credentials -->
<div class="card mb-4">
<div class="card-header">
<h6 class="mb-0">{% trans "API Credentials" %}</h6>
<div class="p-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="tag" class="w-4 h-4 text-gray-400"></i>
{% trans "Name" %}
</label>
<div class="text-gray-900 font-medium">{{ source.name }}</div>
</div>
<div id="api-credentials" class="card-body">
<div class="mb-3">
<label class="form-label text-muted">{% trans "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="{% trans "Copy to clipboard" %}">
<i class="fas fa-copy"></i>
</button>
</div>
<div>
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="server" class="w-4 h-4 text-gray-400"></i>
{% trans "Type" %}
</label>
<span class="inline-block text-[10px] font-bold uppercase tracking-wide px-2.5 py-1 rounded-full bg-temple-red/10 text-temple-red border border-temple-red">
{{ source.get_source_type_display }}
</span>
</div>
</div>
{% if source.description %}
<div class="mt-6">
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="align-left" class="w-4 h-4 text-gray-400"></i>
{% trans "Description" %}
</label>
<div class="text-gray-700 bg-gray-50 rounded-xl p-4">{{ source.description|linebreaks }}</div>
</div>
{% endif %}
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="mail" class="w-4 h-4 text-gray-400"></i>
{% trans "Contact Email" %}
</label>
<div>
{% if source.contact_email %}
<a href="mailto:{{ source.contact_email }}" class="text-temple-red hover:text-[#7a1a29] transition-colors">{{ source.contact_email }}</a>
{% else %}
<span class="text-gray-400">{% trans "Not specified" %}</span>
{% endif %}
</div>
<div class="mb-3">
<label class="form-label text-muted">{% trans "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="{% trans "Copy to clipboard" %}">
<i class="fas fa-copy"></i>
</button>
</div>
</div>
<div class="text-end">
<a hx-post="{% url 'generate_api_keys' source.pk %}" hx-target="#api-credentials" hx-select="#api-credentials" hx-swap="outerHTML" class="btn btn-main-action btn-sm">
<i class="fas fa-key"></i> {% trans "Generate New Keys" %}
</a>
</div>
<div>
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="phone" class="w-4 h-4 text-gray-400"></i>
{% trans "Contact Phone" %}
</label>
<div>
{% if source.contact_phone %}
<span class="text-gray-700">{{ source.contact_phone }}</span>
{% else %}
<span class="text-gray-400">{% trans "Not specified" %}</span>
{% endif %}
</div>
</div>
</div>
<!-- Statistics -->
<div class="card mb-4">
<div class="card-header">
<h6 class="mb-0">{% trans "Integration Statistics" %}</h6>
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="power" class="w-4 h-4 text-gray-400"></i>
{% trans "Status" %}
</label>
<div id="source-status">
{% if source.is_active %}
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-emerald-100 text-emerald-700 font-semibold text-sm">
<i data-lucide="check-circle-2" class="w-4 h-4"></i> {% trans "Active" %}
</span>
{% else %}
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-gray-100 text-gray-600 font-semibold text-sm">
<i data-lucide="x-circle" class="w-4 h-4"></i> {% trans "Inactive" %}
</span>
{% endif %}
</div>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label text-muted">{% trans "Total API Calls" %}</label>
<div class="h5 mb-0">{{ total_logs }}</div>
<div>
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="shield" class="w-4 h-4 text-gray-400"></i>
{% trans "Requires Authentication" %}
</label>
<div>
{% if source.requires_auth %}
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-yellow-100 text-yellow-700 font-semibold text-sm">
<i data-lucide="lock" class="w-4 h-4"></i> {% trans "Yes" %}
</span>
{% else %}
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-gray-100 text-gray-600 font-semibold text-sm">
<i data-lucide="unlock" class="w-4 h-4"></i> {% trans "No" %}
</span>
{% endif %}
</div>
<div class="mb-3">
<label class="form-label text-muted">{% trans "Successful Calls" %}</label>
<div class="h5 mb-0 text-success">{{ successful_logs }}</div>
</div>
</div>
{% if source.webhook_url %}
<div class="mt-6">
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="link" class="w-4 h-4 text-gray-400"></i>
{% trans "Webhook URL" %}
</label>
<div class="bg-gray-50 rounded-xl p-4">
<code class="text-sm text-temple-red break-all">{{ source.webhook_url }}</code>
</div>
<div class="mb-3">
<label class="form-label text-muted">{% trans "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">{% trans "Success Rate" %}</label>
<div class="h5 mb-0">
{% widthratio successful_logs total_logs 100 %}%
</div>
</div>
{% endif %}
</div>
{% endif %}
{% if source.api_timeout %}
<div class="mt-6">
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="clock" class="w-4 h-4 text-gray-400"></i>
{% trans "API Timeout" %}
</label>
<div class="text-gray-700">{{ source.api_timeout }} {% trans "seconds" %}</div>
</div>
{% endif %}
{% if source.notes %}
<div class="mt-6">
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="file-text" class="w-4 h-4 text-gray-400"></i>
{% trans "Notes" %}
</label>
<div class="text-gray-700 bg-gray-50 rounded-xl p-4">{{ source.notes|linebreaks }}</div>
</div>
{% endif %}
<div class="mt-6 grid grid-cols-1 md:grid-cols-2 gap-6 pt-6 border-t border-gray-100">
<div>
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="calendar-plus" class="w-4 h-4 text-gray-400"></i>
{% trans "Created" %}
</label>
<div class="text-gray-700">{{ source.created_at|date:"M d, Y H:i" }}</div>
</div>
<div>
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="refresh-cw" class="w-4 h-4 text-gray-400"></i>
{% trans "Last Updated" %}
</label>
<div class="text-gray-700">{{ source.updated_at|date:"M d, Y H:i" }}</div>
</div>
</div>
</div>
</div>
<!-- Integration Logs -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="mb-0">{% trans "Recent Integration Logs" %}</h6>
<small class="text-muted">{% trans "Last 10 logs" %}</small>
<!-- Integration Logs Card -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white flex justify-between items-center">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-temple-red/10 flex items-center justify-center">
<i data-lucide="list" class="w-5 h-5 text-temple-red"></i>
</div>
<div>
<h2 class="text-lg font-bold text-gray-900">{% trans "Recent Integration Logs" %}</h2>
<p class="text-sm text-gray-500">{% trans "Last 10 logs" %}</p>
</div>
</div>
</div>
<div class="card-body">
<div class="p-6">
{% if integration_logs %}
<div class="table-responsive">
<table class="table table-sm">
<thead class="table-light">
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th>{% trans "Timestamp" %}</th>
<th>{% trans "Method" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Response Time" %}</th>
<th>{% trans "Details" %}</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-bold text-gray-700 tracking-wider whitespace-nowrap">{% trans "Timestamp" %}</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-bold text-gray-700 tracking-wider whitespace-nowrap">{% trans "Method" %}</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-bold text-gray-700 tracking-wider whitespace-nowrap">{% trans "Status" %}</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-bold text-gray-700 tracking-wider whitespace-nowrap">{% trans "Response Time" %}</th>
<th scope="col" class="px-4 py-3 text-left text-xs font-bold text-gray-700 tracking-wider whitespace-nowrap">{% trans "Details" %}</th>
</tr>
</thead>
<tbody>
<tbody class="bg-white divide-y divide-gray-100">
{% for log in integration_logs %}
<tr>
<td>
<tr class="hover:bg-gray-50 transition-colors">
<td class="px-4 py-4 text-sm text-gray-700">
<small>{{ log.created_at|date:"M d, Y H:i:s" }}</small>
</td>
<td>
<span class="badge bg-secondary">{{ log.method }}</span>
<td class="px-4 py-4">
<span class="inline-block text-[10px] font-bold uppercase tracking-wide px-2 py-0.5 rounded bg-gray-100 text-gray-600">
{{ log.method }}
</span>
</td>
<td>
<td class="px-4 py-4">
{% if log.status_code >= 200 and log.status_code < 300 %}
<span class="badge bg-success">{{ log.status_code }}</span>
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-emerald-100 text-emerald-700 font-semibold text-xs">
<i data-lucide="check-circle" class="w-3 h-3"></i> {{ log.status_code }}
</span>
{% elif log.status_code >= 400 %}
<span class="badge bg-danger">{{ log.status_code }}</span>
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-100 text-red-700 font-semibold text-xs">
<i data-lucide="x-circle" class="w-3 h-3"></i> {{ log.status_code }}
</span>
{% else %}
<span class="badge bg-warning">{{ log.status_code }}</span>
<span class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-700 font-semibold text-xs">
<i data-lucide="alert-circle" class="w-3 h-3"></i> {{ log.status_code }}
</span>
{% endif %}
</td>
<td>
<td class="px-4 py-4 text-sm text-gray-700">
{% if log.response_time_ms %}
<small>{{ log.response_time_ms }}ms</small>
{% else %}
<span class="text-muted">-</span>
<span class="text-gray-400">-</span>
{% endif %}
</td>
<td>
<td class="px-4 py-4">
{% 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 type="button"
class="inline-flex items-center justify-center w-8 h-8 rounded-lg border border-temple-red/30 text-temple-red hover:bg-temple-red hover:text-white transition"
onclick="openLogDetailModal({{ log.id }})"
title="{% trans 'View details' %}">
<i data-lucide="eye" class="w-4 h-4"></i>
</button>
{% else %}
<span class="text-muted">{{ _("No data") }}</span>
<span class="text-gray-400 text-sm">{% trans "No data" %}</span>
{% endif %}
</td>
</tr>
@ -303,9 +301,116 @@
</table>
</div>
{% else %}
<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 class="text-center py-12">
<i data-lucide="clipboard-list" class="w-16 h-16 text-gray-300 mx-auto mb-4"></i>
<h3 class="text-lg font-semibold text-gray-900 mb-2">{% trans "No integration logs found" %}</h3>
<p class="text-gray-500">{% trans "There are no logs recorded for this source yet." %}</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Sidebar Column -->
<div class="space-y-6">
<!-- API Credentials Card -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-temple-red/10 flex items-center justify-center">
<i data-lucide="key" class="w-5 h-5 text-temple-red"></i>
</div>
<div>
<h2 class="text-lg font-bold text-gray-900">{% trans "API Credentials" %}</h2>
<p class="text-sm text-gray-500">{% trans "API access keys" %}</p>
</div>
</div>
</div>
<div id="api-credentials" class="p-6 space-y-4">
<div>
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="key" class="w-4 h-4 text-gray-400"></i>
{% trans "API Key" %}
</label>
<div class="flex">
<input type="text" value="{{ source.api_key }}" readonly
class="flex-1 px-4 py-2.5 border border-gray-200 border-r-0 rounded-l-xl bg-gray-50 text-sm focus:outline-none">
<button type="button"
hx-post="{% url 'copy_to_clipboard' %}"
hx-vals='{"text": "{{ source.api_key }}"}'
title="{% trans 'Copy to clipboard' %}"
class="inline-flex items-center justify-center px-3 border border-gray-200 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded-r-xl transition">
<i data-lucide="copy" class="w-4 h-4"></i>
</button>
</div>
</div>
<div>
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="lock" class="w-4 h-4 text-gray-400"></i>
{% trans "API Secret" %}
</label>
<div class="flex">
<input type="password" value="{{ source.api_secret }}" readonly id="api-secret"
class="flex-1 px-4 py-2.5 border border-gray-200 rounded-l-xl bg-gray-50 text-sm focus:outline-none">
<button type="button" onclick="toggleSecretVisibility()"
class="inline-flex items-center justify-center px-3 border border-l-0 border-r-0 border-gray-200 bg-gray-100 hover:bg-gray-200 text-gray-600 transition">
<i data-lucide="eye" id="secret-toggle-icon" class="w-4 h-4"></i>
</button>
<button type="button"
hx-post="{% url 'copy_to_clipboard' %}"
hx-vals='{"text": "{{ source.api_secret }}"}'
title="{% trans 'Copy to clipboard' %}"
class="inline-flex items-center justify-center px-3 border border-l-0 border-gray-200 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded-r-xl transition">
<i data-lucide="copy" class="w-4 h-4"></i>
</button>
</div>
</div>
<div class="pt-2">
<a hx-post="{% url 'generate_api_keys' source.pk %}"
hx-target="#api-credentials"
hx-select="#api-credentials"
hx-swap="outerHTML"
class="inline-flex items-center justify-center gap-2 w-full px-5 py-3 bg-yellow-500 text-white rounded-xl font-semibold shadow hover:shadow-lg transition">
<i data-lucide="refresh-cw" class="w-5 h-5"></i>
{% trans "Generate New Keys" %}
</a>
</div>
</div>
</div>
<!-- Statistics Card -->
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 bg-gradient-to-r from-gray-50 to-white">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-temple-red/10 flex items-center justify-center">
<i data-lucide="bar-chart-2" class="w-5 h-5 text-temple-red"></i>
</div>
<div>
<h2 class="text-lg font-bold text-gray-900">{% trans "Integration Statistics" %}</h2>
<p class="text-sm text-gray-500">{% trans "API call metrics" %}</p>
</div>
</div>
</div>
<div class="p-6 space-y-4">
<div>
<label class="text-sm font-semibold text-gray-700 mb-2">{% trans "Total API Calls" %}</label>
<div class="text-2xl font-bold text-gray-900">{{ total_logs }}</div>
</div>
<div>
<label class="text-sm font-semibold text-gray-700 mb-2">{% trans "Successful Calls" %}</label>
<div class="text-2xl font-bold text-emerald-600">{{ successful_logs }}</div>
</div>
<div>
<label class="text-sm font-semibold text-gray-700 mb-2">{% trans "Failed Calls" %}</label>
<div class="text-2xl font-bold text-red-600">{{ failed_logs }}</div>
</div>
{% if total_logs > 0 %}
<div class="pt-4 border-t border-gray-100">
<label class="text-sm font-semibold text-gray-700 mb-2">{% trans "Success Rate" %}</label>
<div class="text-2xl font-bold text-gray-900">
{% widthratio successful_logs total_logs 100 %}%
</div>
</div>
{% endif %}
</div>
@ -314,68 +419,113 @@
</div>
</div>
<!-- Log Detail Modals -->
<!-- Log Detail Modal -->
{% 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">{% trans "Integration Log Details" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
<div id="logDetailModal{{ log.id }}" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden">
<div class="flex items-center justify-center min-h-screen p-4">
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden">
<div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center bg-gradient-to-r from-gray-50 to-white">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-temple-red/10 flex items-center justify-center">
<i data-lucide="file-text" class="w-5 h-5 text-temple-red"></i>
</div>
<div>
<h3 class="text-lg font-bold text-gray-900">{% trans "Integration Log Details" %}</h3>
<p class="text-sm text-gray-500">ID: {{ log.id }}</p>
</div>
</div>
<button type="button" onclick="closeLogDetailModal({{ log.id }})"
class="inline-flex items-center justify-center w-10 h-10 rounded-lg text-gray-400 hover:bg-gray-100 transition">
<i data-lucide="x" class="w-5 h-5"></i>
</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 class="p-6 overflow-y-auto max-h-[calc(90vh-80px)]">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div>
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="calendar" class="w-4 h-4 text-gray-400"></i>
{% trans "Timestamp" %}
</label>
<div class="text-gray-700 bg-gray-50 rounded-xl p-3">{{ log.created_at|date:"M d, Y H:i:s" }}</div>
</div>
<div class="col-md-6">
<strong>{{ _("Method:") }}:</strong><br>
<span class="badge bg-secondary">{{ log.method }}</span>
<div>
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="git-commit" class="w-4 h-4 text-gray-400"></i>
{% trans "Method" %}
</label>
<span class="inline-block text-[10px] font-bold uppercase tracking-wide px-2 py-0.5 rounded bg-gray-100 text-gray-600">
{{ log.method }}
</span>
</div>
</div>
<hr>
<div class="row">
<div class="col-md-6">
<strong>{{ _("Status Code:") }}:</strong><br>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
<div>
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="badge-check" class="w-4 h-4 text-gray-400"></i>
{% trans "Status Code" %}
</label>
{% if log.status_code >= 200 and log.status_code < 300 %}
<span class="badge bg-success">{{ log.status_code }}</span>
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-emerald-100 text-emerald-700 font-semibold text-sm">
<i data-lucide="check-circle-2" class="w-4 h-4"></i> {{ log.status_code }}
</span>
{% elif log.status_code >= 400 %}
<span class="badge bg-danger">{{ log.status_code }}</span>
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-red-100 text-red-700 font-semibold text-sm">
<i data-lucide="x-circle" class="w-4 h-4"></i> {{ log.status_code }}
</span>
{% else %}
<span class="badge bg-warning">{{ log.status_code }}</span>
<span class="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-yellow-100 text-yellow-700 font-semibold text-sm">
<i data-lucide="alert-circle" class="w-4 h-4"></i> {{ 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>
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="zap" class="w-4 h-4 text-gray-400"></i>
{% trans "Response Time" %}
</label>
<div class="text-gray-700 bg-gray-50 rounded-xl p-3">
{% if log.response_time_ms %}
{{ log.response_time_ms }}ms
{% else %}
-
{% endif %}
</div>
</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 class="mb-6">
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="upload" class="w-4 h-4 text-gray-400"></i>
{% trans "Request Data" %}
</label>
<pre class="bg-gray-900 text-gray-100 rounded-xl p-4 overflow-x-auto text-sm"><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 class="mb-6">
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="download" class="w-4 h-4 text-gray-400"></i>
{% trans "Response Data" %}
</label>
<pre class="bg-gray-900 text-gray-100 rounded-xl p-4 overflow-x-auto text-sm"><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 class="mb-6">
<label class="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-2">
<i data-lucide="alert-triangle" class="w-4 h-4 text-gray-400"></i>
{% trans "Error Message" %}
</label>
<div class="bg-red-50 border border-red-200 rounded-xl p-4 text-red-700">{{ 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 class="px-6 py-4 border-t border-gray-100 bg-gray-50 flex justify-end">
<button type="button" onclick="closeLogDetailModal({{ log.id }})"
class="inline-flex items-center gap-2 px-5 py-3 border-2 border-gray-200 text-gray-700 rounded-xl font-semibold hover:border-gray-300 transition">
<i data-lucide="x" class="w-5 h-5"></i>
{% trans "Close" %}
</button>
</div>
</div>
</div>
</div>
@ -385,46 +535,90 @@
{% block customJS %}
<script>
function toggleSecretVisibility() {
const secretInput = document.getElementById('api-secret');
const toggleIcon = document.getElementById('secret-toggle-icon');
// Initialize Lucide icons
lucide.createIcons();
if (secretInput.type === 'password') {
secretInput.type = 'text';
toggleIcon.classList.remove('fa-eye');
toggleIcon.classList.add('fa-eye-slash');
} else {
secretInput.type = 'password';
toggleIcon.classList.remove('fa-eye-slash');
toggleIcon.classList.add('fa-eye');
// Toggle Secret Visibility
function toggleSecretVisibility() {
const secretInput = document.getElementById('api-secret');
const toggleIcon = document.getElementById('secret-toggle-icon');
if (secretInput.type === 'password') {
secretInput.type = 'text';
toggleIcon.setAttribute('data-lucide', 'eye-off');
} else {
secretInput.type = 'password';
toggleIcon.setAttribute('data-lucide', 'eye');
}
lucide.createIcons();
}
}
// 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);
// Open Log Detail Modal
function openLogDetailModal(logId) {
const modal = document.getElementById('logDetailModal' + logId);
if (modal) {
modal.classList.remove('hidden');
document.body.style.overflow = 'hidden';
}
}
});
// 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);
// Close Log Detail Modal
function closeLogDetailModal(logId) {
const modal = document.getElementById('logDetailModal' + logId);
if (modal) {
modal.classList.add('hidden');
document.body.style.overflow = '';
}
}
});
// Close modal on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
document.querySelectorAll('[id^="logDetailModal"]').forEach(modal => {
if (!modal.classList.contains('hidden')) {
modal.classList.add('hidden');
document.body.style.overflow = '';
}
});
}
});
// Close modal when clicking outside
document.querySelectorAll('[id^="logDetailModal"]').forEach(modal => {
modal.addEventListener('click', (e) => {
if (e.target === modal) {
modal.classList.add('hidden');
document.body.style.overflow = '';
}
});
});
// 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 originalHTML = button.innerHTML;
button.innerHTML = '<i data-lucide="check" class="w-4 h-4"></i>';
button.classList.remove('bg-gray-100', 'hover:bg-gray-200', 'text-gray-600', 'border-gray-200');
button.classList.add('bg-emerald-500', 'text-white', 'border-emerald-500');
lucide.createIcons();
setTimeout(() => {
button.innerHTML = originalHTML;
button.classList.remove('bg-emerald-500', 'text-white', 'border-emerald-500');
button.classList.add('bg-gray-100', 'hover:bg-gray-200', 'text-gray-600', 'border-gray-200');
lucide.createIcons();
}, 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"]')) {
setTimeout(() => {
window.location.reload();
}, 500);
}
});
</script>
{% endblock %}
{% endblock %}

View File

@ -167,6 +167,77 @@
color: #dc2626;
font-weight: 500;
}
/* Custom Checkbox Styling */
.custom-checkbox {
position: relative;
display: inline-flex;
align-items: center;
cursor: pointer;
user-select: none;
}
.custom-checkbox input {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.checkbox-visual {
position: relative;
width: 1.375rem;
height: 1.375rem;
background-color: white;
border: 2px solid #d1d5db;
border-radius: 0.5rem;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
flex-shrink: 0;
}
.custom-checkbox:hover .checkbox-visual {
border-color: #9d2235;
box-shadow: 0 0 0 3px rgba(157, 34, 53, 0.1);
}
.custom-checkbox input:checked ~ .checkbox-visual {
background-color: #9d2235;
border-color: #9d2235;
background-image: linear-gradient(135deg, #9d2235 0%, #8b1228 100%);
box-shadow: 0 4px 12px rgba(157, 34, 53, 0.3);
transform: scale(1.02);
}
.custom-checkbox input:focus-visible ~ .checkbox-visual {
outline: 2px solid #9d2235;
outline-offset: 2px;
box-shadow: 0 0 0 4px rgba(157, 34, 53, 0.2);
}
.checkbox-checkmark {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%) scale(0);
transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.custom-checkbox input:checked ~ .checkbox-visual .checkbox-checkmark {
transform: translate(-50%, -50%) scale(1);
}
.checkbox-label {
margin-left: 0.75rem;
color: #374151;
font-size: 0.95rem;
font-weight: 500;
transition: color 0.2s ease;
}
.custom-checkbox:hover .checkbox-label {
color: #9d2235;
}
</style>
{% endblock %}
@ -421,10 +492,18 @@
<i data-lucide="power" class="w-4 h-4 text-gray-400"></i>
{% trans "Active Status" %}
</label>
<div class="flex items-center gap-3">
{{ form.is_active.as_widget }}
<span class="text-sm text-gray-600">{% trans "Enable this integration source" %}</span>
</div>
<label class="custom-checkbox" for="{{ form.is_active.id_for_label }}">
<input type="checkbox"
name="{{ form.is_active.name }}"
id="{{ form.is_active.id_for_label }}"
{% if form.is_active.value %}checked{% endif %}>
<span class="checkbox-visual">
<svg class="checkbox-checkmark w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
</span>
<span class="checkbox-label">{% trans "Enable this integration source" %}</span>
</label>
{% if form.is_active.help_text %}
<p class="form-helptext">{{ form.is_active.help_text }}</p>
{% endif %}
@ -535,11 +614,6 @@
sourceTypeSelect.classList.add('form-select');
}
// Apply Tailwind classes to is_active checkbox
const isActiveCheckbox = document.querySelector('input[name="is_active"]');
if (isActiveCheckbox) {
isActiveCheckbox.classList.add('w-5', 'h-5', 'text-temple-red', 'rounded', 'border-gray-300', 'focus:ring-temple-red', 'focus:border-temple-red');
}
// Form Validation
const form = document.getElementById('source-form');