update the form builder
This commit is contained in:
parent
7f23cc18fb
commit
c5c7963df5
Binary file not shown.
@ -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/'
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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')
|
||||||
|
)
|
||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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")
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
)
|
||||||
@ -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'),
|
||||||
|
|||||||
@ -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"""
|
||||||
|
|||||||
208
templates/forms/create_form_template.html
Normal file
208
templates/forms/create_form_template.html
Normal 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 %}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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 %}
|
||||||
Loading…
x
Reference in New Issue
Block a user