after updating the stages
This commit is contained in:
parent
dba9af100a
commit
b50e48f510
1011
PRODUCTION_SETUP.md
Normal file
1011
PRODUCTION_SETUP.md
Normal file
File diff suppressed because it is too large
Load Diff
@ -1049,6 +1049,10 @@ class Application(Base):
|
||||
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"""
|
||||
now = timezone.now()
|
||||
|
||||
@ -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>
|
||||
'''
|
||||
|
||||
|
||||
@ -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>
|
||||
{% block title %}{% trans "Email Addresses" %} - {{ block.super }}{% endblock %}
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'temple-red': '#9d2235',
|
||||
'temple-dark': '#1a1a1a',
|
||||
'temple-cream': '#f8f7f2',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto py-6 px-4">
|
||||
|
||||
<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">
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
<!-- 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>
|
||||
<h1 class="text-2xl font-bold text-gray-900 mb-2">{% trans "Email Addresses" %}</h1>
|
||||
<p class="text-sm text-gray-500">
|
||||
{% 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,31 +67,45 @@
|
||||
{% 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>
|
||||
<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>
|
||||
<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="px-4 py-2 text-sm font-medium text-temple-red border border-temple-red rounded-lg hover:bg-red-50 transition">
|
||||
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>
|
||||
@ -94,7 +117,8 @@
|
||||
{% 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">
|
||||
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>
|
||||
@ -106,7 +130,8 @@
|
||||
{% 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">
|
||||
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>
|
||||
@ -116,47 +141,96 @@
|
||||
{% 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 -->
|
||||
<!-- Add Email Card -->
|
||||
{% 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">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</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 %}
|
||||
@ -1,64 +1,62 @@
|
||||
{% 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>
|
||||
{% block title %}{% trans "Change Password" %} - {{ block.super }}{% endblock %}
|
||||
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'temple-red': '#9d2235',
|
||||
'temple-dark': '#1a1a1a',
|
||||
'temple-cream': '#f8f7f2',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% block content %}
|
||||
<div class="max-w-lg mx-auto py-6 px-4">
|
||||
|
||||
<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">
|
||||
<!-- 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>
|
||||
|
||||
<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>
|
||||
<!-- 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>
|
||||
<h1 class="text-2xl font-bold text-gray-900">{% trans "Change Password" %}</h1>
|
||||
<p class="text-sm text-gray-500 mt-2">
|
||||
|
||||
<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>
|
||||
<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>
|
||||
@ -67,32 +65,73 @@
|
||||
{% if field.errors %}
|
||||
<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 %}
|
||||
|
||||
<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="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="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');
|
||||
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');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
// 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>
|
||||
{% endblock %}
|
||||
@ -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">
|
||||
© {% now "Y" %} KAAUH ATS. {% trans "All rights reserved." %}
|
||||
© {% 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 = '';
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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 %}
|
||||
|
||||
@ -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') {
|
||||
// Reinitialize Lucide icons after content loads
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -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" %}
|
||||
<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>
|
||||
<p class="text-muted mb-0">
|
||||
{% trans "Assign a job to an external hiring agency" %}
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<div class="kaauh-card">
|
||||
<form method="post" novalidate>
|
||||
<!-- 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="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>
|
||||
<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>
|
||||
{# Wrapper Div for styling consistency (Assumes agency is a SELECT field) #}
|
||||
<div class="kaauh-field-control">
|
||||
{{ form.agency|attr:'class:form-select' }}
|
||||
</div>
|
||||
<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-danger small mt-1">
|
||||
{% for error in form.agency.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="{{ form.job.id_for_label }}" class="form-label">
|
||||
{{ form.job.label }} <span class="text-danger">*</span>
|
||||
<!-- 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>
|
||||
{# Wrapper Div for styling consistency (Assumes job is a SELECT field) #}
|
||||
<div class="kaauh-field-control">
|
||||
{{ form.job|attr:'class:form-select' }}
|
||||
</div>
|
||||
<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-danger small mt-1">
|
||||
{% for error in form.job.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
<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>
|
||||
</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>
|
||||
<!-- 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>
|
||||
{# 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>
|
||||
<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-danger small mt-1">
|
||||
{% for error in form.max_candidates.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
<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 %}
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
{# 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>
|
||||
<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-danger small mt-1">
|
||||
{% for error in form.deadline_date.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
<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 %}
|
||||
<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">
|
||||
<!-- 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>
|
||||
{# 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>
|
||||
<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-danger small mt-1">
|
||||
{% for error in form.admin_notes.errors %}{{ error }}{% endfor %}
|
||||
</div>
|
||||
<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 %}
|
||||
<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" %}
|
||||
<!-- 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="btn btn-main-action">
|
||||
<i class="fas fa-save me-1"></i> {% trans "Create Assignment" %}
|
||||
<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>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block customJS %}
|
||||
</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 }}');
|
||||
// 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 }}');
|
||||
|
||||
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);
|
||||
// Agency validation
|
||||
if (agencySelect && !agencySelect.value) {
|
||||
e.preventDefault();
|
||||
alert("{% trans 'Please select an agency.' %}");
|
||||
agencySelect.focus();
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 %}
|
||||
@ -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">
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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 -->
|
||||
<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>
|
||||
{{ 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>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="flex 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">
|
||||
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>
|
||||
<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">
|
||||
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-4 py-2 bg-white/10 hover:bg-white/20 rounded-lg text-white font-medium text-sm transition backdrop-blur-sm touch-target">
|
||||
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>
|
||||
</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">
|
||||
<!-- 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="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">
|
||||
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,101 +295,54 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Reinitialize Lucide icons for dynamically added content
|
||||
// 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...' %}
|
||||
`;
|
||||
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 }}');
|
||||
|
||||
// Basic validation
|
||||
const name = document.getElementById('{{ form.name.id_for_label }}');
|
||||
if (name && !name.value.trim()) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Email validation helper
|
||||
function isValidEmail(email) {
|
||||
// Email validation
|
||||
if (emailInput && emailInput.value.trim()) {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
// URL validation helper
|
||||
function isValidURL(url) {
|
||||
try {
|
||||
new URL(url);
|
||||
return true;
|
||||
} catch (_) {
|
||||
if (!emailRegex.test(emailInput.value.trim())) {
|
||||
e.preventDefault();
|
||||
alert("{% trans 'Please enter a valid email address.' %}");
|
||||
emailInput.focus();
|
||||
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) {
|
||||
// Website validation
|
||||
if (websiteInput && websiteInput.value.trim()) {
|
||||
try {
|
||||
new URL(websiteInput.value.trim());
|
||||
} catch (_) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '{% trans "You have unsaved changes. Are you sure you want to leave?" %}';
|
||||
return e.returnValue;
|
||||
alert("{% trans 'Please enter a valid website URL.' %}");
|
||||
websiteInput.focus();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (form) {
|
||||
form.addEventListener('submit', function() {
|
||||
formChanged = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -1,47 +1,77 @@
|
||||
{% 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>
|
||||
<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>
|
||||
<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">
|
||||
<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="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' %}">
|
||||
<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>
|
||||
</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">
|
||||
{% 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" enctype="multipart/form-data" id="application-form" class="space-y-6">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<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 }}
|
||||
@ -49,44 +79,81 @@
|
||||
</label>
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<p class="text-sm text-gray-500 mt-1">{{ field.help_text }}</p>
|
||||
<p class="text-xs 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>
|
||||
<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">​</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" %}
|
||||
<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="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 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
|
||||
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) {
|
||||
@ -257,21 +344,30 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
}
|
||||
});
|
||||
|
||||
// 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');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
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 selected>----------</option>
|
||||
<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="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 disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<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">
|
||||
<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" %}
|
||||
</button>
|
||||
</form>
|
||||
</a>
|
||||
|
||||
<div class="hidden sm:block w-px h-7 bg-gray-300"></div>
|
||||
|
||||
<!-- 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 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,7 +179,6 @@
|
||||
<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' %}')"
|
||||
@ -200,39 +186,11 @@
|
||||
{{ application.interview_status }}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -1,18 +1,28 @@
|
||||
<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 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">
|
||||
<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 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>
|
||||
@ -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">
|
||||
<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>
|
||||
{% else %}
|
||||
--
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<script>
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
</script>
|
||||
@ -1,16 +1,22 @@
|
||||
{% 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>
|
||||
|
||||
@ -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 %}
|
||||
{% endif %}
|
||||
</td>
|
||||
|
||||
<script>
|
||||
if (typeof lucide !== 'undefined') {
|
||||
lucide.createIcons();
|
||||
}
|
||||
</script>
|
||||
@ -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 %}
|
||||
|
||||
<!-- 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>
|
||||
{{ 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="btn btn-outline-{{ source.is_active|yesno:'warning,success' }}"
|
||||
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 class="fas fa-{{ source.is_active|yesno:'pause,play' }}"></i>
|
||||
{{ source.is_active|yesno:'Deactivate,Activate' }}
|
||||
<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_delete' source.pk %}" class="btn btn-outline-danger">
|
||||
<i class="fas fa-trash"></i> {% trans "Delete" %}
|
||||
<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>
|
||||
<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="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>
|
||||
<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="mb-3">
|
||||
<label class="form-label text-muted">{{ _("Description") }}</label>
|
||||
<div>{{ source.description|linebreaks }}</div>
|
||||
<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="row">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">{{ _("Contact Email") }}</label>
|
||||
<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 }}">{{ source.contact_email }}</a>
|
||||
<a href="mailto:{{ source.contact_email }}" class="text-temple-red hover:text-[#7a1a29] transition-colors">{{ source.contact_email }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">{{ _("Not specified") }}</span>
|
||||
<span class="text-gray-400">{% trans "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>
|
||||
<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 %}
|
||||
{{ source.contact_phone }}
|
||||
<span class="text-gray-700">{{ source.contact_phone }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">{{ _("Not specified") }}</span>
|
||||
<span class="text-gray-400">{% trans "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 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="badge bg-success">{{ _("Active") }}</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> {% trans "Active" %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ _("Inactive") }}</span>
|
||||
<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>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">{% trans "Requires Authentication" %}</label>
|
||||
<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="badge bg-warning">{% trans "Yes" %}</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="lock" class="w-4 h-4"></i> {% trans "Yes" %}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{% trans "No" %}</span>
|
||||
<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>
|
||||
</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 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>
|
||||
{% 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 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="mb-3">
|
||||
<label class="form-label text-muted">{{ _("Notes") }}</label>
|
||||
<div>{{ source.notes|linebreaks }}</div>
|
||||
<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="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 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>
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label class="form-label text-muted">{{ _("Last Updated") }}</label>
|
||||
<div>{{ source.updated_at|date:"M d, Y H:i" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<!-- API Credentials -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">{% trans "API Credentials" %}</h6>
|
||||
</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>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div class="card mb-4">
|
||||
<div class="card-header">
|
||||
<h6 class="mb-0">{% trans "Integration Statistics" %}</h6>
|
||||
</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>
|
||||
<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 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>
|
||||
<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 class="card-body">
|
||||
<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="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,41 +419,72 @@
|
||||
</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 class="modal-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<strong>{{ _("Timestamp:") }}:</strong><br>
|
||||
{{ log.created_at|date:"M d, Y H:i:s" }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<strong>{{ _("Method:") }}:</strong><br>
|
||||
<span class="badge bg-secondary">{{ log.method }}</span>
|
||||
<div>
|
||||
<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>
|
||||
<hr>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<strong>{{ _("Status Code:") }}:</strong><br>
|
||||
<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="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>
|
||||
<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>
|
||||
<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>
|
||||
<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 %}
|
||||
@ -356,26 +492,40 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="mb-3">
|
||||
<strong>{{ _("Request Data:") }}:</strong>
|
||||
<pre class="bg-light p-2 rounded"><code>{{ log.request_data|pprint }}</code></pre>
|
||||
</div>
|
||||
<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() {
|
||||
// Initialize Lucide icons
|
||||
lucide.createIcons();
|
||||
|
||||
// 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.classList.remove('fa-eye');
|
||||
toggleIcon.classList.add('fa-eye-slash');
|
||||
toggleIcon.setAttribute('data-lucide', 'eye-off');
|
||||
} else {
|
||||
secretInput.type = 'password';
|
||||
toggleIcon.classList.remove('fa-eye-slash');
|
||||
toggleIcon.classList.add('fa-eye');
|
||||
toggleIcon.setAttribute('data-lucide', 'eye');
|
||||
}
|
||||
lucide.createIcons();
|
||||
}
|
||||
}
|
||||
|
||||
// Handle HTMX copy to clipboard feedback
|
||||
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||
// Open Log Detail Modal
|
||||
function openLogDetailModal(logId) {
|
||||
const modal = document.getElementById('logDetailModal' + logId);
|
||||
if (modal) {
|
||||
modal.classList.remove('hidden');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
}
|
||||
|
||||
// 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 originalIcon = button.innerHTML;
|
||||
button.innerHTML = '<i class="fas fa-check"></i>';
|
||||
button.classList.remove('btn-outline-secondary');
|
||||
button.classList.add('btn-success');
|
||||
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 = originalIcon;
|
||||
button.classList.remove('btn-success');
|
||||
button.classList.add('btn-outline-secondary');
|
||||
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) {
|
||||
// Auto-refresh after status toggle
|
||||
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||
if (evt.detail.successful && evt.detail.target.matches('[hx-post*="toggle_source_status"]')) {
|
||||
// Reload the page after a short delay to show updated status
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 500);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@ -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');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user