update the form builder

This commit is contained in:
ismail 2025-10-07 16:23:58 +03:00
parent 7f23cc18fb
commit c5c7963df5
27 changed files with 1249 additions and 420 deletions

View File

@ -212,8 +212,6 @@ CELERY_RESULT_SERIALIZER = 'json'
CELERY_TIMEZONE = 'UTC' CELERY_TIMEZONE = 'UTC'
LINKEDIN_CLIENT_ID = '867jwsiyem1504' LINKEDIN_CLIENT_ID = '867jwsiyem1504'
LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw==' LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='
LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/' LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/'

View File

@ -4,7 +4,7 @@ from crispy_forms.helper import FormHelper
from django.core.validators import URLValidator from django.core.validators import URLValidator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from crispy_forms.layout import Layout, Submit, HTML, Div, Field from crispy_forms.layout import Layout, Submit, HTML, Div, Field
from .models import ZoomMeeting, Candidate,TrainingMaterial,JobPosting from .models import ZoomMeeting, Candidate,TrainingMaterial,JobPosting,FormTemplate
class CandidateForm(forms.ModelForm): class CandidateForm(forms.ModelForm):
class Meta: class Meta:
@ -366,3 +366,46 @@ class JobPostingForm(forms.ModelForm):
# 'Job description is required for active jobs.') # 'Job description is required for active jobs.')
return cleaned_data return cleaned_data
class FormTemplateForm(forms.ModelForm):
"""Form for creating form templates"""
class Meta:
model = FormTemplate
fields = ['job','name', 'description', 'is_active']
labels = {
'job': _('Job'),
'name': _('Template Name'),
'description': _('Description'),
'is_active': _('Active'),
}
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Enter template name'),
'required': True
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': _('Enter template description (optional)')
}),
'is_active': forms.CheckboxInput(attrs={
'class': 'form-check-input'
})
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.form_method = 'post'
self.helper.form_class = 'form-horizontal'
self.helper.label_class = 'col-md-3'
self.helper.field_class = 'col-md-9'
self.helper.layout = Layout(
Field('job', css_class='form-control'),
Field('name', css_class='form-control'),
Field('description', css_class='form-control'),
Field('is_active', css_class='form-check-input'),
Submit('submit', _('Create Template'), css_class='btn btn-primary mt-3')
)

View File

@ -72,8 +72,6 @@ class LinkedInService:
def register_image_upload(self, person_urn): def register_image_upload(self, person_urn):
"""Step 1: Register image upload with LinkedIn""" """Step 1: Register image upload with LinkedIn"""
url = "https://api.linkedin.com/v2/assets?action=registerUpload" url = "https://api.linkedin.com/v2/assets?action=registerUpload"
@ -261,4 +259,3 @@ class LinkedInService:
return tags return tags

View File

@ -0,0 +1,23 @@
# Generated by Django 5.2.6 on 2025-10-07 12:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0024_fieldresponse_created_at_fieldresponse_slug_and_more'),
]
operations = [
migrations.AddField(
model_name='formfield',
name='max_files',
field=models.PositiveIntegerField(default=1, help_text='Maximum number of files allowed (when multiple_files is True)'),
),
migrations.AddField(
model_name='formfield',
name='multiple_files',
field=models.BooleanField(default=False, help_text='Allow multiple files to be uploaded'),
),
]

View File

@ -7,6 +7,7 @@ from django.utils.translation import gettext_lazy as _
from django_extensions.db.fields import RandomCharField from django_extensions.db.fields import RandomCharField
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django_countries.fields import CountryField from django_countries.fields import CountryField
from django.urls import reverse
class Base(models.Model): class Base(models.Model):
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created at'))
@ -167,6 +168,12 @@ class JobPosting(Base):
return self.application_deadline < timezone.now().date() return self.application_deadline < timezone.now().date()
return False return False
def publish(self):
self.status = 'PUBLISHED'
self.published_at = timezone.now()
self.application_url = reverse('form_wizard', kwargs={'slug': self.form_template.slug})
self.save()
class Candidate(Base): class Candidate(Base):
class Stage(models.TextChoices): class Stage(models.TextChoices):
@ -339,6 +346,7 @@ class FormTemplate(Base):
return sum(stage.fields.count() for stage in self.stages.all()) return sum(stage.fields.count() for stage in self.stages.all())
class FormStage(Base): class FormStage(Base):
""" """
Represents a stage/section within a form template Represents a stage/section within a form template
@ -402,15 +410,20 @@ class FormField(Base):
default=5, default=5,
help_text="Maximum file size in MB (default: 5MB)" help_text="Maximum file size in MB (default: 5MB)"
) )
multiple_files = models.BooleanField(
default=False,
help_text="Allow multiple files to be uploaded"
)
max_files = models.PositiveIntegerField(
default=1,
help_text="Maximum number of files allowed (when multiple_files is True)"
)
class Meta: class Meta:
ordering = ['order'] ordering = ['order']
verbose_name = 'Form Field' verbose_name = 'Form Field'
verbose_name_plural = 'Form Fields' verbose_name_plural = 'Form Fields'
def __str__(self):
return f"{self.stage.name} - {self.label}"
def clean(self): def clean(self):
# Validate options for selection fields # Validate options for selection fields
if self.field_type in ['select', 'radio', 'checkbox']: if self.field_type in ['select', 'radio', 'checkbox']:
@ -427,11 +440,18 @@ class FormField(Base):
self.file_types = '.pdf,.doc,.docx' self.file_types = '.pdf,.doc,.docx'
if self.max_file_size <= 0: if self.max_file_size <= 0:
raise ValidationError("Max file size must be greater than 0") raise ValidationError("Max file size must be greater than 0")
if self.multiple_files and self.max_files <= 0:
raise ValidationError("Max files must be greater than 0 when multiple files are allowed")
if not self.multiple_files:
self.max_files = 1
else: else:
# Clear file settings for non-file fields # Clear file settings for non-file fields
self.file_types = '' self.file_types = ''
self.max_file_size = 0 self.max_file_size = 0
self.multiple_files = False
self.max_files = 1
# Validate order
if self.order < 0: if self.order < 0:
raise ValidationError("Order must be a positive integer") raise ValidationError("Order must be a positive integer")

View File

