2025-10-06 16:23:40 +03:00

1020 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends 'base.html' %}
{% load static i18n %}
{% block title %}
Edit Application Form - {{ applicant_form.name }}
{% endblock %}
{% block customCSS %}
<style>
:root {
--primary: #00636e;
--primary-light: #89bcc2ff;
--primary-dark: #00636e;
--secondary: #7209b7;
--success: #4cc9f0;
--warning: #f72585;
--light: #f8f9fa;
--dark: #212529;
--gray-100: #f8f9fa;
--gray-200: #e9ecef;
--gray-300: #dee2e6;
--gray-400: #ced4da;
--gray-500: #adb5bd;
--gray-600: #6c757d;
--gray-700: #495057;
--gray-800: #343a40;
--gray-900: #212529;
--border-radius: 12px;
--box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
--transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
{% comment %} body {
font-family: var(--font-family);
background: linear-gradient(135deg, #f5f7fa 0%, #e4edf9 100%);
color: var(--gray-800);
line-height: 1.6;
padding: 24px;
min-height: 100vh;
} {% endcomment %}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
padding-bottom: 20px;
border-bottom: 1px solid var(--gray-200);
}
.header h1 {
font-weight: 700;
font-size: 28px;
color: var(--gray-900);
margin: 0;
}
.breadcrumb {
display: flex;
align-items: center;
gap: 8px;
color: var(--gray-600);
font-size: 14px;
}
.breadcrumb a {
color: var(--primary);
text-decoration: none;
transition: var(--transition);
}
.breadcrumb a:hover {
color: var(--primary-dark);
}
.breadcrumb i {
font-size: 12px;
color: var(--gray-400);
}
.card {
background: white;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
padding: 28px;
margin-bottom: 28px;
transition: var(--transition);
}
.card:hover {
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.card-title {
font-size: 20px;
font-weight: 600;
color: var(--gray-900);
}
.form-details-edit {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
.form-details-edit label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--gray-700);
}
.form-details-edit input[type="text"],
.form-details-edit textarea {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--gray-300);
border-radius: 8px;
font-size: 16px;
transition: var(--transition);
}
.form-details-edit input[type="text"]:focus,
.form-details-edit textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2);
}
.form-details-edit textarea {
min-height: 100px;
resize: vertical;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 12px 24px;
border-radius: 8px;
font-weight: 600;
font-size: 16px;
cursor: pointer;
transition: var(--transition);
text-decoration: none;
border: none;
gap: 8px;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-2px);
}
.btn-secondary {
background: var(--gray-100);
color: var(--gray-800);
border: 1px solid var(--gray-300);
}
.btn-secondary:hover {
background: var(--gray-200);
}
.form-builder-container {
display: flex;
gap: 28px;
margin-top: 16px;
}
.palette {
width: 300px;
background: white;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
padding: 24px;
height: fit-content;
position: sticky;
top: 24px;
}
.palette h2 {
font-size: 18px;
font-weight: 600;
margin-bottom: 20px;
color: var(--gray-900);
display: flex;
align-items: center;
gap: 10px;
}
.palette h2 i {
color: var(--primary);
}
.field-type {
padding: 16px;
margin-bottom: 16px;
border: 2px solid var(--gray-200);
border-radius: 10px;
cursor: grab;
background: var(--gray-50);
transition: var(--transition);
display: flex;
align-items: center;
gap: 12px;
font-weight: 500;
}
.field-type:hover {
border-color: var(--primary);
background: rgba(67, 97, 238, 0.05);
transform: translateY(-2px);
}
.field-type i {
font-size: 20px;
color: var(--primary);
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(67, 97, 238, 0.1);
border-radius: 6px;
}
.canvas-container {
flex-grow: 1;
background: white;
border-radius: var(--border-radius);
box-shadow: var(--box-shadow);
padding: 28px;
}
.canvas-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.canvas-title {
font-size: 20px;
font-weight: 600;
color: var(--gray-900);
}
.form-canvas {
min-height: 400px;
border: 2px dashed var(--gray-300);
border-radius: 12px;
padding: 24px;
margin-bottom: 28px;
background: var(--gray-50);
transition: var(--transition);
}
.form-canvas.drag-over {
border-color: var(--primary);
background: rgba(67, 97, 238, 0.05);
}
.drop-hint {
text-align: center;
color: var(--gray-500);
padding: 60px 20px;
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.drop-hint i {
font-size: 48px;
color: var(--gray-400);
}
.drop-hint p {
font-size: 18px;
max-width: 400px;
line-height: 1.5;
}
.form-field-item {
padding: 20px;
margin-bottom: 16px;
border: 1px solid var(--gray-200);
border-left: 4px solid var(--primary);
border-radius: 10px;
background: white;
cursor: move;
transition: var(--transition);
position: relative;
overflow: hidden;
}
.form-field-item:hover {
border-color: var(--primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
.form-field-item.dragging {
opacity: 0.8;
background: var(--gray-50);
border: 2px dashed var(--primary);
}
.form-field-item.drag-over-top {
border-top: 3px solid var(--success);
}
.form-field-item.drag-over-bottom {
border-bottom: 3px solid var(--success);
}
.field-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.field-title {
font-weight: 600;
font-size: 18px;
margin: 0;
color: var(--gray-900);
}
.field-type-label {
background: var(--primary);
color: white;
padding: 4px 10px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.field-meta {
display: flex;
gap: 20px;
margin-bottom: 16px;
color: var(--gray-600);
font-size: 14px;
}
.field-meta div {
display: flex;
align-items: center;
gap: 6px;
}
.field-meta i {
color: var(--primary);
font-size: 16px;
}
.field-controls {
display: flex;
justify-content: flex-end;
gap: 12px;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--gray-200);
}
.field-config {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--gray-200);
display: none;
}
.field-config label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: var(--gray-700);
}
.field-config input[type="text"],
.field-config textarea {
width: 100%;
padding: 12px 16px;
border: 1px solid var(--gray-300);
border-radius: 8px;
font-size: 16px;
transition: var(--transition);
margin-bottom: 16px;
}
.field-config input[type="text"]:focus,
.field-config textarea:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.2);
}
.field-config textarea {
min-height: 80px;
resize: vertical;
}
.field-config .field-name {
background: var(--gray-100);
padding: 12px 16px;
border-radius: 8px;
font-family: monospace;
font-size: 14px;
color: var(--gray-700);
}
.action-buttons {
display: flex;
gap: 16px;
margin-top: 24px;
}
.output-data {
background: var(--gray-100);
padding: 20px;
margin-top: 24px;
border-radius: 10px;
white-space: pre-wrap;
font-family: monospace;
font-size: 14px;
max-height: 200px;
overflow: auto;
border: 1px solid var(--gray-300);
}
.messages {
list-style: none;
padding: 0;
margin-bottom: 24px;
}
.messages li {
padding: 16px 20px;
border-radius: 10px;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 12px;
font-weight: 500;
}
.messages .success {
background: rgba(76, 201, 240, 0.15);
color: #0a7ea4;
border: 1px solid rgba(76, 201, 240, 0.3);
}
.messages .error {
background: rgba(247, 37, 133, 0.15);
color: #a11a5d;
border: 1px solid rgba(247, 37, 133, 0.3);
}
.messages i {
font-size: 20px;
}
/* Responsive adjustments */
@media (max-width: 992px) {
.form-builder-container {
flex-direction: column;
}
.palette {
position: static;
width: 100%;
}
.form-details-edit {
grid-template-columns: 1fr;
}
}
/* Animation for field appearance */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.form-field-item {
animation: fadeInUp 0.4s ease-out;
}
/* Loading state */
.btn.loading {
position: relative;
color: transparent;
}
.btn.loading::after {
content: "";
position: absolute;
width: 20px;
height: 20px;
top: 50%;
left: 50%;
margin: -10px 0 0 -10px;
border: 2px solid transparent;
border-top-color: white;
border-radius: 50%;
animation: button-loading 1s ease infinite;
}
@keyframes button-loading {
to {
transform: rotate(360deg);
}
}
</style>
{% endblock %}
{% block content %}
<div class="container">
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li class="{{ message.tags }}">
{% if message.tags == "success" %}
<i></i>
{% else %}
<i>!</i>
{% endif %}
{{ message }}
</li>
{% endfor %}
</ul>
{% endif %}
<div class="header">
<div>
<h1>Edit Application Form</h1>
<div class="breadcrumb">
<a href="{% url 'applicant:job_forms_list' job_id=job.internal_job_id %}">Forms</a>
<i></i>
<span>{{ applicant_form.name }}</span>
</div>
</div>
<div class="action-buttons">
<a href="{% url 'applicant:job_forms_list' job_id=job.internal_job_id %}" class="btn btn-secondary">
<i></i> Back to Forms
</a>
</div>
</div>
<div class="card">
<div class="card-header">
<h2 class="card-title">Form Details</h2>
</div>
<form method="post" class="form-details-edit">
{% csrf_token %}
<div>
<label for="{{ form_details.name.id_for_label }}">Form Name</label>
{{ form_details.name }}
</div>
<div>
<label for="{{ form_details.description.id_for_label }}">Description</label>
{{ form_details.description }}
</div>
<div class="action-buttons" style="margin-top: 20px;">
<button type="submit" name="save_form_details" class="btn univ-color">
Save Form Details
</button>
</div>
</form>
</div>
<div class="card">
<div class="form-builder-container">
<div class="palette">
<h2><i>+</i> Add Field</h2>
<div class="field-type" data-type="text" draggable="true">
<i>T</i> Text Input
</div>
<div class="field-type" data-type="email" draggable="true">
<i>@</i> Email
</div>
<div class="field-type" data-type="select" draggable="true">
<i></i> Dropdown
</div>
<div class="field-type" data-type="checkbox" draggable="true">
<i></i> Checkbox
</div>
<div class="field-type" data-type="textarea" draggable="true">
<i></i> Paragraph
</div>
<div class="field-type" data-type="date" draggable="true">
<i>📅</i> Date
</div>
<div class="field-type" data-type="radio" draggable="true">
<i></i> Radio Buttons
</div>
</div>
<div class="canvas-container">
<div class="canvas-header">
<h2 class="canvas-title">Form Structure</h2>
<button id="save-form-btn" class="btn univ-color">
Save Form Structure
</button>
</div>
<div id="form-canvas" class="form-canvas">
<div class="drop-hint">
<i>+</i>
<p>Drag fields from the left panel to build your form</p>
</div>
</div>
<div class="output-data" id="output-data" style="display: none;"></div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script>
// --- DATA INITIALIZATION ---
let formFields = JSON.parse('{{ initial_fields_json|safe }}');
const formCanvas = document.getElementById('form-canvas');
const dropHint = formCanvas.querySelector('.drop-hint');
const saveBtn = document.getElementById('save-form-btn');
// --- Utility Functions ---
function generateFieldName(label, existingFieldNames) {
let baseName = label.toLowerCase().replace(/[^a-z0-9]+/g, '_').trim('_');
if (!baseName) baseName = 'field';
let fieldName = baseName;
let counter = 1;
while (existingFieldNames.includes(fieldName)) {
fieldName = `${baseName}_${counter}`;
counter++;
}
return fieldName;
}
function renderForm() {
formCanvas.innerHTML = '';
if (formFields.length === 0) {
formCanvas.appendChild(dropHint);
}
const existingNames = [];
formFields.forEach(field => {
field.field_name = generateFieldName(field.label, existingNames);
existingNames.push(field.field_name);
});
formFields.forEach((field, index) => {
const fieldElement = createFieldElement(field, index);
formCanvas.appendChild(fieldElement);
});
}
function createFieldElement(field, index) {
const item = document.createElement('div');
item.className = 'form-field-item';
item.dataset.index = index;
item.dataset.id = field.id || index;
item.draggable = true;
const typeIcons = {
'text': 'T',
'email': '@',
'select': '▼',
'checkbox': '✓',
'textarea': '≡',
'date': '📅',
'radio': '●'
};
const fieldDisplay = `
<div class="field-header">
<h3 class="field-title">${field.label}</h3>
<span class="field-type-label">${field.field_type}</span>
</div>
<div class="field-meta">
<div><i>🏷️</i> ${field.field_name}</div>
<div><i>${field.required ? '✅' : '❌'}</i> ${field.required ? 'Required' : 'Optional'}</div>
${field.field_type === 'select' || field.field_type === 'radio' ?
`<div><i>📋</i> ${field.choices ? field.choices.split(',').filter(c => c.trim()).length + ' options' : 'No options'}</div>` : ''}
</div>
`;
const configPanel = `
<div class="field-config">
<label>Label</label>
<input type="text" value="${field.label}" data-prop="label" class="config-input">
<label>
<input type="checkbox" ${field.required ? 'checked' : ''} data-prop="required" class="config-input">
Required field
</label>
${field.field_type === 'select' || field.field_type === 'radio' ?
`<label>Options (comma separated)</label>
<textarea data-prop="choices" class="config-input">${field.choices}</textarea>` : ''}
<label>Help Text (optional)</label>
<textarea data-prop="help_text" class="config-input">${field.help_text || ''}</textarea>
<label>Field Name (auto-generated)</label>
<div class="field-name">${field.field_name}</div>
</div>
`;
const controls = `
<div class="field-controls">
<button type="button" class="btn btn-secondary toggle-config">Configure</button>
<button type="button" class="btn btn-secondary remove-field">Remove</button>
</div>
`;
item.innerHTML = fieldDisplay + configPanel + controls;
return item;
}
// ✅ NEW: Update field display without re-rendering entire form
function updateFieldDisplay(fieldElement, fieldData) {
// Update label
const titleEl = fieldElement.querySelector('.field-title');
if (titleEl) titleEl.textContent = fieldData.label;
// Update field name display
const fieldNameDisplay = fieldElement.querySelector('.field-name');
if (fieldNameDisplay) fieldNameDisplay.textContent = fieldData.field_name;
// Update required status
const requiredEl = fieldElement.querySelector('.field-meta div:nth-child(2)');
if (requiredEl) {
requiredEl.innerHTML = `<i>${fieldData.required ? '✅' : '❌'}</i> ${fieldData.required ? 'Required' : 'Optional'}`;
}
// Update choices count (if applicable)
if (fieldData.field_type === 'select' || fieldData.field_type === 'radio') {
const choicesEl = fieldElement.querySelector('.field-meta div:nth-child(3)');
if (choicesEl) {
const count = fieldData.choices ? fieldData.choices.split(',').filter(c => c.trim()).length : 0;
choicesEl.innerHTML = `<i>📋</i> ${count > 0 ? count + ' options' : 'No options'}`;
}
}
}
function handleConfigChange(event) {
const input = event.target;
if (!input.closest('.field-config')) return;
const prop = input.dataset.prop;
const fieldItem = input.closest('.form-field-item');
const index = parseInt(fieldItem.dataset.index);
let value;
if (prop === 'required') {
value = input.checked;
} else if (input.tagName === 'INPUT' || input.tagName === 'TEXTAREA') {
value = input.value;
} else {
return;
}
// Update data model
formFields[index][prop] = value;
// Regenerate field_name for this field only (ensure uniqueness)
const otherNames = formFields
.filter((f, i) => i !== index)
.map(f => f.field_name);
formFields[index].field_name = generateFieldName(formFields[index].label, otherNames);
// ✅ Update DOM directly instead of re-rendering
updateFieldDisplay(fieldItem, formFields[index]);
}
// Prevent config panel from closing when clicking inside inputs
formCanvas.addEventListener('mousedown', (e) => {
if (e.target.closest('.field-config')) {
e.stopPropagation();
}
});
function toggleConfig(event) {
if (event.target.classList.contains('toggle-config')) {
const configPanel = event.target.closest('.form-field-item').querySelector('.field-config');
const isHidden = configPanel.style.display === 'none';
configPanel.style.display = isHidden ? 'block' : 'none';
event.target.textContent = isHidden ? 'Hide Configuration' : 'Configure';
event.stopPropagation();
}
}
function removeField(event) {
if (event.target.classList.contains('remove-field')) {
if (confirm("Are you sure you want to remove this field? This action cannot be undone.")) {
const index = parseInt(event.target.closest('.form-field-item').dataset.index);
formFields.splice(index, 1);
renderForm(); // Only re-render on remove
}
event.stopPropagation();
}
}
// --- Drag and Drop Logic ---
let draggedItem = null;
let draggedFieldElement = null;
document.querySelectorAll('.field-type').forEach(typeEl => {
typeEl.addEventListener('dragstart', (e) => {
draggedItem = { type: typeEl.dataset.type, label: typeEl.textContent.trim().replace(/^[^a-zA-Z]+/, '') };
e.dataTransfer.setData('text/plain', typeEl.dataset.type);
typeEl.classList.add('dragging');
});
typeEl.addEventListener('dragend', () => {
draggedItem = null;
typeEl.classList.remove('dragging');
});
});
formCanvas.addEventListener('dragstart', (e) => {
if (e.target.classList.contains('form-field-item')) {
draggedFieldElement = e.target;
e.dataTransfer.setData('text/plain', 'reorder');
setTimeout(() => e.target.classList.add('dragging'), 0);
}
});
formCanvas.addEventListener('dragend', (e) => {
if (e.target.classList.contains('form-field-item')) {
e.target.classList.remove('dragging');
draggedFieldElement = null;
}
});
formCanvas.addEventListener('dragover', (e) => {
e.preventDefault();
formCanvas.classList.add('drag-over');
const isDraggingField = !!draggedFieldElement;
if (draggedItem && !isDraggingField) {
return;
}
if (isDraggingField) {
const target = e.target.closest('.form-field-item');
if (target && target !== draggedFieldElement) {
const targetRect = target.getBoundingClientRect();
const isTopHalf = e.clientY < targetRect.top + targetRect.height / 2;
document.querySelectorAll('.drag-over-top, .drag-over-bottom').forEach(el => {
el.classList.remove('drag-over-top', 'drag-over-bottom');
});
if (isTopHalf) {
target.classList.add('drag-over-top');
} else {
target.classList.add('drag-over-bottom');
}
}
}
});
formCanvas.addEventListener('dragleave', (e) => {
if (e.relatedTarget && !formCanvas.contains(e.relatedTarget)) {
formCanvas.classList.remove('drag-over');
}
document.querySelectorAll('.drag-over-top, .drag-over-bottom').forEach(el => {
el.classList.remove('drag-over-top', 'drag-over-bottom');
});
});
formCanvas.addEventListener('drop', (e) => {
e.preventDefault();
formCanvas.classList.remove('drag-over');
document.querySelectorAll('.drag-over-top, .drag-over-bottom').forEach(el => {
el.classList.remove('drag-over-top', 'drag-over-bottom');
});
if (draggedItem) {
const newField = {
id: Date.now(),
label: draggedItem.label,
field_type: draggedItem.type,
required: true,
help_text: '',
choices: '',
field_name: '',
order: formFields.length,
};
formFields.push(newField);
renderForm(); // Re-render on add
draggedItem = null;
return;
}
if (draggedFieldElement) {
const dropTarget = e.target.closest('.form-field-item');
if (dropTarget && dropTarget !== draggedFieldElement) {
const fromIndex = parseInt(draggedFieldElement.dataset.index);
let toIndex = parseInt(dropTarget.dataset.index);
const targetRect = dropTarget.getBoundingClientRect();
const isTopHalf = e.clientY < targetRect.top + targetRect.height / 2;
const [itemToMove] = formFields.splice(fromIndex, 1);
if (!isTopHalf) {
toIndex += 1;
}
formFields.splice(toIndex, 0, itemToMove);
renderForm(); // Re-render on reorder
}
draggedFieldElement = null;
}
});
// --- Event Listeners ---
// Only re-render on structural changes (add/remove/reorder)
// Config changes update DOM directly
formCanvas.addEventListener('change', handleConfigChange);
formCanvas.addEventListener('input', handleConfigChange);
formCanvas.addEventListener('click', toggleConfig);
formCanvas.addEventListener('click', removeField);
// Save button handler
saveBtn.addEventListener('click', async () => {
// Update order
formFields.forEach((field, index) => {
field.order = index;
});
// Show loading state
saveBtn.classList.add('loading');
saveBtn.disabled = true;
try {
const response = await fetch(window.location.href, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify(formFields)
});
const result = await response.json();
if (response.ok) {
// Show success message
const message = document.createElement('li');
message.className = 'success';
message.innerHTML = '<i>✓</i> Form structure saved successfully!';
const messagesContainer = document.querySelector('.messages') ||
document.querySelector('.container').insertAdjacentHTML('afterbegin', '<ul class="messages"></ul>') ||
document.querySelector('.messages');
if (messagesContainer) {
messagesContainer.appendChild(message);
setTimeout(() => message.remove(), 5000);
}
// Update output for debugging
document.getElementById('output-data').textContent = JSON.stringify(formFields, null, 2);
document.getElementById('output-data').style.display = 'block';
} else {
alert(`Error saving form: ${result.message}`);
}
} catch (e) {
console.error("Fetch Error:", e);
alert("A network error occurred while saving the form.");
} finally {
saveBtn.classList.remove('loading');
saveBtn.disabled = false;
}
});
// Initial render
renderForm();
</script>
{% endblock %}