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'
|
||||
|
||||
|
||||
|
||||
|
||||
LINKEDIN_CLIENT_ID = '867jwsiyem1504'
|
||||
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.utils.translation import gettext_lazy as _
|
||||
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 Meta:
|
||||
@ -366,3 +366,46 @@ class JobPostingForm(forms.ModelForm):
|
||||
# 'Job description is required for active jobs.')
|
||||
|
||||
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')
|
||||
)
|
||||
@ -14,7 +14,7 @@ class LinkedInService:
|
||||
self.client_secret = settings.LINKEDIN_CLIENT_SECRET
|
||||
self.redirect_uri = settings.LINKEDIN_REDIRECT_URI
|
||||
self.access_token = None
|
||||
|
||||
|
||||
def get_auth_url(self):
|
||||
"""Generate LinkedIn OAuth URL"""
|
||||
params = {
|
||||
@ -25,7 +25,7 @@ class LinkedInService:
|
||||
'state': 'university_ats_linkedin'
|
||||
}
|
||||
return f"https://www.linkedin.com/oauth/v2/authorization?{urlencode(params)}"
|
||||
|
||||
|
||||
def get_access_token(self, code):
|
||||
"""Exchange authorization code for access token"""
|
||||
# This function exchanges LinkedIn’s temporary authorization code for a usable access token.
|
||||
@ -37,7 +37,7 @@ class LinkedInService:
|
||||
'client_id': self.client_id,
|
||||
'client_secret': self.client_secret
|
||||
}
|
||||
|
||||
|
||||
try:
|
||||
response = requests.post(url, data=data, timeout=60)
|
||||
response.raise_for_status()
|
||||
@ -53,15 +53,15 @@ class LinkedInService:
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting access token: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def get_user_profile(self):
|
||||
"""Get user profile information"""
|
||||
if not self.access_token:
|
||||
raise Exception("No access token available")
|
||||
|
||||
|
||||
url = "https://api.linkedin.com/v2/userinfo"
|
||||
headers = {'Authorization': f'Bearer {self.access_token}'}
|
||||
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers, timeout=60)
|
||||
response.raise_for_status() # Ensure we raise an error for bad responses(4xx, 5xx) and does nothing for 2xx(success)
|
||||
@ -72,8 +72,6 @@ class LinkedInService:
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def register_image_upload(self, person_urn):
|
||||
"""Step 1: Register image upload with LinkedIn"""
|
||||
url = "https://api.linkedin.com/v2/assets?action=registerUpload"
|
||||
@ -82,7 +80,7 @@ class LinkedInService:
|
||||
'Content-Type': 'application/json',
|
||||
'X-Restli-Protocol-Version': '2.0.0'
|
||||
}
|
||||
|
||||
|
||||
payload = {
|
||||
"registerUploadRequest": {
|
||||
"recipes": ["urn:li:digitalmediaRecipe:feedshare-image"],
|
||||
@ -93,10 +91,10 @@ class LinkedInService:
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=30)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
data = response.json()
|
||||
return {
|
||||
'upload_url': data['value']['uploadMechanism']['com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest']['uploadUrl'],
|
||||
@ -109,11 +107,11 @@ class LinkedInService:
|
||||
image_file.open()
|
||||
image_content = image_file.read()
|
||||
image_file.close()
|
||||
|
||||
|
||||
headers = {
|
||||
'Authorization': f'Bearer {self.access_token}',
|
||||
}
|
||||
|
||||
|
||||
response = requests.post(upload_url, headers=headers, data=image_content, timeout=60)
|
||||
response.raise_for_status()
|
||||
return True
|
||||
@ -121,59 +119,59 @@ class LinkedInService:
|
||||
"""Create a job announcement post on LinkedIn (with image support)"""
|
||||
if not self.access_token:
|
||||
raise Exception("Not authenticated with LinkedIn")
|
||||
|
||||
|
||||
try:
|
||||
# Get user profile for person URN
|
||||
profile = self.get_user_profile()
|
||||
person_urn = profile.get('sub')
|
||||
|
||||
|
||||
if not person_urn:
|
||||
raise Exception("Could not retrieve LinkedIn user ID")
|
||||
|
||||
|
||||
# Check if job has an image
|
||||
try:
|
||||
image_upload = job_posting.files.first()
|
||||
has_image = image_upload and image_upload.linkedinpost_image
|
||||
except Exception:
|
||||
has_image = False
|
||||
|
||||
|
||||
if has_image:
|
||||
# === POST WITH IMAGE ===
|
||||
try:
|
||||
# Step 1: Register image upload
|
||||
upload_info = self.register_image_upload(person_urn)
|
||||
|
||||
|
||||
# Step 2: Upload image
|
||||
self.upload_image_to_linkedin(
|
||||
upload_info['upload_url'],
|
||||
upload_info['upload_url'],
|
||||
image_upload.linkedinpost_image
|
||||
)
|
||||
|
||||
|
||||
# Step 3: Create post with image
|
||||
return self.create_job_post_with_image(
|
||||
job_posting,
|
||||
job_posting,
|
||||
image_upload.linkedinpost_image,
|
||||
person_urn,
|
||||
upload_info['asset']
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Image upload failed: {e}")
|
||||
# Fall back to text-only post if image upload fails
|
||||
has_image = False
|
||||
|
||||
|
||||
# === FALLBACK TO URL/ARTICLE POST ===
|
||||
# Add unique timestamp to prevent duplicates
|
||||
from django.utils import timezone
|
||||
import random
|
||||
unique_suffix = f"\n\nPosted: {timezone.now().strftime('%b %d, %Y at %I:%M %p')} (ID: {random.randint(1000, 9999)})"
|
||||
|
||||
|
||||
message_parts = [f"🚀 **We're Hiring: {job_posting.title}**"]
|
||||
if job_posting.department:
|
||||
message_parts.append(f"**Department:** {job_posting.department}")
|
||||
if job_posting.description:
|
||||
message_parts.append(f"\n{job_posting.description}")
|
||||
|
||||
|
||||
details = []
|
||||
if job_posting.job_type:
|
||||
details.append(f"💼 {job_posting.get_job_type_display()}")
|
||||
@ -183,22 +181,22 @@ class LinkedInService:
|
||||
details.append(f"🏠 {job_posting.get_workplace_type_display()}")
|
||||
if job_posting.salary_range:
|
||||
details.append(f"💰 {job_posting.salary_range}")
|
||||
|
||||
|
||||
if details:
|
||||
message_parts.append("\n" + " | ".join(details))
|
||||
|
||||
|
||||
if job_posting.application_url:
|
||||
message_parts.append(f"\n🔗 **Apply now:** {job_posting.application_url}")
|
||||
|
||||
|
||||
hashtags = self.hashtags_list(job_posting.hash_tags)
|
||||
if job_posting.department:
|
||||
dept_hashtag = f"#{job_posting.department.replace(' ', '')}"
|
||||
hashtags.insert(0, dept_hashtag)
|
||||
|
||||
|
||||
message_parts.append("\n\n" + " ".join(hashtags))
|
||||
message_parts.append(unique_suffix)
|
||||
message = "\n".join(message_parts)
|
||||
|
||||
|
||||
# 🔥 FIX URL - REMOVE TRAILING SPACES 🔥
|
||||
url = "https://api.linkedin.com/v2/ugcPosts"
|
||||
headers = {
|
||||
@ -206,7 +204,7 @@ class LinkedInService:
|
||||
'Content-Type': 'application/json',
|
||||
'X-Restli-Protocol-Version': '2.0.0'
|
||||
}
|
||||
|
||||
|
||||
payload = {
|
||||
"author": f"urn:li:person:{person_urn}",
|
||||
"lifecycleState": "PUBLISHED",
|
||||
@ -226,29 +224,29 @@ class LinkedInService:
|
||||
"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=60)
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
post_id = response.headers.get('x-restli-id', '')
|
||||
post_url = f"https://www.linkedin.com/feed/update/{quote(post_id)}/" if post_id else ""
|
||||
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'post_id': post_id,
|
||||
'post_url': post_url,
|
||||
'status_code': response.status_code
|
||||
}
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating LinkedIn post: {e}")
|
||||
return {
|
||||
'success': False,
|
||||
'error': str(e),
|
||||
'status_code': getattr(e.response, 'status_code', 500) if hasattr(e, 'response') else 500
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
def hashtags_list(self,hash_tags_str):
|
||||
"""Convert comma-separated hashtags string to list"""
|
||||
@ -257,8 +255,7 @@ class LinkedInService:
|
||||
tags = [tag.strip() for tag in hash_tags_str.split(',') if tag.strip()]
|
||||
if not tags:
|
||||
return ["#HigherEd", "#Hiring", "#FacultyJobs", "#UniversityJobs"]
|
||||
|
||||
|
||||
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.core.exceptions import ValidationError
|
||||
from django_countries.fields import CountryField
|
||||
from django.urls import reverse
|
||||
|
||||
class Base(models.Model):
|
||||
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 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 Stage(models.TextChoices):
|
||||
@ -339,6 +346,7 @@ class FormTemplate(Base):
|
||||
return sum(stage.fields.count() for stage in self.stages.all())
|
||||
|
||||
|
||||
|
||||
class FormStage(Base):
|
||||
"""
|
||||
Represents a stage/section within a form template
|
||||
@ -402,15 +410,20 @@ class FormField(Base):
|
||||
default=5,
|
||||
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:
|
||||
ordering = ['order']
|
||||
verbose_name = 'Form Field'
|
||||
verbose_name_plural = 'Form Fields'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.stage.name} - {self.label}"
|
||||
|
||||
def clean(self):
|
||||
# Validate options for selection fields
|
||||
if self.field_type in ['select', 'radio', 'checkbox']:
|
||||
@ -427,11 +440,18 @@ class FormField(Base):
|
||||
self.file_types = '.pdf,.doc,.docx'
|
||||
if self.max_file_size <= 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:
|
||||
# Clear file settings for non-file fields
|
||||
self.file_types = ''
|
||||
self.max_file_size = 0
|
||||
self.multiple_files = False
|
||||
self.max_files = 1
|
||||
|
||||
# Validate order
|
||||
if self.order < 0:
|
||||
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 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)
|
||||
# def parse_resume(sender, instance, created, **kwargs):
|
||||
@ -19,8 +22,6 @@ import os
|
||||
from .utils import extract_text_from_pdf,score_resume_with_openrouter
|
||||
import asyncio
|
||||
|
||||
|
||||
|
||||
@receiver(post_save, sender=models.Candidate)
|
||||
def score_candidate_resume(sender, instance, created, **kwargs):
|
||||
# Skip if no resume or OpenRouter not configured
|
||||
@ -105,9 +106,9 @@ def score_candidate_resume(sender, instance, created, **kwargs):
|
||||
|
||||
Only output valid JSON. Do not include any other text.
|
||||
"""
|
||||
|
||||
result1 = score_resume_with_openrouter(prompt)
|
||||
|
||||
|
||||
result1 = score_resume_with_openrouter(prompt)
|
||||
|
||||
instance.parsed_summary = str(result)
|
||||
|
||||
# Update candidate with scoring results
|
||||
@ -115,8 +116,8 @@ def score_candidate_resume(sender, instance, created, **kwargs):
|
||||
instance.strengths = result1.get('strengths', '')
|
||||
instance.weaknesses = result1.get('weaknesses', '')
|
||||
instance.criteria_checklist = result1.get('criteria_checklist', {})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Save only scoring-related fields to avoid recursion
|
||||
instance.save(update_fields=[
|
||||
@ -131,10 +132,291 @@ def score_candidate_resume(sender, instance, created, **kwargs):
|
||||
# instance.scoring_error = error_msg
|
||||
# instance.save(update_fields=['scoring_error'])
|
||||
logger.error(f"Failed to score resume for candidate {instance.id}: {e}")
|
||||
|
||||
|
||||
|
||||
# @receiver(post_save,sender=models.Candidate)
|
||||
# def trigger_scoring(sender,intance,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/<int:template_id>/', views.form_builder, name='form_builder'),
|
||||
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>/submit/', views.submit_form, name='submit_form'),
|
||||
|
||||
@ -7,8 +7,9 @@ from datetime import datetime
|
||||
from django.views import View
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from .forms import ZoomMeetingForm,JobPostingForm
|
||||
from .forms import ZoomMeetingForm,JobPostingForm,FormTemplateForm
|
||||
from rest_framework import viewsets
|
||||
from django.contrib import messages
|
||||
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 .utils import create_zoom_meeting, delete_zoom_meeting, update_zoom_meeting
|
||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||
|
||||
import logging
|
||||
|
||||
logger=logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -321,6 +322,7 @@ def linkedin_callback(request):
|
||||
access_token=service.get_access_token(code)
|
||||
request.session['linkedin_access_token']=access_token
|
||||
request.session['linkedin_authenticated']=True
|
||||
settings.LINKEDIN_IS_CONNECTED = True
|
||||
messages.success(request,'Successfully authenticated with LinkedIn!')
|
||||
except Exception as e:
|
||||
logger.error(f"LinkedIn authentication error: {e}")
|
||||
@ -685,10 +687,11 @@ def load_form_template(request, template_id):
|
||||
'id': template.id,
|
||||
'name': template.name,
|
||||
'description': template.description,
|
||||
'is_active': template.is_active,
|
||||
'job': template.job_id if template.job else None,
|
||||
'stages': stages
|
||||
}
|
||||
})
|
||||
|
||||
def form_templates_list(request):
|
||||
"""List all form templates for the current user"""
|
||||
query = request.GET.get('q', '')
|
||||
@ -703,13 +706,32 @@ def form_templates_list(request):
|
||||
paginator = Paginator(templates, 10) # Show 10 templates per page
|
||||
page_number = request.GET.get('page')
|
||||
page_obj = paginator.get_page(page_number)
|
||||
|
||||
form = FormTemplateForm()
|
||||
form.fields['job'].queryset = JobPosting.objects.filter(form_template__isnull=True)
|
||||
context = {
|
||||
'templates': page_obj,
|
||||
'query': query,
|
||||
'form': form
|
||||
}
|
||||
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"])
|
||||
def list_form_templates(request):
|
||||
"""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,21 +12,23 @@
|
||||
--primary: #004a53; /* Deep Teal/Cyan for main actions */
|
||||
--primary-light: #00b4d8; /* Brighter Aqua/Cyan */
|
||||
--secondary: #005a78; /* Darker Teal for hover/accent */
|
||||
--success: #00cc99; /* Bright Greenish-Teal for success */
|
||||
|
||||
--success: #005a78; /* Bright Greenish-Teal for success */
|
||||
|
||||
|
||||
/* Neutral Colors (Kept for consistency) */
|
||||
--light: #f4fcfc; /* Very light off-white (slightly blue tinted) */
|
||||
--dark: #212529; /* Near black text */
|
||||
--gray: #6c757d; /* Standard gray text */
|
||||
--light-gray: #e0f0f4; /* Lighter background for hover/disabled */
|
||||
--border: #c4d7e0; /* Lighter, softer border color */
|
||||
|
||||
|
||||
/* Structural Variables (Kept exactly the same) */
|
||||
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
--radius: 8px;
|
||||
--transition: all 0.3s ease;
|
||||
}
|
||||
/* All other structural and component styles below remain the same,
|
||||
/* All other structural and component styles below remain the same,
|
||||
but will automatically adopt the new colors defined above. */
|
||||
* {
|
||||
margin: 0;
|
||||
@ -670,18 +672,20 @@
|
||||
<!-- Pass Django CSRF token and other data -->
|
||||
<script>
|
||||
// Django template variables - these will be processed by Django
|
||||
const djangoConfig = {
|
||||
csrfToken: "{{ csrf_token }}",
|
||||
saveUrl: "{% url 'save_form_template' %}",
|
||||
loadUrl: {% if template_id %}"{% url 'load_form_template' template_id %}"{% else %}null{% endif %},
|
||||
templateId: {% if template_id %}{{ template_id }}{% else %}null{% endif %}
|
||||
};
|
||||
const djangoConfig = {
|
||||
csrfToken: "{{ csrf_token }}",
|
||||
saveUrl: "{% url 'save_form_template' %}",
|
||||
loadUrl: {% if template_id %}"{% url 'load_form_template' 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>
|
||||
|
||||
|
||||
<div class="container">
|
||||
<!-- Sidebar with form elements -->
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-header">
|
||||
<a class="" href="{% url 'form_templates_list' %}"></a>
|
||||
<h2><i class="fas fa-cube"></i> Form Elements</h2>
|
||||
</div>
|
||||
<div class="field-categories">
|
||||
@ -853,29 +857,51 @@
|
||||
</div>
|
||||
<!-- File Type Specific Settings -->
|
||||
<div class="editor-section" id="fileSettings" style="display: none;">
|
||||
<h4><i class="fas fa-file"></i> File Settings</h4>
|
||||
<div class="form-group">
|
||||
<label for="fileTypes">Allowed File Types</label>
|
||||
<input
|
||||
type="text"
|
||||
id="fileTypes"
|
||||
class="form-control"
|
||||
placeholder=".pdf, .doc, .docx"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="maxFileSize">Max File Size (MB)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="maxFileSize"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="100"
|
||||
value="5"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h4><i class="fas fa-file"></i> File Settings</h4>
|
||||
<div class="form-group">
|
||||
<label for="fileTypes">Allowed File Types</label>
|
||||
<input
|
||||
type="text"
|
||||
id="fileTypes"
|
||||
class="form-control"
|
||||
placeholder=".pdf, .doc, .docx"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="maxFileSize">Max File Size (MB)</label>
|
||||
<input
|
||||
type="number"
|
||||
id="maxFileSize"
|
||||
class="form-control"
|
||||
min="1"
|
||||
max="100"
|
||||
value="5"
|
||||
>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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>
|
||||
@ -1156,97 +1182,109 @@
|
||||
|
||||
// API Functions
|
||||
async function saveFormTemplate() {
|
||||
const formData = {
|
||||
name: state.formName,
|
||||
description: state.formDescription,
|
||||
is_active: state.formActive,
|
||||
template_id: state.templateId, // Include template_id for updates
|
||||
stages: state.stages.map(stage => ({
|
||||
name: stage.name,
|
||||
predefined: stage.predefined,
|
||||
fields: stage.fields.map(field => ({
|
||||
type: field.type,
|
||||
label: field.label,
|
||||
placeholder: field.placeholder || '',
|
||||
required: field.required || false,
|
||||
options: field.options || [],
|
||||
fileTypes: field.fileTypes || '',
|
||||
maxFileSize: field.maxFileSize || 5,
|
||||
predefined: field.predefined
|
||||
}))
|
||||
}))
|
||||
};
|
||||
const formData = {
|
||||
name: state.formName,
|
||||
description: state.formDescription,
|
||||
is_active: state.formActive,
|
||||
template_id: state.templateId,
|
||||
stages: state.stages.map(stage => ({
|
||||
name: stage.name,
|
||||
predefined: stage.predefined,
|
||||
fields: stage.fields.map(field => ({
|
||||
type: field.type,
|
||||
label: field.label,
|
||||
placeholder: field.placeholder || '',
|
||||
required: field.required || false,
|
||||
options: field.options || [],
|
||||
fileTypes: field.fileTypes || '',
|
||||
maxFileSize: field.maxFileSize || 5,
|
||||
predefined: field.predefined
|
||||
}))
|
||||
}))
|
||||
};
|
||||
|
||||
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 there's a job_id in the Django context, include it
|
||||
if (djangoConfig.jobId) {
|
||||
formData.job = djangoConfig.jobId;
|
||||
}
|
||||
|
||||
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) {
|
||||
alert('Form template saved successfully! Template ID: ' + result.template_id);
|
||||
// Update templateId for future saves (important for new templates)
|
||||
state.templateId = result.template_id;
|
||||
} else {
|
||||
alert('Error saving form template: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error saving form template. Please try again.');
|
||||
}
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
state.templateId = result.template_id;
|
||||
window.location.href = "{% url 'form_templates_list' %}";
|
||||
|
||||
} else {
|
||||
alert('Error saving form template: ' + result.error);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('Error saving form template. Please try again.');
|
||||
}
|
||||
}
|
||||
// Load existing template if editing
|
||||
async function loadExistingTemplate() {
|
||||
if (djangoConfig.loadUrl) {
|
||||
try {
|
||||
const response = await fetch(djangoConfig.loadUrl);
|
||||
const result = await response.json();
|
||||
if (djangoConfig.loadUrl) {
|
||||
try {
|
||||
const response = await fetch(djangoConfig.loadUrl);
|
||||
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) {
|
||||
const templateData = result.template;
|
||||
// Set form settings
|
||||
state.formName = templateData.name || 'Untitled Form';
|
||||
state.formDescription = templateData.description || '';
|
||||
state.formActive = templateData.is_active !== false; // Default to true if not set
|
||||
// Update form title
|
||||
elements.formTitle.textContent = state.formName;
|
||||
elements.formName.value = state.formName;
|
||||
elements.formDescription.value = state.formDescription;
|
||||
elements.formActive.checked = state.formActive;
|
||||
|
||||
// Update form title
|
||||
elements.formTitle.textContent = state.formName;
|
||||
elements.formName.value = state.formName;
|
||||
elements.formDescription.value = state.formDescription;
|
||||
elements.formActive.checked = state.formActive;
|
||||
// Set stages (this is where your actual stages come from)
|
||||
state.stages = templateData.stages;
|
||||
state.templateId = templateData.id;
|
||||
|
||||
// Set stages
|
||||
state.stages = templateData.stages;
|
||||
state.templateId = templateData.id;
|
||||
// Update next IDs to avoid conflicts
|
||||
let maxFieldId = 0;
|
||||
let maxStageId = 0;
|
||||
templateData.stages.forEach(stage => {
|
||||
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;
|
||||
renderStageNavigation();
|
||||
renderCurrentStage();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading template:', error);
|
||||
alert('Error loading template data.');
|
||||
}
|
||||
// Update next IDs to avoid conflicts
|
||||
let maxFieldId = 0;
|
||||
let maxStageId = 0;
|
||||
templateData.stages.forEach(stage => {
|
||||
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;
|
||||
|
||||
// Now show the form content
|
||||
elements.formStage.style.display = 'block';
|
||||
elements.emptyState.style.display = 'none';
|
||||
|
||||
renderStageNavigation();
|
||||
renderCurrentStage();
|
||||
}
|
||||
} 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)
|
||||
function renderStageNavigation() {
|
||||
@ -1319,164 +1357,255 @@
|
||||
}
|
||||
|
||||
function createFieldElement(field, index) {
|
||||
const fieldDiv = document.createElement('div');
|
||||
fieldDiv.className = `form-field ${state.selectedField && state.selectedField.id === field.id ? 'selected' : ''}`;
|
||||
fieldDiv.dataset.fieldId = field.id;
|
||||
fieldDiv.dataset.fieldIndex = index;
|
||||
const fieldHeader = document.createElement('div');
|
||||
fieldHeader.className = 'field-header';
|
||||
fieldHeader.innerHTML = `
|
||||
<div class="field-title">
|
||||
<i class="${getFieldIcon(field.type)}"></i>
|
||||
${field.label || field.type.charAt(0).toUpperCase() + field.type.slice(1)}
|
||||
${field.required ? '<span class="required-indicator"> *</span>' : ''}
|
||||
</div>
|
||||
<div class="field-actions">
|
||||
<div class="action-btn edit-field" data-field-id="${field.id}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</div>
|
||||
${!field.predefined ? `<div class="action-btn remove-field" data-field-index="${index}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
const fieldContent = document.createElement('div');
|
||||
fieldContent.className = 'field-content';
|
||||
fieldContent.innerHTML = `
|
||||
<label class="field-label">
|
||||
${field.label || 'Field Label'}
|
||||
${field.required ? '<span class="required-indicator"> *</span>' : ''}
|
||||
</label>
|
||||
`;
|
||||
// Add field input based on type
|
||||
if (field.type === 'text' || field.type === 'email' || field.type === 'phone' || field.type === 'date') {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'field-input';
|
||||
input.placeholder = field.placeholder || 'Enter value';
|
||||
input.disabled = true;
|
||||
fieldContent.appendChild(input);
|
||||
} else if (field.type === 'textarea') {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.className = 'field-input';
|
||||
textarea.rows = 3;
|
||||
textarea.placeholder = field.placeholder || 'Enter text';
|
||||
textarea.disabled = true;
|
||||
fieldContent.appendChild(textarea);
|
||||
} else if (field.type === 'file') {
|
||||
const fileUpload = document.createElement('div');
|
||||
fileUpload.className = 'file-upload-area';
|
||||
fileUpload.innerHTML = `
|
||||
<div class="file-upload-icon">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
</div>
|
||||
<div class="file-upload-text">
|
||||
<p>Drag & drop your resume here or <strong>click to browse</strong></p>
|
||||
</div>
|
||||
<div class="file-upload-info">
|
||||
<p>Supported formats: ${field.fileTypes || '.pdf, .doc, .docx'} (Max ${field.maxFileSize || 5}MB)</p>
|
||||
</div>
|
||||
<input type="file" class="file-input" style="display: none;" accept="${field.fileTypes || '.pdf,.doc,.docx'}">
|
||||
`;
|
||||
if (field.uploadedFile) {
|
||||
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">${field.uploadedFile.name}</div>
|
||||
<div class="file-size">${formatFileSize(field.uploadedFile.size)}</div>
|
||||
</div>
|
||||
const fieldDiv = document.createElement('div');
|
||||
fieldDiv.className = `form-field ${state.selectedField && state.selectedField.id === field.id ? 'selected' : ''}`;
|
||||
fieldDiv.dataset.fieldId = field.id;
|
||||
fieldDiv.dataset.fieldIndex = index;
|
||||
|
||||
const fieldHeader = document.createElement('div');
|
||||
fieldHeader.className = 'field-header';
|
||||
fieldHeader.innerHTML = `
|
||||
<div class="field-title">
|
||||
<i class="${getFieldIcon(field.type)}"></i>
|
||||
${field.label || field.type.charAt(0).toUpperCase() + field.type.slice(1)}
|
||||
${field.required ? '<span class="required-indicator"> *</span>' : ''}
|
||||
</div>
|
||||
<div class="field-actions">
|
||||
<div class="action-btn edit-field" data-field-id="${field.id}">
|
||||
<i class="fas fa-edit"></i>
|
||||
</div>
|
||||
${!field.predefined ? `<div class="action-btn remove-field" data-field-index="${index}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const fieldContent = document.createElement('div');
|
||||
fieldContent.className = 'field-content';
|
||||
fieldContent.innerHTML = `
|
||||
<label class="field-label">
|
||||
${field.label || 'Field Label'}
|
||||
${field.required ? '<span class="required-indicator"> *</span>' : ''}
|
||||
</label>
|
||||
`;
|
||||
|
||||
// Add field input based on type
|
||||
if (field.type === 'text' || field.type === 'email' || field.type === 'phone' || field.type === 'date') {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'text';
|
||||
input.className = 'field-input';
|
||||
input.placeholder = field.placeholder || 'Enter value';
|
||||
input.disabled = true;
|
||||
fieldContent.appendChild(input);
|
||||
} else if (field.type === 'textarea') {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.className = 'field-input';
|
||||
textarea.rows = 3;
|
||||
textarea.placeholder = field.placeholder || 'Enter text';
|
||||
textarea.disabled = true;
|
||||
fieldContent.appendChild(textarea);
|
||||
} else if (field.type === 'file') {
|
||||
const fileUpload = document.createElement('div');
|
||||
fileUpload.className = 'file-upload-area';
|
||||
fileUpload.innerHTML = `
|
||||
<div class="file-upload-icon">
|
||||
<i class="fas fa-cloud-upload-alt"></i>
|
||||
</div>
|
||||
<div class="file-upload-text">
|
||||
<p>Drag & drop your ${field.label.toLowerCase()} here or <strong>click to browse</strong></p>
|
||||
</div>
|
||||
<div class="file-upload-info">
|
||||
<p>Supported formats: ${field.fileTypes || '.pdf, .doc, .docx'} (Max ${field.maxFileSize || 5}MB)</p>
|
||||
${field.multipleFiles ? `<p>Multiple files allowed (Max ${field.maxFiles || 1} files)</p>` : ''}
|
||||
</div>
|
||||
<input type="file" class="file-input" style="display: none;"
|
||||
accept="${field.fileTypes || '.pdf,.doc,.docx'}"
|
||||
${field.multipleFiles ? 'multiple' : ''}>
|
||||
`;
|
||||
|
||||
// Show uploaded files
|
||||
if (field.uploadedFiles && field.uploadedFiles.length > 0) {
|
||||
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>
|
||||
<button class="remove-file-btn">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
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);
|
||||
}
|
||||
</div>
|
||||
<button class="remove-file-btn" data-file-index="${fileIndex}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
fileUpload.appendChild(uploadedFile);
|
||||
});
|
||||
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) {
|
||||
elements.fieldEditor.style.display = 'flex';
|
||||
elements.fieldLabel.value = field.label || '';
|
||||
@ -1499,30 +1628,51 @@
|
||||
}
|
||||
|
||||
function renderOptionsEditor(field) {
|
||||
elements.optionsList.innerHTML = '';
|
||||
field.options.forEach((option, index) => {
|
||||
const optionInput = document.createElement('div');
|
||||
optionInput.className = 'option-input';
|
||||
optionInput.innerHTML = `
|
||||
<input type="text" class="form-control" value="${option}" placeholder="Option ${index + 1}">
|
||||
<button class="remove-option">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
elements.optionsList.appendChild(optionInput);
|
||||
const input = optionInput.querySelector('input');
|
||||
const removeBtn = optionInput.querySelector('.remove-option');
|
||||
input.addEventListener('input', () => {
|
||||
field.options[index] = input.value;
|
||||
});
|
||||
removeBtn.addEventListener('click', () => {
|
||||
if (field.options.length > 1) {
|
||||
field.options.splice(index, 1);
|
||||
renderOptionsEditor(field);
|
||||
elements.optionsList.innerHTML = '';
|
||||
field.options.forEach((option, index) => {
|
||||
const optionInput = document.createElement('div');
|
||||
optionInput.className = 'option-input';
|
||||
optionInput.innerHTML = `
|
||||
<input type="text" class="form-control" value="${option}" placeholder="Option ${index + 1}">
|
||||
<button class="remove-option">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
`;
|
||||
elements.optionsList.appendChild(optionInput);
|
||||
|
||||
const input = optionInput.querySelector('input');
|
||||
const removeBtn = optionInput.querySelector('.remove-option');
|
||||
|
||||
input.addEventListener('input', () => {
|
||||
field.options[index] = input.value;
|
||||
});
|
||||
|
||||
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)
|
||||
function selectField(field) {
|
||||
@ -1668,29 +1818,33 @@
|
||||
}
|
||||
|
||||
function drop(event) {
|
||||
event.preventDefault();
|
||||
event.target.classList.remove('drag-over');
|
||||
if (state.draggedField) {
|
||||
const newField = {
|
||||
id: state.nextFieldId++,
|
||||
type: state.draggedField.type,
|
||||
label: state.draggedField.label,
|
||||
placeholder: '',
|
||||
required: false,
|
||||
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,
|
||||
predefined: false,
|
||||
uploadedFile: null
|
||||
};
|
||||
state.stages[state.currentStage].fields.push(newField);
|
||||
selectField(newField);
|
||||
state.draggedField = null;
|
||||
renderCurrentStage();
|
||||
}
|
||||
}
|
||||
event.preventDefault();
|
||||
event.target.classList.remove('drag-over');
|
||||
|
||||
if (state.draggedField) {
|
||||
const newField = {
|
||||
id: state.nextFieldId++,
|
||||
type: state.draggedField.type,
|
||||
label: state.draggedField.label,
|
||||
placeholder: '',
|
||||
required: false,
|
||||
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,
|
||||
multipleFiles: state.draggedField.type === 'file' ? false : undefined,
|
||||
maxFiles: state.draggedField.type === 'file' ? 1 : undefined,
|
||||
predefined: false,
|
||||
uploadedFiles: state.draggedField.type === 'file' ? [] : undefined
|
||||
};
|
||||
|
||||
state.stages[state.currentStage].fields.push(newField);
|
||||
selectField(newField);
|
||||
state.draggedField = null;
|
||||
renderCurrentStage();
|
||||
}
|
||||
}
|
||||
|
||||
function dropField(targetIndex) {
|
||||
if (state.draggedFieldIndex !== null && state.draggedFieldIndex !== targetIndex) {
|
||||
@ -1790,15 +1944,23 @@
|
||||
// Initialize Application
|
||||
function init() {
|
||||
// Initialize form title
|
||||
elements.formTitle.textContent = state.formName;
|
||||
elements.formTitle.textContent = 'Loading...';
|
||||
|
||||
renderStageNavigation();
|
||||
renderCurrentStage();
|
||||
initEventListeners();
|
||||
// Load existing template if editing
|
||||
if (djangoConfig.loadUrl) {
|
||||
loadExistingTemplate();
|
||||
}
|
||||
// Hide the form stage initially to prevent flickering
|
||||
elements.formStage.style.display = 'none';
|
||||
elements.emptyState.style.display = 'block';
|
||||
elements.emptyState.innerHTML = '<i class="fas fa-spinner fa-spin"></i><p>Loading form template...</p>';
|
||||
|
||||
// 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
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static i18n %}
|
||||
{% load static i18n crispy_forms_tags %}
|
||||
|
||||
{% block title %}Form Templates - ATS{% endblock %}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
--kaauh-teal-dark: #004a53;
|
||||
--kaauh-border: #eaeff3;
|
||||
--kaauh-primary-text: #343a40;
|
||||
--kaauh-gray-light: #f8f9fa;
|
||||
--kaauh-gray-light: #f8f9fa;
|
||||
}
|
||||
|
||||
/* --- Typography and Color Overrides --- */
|
||||
@ -25,7 +25,7 @@
|
||||
border-color: var(--kaauh-teal);
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
padding: 0.375rem 0.75rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-flex;
|
||||
@ -37,7 +37,7 @@
|
||||
border-color: var(--kaauh-teal-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||
color: white;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Secondary Button Style (for Edit/Preview) */
|
||||
@ -69,39 +69,41 @@
|
||||
background-color: white;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
|
||||
/* Template Card Hover Effect (Consistent with job list card hover) */
|
||||
.template-card {
|
||||
height: 100%;
|
||||
}
|
||||
.template-card:hover {
|
||||
transform: translateY(-2px);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0,0,0,0.1) !important;
|
||||
}
|
||||
|
||||
|
||||
/* Card Header Theming */
|
||||
.card-header {
|
||||
/* FIX: Use !important to override default white/light backgrounds from Bootstrap */
|
||||
background-color: var(--kaauh-teal-dark) !important;
|
||||
background-color: var(--kaauh-teal-dark) !important;
|
||||
border-bottom: 1px solid var(--kaauh-border);
|
||||
color: white !important; /* Base color for header text */
|
||||
font-weight: 600;
|
||||
padding: 1rem 1.25rem;
|
||||
border-radius: 0.75rem 0.75rem 0 0;
|
||||
}
|
||||
|
||||
|
||||
/* Ensure all elements within the header are visible */
|
||||
.card-header h3 {
|
||||
color: white !important;
|
||||
color: white !important;
|
||||
font-weight: 700;
|
||||
}
|
||||
.card-header .fas {
|
||||
color: white !important;
|
||||
color: white !important;
|
||||
}
|
||||
.card-header .small {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
}
|
||||
|
||||
/* Stats Theming */
|
||||
|
||||
/* --- Content Styles (Stats, Description) --- */
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
@ -116,14 +118,13 @@
|
||||
.card-description {
|
||||
min-height: 60px;
|
||||
color: var(--kaauh-primary-text);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* --- Form/Search Input Theming (Matching Job List) --- */
|
||||
.form-control-search {
|
||||
box-shadow: none;
|
||||
/* Search Input Theming */
|
||||
.form-control {
|
||||
border-radius: 0.5rem 0 0 0.5rem;
|
||||
border-color: var(--kaauh-border);
|
||||
border-radius: 0 0.5rem 0.5rem 0;
|
||||
border-radius: 0 0.5rem 0.5rem 0;
|
||||
}
|
||||
.form-control-search:focus {
|
||||
border-color: var(--kaauh-teal);
|
||||
@ -146,8 +147,8 @@
|
||||
--bs-btn-hover-bg: #dc3545;
|
||||
--bs-btn-hover-color: white;
|
||||
}
|
||||
|
||||
/* --- Empty State Theming --- */
|
||||
|
||||
/* Empty State Theming */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 3rem 1rem;
|
||||
@ -159,7 +160,7 @@
|
||||
.empty-state i {
|
||||
font-size: 3.5rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--kaauh-teal-dark);
|
||||
color: var(--kaauh-teal-dark);
|
||||
}
|
||||
.empty-state .btn-main-action .fas {
|
||||
color: white !important;
|
||||
@ -188,23 +189,23 @@
|
||||
<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" %}
|
||||
</h1>
|
||||
<a href="{% url 'form_builder' %}" class="btn btn-main-action">
|
||||
<i class="fas fa-plus me-1"></i> {% trans "Create New Template" %}
|
||||
</a>
|
||||
<button type="button" class="btn btn-main-action" data-bs-toggle="modal" data-bs-target="#createTemplateModal">
|
||||
<i class="fas fa-plus me-1"></i> Create New Template
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{# Search/Filter Area - Matching Job List Structure #}
|
||||
<div class="card mb-4 shadow-sm no-hover">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-muted mb-3" style="font-weight: 500;">Search Templates</h5>
|
||||
<form method="get" class="row g-3 align-items-end">
|
||||
|
||||
<form method="get" class="row g-3 align-items-end">
|
||||
|
||||
<div class="col-md-6">
|
||||
<label for="search" class="form-label small text-muted">Search by Template Name</label>
|
||||
<div class="input-group input-group-lg input-group-search">
|
||||
<span class="input-group-text"><i class="fas fa-search text-muted"></i></span>
|
||||
<input type="text" name="q" id="searchInput" class="form-control form-control-search"
|
||||
placeholder="{% trans 'Search templates by name...' %}"
|
||||
<input type="text" name="q" id="searchInput" class="form-control form-control-search"
|
||||
placeholder="{% trans 'Search templates by name...' %}"
|
||||
value="{{ query|default_if_none:'' }}">
|
||||
</div>
|
||||
</div>
|
||||
@ -214,7 +215,7 @@
|
||||
<button type="submit" class="btn btn-main-action btn-lg">
|
||||
<i class="fas fa-filter me-1"></i> Search
|
||||
</button>
|
||||
|
||||
|
||||
{# Show Clear button if search is active #}
|
||||
{% if query %}
|
||||
<a href="{% url 'form_templates_list' %}" class="btn btn-outline-danger btn-sm">
|
||||
@ -236,13 +237,14 @@
|
||||
<div class="card template-card h-100">
|
||||
<div class="card-header ">
|
||||
<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-sync-alt me-1"></i> {{ template.updated_at|timesince }} {% trans "ago" %}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
|
||||
|
||||
{# Content area - includes stats and description #}
|
||||
<div class="flex-grow-1">
|
||||
<div class="row text-center mb-3">
|
||||
@ -263,7 +265,7 @@
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
{# Action area - visually separated with pt-2 border-top #}
|
||||
<div class="mt-auto pt-2 border-top">
|
||||
<div class="d-grid gap-2 d-md-flex justify-content-md-end">
|
||||
@ -336,7 +338,33 @@
|
||||
{% endif %}
|
||||
</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 %}
|
||||
|
||||
{% block customJS %}
|
||||
@ -387,7 +415,7 @@
|
||||
window.location.href = query ? `?q=${encodeURIComponent(query)}` : '{% url "form_templates_list" %}';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Bind search form submit to the main button click event for consistency
|
||||
document.querySelector('.filter-buttons button[type="submit"]').addEventListener('click', function(e) {
|
||||
// Prevent default submission to handle URL construction correctly
|
||||
@ -415,18 +443,18 @@
|
||||
e.preventDefault();
|
||||
|
||||
if (!templateToDelete) return;
|
||||
|
||||
// This CSRF token selector assumes it's present in your base template or form
|
||||
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
|
||||
// This relies on 'csrfToken' being defined somewhere, which is typical for Django templates.
|
||||
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]').value;
|
||||
|
||||
try {
|
||||
// NOTE: Update this URL to match your actual Django API endpoint for deletion
|
||||
const response = await fetch(`/api/templates/${templateToDelete}/delete/`, {
|
||||
const response = await fetch(`/api/templates/${templateToDelete}/delete/`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRFToken': csrfToken,
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Content-Type': 'application/json'
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
@ -473,5 +501,50 @@
|
||||
document.getElementById('deleteModal').addEventListener('hidden.bs.modal', function() {
|
||||
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>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user