@ -1,6 +1,9 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from . import models from . import models
from django.urls import reverse
from django.db import transaction
from django.dispatch import receiver
from django.db.models.signals import post_save
from .models import FormField,FormStage,FormTemplate
# @receiver(post_save, sender=models.Candidate) # @receiver(post_save, sender=models.Candidate)
# def parse_resume(sender, instance, created, **kwargs): # def parse_resume(sender, instance, created, **kwargs):
@ -19,8 +22,6 @@ import os
from .utils import extract_text_from_pdf,score_resume_with_openrouter from .utils import extract_text_from_pdf,score_resume_with_openrouter
import asyncio import asyncio
@receiver(post_save, sender=models.Candidate) @receiver(post_save, sender=models.Candidate)
def score_candidate_resume(sender, instance, created, **kwargs): def score_candidate_resume(sender, instance, created, **kwargs):
# Skip if no resume or OpenRouter not configured # Skip if no resume or OpenRouter not configured
@ -138,3 +139,284 @@ def score_candidate_resume(sender, instance, created, **kwargs):
@receiver(post_save, sender=FormTemplate)
def create_default_stages(sender, instance, created, **kwargs):
"""
Create default resume stages when a new FormTemplate is created
"""
if created: # Only run for new templates, not updates
with transaction.atomic():
# Stage 1: Contact Information
contact_stage = FormStage.objects.create(
template=instance,
name='Contact Information',
order=0,
is_predefined=True
)
FormField.objects.create(
stage=contact_stage,
label='Full Name',
field_type='text',
required=True,
order=0,
is_predefined=True
)
FormField.objects.create(
stage=contact_stage,
label='Email Address',
field_type='email',
required=True,
order=1,
is_predefined=True
)
FormField.objects.create(
stage=contact_stage,
label='Phone Number',
field_type='phone',
required=True,
order=2,
is_predefined=True
)
FormField.objects.create(
stage=contact_stage,
label='Address',
field_type='text',
required=False,
order=3,
is_predefined=True
)
FormField.objects.create(
stage=contact_stage,
label='Resume Upload',
field_type='file',
required=True,
order=4,
is_predefined=True,
file_types='.pdf,.doc,.docx',
max_file_size=5
)
# Stage 2: Resume Objective
objective_stage = FormStage.objects.create(
template=instance,
name='Resume Objective',
order=1,
is_predefined=True
)
FormField.objects.create(
stage=objective_stage,
label='Career Objective',
field_type='textarea',
required=False,
order=0,
is_predefined=True
)
# Stage 3: Education
education_stage = FormStage.objects.create(
template=instance,
name='Education',
order=2,
is_predefined=True
)
FormField.objects.create(
stage=education_stage,
label='Degree',
field_type='text',
required=True,
order=0,
is_predefined=True
)
FormField.objects.create(
stage=education_stage,
label='Institution',
field_type='text',
required=True,
order=1,
is_predefined=True
)
FormField.objects.create(
stage=education_stage,
label='Location',
field_type='text',
required=False,
order=2,
is_predefined=True
)
FormField.objects.create(
stage=education_stage,
label='Graduation Date',
field_type='date',
required=False,
order=3,
is_predefined=True
)
# Stage 4: Experience
experience_stage = FormStage.objects.create(
template=instance,
name='Experience',
order=3,
is_predefined=True
)
FormField.objects.create(
stage=experience_stage,
label='Position Title',
field_type='text',
required=True,
order=0,
is_predefined=True
)
FormField.objects.create(
stage=experience_stage,
label='Company Name',
field_type='text',
required=True,
order=1,
is_predefined=True
)
FormField.objects.create(
stage=experience_stage,
label='Location',
field_type='text',
required=False,
order=2,
is_predefined=True
)
FormField.objects.create(
stage=experience_stage,
label='Start Date',
field_type='date',
required=True,
order=3,
is_predefined=True
)
FormField.objects.create(
stage=experience_stage,
label='End Date',
field_type='date',
required=True,
order=4,
is_predefined=True
)
FormField.objects.create(
stage=experience_stage,
label='Responsibilities & Achievements',
field_type='textarea',
required=False,
order=5,
is_predefined=True
)
# Stage 5: Skills
skills_stage = FormStage.objects.create(
template=instance,
name='Skills',
order=4,
is_predefined=True
)
FormField.objects.create(
stage=skills_stage,
label='Technical Skills',
field_type='checkbox',
required=False,
order=0,
is_predefined=True,
options=['Programming Languages', 'Frameworks', 'Tools & Technologies']
)
# Stage 6: Summary
summary_stage = FormStage.objects.create(
template=instance,
name='Summary',
order=5,
is_predefined=True
)
FormField.objects.create(
stage=summary_stage,
label='Professional Summary',
field_type='textarea',
required=False,
order=0,
is_predefined=True
)
# Stage 7: Certifications
certifications_stage = FormStage.objects.create(
template=instance,
name='Certifications',
order=6,
is_predefined=True
)
FormField.objects.create(
stage=certifications_stage,
label='Certification Name',
field_type='text',
required=False,
order=0,
is_predefined=True
)
FormField.objects.create(
stage=certifications_stage,
label='Issuing Organization',
field_type='text',
required=False,
order=1,
is_predefined=True
)
FormField.objects.create(
stage=certifications_stage,
label='Issue Date',
field_type='date',
required=False,
order=2,
is_predefined=True
)
FormField.objects.create(
stage=certifications_stage,
label='Expiration Date',
field_type='date',
required=False,
order=3,
is_predefined=True
)
# Stage 8: Awards and Recognitions
awards_stage = FormStage.objects.create(
template=instance,
name='Awards and Recognitions',
order=7,
is_predefined=True
)
FormField.objects.create(
stage=awards_stage,
label='Award Name',
field_type='text',
required=False,
order=0,
is_predefined=True
)
FormField.objects.create(
stage=awards_stage,
label='Issuing Organization',
field_type='text',
required=False,
order=1,
is_predefined=True
)
FormField.objects.create(
stage=awards_stage,
label='Date Received',
field_type='date',
required=False,
order=2,
is_predefined=True
)
FormField.objects.create(
stage=awards_stage,
label='Description',
field_type='textarea',
required=False,
order=3,
is_predefined=True
)

View File

