HH/templates/surveys/public_form.html
ismail c5f76b3855
Some checks are pending
Build and Push Docker Image / build (push) Waiting to run
updates
2026-05-11 14:45:30 +03:00

533 lines
28 KiB
HTML

{% load i18n %}
{% load survey_filters %}
{% load static %}
<!DOCTYPE html>
<html lang="{{ language }}" dir="{% if language == 'ar' %}rtl{% else %}ltr{% endif %}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>{% if language == 'ar' %}استبيان رضا المرضى{% else %}Patient Satisfaction Survey{% endif %} - PX360</title>
<link rel="stylesheet" href="{% static 'dist/css/tailwind.css' %}">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
body { font-family: 'Inter', sans-serif; }
.rating-star { transition: all 0.2s; cursor: pointer; }
.rating-star svg { transition: all 0.15s ease; }
.rating-star:hover svg { transform: scale(1.2); }
.nps-button { transition: all 0.15s; }
.nps-button.selected {
background: #005696;
color: white;
border-color: #005696;
transform: scale(1.08);
}
.likert-option.selected { color: #005696; font-weight: 600; }
.form-input {
border: 2px solid #e2e8f0;
border-radius: 0.75rem;
padding: 0.75rem 1rem;
width: 100%;
transition: all 0.2s;
outline: none;
}
.form-input:focus {
border-color: #005696;
box-shadow: 0 0 0 3px rgba(0, 86, 150, 0.1);
}
.question-block {
border-bottom: 1px solid #e2e8f0;
padding-bottom: 1.5rem;
}
.question-block:last-of-type {
border-bottom: none;
}
.validation-error {
color: #ef4444;
font-size: 0.875rem;
font-weight: 500;
margin-top: 0.5rem;
}
.form-hidden {
display: none !important;
}
.form-visible {
display: block;
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<!-- Language Toggle -->
<div class="fixed top-4 {% if language == 'ar' %}left-4{% else %}right-4{% endif %} z-50">
<a href="?lang={% if language == 'ar' %}en{% else %}ar{% endif %}"
class="flex items-center gap-1.5 bg-white text-navy px-4 py-2 rounded-lg shadow-sm hover:shadow transition-shadow text-sm font-semibold border border-gray-200">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"/><path d="M2 12h20"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
</svg>
{% if language == 'ar' %}English{% else %}العربية{% endif %}
</a>
</div>
<div class="max-w-3xl mx-auto w-full bg-white min-h-screen shadow-sm">
<!-- Survey Header -->
<div class="p-6 md:p-8 pb-4 flex justify-between items-start border-b border-gray-200 bg-white">
<div class="flex items-center">
<img src="{% static 'img/HH_P_H_Logo.png' %}" alt="HH Hospital" class="h-10 md:h-12 object-contain">
</div>
<div class="text-end">
<h1 class="text-xl md:text-2xl font-bold text-[#2c3e50]">{% if language == 'ar' %}استبيان رضا{% else %}Patient{% endif %}</h1>
<h1 class="text-xl md:text-2xl font-bold text-[#2c3e50]">{% if language == 'ar' %}المرضى{% else %}Satisfaction Survey{% endif %}</h1>
</div>
</div>
<!-- Instructions (if any) -->
{% if language == 'ar' and survey.survey_template.instructions_ar or language != 'ar' and survey.survey_template.instructions_en %}
<div class="px-6 md:px-8 py-4 bg-blue-50 border-b border-blue-100">
<div class="flex items-center gap-2 mb-2">
<svg class="w-5 h-5 text-navy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>
</svg>
<h2 class="font-bold text-navy">{% if language == 'ar' %}تعليمات{% else %}Instructions{% endif %}</h2>
</div>
<p class="text-sm text-gray-700 leading-relaxed whitespace-pre-line">
{% if language == 'ar' %}{{ survey.survey_template.instructions_ar }}{% else %}{{ survey.survey_template.instructions_en }}{% endif %}
</p>
</div>
{% endif %}
<!-- Consent (if required) -->
{% if survey.survey_template.requires_consent %}
{% if language == 'ar' and survey.survey_template.consent_text_ar or language != 'ar' and survey.survey_template.consent_text_en %}
<div class="px-6 md:px-8 py-4 bg-amber-50 border-b border-amber-100">
<div class="flex items-center gap-2 mb-2">
<svg class="w-5 h-5 text-amber-600" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
<h2 class="font-bold text-amber-800">{% if language == 'ar' %}الموافقة{% else %}Consent{% endif %}</h2>
</div>
<p class="text-sm text-gray-700 leading-relaxed whitespace-pre-line mb-3">
{% if language == 'ar' %}{{ survey.survey_template.consent_text_ar }}{% else %}{{ survey.survey_template.consent_text_en }}{% endif %}
</p>
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="consent-checkbox" class="w-5 h-5 accent-navy rounded">
<span class="text-sm font-semibold text-gray-800">
{% if language == 'ar' %}أوافق على الشروط المذكورة أعلاه{% else %}I agree to the terms above{% endif %}
</span>
</label>
<div id="consent-error" class="validation-error hidden mt-2">
{% if language == 'ar' %}يجب الموافقة قبل المتابعة{% else %}You must agree to continue{% endif %}
</div>
</div>
{% endif %}
{% endif %}
<!-- Survey Form -->
<form id="survey-form" class="px-6 md:px-8 py-6 space-y-6 {% if survey.survey_template.requires_consent %}form-hidden{% else %}form-visible{% endif %}">
{% csrf_token %}
<input type="hidden" name="language" value="{{ language }}">
<!-- Questions -->
{% for question in questions %}
{% if not question.is_conditional %}
<div class="question-block" data-question-id="{{ question.id }}" data-required="{{ question.is_required|yesno:'true,false' }}">
<div class="flex items-start gap-3 mb-4">
<span class="font-bold text-navy text-xl flex-shrink-0">{{ forloop.counter }}.</span>
<div>
<p class="font-bold text-base leading-relaxed text-gray-800">
{% if language == 'ar' and question.text_ar %}{{ question.text_ar }}{% else %}{{ question.text }}{% endif %}
{% if question.is_required %}<span class="text-red-500 ms-1">*</span>{% endif %}
</p>
</div>
</div>
<div class="ms-8 md:ms-10">
{% if question.question_type == 'rating' %}
<div class="flex gap-3 justify-center flex-wrap">
{% for i in "12345" %}
<button type="button" class="rating-star text-gray-300 focus:outline-none" data-value="{{ i }}" onclick="selectRating(this)">
<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/></svg>
</button>
{% endfor %}
</div>
<input type="hidden" name="question_{{ question.id }}" id="answer_{{ question.id }}" data-question-id="{{ question.id }}">
{% elif question.question_type == 'nps' %}
<div class="nps-scale flex gap-2 justify-center flex-wrap">
{% for i in "012345678910" %}
<button type="button" class="nps-button w-9 h-9 md:w-10 md:h-10 border border-gray-300 rounded-lg bg-white font-semibold text-sm focus:outline-none hover:border-navy hover:text-navy" data-value="{{ i }}" onclick="selectNPS(this)">
{{ i }}
</button>
{% endfor %}
</div>
<div class="flex justify-between text-xs text-gray-400 mt-2 px-2">
<span>{% if language == 'ar' %}غير محتمل{% else %}Not likely{% endif %}</span>
<span>{% if language == 'ar' %}محتمل جداً{% else %}Very likely{% endif %}</span>
</div>
<input type="hidden" name="question_{{ question.id }}" id="answer_{{ question.id }}" data-question-id="{{ question.id }}">
{% elif question.question_type == 'likert' %}
<div class="space-y-2">
{% if language == 'ar' %}
<label class="likert-option flex items-center cursor-pointer gap-3 py-2 hover:text-navy transition-colors text-base">
<input type="radio" name="radio_{{ question.id }}" value="1" class="w-5 h-5 accent-navy" onchange="selectOption(this)">
<span>1: أرفض بشدة</span>
</label>
<label class="likert-option flex items-center cursor-pointer gap-3 py-2 hover:text-navy transition-colors text-base">
<input type="radio" name="radio_{{ question.id }}" value="2" class="w-5 h-5 accent-navy" onchange="selectOption(this)">
<span>2: أرفض</span>
</label>
<label class="likert-option flex items-center cursor-pointer gap-3 py-2 hover:text-navy transition-colors text-base">
<input type="radio" name="radio_{{ question.id }}" value="3" class="w-5 h-5 accent-navy" onchange="selectOption(this)">
<span>3: محايد</span>
</label>
<label class="likert-option flex items-center cursor-pointer gap-3 py-2 hover:text-navy transition-colors text-base">
<input type="radio" name="radio_{{ question.id }}" value="4" class="w-5 h-5 accent-navy" onchange="selectOption(this)">
<span>4: أوافق</span>
</label>
<label class="likert-option flex items-center cursor-pointer gap-3 py-2 hover:text-navy transition-colors text-base">
<input type="radio" name="radio_{{ question.id }}" value="5" class="w-5 h-5 accent-navy" onchange="selectOption(this)">
<span>5: أوافق بشدة</span>
</label>
{% else %}
<label class="likert-option flex items-center cursor-pointer gap-3 py-2 hover:text-navy transition-colors text-base">
<input type="radio" name="radio_{{ question.id }}" value="1" class="w-5 h-5 accent-navy" onchange="selectOption(this)">
<span>1: Strongly Disagree</span>
</label>
<label class="likert-option flex items-center cursor-pointer gap-3 py-2 hover:text-navy transition-colors text-base">
<input type="radio" name="radio_{{ question.id }}" value="2" class="w-5 h-5 accent-navy" onchange="selectOption(this)">
<span>2: Disagree</span>
</label>
<label class="likert-option flex items-center cursor-pointer gap-3 py-2 hover:text-navy transition-colors text-base">
<input type="radio" name="radio_{{ question.id }}" value="3" class="w-5 h-5 accent-navy" onchange="selectOption(this)">
<span>3: Neutral</span>
</label>
<label class="likert-option flex items-center cursor-pointer gap-3 py-2 hover:text-navy transition-colors text-base">
<input type="radio" name="radio_{{ question.id }}" value="4" class="w-5 h-5 accent-navy" onchange="selectOption(this)">
<span>4: Agree</span>
</label>
<label class="likert-option flex items-center cursor-pointer gap-3 py-2 hover:text-navy transition-colors text-base">
<input type="radio" name="radio_{{ question.id }}" value="5" class="w-5 h-5 accent-navy" onchange="selectOption(this)">
<span>5: Strongly Agree</span>
</label>
{% endif %}
</div>
<input type="hidden" name="question_{{ question.id }}" id="answer_{{ question.id }}" data-question-id="{{ question.id }}">
{% elif question.question_type == 'yes_no' %}
<div class="flex gap-6">
<label class="flex items-center cursor-pointer gap-3 py-2">
<input type="radio" name="radio_{{ question.id }}" value="yes" class="w-5 h-5 accent-navy" onchange="selectOption(this)">
<span class="text-lg">{% if language == 'ar' %}نعم{% else %}Yes{% endif %}</span>
</label>
<label class="flex items-center cursor-pointer gap-3 py-2">
<input type="radio" name="radio_{{ question.id }}" value="no" class="w-5 h-5 accent-navy" onchange="selectOption(this)">
<span class="text-lg">{% if language == 'ar' %}لا{% else %}No{% endif %}</span>
</label>
</div>
<input type="hidden" name="question_{{ question.id }}" id="answer_{{ question.id }}" data-question-id="{{ question.id }}">
{% elif question.question_type == 'multiple_choice' %}
<div class="space-y-2">
{% for choice in question.choices_json %}
<label class="likert-option flex items-center cursor-pointer gap-3 py-2 hover:text-navy transition-colors text-base">
<input type="radio" name="radio_{{ question.id }}" value="{{ choice.value|default:choice }}" class="w-5 h-5 accent-navy" onchange="selectOption(this)">
<span>{% if language == 'ar' and choice.label_ar %}{{ choice.label_ar }}{% else %}{{ choice.label|default:choice }}{% endif %}</span>
</label>
{% endfor %}
</div>
<input type="hidden" name="question_{{ question.id }}" id="answer_{{ question.id }}" data-question-id="{{ question.id }}">
{% elif question.question_type == 'text' %}
<input type="text" name="question_{{ question.id }}" id="answer_{{ question.id }}" data-question-id="{{ question.id }}"
class="form-input text-base" placeholder="{% if language == 'ar' %}أدخل إجابتك هنا...{% else %}Type your answer here...{% endif %}">
{% elif question.question_type == 'textarea' %}
<textarea name="question_{{ question.id }}" id="answer_{{ question.id }}" data-question-id="{{ question.id }}"
class="form-input text-base resize-none" rows="3" placeholder="{% if language == 'ar' %}أدخل إجابتك هنا...{% else %}Type your answer here...{% endif %}"></textarea>
{% endif %}
<div class="validation-error hidden" id="error_{{ question.id }}">
{% if language == 'ar' %}هذا السؤال مطلوب{% else %}This question is required{% endif %}
</div>
</div>
</div>
{% endif %}
{% endfor %}
<!-- Comment Section -->
<div class="question-block">
<p class="font-bold text-base flex items-center gap-2 mb-2">
{% if language == 'ar' %}
ملاحظات إضافية (اختياري)
{% else %}
Additional Comments (Optional)
{% endif %}
</p>
<p class="text-sm text-gray-500 mb-3">
{% if language == 'ar' %}
شارك أي ملاحظات أو اقتراحات إضافية حول تجربتك
{% else %}
Share any additional comments or suggestions about your experience
{% endif %}
</p>
<textarea name="comment" id="survey_comment"
class="form-input resize-none" rows="3"
placeholder="{% if language == 'ar' %}اكتب ملاحظاتك هنا...{% else %}Write your comments here...{% endif %}"></textarea>
</div>
<!-- Submit Button -->
<div class="pt-4">
<button type="submit" id="submitBtn" class="w-full bg-navy hover:bg-navy-dark text-white px-8 py-4 rounded-xl font-bold text-lg transition-all flex items-center justify-center gap-2" {% if survey.survey_template.requires_consent %}disabled{% endif %}>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
{% if language == 'ar' %}إرسال الاستبيان{% else %}Submit Survey{% endif %}
</button>
</div>
</form>
<!-- Footer -->
<div class="bg-gray-50 border-t border-gray-200 p-6 text-center space-y-1">
<p class="text-sm font-medium text-gray-600">
{% if language == 'ar' %}
شكراً لمشاركتك في تحسين تجربة المرضى
{% else %}
Thank you for taking the time to complete our patient satisfaction survey.
{% endif %}
</p>
<p class="text-sm font-medium text-gray-500">
{% if language == 'ar' %}
ملاحظاتك ستساعدنا في تقديم رعاية أفضل
{% else %}
Your feedback will help us improve patient experience.
{% endif %}
</p>
<p class="text-xs font-medium text-gray-400 mt-2">
Powered by <a href="https://tenhal.sa" target="_blank" class="text-gray-500 hover:underline">tenhal.sa</a>
</p>
</div>
</div>
<script>
(function() {
var surveyToken = "{{ survey.access_token }}";
var language = "{{ language }}";
var isRtl = language === 'ar';
var requiresConsent = {{ survey.survey_template.requires_consent|yesno:"true,false" }};
function getCsrf() {
var el = document.querySelector('input[name=csrfmiddlewaretoken]');
return el ? el.value : '';
}
// Consent checkbox handler
if (requiresConsent) {
var consentCheckbox = document.getElementById('consent-checkbox');
var surveyForm = document.getElementById('survey-form');
var submitBtn = document.getElementById('submitBtn');
if (consentCheckbox && surveyForm && submitBtn) {
consentCheckbox.addEventListener('change', function() {
if (this.checked) {
surveyForm.classList.remove('form-hidden');
surveyForm.classList.add('form-visible');
submitBtn.disabled = false;
// Clear consent error if visible
var consentError = document.getElementById('consent-error');
if (consentError) consentError.classList.add('hidden');
// Smooth scroll to form
setTimeout(function() {
surveyForm.scrollIntoView({ behavior: 'smooth', block: 'start' });
}, 100);
} else {
surveyForm.classList.remove('form-visible');
surveyForm.classList.add('form-hidden');
submitBtn.disabled = true;
}
});
}
}
// Rating selection
window.selectRating = function(star) {
var value = parseInt(star.getAttribute('data-value'));
var container = star.parentElement;
var stars = container.querySelectorAll('.rating-star');
var hiddenInput = container.parentElement.querySelector('input[type="hidden"]');
stars.forEach(function(s, i) {
var svg = s.querySelector('svg');
if (i < value) {
svg.setAttribute('fill', '#fbbf24');
svg.setAttribute('stroke', '#fbbf24');
s.classList.add('active');
} else {
svg.setAttribute('fill', 'none');
svg.setAttribute('stroke', '#94a3b8');
s.classList.remove('active');
}
});
if (hiddenInput) hiddenInput.value = value;
clearError(hiddenInput);
};
// NPS selection
window.selectNPS = function(button) {
var container = button.parentElement;
container.querySelectorAll('.nps-button').forEach(function(b) { b.classList.remove('selected'); });
button.classList.add('selected');
var hiddenInput = container.parentElement.querySelector('input[type="hidden"]');
if (hiddenInput) {
hiddenInput.value = button.getAttribute('data-value');
clearError(hiddenInput);
}
};
// Radio option selection
window.selectOption = function(radio) {
var questionBlock = radio.closest('.question-block');
if (questionBlock) {
questionBlock.querySelectorAll('.likert-option').forEach(function(opt) { opt.classList.remove('selected'); });
var label = radio.closest('.likert-option');
if (label) label.classList.add('selected');
}
var hiddenInput = questionBlock.querySelector('input[type="hidden"]');
if (hiddenInput) {
hiddenInput.value = radio.value;
clearError(hiddenInput);
}
};
function clearError(input) {
if (!input) return;
var questionBlock = input.closest('.question-block');
if (questionBlock) {
var errorEl = questionBlock.querySelector('.validation-error');
if (errorEl) errorEl.classList.add('hidden');
}
}
// Form validation
function validateForm() {
var isValid = true;
var questionBlocks = document.querySelectorAll('.question-block[data-required="true"]');
questionBlocks.forEach(function(block) {
var hiddenInput = block.querySelector('input[type="hidden"][data-question-id]');
var textInput = block.querySelector('input[type="text"][data-question-id], textarea[data-question-id]');
var errorEl = block.querySelector('.validation-error');
var hasValue = false;
if (hiddenInput) {
hasValue = hiddenInput.value && hiddenInput.value.trim() !== '';
} else if (textInput) {
hasValue = textInput.value && textInput.value.trim() !== '';
}
if (!hasValue) {
isValid = false;
if (errorEl) errorEl.classList.remove('hidden');
} else {
if (errorEl) errorEl.classList.add('hidden');
}
});
// Validate consent if required
if (requiresConsent) {
var consentCb = document.getElementById('consent-checkbox');
var consentError = document.getElementById('consent-error');
if (consentCb && !consentCb.checked) {
isValid = false;
if (consentError) consentError.classList.remove('hidden');
} else if (consentError) {
consentError.classList.add('hidden');
}
}
return isValid;
}
// Track survey start
var surveyStarted = false;
function trackSurveyStart() {
if (surveyStarted) return;
fetch('/surveys/s/' + surveyToken + '/track-start/', {
method: 'POST',
headers: { 'X-CSRFToken': getCsrf(), 'Content-Type': 'application/json' },
credentials: 'same-origin'
}).catch(function() {});
surveyStarted = true;
}
// Form submission
document.getElementById('survey-form').addEventListener('submit', function(e) {
e.preventDefault();
trackSurveyStart();
if (!validateForm()) {
// Scroll to first error
var firstError = document.querySelector('.validation-error:not(.hidden)');
if (firstError) {
firstError.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
return;
}
var formData = new FormData(this);
fetch('/surveys/s/' + surveyToken + '/', {
method: 'POST',
body: formData,
headers: { 'X-CSRFToken': getCsrf() },
credentials: 'same-origin'
}).then(function(r) {
if (r.redirected) {
window.location.href = r.url;
} else {
window.location.href = '/surveys/s/' + surveyToken + '/thank-you/?lang=' + language;
}
}).catch(function() {
// Fallback: submit the form normally
var fallbackForm = document.createElement('form');
fallbackForm.method = 'POST';
fallbackForm.action = '/surveys/s/' + surveyToken + '/';
fallbackForm.innerHTML = '<input type="hidden" name="csrfmiddlewaretoken" value="' + getCsrf() + '">';
var formInputs = document.getElementById('survey-form').querySelectorAll('input, textarea');
formInputs.forEach(function(input) {
if (input.name && input.value) {
var hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = input.name;
hidden.value = input.value;
fallbackForm.appendChild(hidden);
}
});
document.body.appendChild(fallbackForm);
fallbackForm.submit();
});
});
// Clear validation errors on input
document.querySelectorAll('input[type="text"], textarea').forEach(function(input) {
input.addEventListener('input', function() {
clearError(this);
});
});
})();
</script>
</body>
</html>