kaauh_ats/templates/forms/form_preview.html
2025-10-05 12:19:45 +03:00

716 lines
27 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ form.title }} - Form Preview{% endblock %}
{% block content %}
{% if not is_embed %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1><i class="fas fa-wpforms"></i> Form Preview</h1>
<div>
<a href="{% url 'form_list' %}" class="btn btn-outline-secondary me-2">
<i class="fas fa-arrow-left"></i> Back to Forms
</a>
<a href="{% url 'form_embed' form.id %}" class="btn btn-outline-primary" target="_blank">
<i class="fas fa-code"></i> Get Embed Code
</a>
</div>
</div>
</div>
</div>
</div>
{% endif %}
<!-- Form Preview Container -->
<div class="{% if is_embed %}embed-container{% else %}container-fluid{% endif %}">
<div class="{% if is_embed %}embed-form-wrapper{% else %}row justify-content-center{% endif %}">
<div class="{% if is_embed %}embed-form{% else %}col-lg-8 col-md-10{% endif %}">
<!-- Form Header -->
<div class="card shadow-sm mb-4">
<div class="card-header bg-primary text-white">
<h3 class="mb-1">{{ form.title }}</h3>
{% if form.description %}
<p class="mb-0 opacity-90">{{ form.description }}</p>
{% endif %}
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<small class="text-muted">
<i class="fas fa-chart-bar"></i> {{ submission_count }} submissions
</small>
<small class="text-muted">
<i class="fas fa-calendar"></i> Created {{ form.created_at|date:"M d, Y" }}
</small>
</div>
</div>
</div>
<!-- Live Form Preview -->
<div class="card shadow-sm">
<div class="card-body p-0">
<div id="form-preview-container">
<!-- Form will be rendered here by Preact -->
<div class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading form...</span>
</div>
<p class="mt-3 text-muted">Loading form preview...</p>
</div>
</div>
</div>
</div>
<!-- Form Analytics (only shown if not embedded) -->
{% if not is_embed %}
<div class="card shadow-sm mt-4">
<div class="card-header">
<h5><i class="fas fa-chart-line"></i> Form Analytics</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="text-center">
<h4 class="text-primary">{{ submission_count }}</h4>
<small class="text-muted">Total Submissions</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-success">{{ form.created_at|timesince }}</h4>
<small class="text-muted">Time Created</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-info">{{ form.structure.wizards|length|default:0 }}</h4>
<small class="text-muted">Form Steps</small>
</div>
</div>
<div class="col-md-3">
<div class="text-center">
<h4 class="text-warning">
{% if form.structure.wizards %}
{{ form.structure.wizards.0.fields|length|default:0 }}
{% else %}
0
{% endif %}
</h4>
<small class="text-muted">First Step Fields</small>
</div>
</div>
</div>
</div>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Success Modal -->
<div class="modal fade" id="successModal" tabindex="-1" aria-labelledby="successModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-success text-white">
<h5 class="modal-title" id="successModalLabel">
<i class="fas fa-check-circle"></i> Form Submitted Successfully!
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="mb-0">Thank you for submitting the form. Your response has been recorded.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" onclick="resetForm()">Submit Another Response</button>
</div>
</div>
</div>
</div>
<!-- Error Modal -->
<div class="modal fade" id="errorModal" tabindex="-1" aria-labelledby="errorModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title" id="errorModalLabel">
<i class="fas fa-exclamation-triangle"></i> Submission Error
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="mb-0" id="errorMessage">An error occurred while submitting the form. Please try again.</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_css %}
{% if is_embed %}
<style>
.embed-container {
background: #f8f9fa;
padding: 0;
}
.embed-form-wrapper {
margin: 0;
}
.embed-form {
padding: 0;
}
.embed-form .card {
border: none;
border-radius: 0;
box-shadow: none;
}
.embed-form .card-header {
border-radius: 0;
}
body {
background: #f8f9fa;
margin: 0;
padding: 0;
}
</style>
{% endif %}
{% endblock %}
{% block extra_js %}
<script src="https://unpkg.com/preact@10.19.3/dist/preact.umd.js"></script>
<script src="https://unpkg.com/htm@3.1.1/dist/htm.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<!-- FilePond for file uploads -->
<link href="https://unpkg.com/filepond/dist/filepond.css" rel="stylesheet">
<script src="https://unpkg.com/filepond/dist/filepond.js"></script>
<script src="https://unpkg.com/filepond-plugin-file-validate-type/dist/filepond-plugin-file-validate-type.js"></script>
<script src="https://unpkg.com/filepond-plugin-file-validate-size/dist/filepond-plugin-file-validate-size.js"></script>
<script>
// Form data from Django
const formId = {{ form.id }};
const formStructure = {{ form.structure|safe }};
const isEmbed = {{ is_embed|yesno:"true,false" }};
const { h, Component, render, useState, useEffect, useRef } = preact;
const html = htm.bind(h);
// Field Components for Preview
class PreviewTextField extends Component {
render() {
const { field, value, onChange, error } = this.props;
return html`
<div class="mb-3">
<label class="form-label">
${field.label}
${field.required ? html`<span class="text-danger">*</span>` : ''}
</label>
<input type="text"
class="form-control ${error ? 'is-invalid' : ''}"
placeholder=${field.placeholder || ''}
value=${value || ''}
onInput=${(e) => onChange(field.id, e.target.value)}
required=${field.required} />
${error ? html`<div class="invalid-feedback">${error}</div>` : ''}
</div>
`;
}
}
class PreviewEmailField extends Component {
render() {
const { field, value, onChange, error } = this.props;
return html`
<div class="mb-3">
<label class="form-label">
${field.label}
${field.required ? html`<span class="text-danger">*</span>` : ''}
</label>
<input type="email"
class="form-control ${error ? 'is-invalid' : ''}"
placeholder=${field.placeholder || ''}
value=${value || ''}
onInput=${(e) => onChange(field.id, e.target.value)}
required=${field.required} />
${error ? html`<div class="invalid-feedback">${error}</div>` : ''}
</div>
`;
}
}
class PreviewPhoneField extends Component {
render() {
const { field, value, onChange, error } = this.props;
return html`
<div class="mb-3">
<label class="form-label">
${field.label}
${field.required ? html`<span class="text-danger">*</span>` : ''}
</label>
<input type="tel"
class="form-control ${error ? 'is-invalid' : ''}"
placeholder=${field.placeholder || ''}
value=${value || ''}
onInput=${(e) => onChange(field.id, e.target.value)}
required=${field.required} />
${error ? html`<div class="invalid-feedback">${error}</div>` : ''}
</div>
`;
}
}
class PreviewDateField extends Component {
render() {
const { field, value, onChange, error } = this.props;
return html`
<div class="mb-3">
<label class="form-label">
${field.label}
${field.required ? html`<span class="text-danger">*</span>` : ''}
</label>
<input type="date"
class="form-control ${error ? 'is-invalid' : ''}"
value=${value || ''}
onInput=${(e) => onChange(field.id, e.target.value)}
required=${field.required} />
${error ? html`<div class="invalid-feedback">${error}</div>` : ''}
</div>
`;
}
}
class PreviewFileField extends Component {
constructor(props) {
super(props);
this.filePondRef = null;
}
componentDidMount() {
this.initFilePond();
}
componentWillUnmount() {
if (this.filePondRef) {
this.filePondRef.destroy();
}
}
initFilePond() {
const { field, onChange } = this.props;
const inputElement = document.getElementById(`file-upload-${field.id}`);
if (!inputElement || typeof FilePond === 'undefined') return;
this.filePondRef = FilePond.create(inputElement, {
allowMultiple: field.multiple || false,
maxFiles: field.maxFiles || 1,
maxFileSize: field.maxFileSize ? `${field.maxFileSize}MB` : '5MB',
acceptedFileTypes: field.fileTypes || ['*'],
labelIdle: 'Drag & Drop your files or <span class="filepond--label-action">Browse</span>',
credits: false,
onupdatefiles: (fileItems) => {
const files = fileItems.map(item => item.file);
onChange(field.id, files);
}
});
}
render() {
const { field, error } = this.props;
return html`
<div class="mb-3">
<label class="form-label">
${field.label}
${field.required ? html`<span class="text-danger">*</span>` : ''}
</label>
<input id="file-upload-${field.id}" type="file" />
${error ? html`<div class="text-danger small mt-1">${error}</div>` : ''}
${field.fileTypes ? html`<div class="form-text">Accepted: ${field.fileTypes.join(', ')}</div>` : ''}
</div>
`;
}
}
class PreviewDropdownField extends Component {
render() {
const { field, value, onChange, error } = this.props;
return html`
<div class="mb-3">
<label class="form-label">
${field.label}
${field.required ? html`<span class="text-danger">*</span>` : ''}
</label>
<select class="form-select ${error ? 'is-invalid' : ''}"
value=${value || ''}
onChange=${(e) => onChange(field.id, e.target.value)}
required=${field.required}>
<option value="">Select an option...</option>
${field.options.map(option => html`
<option value=${option.value}>${option.value}</option>
`)}
</select>
${error ? html`<div class="invalid-feedback">${error}</div>` : ''}
</div>
`;
}
}
class PreviewRadioField extends Component {
render() {
const { field, value, onChange, error } = this.props;
return html`
<div class="mb-3">
<label class="form-label">
${field.label}
${field.required ? html`<span class="text-danger">*</span>` : ''}
</label>
${field.options.map(option => html`
<div class="form-check">
<input class="form-check-input"
type="radio"
name=${field.id}
id=${option.id}
value=${option.value}
checked=${value === option.value}
onChange=${(e) => onChange(field.id, e.target.value)}
required=${field.required} />
<label class="form-check-label" for=${option.id}>
${option.value}
</label>
</div>
`)}
${error ? html`<div class="text-danger small mt-1">${error}</div>` : ''}
</div>
`;
}
}
class PreviewCheckboxField extends Component {
render() {
const { field, value = [], onChange, error } = this.props;
return html`
<div class="mb-3">
<label class="form-label">
${field.label}
${field.required ? html`<span class="text-danger">*</span>` : ''}
</label>
${field.options.map(option => html`
<div class="form-check">
<input class="form-check-input"
type="checkbox"
id=${option.id}
value=${option.value}
checked=${value.includes(option.value)}
onChange=${(e) => {
const newValue = e.target.checked
? [...value, option.value]
: value.filter(v => v !== option.value);
onChange(field.id, newValue);
}} />
<label class="form-check-label" for=${option.id}>
${option.value}
</label>
</div>
`)}
${error ? html`<div class="text-danger small mt-1">${error}</div>` : ''}
</div>
`;
}
}
class PreviewRatingField extends Component {
render() {
const { field, value, onChange, error } = this.props;
return html`
<div class="mb-3">
<label class="form-label">
${field.label}
${field.required ? html`<span class="text-danger">*</span>` : ''}
</label>
<div class="rating-container">
${[1, 2, 3, 4, 5].map(n => html`
<span class="rating-star ${value >= n ? 'active' : ''}"
style="font-size: 24px; color: ${value >= n ? '#ffc107' : '#ddd'}; cursor: pointer; margin-right: 5px;"
onClick=${() => onChange(field.id, n)}>
</span>
`)}
</div>
${error ? html`<div class="text-danger small mt-1">${error}</div>` : ''}
</div>
`;
}
}
// Field Factory
function createPreviewField(field, props) {
const fieldProps = { ...props, field };
switch (field.type) {
case 'text': return html`<${PreviewTextField} ...${fieldProps} />`;
case 'email': return html`<${PreviewEmailField} ...${fieldProps} />`;
case 'phone': return html`<${PreviewPhoneField} ...${fieldProps} />`;
case 'date': return html`<${PreviewDateField} ...${fieldProps} />`;
case 'file': return html`<${PreviewFileField} ...${fieldProps} />`;
case 'dropdown': return html`<${PreviewDropdownField} ...${fieldProps} />`;
case 'radio': return html`<${PreviewRadioField} ...${fieldProps} />`;
case 'checkbox': return html`<${PreviewCheckboxField} ...${fieldProps} />`;
case 'rating': return html`<${PreviewRatingField} ...${fieldProps} />`;
default: return html`<${PreviewTextField} ...${fieldProps} />`;
}
}
// Main Form Preview Component
class FormPreview extends Component {
constructor(props) {
super(props);
this.state = {
currentWizardIndex: 0,
formData: {},
errors: {},
isSubmitting: false,
isSubmitted: false
};
}
getCurrentWizard() {
return formStructure.wizards[this.state.currentWizardIndex] || null;
}
getCurrentWizardFields() {
const wizard = this.getCurrentWizard();
return wizard ? wizard.fields : [];
}
getTotalWizards() {
return formStructure.wizards ? formStructure.wizards.length : 0;
}
getProgress() {
return ((this.state.currentWizardIndex + 1) / this.getTotalWizards()) * 100;
}
validateCurrentWizard() {
const fields = this.getCurrentWizardFields();
const errors = {};
fields.forEach(field => {
const value = this.state.formData[field.id];
if (field.required && (!value || (Array.isArray(value) && value.length === 0))) {
errors[field.id] = `${field.label} is required`;
}
// Email validation
if (field.type === 'email' && value) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(value)) {
errors[field.id] = 'Please enter a valid email address';
}
}
// Phone validation
if (field.type === 'phone' && value) {
const phoneRegex = /^[\d\s\-\+\(\)]+$/;
if (!phoneRegex.test(value)) {
errors[field.id] = 'Please enter a valid phone number';
}
}
});
this.setState({ errors });
return Object.keys(errors).length === 0;
}
handleFieldChange = (fieldId, value) => {
this.setState({
formData: { ...this.state.formData, [fieldId]: value },
errors: { ...this.state.errors, [fieldId]: null }
});
}
handleNext = () => {
if (this.validateCurrentWizard()) {
if (this.state.currentWizardIndex < this.getTotalWizards() - 1) {
this.setState({ currentWizardIndex: this.state.currentWizardIndex + 1 });
} else {
this.handleSubmit();
}
}
}
handlePrevious = () => {
if (this.state.currentWizardIndex > 0) {
this.setState({ currentWizardIndex: this.state.currentWizardIndex - 1 });
}
}
handleSubmit = async () => {
if (!this.validateCurrentWizard()) return;
this.setState({ isSubmitting: true });
try {
const formData = new FormData();
// Add form data
Object.keys(this.state.formData).forEach(key => {
const value = this.state.formData[key];
if (Array.isArray(value)) {
value.forEach((file, index) => {
formData.append(`${key}_${index}`, file);
});
} else {
formData.append(key, value);
}
});
// Add CSRF token
const csrfToken = document.querySelector('[name=csrfmiddlewaretoken]');
if (csrfToken) {
formData.append('csrfmiddlewaretoken', csrfToken.value);
}
const response = await fetch(`/recruitment/forms/${formId}/submit/`, {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
this.setState({ isSubmitted: true });
const modal = new bootstrap.Modal(document.getElementById('successModal'));
modal.show();
} else {
throw new Error(result.error || 'Submission failed');
}
} catch (error) {
console.error('Submission error:', error);
document.getElementById('errorMessage').textContent = error.message;
const modal = new bootstrap.Modal(document.getElementById('errorModal'));
modal.show();
} finally {
this.setState({ isSubmitting: false });
}
}
resetForm = () => {
this.setState({
currentWizardIndex: 0,
formData: {},
errors: {},
isSubmitting: false,
isSubmitted: false
});
}
render() {
const currentWizard = this.getCurrentWizard();
const currentFields = this.getCurrentWizardFields();
if (this.state.isSubmitted) {
return html`
<div class="text-center py-5">
<i class="fas fa-check-circle text-success fa-4x mb-3"></i>
<h3>Thank You!</h3>
<p class="text-muted">Your form has been submitted successfully.</p>
<button class="btn btn-primary" onClick=${this.resetForm}>
Submit Another Response
</button>
</div>
`;
}
return html`
<div class="form-preview">
<!-- Progress Bar -->
${formStructure.settings && formStructure.settings.showProgress && this.getTotalWizards() > 1 ? html`
<div class="progress mb-4" style="height: 6px;">
<div class="progress-bar"
style="width: ${this.getProgress()}%; transition: width 0.3s ease;">
</div>
</div>
` : ''}
<!-- Wizard Title -->
${currentWizard ? html`
<h4 class="mb-4">${currentWizard.title || 'Step ' + (this.state.currentWizardIndex + 1)}</h4>
` : ''}
<!-- Form Fields -->
<form id="preview-form">
${currentFields.map(field =>
createPreviewField(field, {
key: field.id,
value: this.state.formData[field.id],
onChange: this.handleFieldChange,
error: this.state.errors[field.id]
})
)}
</form>
<!-- Navigation Buttons -->
<div class="d-flex justify-content-between mt-4">
<button type="button"
class="btn btn-outline-secondary"
onClick=${this.handlePrevious}
disabled=${this.state.currentWizardIndex === 0 || this.state.isSubmitting}>
<i class="fas fa-arrow-left"></i> Previous
</button>
<button type="button"
class="btn btn-primary"
onClick=${this.handleNext}
disabled=${this.state.isSubmitting}>
${this.state.isSubmitting ? html`
<span class="spinner-border spinner-border-sm me-2" role="status"></span>
Submitting...
` : ''}
${this.state.currentWizardIndex === this.getTotalWizards() - 1 ?
html`<i class="fas fa-check"></i> Submit` :
html`Next <i class="fas fa-arrow-right"></i>`
}
</button>
</div>
</div>
`;
}
}
// Initialize FilePond plugins
if (typeof FilePond !== 'undefined') {
FilePond.registerPlugin(
FilePondPluginFileValidateType,
FilePondPluginFileValidateSize
);
}
// Render the form preview
document.addEventListener('DOMContentLoaded', function() {
const container = document.getElementById('form-preview-container');
if (container) {
render(html`<${FormPreview} />`, container);
}
});
// Global function for modal reset
window.resetForm = function() {
const container = document.getElementById('form-preview-container');
if (container) {
render(html`<${FormPreview} />`, container);
}
};
</script>
{% endblock %}