@ -57,6 +57,7 @@ urlpatterns = [
path('forms/builder/', views.form_builder, name='form_builder'), path('forms/builder/', views.form_builder, name='form_builder'),
path('forms/builder/<int:template_id>/', views.form_builder, name='form_builder'), path('forms/builder/<int:template_id>/', views.form_builder, name='form_builder'),
path('forms/', views.form_templates_list, name='form_templates_list'), path('forms/', views.form_templates_list, name='form_templates_list'),
path('forms/create-template/', views.create_form_template, name='create_form_template'),
path('forms/form/<int:template_id>/', views.form_wizard_view, name='form_wizard'), path('forms/form/<int:template_id>/', views.form_wizard_view, name='form_wizard'),
path('forms/form/<int:template_id>/submit/', views.submit_form, name='submit_form'), path('forms/form/<int:template_id>/submit/', views.submit_form, name='submit_form'),

View File

@ -7,8 +7,9 @@ from datetime import datetime
from django.views import View from django.views import View
from django.db.models import Q from django.db.models import Q
from django.urls import reverse from django.urls import reverse
from django.conf import settings
from django.utils import timezone from django.utils import timezone
from .forms import ZoomMeetingForm,JobPostingForm from .forms import ZoomMeetingForm,JobPostingForm,FormTemplateForm
from rest_framework import viewsets from rest_framework import viewsets
from django.contrib import messages from django.contrib import messages
from django.core.paginator import Paginator from django.core.paginator import Paginator
@ -20,8 +21,8 @@ from django.shortcuts import get_object_or_404, render, redirect
from django.views.generic import CreateView,UpdateView,DetailView,ListView from django.views.generic import CreateView,UpdateView,DetailView,ListView
from .utils import create_zoom_meeting, delete_zoom_meeting, update_zoom_meeting from .utils import create_zoom_meeting, delete_zoom_meeting, update_zoom_meeting
from django.views.decorators.csrf import ensure_csrf_cookie from django.views.decorators.csrf import ensure_csrf_cookie
import logging import logging
logger=logging.getLogger(__name__) logger=logging.getLogger(__name__)
@ -321,6 +322,7 @@ def linkedin_callback(request):
access_token=service.get_access_token(code) access_token=service.get_access_token(code)
request.session['linkedin_access_token']=access_token request.session['linkedin_access_token']=access_token
request.session['linkedin_authenticated']=True request.session['linkedin_authenticated']=True
settings.LINKEDIN_IS_CONNECTED = True
messages.success(request,'Successfully authenticated with LinkedIn!') messages.success(request,'Successfully authenticated with LinkedIn!')
except Exception as e: except Exception as e:
logger.error(f"LinkedIn authentication error: {e}") logger.error(f"LinkedIn authentication error: {e}")
@ -685,10 +687,11 @@ def load_form_template(request, template_id):
'id': template.id, 'id': template.id,
'name': template.name, 'name': template.name,
'description': template.description, 'description': template.description,
'is_active': template.is_active,
'job': template.job_id if template.job else None,
'stages': stages 'stages': stages
} }
}) })
def form_templates_list(request): def form_templates_list(request):
"""List all form templates for the current user""" """List all form templates for the current user"""
query = request.GET.get('q', '') query = request.GET.get('q', '')
@ -703,13 +706,32 @@ def form_templates_list(request):
paginator = Paginator(templates, 10) # Show 10 templates per page paginator = Paginator(templates, 10) # Show 10 templates per page
page_number = request.GET.get('page') page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number) page_obj = paginator.get_page(page_number)
form = FormTemplateForm()
form.fields['job'].queryset = JobPosting.objects.filter(form_template__isnull=True)
context = { context = {
'templates': page_obj, 'templates': page_obj,
'query': query, 'query': query,
'form': form
} }
return render(request, 'forms/form_templates_list.html', context) return render(request, 'forms/form_templates_list.html', context)
def create_form_template(request):
"""Create a new form template"""
if request.method == 'POST':
form = FormTemplateForm(request.POST)
if form.is_valid():
template = form.save(commit=False)
template.created_by = request.user
template.save()
messages.success(request, f'Form template "{template.name}" created successfully!')
return redirect('form_builder', template_id=template.id)
else:
form = FormTemplateForm()
return render(request, 'forms/create_form_template.html', {'form': form})
@require_http_methods(["GET"]) @require_http_methods(["GET"])
def list_form_templates(request): def list_form_templates(request):
"""List all form templates for the current user""" """List all form templates for the current user"""

View File

@ -0,0 +1,208 @@
{% extends 'base.html' %}
{% load static i18n %}
{% load crispy_forms_tags %}
{% block title %}Create Form Template - ATS{% endblock %}
{% block customCSS %}
<style>
/* ================================================= */
/* THEME VARIABLES AND GLOBAL STYLES */
/* ================================================= */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
}
/* Primary Color Overrides */
.text-primary { color: var(--kaauh-teal) !important; }
/* Main Action Button Style */
.btn-main-action, .btn-primary {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
padding: 0.6rem 1.2rem;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-main-action:hover, .btn-primary:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
/* Card enhancements */
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.05);
background-color: white;
}
/* Card Header Theming */
.card-header {
background-color: #f0f8ff !important; /* Light blue tint for header */
border-bottom: 1px solid var(--kaauh-border);
color: var(--kaauh-teal-dark);
font-weight: 600;
padding: 1rem 1.25rem;
border-radius: 0.75rem 0.75rem 0 0;
}
.card-header h3 {
color: var(--kaauh-teal-dark);
font-weight: 700;
}
.card-header .fas {
color: var(--kaauh-teal);
}
/* Form styling */
.form-control {
border-color: var(--kaauh-border);
border-radius: 0.5rem;
}
.form-control:focus {
border-color: var(--kaauh-teal);
box-shadow: 0 0 0 0.2rem rgba(0, 99, 110, 0.25);
}
.form-label {
font-weight: 500;
color: var(--kaauh-primary-text);
margin-bottom: 0.5rem;
}
.form-check-input:checked {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
}
/* Modal styling */
.modal-content {
border-radius: 0.75rem;
border: 1px solid var(--kaauh-border);
}
.modal-header {
border-bottom: 1px solid var(--kaauh-border);
padding: 1.25rem 1.5rem;
background-color: #f0f8ff !important;
}
.modal-footer {
border-top: 1px solid var(--kaauh-border);
padding: 1rem 1.5rem;
}
/* Error message styling */
.invalid-feedback {
display: block;
width: 100%;
margin-top: 0.25rem;
font-size: 0.875em;
color: #dc3545;
}
.form-control.is-invalid {
border-color: #dc3545;
}
/* Success message styling */
.alert-success {
background-color: #d4edda;
border-color: #c3e6cb;
color: #155724;
padding: 1rem 1.25rem;
border-radius: 0.5rem;
margin-bottom: 1rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4 pb-2 border-bottom border-primary">
<h1 class="h3 mb-0 fw-bold text-primary">
<i class="fas fa-file-alt me-2"></i>Create Form Template
</h1>
<a href="{% url 'form_templates_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to Templates
</a>
</div>
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h3 class="h5 mb-0"><i class="fas fa-plus-circle me-2"></i>New Form Template</h3>
</div>
<div class="card-body p-4">
<form method="post" id="createFormTemplate">
{% csrf_token %}
{{ form|crispy }}
<div class="d-flex justify-content-between">
<a href="{% url 'form_templates_list' %}" class="btn btn-secondary">
<i class="fas fa-times me-1"></i>Cancel
</a>
<button type="submit" class="btn btn-main-action">
<i class="fas fa-save me-1"></i>Create Template
</button>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% if messages %}
{% for message in messages %}
<div class="alert alert-success alert-dismissible fade show" role="alert">
<i class="fas fa-check-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endblock %}
{% block extra_js %}
<script>
// Add form validation
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('createFormTemplate');
form.addEventListener('submit', function(event) {
let isValid = true;
// Validate template name
const nameField = form.querySelector('#id_name');
if (!nameField.value.trim()) {
nameField.classList.add('is-invalid');
isValid = false;
} else {
nameField.classList.remove('is-invalid');
}
if (!isValid) {
event.preventDefault();
}
});
// Remove validation errors on input
const nameField = form.querySelector('#id_name');
nameField.addEventListener('input', function() {
if (this.value.trim()) {
this.classList.remove('is-invalid');
}
});
});
</script>
{% endblock %}

View File

@ -12,6 +12,8 @@
--primary: #004a53; /* Deep Teal/Cyan for main actions */ --primary: #004a53; /* Deep Teal/Cyan for main actions */
--primary-light: #00b4d8; /* Brighter Aqua/Cyan */ --primary-light: #00b4d8; /* Brighter Aqua/Cyan */
--secondary: #005a78; /* Darker Teal for hover/accent */ --secondary: #005a78; /* Darker Teal for hover/accent */
--success: #00cc99; /* Bright Greenish-Teal for success */
--success: #005a78; /* Bright Greenish-Teal for success */ --success: #005a78; /* Bright Greenish-Teal for success */
/* Neutral Colors (Kept for consistency) */ /* Neutral Colors (Kept for consistency) */
@ -670,18 +672,20 @@
<!-- Pass Django CSRF token and other data --> <!-- Pass Django CSRF token and other data -->
<script> <script>
// Django template variables - these will be processed by Django // Django template variables - these will be processed by Django
const djangoConfig = { const djangoConfig = {
csrfToken: "{{ csrf_token }}", csrfToken: "{{ csrf_token }}",
saveUrl: "{% url 'save_form_template' %}", saveUrl: "{% url 'save_form_template' %}",
loadUrl: {% if template_id %}"{% url 'load_form_template' template_id %}"{% else %}null{% endif %}, loadUrl: {% if template_id %}"{% url 'load_form_template' template_id %}"{% else %}null{% endif %},
templateId: {% if template_id %}{{ template_id }}{% else %}null{% endif %} templateId: {% if template_id %}{{ template_id }}{% else %}null{% endif %},
}; jobId: {% if job_id %}{{ job_id }}{% else %}null{% endif %} // Add this if you need it
};
</script> </script>
<div class="container"> <div class="container">
<!-- Sidebar with form elements --> <!-- Sidebar with form elements -->
<div class="sidebar"> <div class="sidebar">
<div class="sidebar-header"> <div class="sidebar-header">
<a class="" href="{% url 'form_templates_list' %}"></a>
<h2><i class="fas fa-cube"></i> Form Elements</h2> <h2><i class="fas fa-cube"></i> Form Elements</h2>
</div> </div>
<div class="field-categories"> <div class="field-categories">
@ -853,29 +857,51 @@
</div> </div>
<!-- File Type Specific Settings --> <!-- File Type Specific Settings -->
<div class="editor-section" id="fileSettings" style="display: none;"> <div class="editor-section" id="fileSettings" style="display: none;">
<h4><i class="fas fa-file"></i> File Settings</h4> <h4><i class="fas fa-file"></i> File Settings</h4>
<div class="form-group"> <div class="form-group">
<label for="fileTypes">Allowed File Types</label> <label for="fileTypes">Allowed File Types</label>
<input <input
type="text" type="text"
id="fileTypes" id="fileTypes"
class="form-control" class="form-control"
placeholder=".pdf, .doc, .docx" placeholder=".pdf, .doc, .docx"
> >
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="maxFileSize">Max File Size (MB)</label> <label for="maxFileSize">Max File Size (MB)</label>
<input <input
type="number" type="number"
id="maxFileSize" id="maxFileSize"
class="form-control" class="form-control"
min="1" min="1"
max="100" max="100"
value="5" value="5"
> >
</div> </div>
</div> <div class="form-group">
</div> <div class="checkbox-group">
<input
type="checkbox"
id="multipleFiles"
>
<label for="multipleFiles">Allow Multiple Files</label>
</div>
<small class="form-text text-muted">Enable this to allow uploading multiple files for this field.</small>
</div>
<div class="form-group">
<label for="maxFiles">Maximum Number of Files</label>
<input
type="number"
id="maxFiles"
class="form-control"
min="1"
max="10"
value="1"
disabled
>
<small class="form-text text-muted">Only applicable when multiple files are allowed.</small>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -1156,97 +1182,109 @@
// API Functions // API Functions
async function saveFormTemplate() { async function saveFormTemplate() {
const formData = { const formData = {
name: state.formName, name: state.formName,
description: state.formDescription, description: state.formDescription,
is_active: state.formActive, is_active: state.formActive,
template_id: state.templateId, // Include template_id for updates template_id: state.templateId,
stages: state.stages.map(stage => ({ stages: state.stages.map(stage => ({
name: stage.name, name: stage.name,
predefined: stage.predefined, predefined: stage.predefined,
fields: stage.fields.map(field => ({ fields: stage.fields.map(field => ({
type: field.type, type: field.type,
label: field.label, label: field.label,
placeholder: field.placeholder || '', placeholder: field.placeholder || '',
required: field.required || false, required: field.required || false,
options: field.options || [], options: field.options || [],
fileTypes: field.fileTypes || '', fileTypes: field.fileTypes || '',
maxFileSize: field.maxFileSize || 5, maxFileSize: field.maxFileSize || 5,
predefined: field.predefined predefined: field.predefined
})) }))
})) }))
}; };
try { // If there's a job_id in the Django context, include it
const response = await fetch(djangoConfig.saveUrl, { if (djangoConfig.jobId) {
method: 'POST', formData.job = djangoConfig.jobId;
headers: { }
'Content-Type': 'application/json',
'X-CSRFToken': djangoConfig.csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(formData)
});
const result = await response.json(); try {
const response = await fetch(djangoConfig.saveUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': djangoConfig.csrfToken,
'X-Requested-With': 'XMLHttpRequest'
},
body: JSON.stringify(formData)
});
if (result.success) { const result = await response.json();
alert('Form template saved successfully! Template ID: ' + result.template_id);
// Update templateId for future saves (important for new templates) if (result.success) {
state.templateId = result.template_id; state.templateId = result.template_id;
} else { window.location.href = "{% url 'form_templates_list' %}";
alert('Error saving form template: ' + result.error);
} } else {
} catch (error) { alert('Error saving form template: ' + result.error);
console.error('Error:', error);
alert('Error saving form template. Please try again.');
}
} }
} catch (error) {
console.error('Error:', error);
alert('Error saving form template. Please try again.');
}
}
// Load existing template if editing // Load existing template if editing
async function loadExistingTemplate() { async function loadExistingTemplate() {
if (djangoConfig.loadUrl) { if (djangoConfig.loadUrl) {
try { try {
const response = await fetch(djangoConfig.loadUrl); const response = await fetch(djangoConfig.loadUrl);
const result = await response.json(); const result = await response.json();
if (result.success) {
const templateData = result.template;
// Set form settings
state.formName = templateData.name || 'Untitled Form';
state.formDescription = templateData.description || '';
state.formActive = templateData.is_active !== false;
if (result.success) { // Update form title
const templateData = result.template; elements.formTitle.textContent = state.formName;
// Set form settings elements.formName.value = state.formName;
state.formName = templateData.name || 'Untitled Form'; elements.formDescription.value = state.formDescription;
state.formDescription = templateData.description || ''; elements.formActive.checked = state.formActive;
state.formActive = templateData.is_active !== false; // Default to true if not set
// Update form title // Set stages (this is where your actual stages come from)
elements.formTitle.textContent = state.formName; state.stages = templateData.stages;
elements.formName.value = state.formName; state.templateId = templateData.id;
elements.formDescription.value = state.formDescription;
elements.formActive.checked = state.formActive;
// Set stages // Update next IDs to avoid conflicts
state.stages = templateData.stages; let maxFieldId = 0;
state.templateId = templateData.id; let maxStageId = 0;
// Update next IDs to avoid conflicts templateData.stages.forEach(stage => {
let maxFieldId = 0; maxStageId = Math.max(maxStageId, stage.id);
let maxStageId = 0; stage.fields.forEach(field => {
templateData.stages.forEach(stage => { maxFieldId = Math.max(maxFieldId, field.id);
maxStageId = Math.max(maxStageId, stage.id); });
stage.fields.forEach(field => { });
maxFieldId = Math.max(maxFieldId, field.id); state.nextFieldId = maxFieldId + 1;
}); state.nextStageId = maxStageId + 1;
}); state.currentStage = 0;
state.nextFieldId = maxFieldId + 1;
state.nextStageId = maxStageId + 1; // Now show the form content
state.currentStage = 0; elements.formStage.style.display = 'block';
renderStageNavigation(); elements.emptyState.style.display = 'none';
renderCurrentStage();
} renderStageNavigation();
} catch (error) { renderCurrentStage();
console.error('Error loading template:', error);
alert('Error loading template data.');
}
} }
} catch (error) {
console.error('Error loading template:', error);
elements.formTitle.textContent = 'Error Loading Template';
elements.emptyState.style.display = 'block';
elements.emptyState.innerHTML = '<i class="fas fa-exclamation-triangle"></i><p>Error loading template data.</p>';
elements.formStage.style.display = 'none';
} }
}
}
// DOM Rendering Functions (same as before) // DOM Rendering Functions (same as before)
function renderStageNavigation() { function renderStageNavigation() {
@ -1319,164 +1357,255 @@
} }
function createFieldElement(field, index) { function createFieldElement(field, index) {
const fieldDiv = document.createElement('div'); const fieldDiv = document.createElement('div');
fieldDiv.className = `form-field ${state.selectedField && state.selectedField.id === field.id ? 'selected' : ''}`; fieldDiv.className = `form-field ${state.selectedField && state.selectedField.id === field.id ? 'selected' : ''}`;
fieldDiv.dataset.fieldId = field.id; fieldDiv.dataset.fieldId = field.id;
fieldDiv.dataset.fieldIndex = index; fieldDiv.dataset.fieldIndex = index;
const fieldHeader = document.createElement('div');
fieldHeader.className = 'field-header'; const fieldHeader = document.createElement('div');
fieldHeader.innerHTML = ` fieldHeader.className = 'field-header';
<div class="field-title"> fieldHeader.innerHTML = `
<i class="${getFieldIcon(field.type)}"></i> <div class="field-title">
${field.label || field.type.charAt(0).toUpperCase() + field.type.slice(1)} <i class="${getFieldIcon(field.type)}"></i>
${field.required ? '<span class="required-indicator"> *</span>' : ''} ${field.label || field.type.charAt(0).toUpperCase() + field.type.slice(1)}
</div> ${field.required ? '<span class="required-indicator"> *</span>' : ''}
<div class="field-actions"> </div>
<div class="action-btn edit-field" data-field-id="${field.id}"> <div class="field-actions">
<i class="fas fa-edit"></i> <div class="action-btn edit-field" data-field-id="${field.id}">
</div> <i class="fas fa-edit"></i>
${!field.predefined ? `<div class="action-btn remove-field" data-field-index="${index}"> </div>
<i class="fas fa-trash"></i> ${!field.predefined ? `<div class="action-btn remove-field" data-field-index="${index}">
</div>` : ''} <i class="fas fa-trash"></i>
</div> </div>` : ''}
`; </div>
const fieldContent = document.createElement('div'); `;
fieldContent.className = 'field-content';
fieldContent.innerHTML = ` const fieldContent = document.createElement('div');
<label class="field-label"> fieldContent.className = 'field-content';
${field.label || 'Field Label'} fieldContent.innerHTML = `
${field.required ? '<span class="required-indicator"> *</span>' : ''} <label class="field-label">
</label> ${field.label || 'Field Label'}
`; ${field.required ? '<span class="required-indicator"> *</span>' : ''}
// Add field input based on type </label>
if (field.type === 'text' || field.type === 'email' || field.type === 'phone' || field.type === 'date') { `;
const input = document.createElement('input');
input.type = 'text'; // Add field input based on type
input.className = 'field-input'; if (field.type === 'text' || field.type === 'email' || field.type === 'phone' || field.type === 'date') {
input.placeholder = field.placeholder || 'Enter value'; const input = document.createElement('input');
input.disabled = true; input.type = 'text';
fieldContent.appendChild(input); input.className = 'field-input';
} else if (field.type === 'textarea') { input.placeholder = field.placeholder || 'Enter value';
const textarea = document.createElement('textarea'); input.disabled = true;
textarea.className = 'field-input'; fieldContent.appendChild(input);
textarea.rows = 3; } else if (field.type === 'textarea') {
textarea.placeholder = field.placeholder || 'Enter text'; const textarea = document.createElement('textarea');
textarea.disabled = true; textarea.className = 'field-input';
fieldContent.appendChild(textarea); textarea.rows = 3;
} else if (field.type === 'file') { textarea.placeholder = field.placeholder || 'Enter text';
const fileUpload = document.createElement('div'); textarea.disabled = true;
fileUpload.className = 'file-upload-area'; fieldContent.appendChild(textarea);
fileUpload.innerHTML = ` } else if (field.type === 'file') {
<div class="file-upload-icon"> const fileUpload = document.createElement('div');
<i class="fas fa-cloud-upload-alt"></i> fileUpload.className = 'file-upload-area';
</div> fileUpload.innerHTML = `
<div class="file-upload-text"> <div class="file-upload-icon">
<p>Drag & drop your resume here or <strong>click to browse</strong></p> <i class="fas fa-cloud-upload-alt"></i>
</div> </div>
<div class="file-upload-info"> <div class="file-upload-text">
<p>Supported formats: ${field.fileTypes || '.pdf, .doc, .docx'} (Max ${field.maxFileSize || 5}MB)</p> <p>Drag & drop your ${field.label.toLowerCase()} here or <strong>click to browse</strong></p>
</div> </div>
<input type="file" class="file-input" style="display: none;" accept="${field.fileTypes || '.pdf,.doc,.docx'}"> <div class="file-upload-info">
`; <p>Supported formats: ${field.fileTypes || '.pdf, .doc, .docx'} (Max ${field.maxFileSize || 5}MB)</p>
if (field.uploadedFile) { ${field.multipleFiles ? `<p>Multiple files allowed (Max ${field.maxFiles || 1} files)</p>` : ''}
const uploadedFile = document.createElement('div'); </div>
uploadedFile.className = 'uploaded-file'; <input type="file" class="file-input" style="display: none;"
uploadedFile.innerHTML = ` accept="${field.fileTypes || '.pdf,.doc,.docx'}"
<div class="file-info"> ${field.multipleFiles ? 'multiple' : ''}>
<i class="fas fa-file file-icon"></i> `;
<div>
<div class="file-name">${field.uploadedFile.name}</div> // Show uploaded files
<div class="file-size">${formatFileSize(field.uploadedFile.size)}</div> if (field.uploadedFiles && field.uploadedFiles.length > 0) {
</div> field.uploadedFiles.forEach((file, fileIndex) => {
const uploadedFile = document.createElement('div');
uploadedFile.className = 'uploaded-file';
uploadedFile.innerHTML = `
<div class="file-info">
<i class="fas fa-file file-icon"></i>
<div>
<div class="file-name">${file.name}</div>
<div class="file-size">${formatFileSize(file.size)}</div>
</div> </div>
<button class="remove-file-btn"> </div>
<i class="fas fa-times"></i> <button class="remove-file-btn" data-file-index="${fileIndex}">
</button> <i class="fas fa-times"></i>
`; </button>
fileUpload.appendChild(uploadedFile); `;
} fileUpload.appendChild(uploadedFile);
fieldContent.appendChild(fileUpload);
} else if (field.type === 'select') {
const select = document.createElement('select');
select.className = 'field-input';
select.disabled = true;
field.options.forEach(option => {
const optionEl = document.createElement('option');
optionEl.textContent = option;
select.appendChild(optionEl);
});
fieldContent.appendChild(select);
} else if (field.type === 'radio' || field.type === 'checkbox') {
const optionsDiv = document.createElement('div');
optionsDiv.className = 'field-options';
field.options.forEach((option, idx) => {
const optionItem = document.createElement('div');
optionItem.className = 'option-item';
optionItem.innerHTML = `
<input type="${field.type === 'radio' ? 'radio' : 'checkbox'}"
id="${field.type}-${field.id}-${idx}"
name="${field.type}-${field.id}"
disabled>
<label for="${field.type}-${field.id}-${idx}">${option}</label>
`;
optionsDiv.appendChild(optionItem);
});
fieldContent.appendChild(optionsDiv);
}
fieldDiv.appendChild(fieldHeader);
fieldDiv.appendChild(fieldContent);
// Add event listeners
fieldDiv.addEventListener('click', (e) => {
if (!e.target.closest('.edit-field') && !e.target.closest('.remove-field') &&
!e.target.closest('.remove-file-btn')) {
selectField(field);
}
}); });
const editBtn = fieldDiv.querySelector('.edit-field');
if (editBtn) {
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
selectField(field);
});
}
const removeBtn = fieldDiv.querySelector('.remove-field');
if (removeBtn) {
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
removeField(parseInt(removeBtn.dataset.fieldIndex));
});
}
const removeFileBtn = fieldDiv.querySelector('.remove-file-btn');
if (removeFileBtn) {
removeFileBtn.addEventListener('click', (e) => {
e.stopPropagation();
const fieldId = parseInt(fieldDiv.dataset.fieldId);
const stage = state.stages[state.currentStage];
const field = stage.fields.find(f => f.id === fieldId);
if (field) {
field.uploadedFile = null;
renderCurrentStage();
}
});
}
// Make draggable
fieldDiv.draggable = true;
fieldDiv.addEventListener('dragstart', (e) => {
state.draggedFieldIndex = parseInt(fieldDiv.dataset.fieldIndex);
e.dataTransfer.setData('text/plain', 'reorder');
e.dataTransfer.effectAllowed = 'move';
});
fieldDiv.addEventListener('dragover', (e) => {
e.preventDefault();
});
fieldDiv.addEventListener('drop', (e) => {
e.preventDefault();
const targetIndex = parseInt(fieldDiv.dataset.fieldIndex);
dropField(targetIndex);
});
return fieldDiv;
} }
fieldContent.appendChild(fileUpload);
} else if (field.type === 'select') {
const select = document.createElement('select');
select.className = 'field-input';
select.disabled = true;
field.options.forEach(option => {
const optionEl = document.createElement('option');
optionEl.textContent = option;
select.appendChild(optionEl);
});
fieldContent.appendChild(select);
} else if (field.type === 'radio' || field.type === 'checkbox') {
const optionsDiv = document.createElement('div');
optionsDiv.className = 'field-options';
field.options.forEach((option, idx) => {
const optionItem = document.createElement('div');
optionItem.className = 'option-item';
optionItem.innerHTML = `
<input type="${field.type === 'radio' ? 'radio' : 'checkbox'}"
id="${field.type}-${field.id}-${idx}"
name="${field.type}-${field.id}"
disabled>
<label for="${field.type}-${field.id}-${idx}">${option}</label>
`;
optionsDiv.appendChild(optionItem);
});
fieldContent.appendChild(optionsDiv);
}
fieldDiv.appendChild(fieldHeader);
fieldDiv.appendChild(fieldContent);
// Add event listeners
fieldDiv.addEventListener('click', (e) => {
if (!e.target.closest('.edit-field') && !e.target.closest('.remove-field') &&
!e.target.closest('.remove-file-btn')) {
selectField(field);
}
});
const editBtn = fieldDiv.querySelector('.edit-field');
if (editBtn) {
editBtn.addEventListener('click', (e) => {
e.stopPropagation();
selectField(field);
});
}
const removeBtn = fieldDiv.querySelector('.remove-field');
if (removeBtn) {
removeBtn.addEventListener('click', (e) => {
e.stopPropagation();
removeField(parseInt(removeBtn.dataset.fieldIndex));
});
}
const removeFileBtns = fieldDiv.querySelectorAll('.remove-file-btn');
removeFileBtns.forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const fileIndex = parseInt(btn.dataset.fileIndex);
const fieldId = parseInt(fieldDiv.dataset.fieldId);
const stage = state.stages[state.currentStage];
const field = stage.fields.find(f => f.id === fieldId);
if (field && field.uploadedFiles) {
field.uploadedFiles.splice(fileIndex, 1);
renderCurrentStage();
}
});
});
// Make draggable
fieldDiv.draggable = true;
fieldDiv.addEventListener('dragstart', (e) => {
state.draggedFieldIndex = parseInt(fieldDiv.dataset.fieldIndex);
e.dataTransfer.setData('text/plain', 'reorder');
e.dataTransfer.effectAllowed = 'move';
});
fieldDiv.addEventListener('dragover', (e) => {
e.preventDefault();
});
fieldDiv.addEventListener('drop', (e) => {
e.preventDefault();
const targetIndex = parseInt(fieldDiv.dataset.fieldIndex);
dropField(targetIndex);
});
// Add file input event listener
const fileInput = fieldDiv.querySelector('.file-input');
if (fileInput) {
fileInput.addEventListener('change', (e) => {
handleFileUpload(e, field);
});
// Make the file upload area clickable
const fileUploadArea = fieldDiv.querySelector('.file-upload-area');
if (fileUploadArea) {
fileUploadArea.addEventListener('click', () => {
fileInput.click();
});
}
}
return fieldDiv;
}
function handleFileUpload(event, field) {
const files = Array.from(event.target.files);
if (files.length === 0) return;
// Validate file count for multiple files
if (field.multipleFiles) {
const maxFiles = field.maxFiles || 1;
if (files.length > maxFiles) {
alert(`You can only upload ${maxFiles} files for this field.`);
return;
}
} else if (files.length > 1) {
// For single file fields, only take the first file
files.splice(1);
}
// Validate each file
const validFiles = [];
const allowedTypes = (field.fileTypes || '.pdf,.doc,.docx').split(',').map(type => type.trim().toLowerCase());
const maxFileSize = field.maxFileSize || 5;
for (const file of files) {
// Validate file type
const fileType = '.' + file.name.split('.').pop().toLowerCase();
if (!allowedTypes.includes(fileType)) {
alert(`Invalid file type for ${file.name}. Allowed types: ${field.fileTypes || '.pdf, .doc, .docx'}`);
return;
}
// Validate file size
const fileSizeMB = file.size / (1024 * 1024);
if (fileSizeMB > maxFileSize) {
alert(`File ${file.name} exceeds ${maxFileSize}MB limit.`);
return;
}
validFiles.push(file);
}
// Store the files
if (field.multipleFiles) {
// Initialize or update the uploadedFiles array
if (!field.uploadedFiles) {
field.uploadedFiles = [];
}
field.uploadedFiles = [...validFiles];
} else {
// Single file - store as array with one file for consistency
field.uploadedFiles = [validFiles[0]];
}
// Re-render the current stage to show uploaded files
renderCurrentStage();
}
function showFieldEditor(field) { function showFieldEditor(field) {
elements.fieldEditor.style.display = 'flex'; elements.fieldEditor.style.display = 'flex';
elements.fieldLabel.value = field.label || ''; elements.fieldLabel.value = field.label || '';
@ -1499,30 +1628,51 @@
} }
function renderOptionsEditor(field) { function renderOptionsEditor(field) {
elements.optionsList.innerHTML = ''; elements.optionsList.innerHTML = '';
field.options.forEach((option, index) => { field.options.forEach((option, index) => {
const optionInput = document.createElement('div'); const optionInput = document.createElement('div');
optionInput.className = 'option-input'; optionInput.className = 'option-input';
optionInput.innerHTML = ` optionInput.innerHTML = `
<input type="text" class="form-control" value="${option}" placeholder="Option ${index + 1}"> <input type="text" class="form-control" value="${option}" placeholder="Option ${index + 1}">
<button class="remove-option"> <button class="remove-option">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
`; `;
elements.optionsList.appendChild(optionInput); elements.optionsList.appendChild(optionInput);
const input = optionInput.querySelector('input');
const removeBtn = optionInput.querySelector('.remove-option'); const input = optionInput.querySelector('input');
input.addEventListener('input', () => { const removeBtn = optionInput.querySelector('.remove-option');
field.options[index] = input.value;
}); input.addEventListener('input', () => {
removeBtn.addEventListener('click', () => { field.options[index] = input.value;
if (field.options.length > 1) { });
field.options.splice(index, 1);
renderOptionsEditor(field); removeBtn.addEventListener('click', () => {
if (field.options.length > 1) {
field.options.splice(index, 1);
renderOptionsEditor(field);
}
});
});
// Add event listener for multiple files checkbox if this is a file field
if (field.type === 'file') {
const multipleFilesCheckbox = elements.multipleFiles;
if (multipleFilesCheckbox) {
multipleFilesCheckbox.addEventListener('change', function() {
elements.maxFiles.disabled = !this.checked;
if (!this.checked) {
elements.maxFiles.value = 1;
// Update the field configuration
if (state.selectedField) {
state.selectedField.maxFiles = 1;
} }
}); }
}); });
} }
}
}
// Event Handlers (same as before, but updated saveForm function) // Event Handlers (same as before, but updated saveForm function)
function selectField(field) { function selectField(field) {
@ -1668,29 +1818,33 @@
} }
function drop(event) { function drop(event) {
event.preventDefault(); event.preventDefault();
event.target.classList.remove('drag-over'); event.target.classList.remove('drag-over');
if (state.draggedField) {
const newField = { if (state.draggedField) {
id: state.nextFieldId++, const newField = {
type: state.draggedField.type, id: state.nextFieldId++,
label: state.draggedField.label, type: state.draggedField.type,
placeholder: '', label: state.draggedField.label,
required: false, placeholder: '',
options: state.draggedField.type === 'select' || state.draggedField.type === 'radio' || state.draggedField.type === 'checkbox' required: false,
? ['Option 1', 'Option 2'] options: state.draggedField.type === 'select' || state.draggedField.type === 'radio' || state.draggedField.type === 'checkbox'
: [], ? ['Option 1', 'Option 2']
fileTypes: state.draggedField.type === 'file' ? '.pdf,.doc,.docx' : '', : [],
maxFileSize: state.draggedField.type === 'file' ? 5 : 0, fileTypes: state.draggedField.type === 'file' ? '.pdf,.doc,.docx' : '',
predefined: false, maxFileSize: state.draggedField.type === 'file' ? 5 : 0,
uploadedFile: null multipleFiles: state.draggedField.type === 'file' ? false : undefined,
}; maxFiles: state.draggedField.type === 'file' ? 1 : undefined,
state.stages[state.currentStage].fields.push(newField); predefined: false,
selectField(newField); uploadedFiles: state.draggedField.type === 'file' ? [] : undefined
state.draggedField = null; };
renderCurrentStage();
} state.stages[state.currentStage].fields.push(newField);
} selectField(newField);
state.draggedField = null;
renderCurrentStage();
}
}
function dropField(targetIndex) { function dropField(targetIndex) {
if (state.draggedFieldIndex !== null && state.draggedFieldIndex !== targetIndex) { if (state.draggedFieldIndex !== null && state.draggedFieldIndex !== targetIndex) {
@ -1790,15 +1944,23 @@
// Initialize Application // Initialize Application
function init() { function init() {
// Initialize form title // Initialize form title
elements.formTitle.textContent = state.formName; elements.formTitle.textContent = 'Loading...';
renderStageNavigation(); // Hide the form stage initially to prevent flickering
renderCurrentStage(); elements.formStage.style.display = 'none';
initEventListeners(); elements.emptyState.style.display = 'block';
// Load existing template if editing elements.emptyState.innerHTML = '<i class="fas fa-spinner fa-spin"></i><p>Loading form template...</p>';
if (djangoConfig.loadUrl) {
loadExistingTemplate(); // Only render navigation if we have a template to load
} if (djangoConfig.loadUrl) {
loadExistingTemplate();
} else {
// For new templates, show empty state
elements.formTitle.textContent = 'New Form Template';
elements.formStage.style.display = 'block';
renderStageNavigation();
renderCurrentStage();
}
} }
// Start the application // Start the application

View File

@ -1,5 +1,5 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static i18n %} {% load static i18n crispy_forms_tags %}
{% block title %}Form Templates - ATS{% endblock %} {% block title %}Form Templates - ATS{% endblock %}
@ -102,6 +102,8 @@
color: rgba(255, 255, 255, 0.7) !important; color: rgba(255, 255, 255, 0.7) !important;
} }
/* Stats Theming */
/* --- Content Styles (Stats, Description) --- */ /* --- Content Styles (Stats, Description) --- */
.stat-value { .stat-value {
font-size: 1.5rem; font-size: 1.5rem;
@ -116,12 +118,11 @@
.card-description { .card-description {
min-height: 60px; min-height: 60px;
color: var(--kaauh-primary-text); color: var(--kaauh-primary-text);
margin-bottom: 1rem;
} }
/* --- Form/Search Input Theming (Matching Job List) --- */ /* Search Input Theming */
.form-control-search { .form-control {
box-shadow: none; border-radius: 0.5rem 0 0 0.5rem;
border-color: var(--kaauh-border); border-color: var(--kaauh-border);
border-radius: 0 0.5rem 0.5rem 0; border-radius: 0 0.5rem 0.5rem 0;
} }
@ -147,7 +148,7 @@
--bs-btn-hover-color: white; --bs-btn-hover-color: white;
} }
/* --- Empty State Theming --- */ /* Empty State Theming */
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 3rem 1rem; padding: 3rem 1rem;
@ -188,9 +189,9 @@
<h1 class="h3 mb-0 fw-bold" style="color: var(--kaauh-teal-dark); font-weight: 700;"> <h1 class="h3 mb-0 fw-bold" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-file-alt me-2"></i>{% trans "Form Templates" %} <i class="fas fa-file-alt me-2"></i>{% trans "Form Templates" %}
</h1> </h1>
<a href="{% url 'form_builder' %}" class="btn btn-main-action"> <button type="button" class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#createTemplateModal">
<i class="fas fa-plus me-1"></i> {% trans "Create New Template" %} <i class="fas fa-plus me-1"></i> Create New Template
</a> </button>
</div> </div>
{# Search/Filter Area - Matching Job List Structure #} {# Search/Filter Area - Matching Job List Structure #}
@ -236,7 +237,8 @@
<div class="card template-card h-100"> <div class="card template-card h-100">
<div class="card-header "> <div class="card-header ">
<h3 class="h5 mb-2">{{ template.name }}</h3> <h3 class="h5 mb-2">{{ template.name }}</h3>
<div class="d-flex justify-content-between small"> <span><i class="fas fa-sync-alt me-1"></i> {{ template.job }}</span>
<div class="d-flex justify-content-between text-muted small">
<span><i class="fas fa-calendar me-1"></i> {{ template.created_at|date:"M d, Y" }}</span> <span><i class="fas fa-calendar me-1"></i> {{ template.created_at|date:"M d, Y" }}</span>
<span><i class="fas fa-sync-alt me-1"></i> {{ template.updated_at|timesince }} {% trans "ago" %}</span> <span><i class="fas fa-sync-alt me-1"></i> {{ template.updated_at|timesince }} {% trans "ago" %}</span>
</div> </div>
@ -337,6 +339,32 @@
</div> </div>
{% include 'includes/delete_modal.html' %} {% include 'includes/delete_modal.html' %}
<!-- Create Template Modal -->
<div class="modal fade" id="createTemplateModal" tabindex="-1" aria-labelledby="createTemplateModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createTemplateModalLabel">
<i class="fas fa-file-alt me-2"></i>Create New Form Template
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="createTemplateForm" method="post" action="{% url 'create_form_template' %}">
{% csrf_token %}
{{form|crispy}}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" form="createTemplateForm" class="btn btn-primary">
<i class="fas fa-save me-1"></i>Create Template
</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block customJS %} {% block customJS %}
@ -416,7 +444,7 @@
if (!templateToDelete) return; if (!templateToDelete) return;
// This CSRF token selector assumes it's present in your base template or form // This relies on 'csrfToken' being defined somewhere, which is typical for Django templates.
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value; const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
try { try {
@ -473,5 +501,50 @@
document.getElementById('deleteModal').addEventListener('hidden.bs.modal', function() { document.getElementById('deleteModal').addEventListener('hidden.bs.modal', function() {
templateToDelete = null; templateToDelete = null;
}); });
// Handle create template form submission
document.getElementById('createTemplateForm').addEventListener('submit', async function(e) {
e.preventDefault();
const form = e.target;
const formData = new FormData(form);
try {
const response = await fetch(form.action, {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
}
});
const result = await response.json();
if (response.ok && result.success) {
// Show success toast
createToast(result.message || 'Template created successfully!');
// Close modal
bootstrap.Modal.getInstance(document.getElementById('createTemplateModal')).hide();
// Clear form
form.reset();
// Redirect to form builder with new template ID
if (result.template_id) {
window.location.href = `{% url 'form_builder' %}${result.template_id}/`;
} else {
// Fallback to template list if no ID is returned
window.location.reload();
}
} else {
// Show error toast
createToast('Error: ' + (result.message || 'Could not create template.'), 'error');
}
} catch (error) {
console.error('Error:', error);
createToast('An error occurred while creating the template.', 'error');
}
});
</script> </script>
{% endblock %} {% endblock %}