811 lines
39 KiB
HTML
811 lines
39 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; }
|
|
.input-line { border-bottom: 2px solid #cbd5e1; outline: none; transition: border-color 0.3s; }
|
|
.input-line:focus { border-bottom-color: #005696; }
|
|
.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; }
|
|
|
|
.step-slide {
|
|
position: relative;
|
|
width: 100%;
|
|
}
|
|
.step-slide.active {
|
|
}
|
|
.step-slide.exit-left {
|
|
display: none;
|
|
}
|
|
.step-slide.exit-right {
|
|
display: none;
|
|
}
|
|
.step-slide.enter-right {
|
|
display: none;
|
|
}
|
|
.step-slide.enter-left {
|
|
display: none;
|
|
}
|
|
|
|
.step-container {
|
|
position: relative;
|
|
}
|
|
|
|
.nav-btn {
|
|
transition: all 0.2s ease;
|
|
}
|
|
.nav-btn:disabled {
|
|
opacity: 0.4;
|
|
cursor: not-allowed;
|
|
}
|
|
.nav-btn:not(:disabled):hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.ok-btn {
|
|
background: #005696;
|
|
transition: all 0.2s ease;
|
|
}
|
|
.ok-btn:hover { background: #007bbd; }
|
|
.ok-btn:disabled { opacity: 0.3; cursor: not-allowed; }
|
|
.ok-btn:not(:disabled):hover { transform: scale(1.05); }
|
|
|
|
.fade-in {
|
|
animation: fadeIn 0.4s ease-out;
|
|
}
|
|
@keyframes fadeIn {
|
|
from { opacity: 0; transform: translateY(10px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-100 p-4 md:p-10 min-h-screen flex flex-col">
|
|
|
|
<!-- 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-full shadow-md hover:shadow-lg 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-4xl mx-auto w-full bg-white shadow-2xl relative border-t-[20px] border-navy flex-1 flex flex-col">
|
|
|
|
<!-- Survey Header -->
|
|
<div class="p-6 md:p-8 pb-2 flex justify-between items-start">
|
|
<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>
|
|
|
|
<!-- Progress Bar -->
|
|
<div class="px-6 md:px-10 pt-2 pb-2">
|
|
<div class="h-1.5 bg-gray-200 rounded-full overflow-hidden">
|
|
<div id="progressBar" class="h-full bg-navy rounded-full transition-all duration-500" style="width: 0%"></div>
|
|
</div>
|
|
<p id="progressText" class="text-xs text-gray-400 mt-1.5 text-center">
|
|
{% if language == 'ar' %}السؤال 1 من {{ total_questions }}{% else %}Question 1 of {{ total_questions }}{% endif %}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Step-by-step Question Container -->
|
|
<div class="flex-1 flex flex-col">
|
|
<div id="step-container" class="step-container flex-1 px-6 md:px-10 py-6">
|
|
<!-- Questions are rendered here by JavaScript -->
|
|
</div>
|
|
|
|
<!-- Navigation Buttons -->
|
|
<div id="nav-buttons" class="px-6 md:px-10 pb-6 flex items-center justify-between gap-4">
|
|
<button type="button" id="prevBtn" class="nav-btn flex items-center gap-2 px-5 py-3 text-gray-500 hover:text-navy rounded-xl font-semibold" disabled>
|
|
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="{% if language == 'ar' %}9 18 15 12 9 6{% else %}15 18 9 12 15 6{% endif %}"/>
|
|
</svg>
|
|
{% if language == 'ar' %}السابق{% else %}Back{% endif %}
|
|
</button>
|
|
<button type="button" id="nextBtn" class="ok-btn flex items-center gap-2 px-8 py-3 text-white rounded-xl font-bold text-base">
|
|
{% if language == 'ar' %}التالي{% else %}Next{% 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">
|
|
<polyline points="{% if language == 'ar' %}15 18 9 12 15 6{% else %}9 18 15 12 9 6{% endif %}"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comment & Submit (shown on final step) -->
|
|
<div id="final-section" class="hidden px-6 md:px-10 pb-8">
|
|
<div class="border-t border-gray-200 pt-6">
|
|
<p class="font-bold text-base flex items-center gap-2 mb-1">
|
|
{% 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 id="survey_comment"
|
|
class="w-full border-b-2 border-gray-300 outline-none bg-transparent text-[#2c3e50] focus:border-navy transition-colors resize-none" rows="3"
|
|
placeholder="{% if language == 'ar' %}اكتب ملاحظاتك هنا...{% else %}Write your comments here...{% endif %}"></textarea>
|
|
<button type="button" id="submitBtn" class="w-full mt-6 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">
|
|
<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>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="mt-auto bg-navy text-white p-6 text-center space-y-1">
|
|
<p class="text-sm font-medium">
|
|
{% if language == 'ar' %}
|
|
شكراً لمشاركتك في تحسين تجربة المرضى
|
|
{% else %}
|
|
Thank you for taking the time to complete our patient satisfaction survey.
|
|
{% endif %}
|
|
</p>
|
|
<p class="text-sm font-medium opacity-75">
|
|
{% if language == 'ar' %}
|
|
ملاحظاتك ستساعدنا في تقديم رعاية أفضل
|
|
{% else %}
|
|
Your feedback will help us improve patient experience.
|
|
{% endif %}
|
|
</p>
|
|
<p class="text-xs font-medium opacity-50 mt-2">
|
|
Powered by <a href="https://tenhal.sa" target="_blank" class="text-white hover:underline">tenhal.sa</a>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Decorative Corner Triangles -->
|
|
<div class="absolute bottom-0 left-0 w-full h-12 pointer-events-none z-10">
|
|
<div class="absolute bottom-0 left-0 border-r-[100px] border-b-[50px] border-r-transparent border-b-navy-dark"></div>
|
|
<div class="absolute bottom-0 right-0 border-l-[100px] border-b-[50px] border-l-transparent border-b-navy-dark"></div>
|
|
</div>
|
|
</div>
|
|
|
|
{% csrf_token %}
|
|
|
|
<!-- Fallback form for no-JS -->
|
|
<noscript>
|
|
<form method="post" id="fallbackForm" class="max-w-3xl mx-auto mt-4 bg-white p-8 rounded-xl shadow">
|
|
<input type="hidden" name="language" value="{{ language }}">
|
|
{% for question in questions %}
|
|
<div class="mb-6">
|
|
<p class="font-bold mb-2">{{ forloop.counter }}. {{ question.text }}</p>
|
|
<input type="text" name="question_{{ question.id }}" {% if question.is_required %}required{% endif %}>
|
|
</div>
|
|
{% endfor %}
|
|
<textarea name="comment" placeholder="Comments (optional)" class="w-full border p-2 mb-4" rows="3"></textarea>
|
|
<button type="submit" class="bg-navy text-white px-8 py-3 rounded-xl font-bold">Submit</button>
|
|
</form>
|
|
</noscript>
|
|
|
|
<script>
|
|
(function() {
|
|
var surveyToken = "{{ survey.access_token }}";
|
|
var language = "{{ language }}";
|
|
var questionsData = {{ questions_json|safe }};
|
|
var routingRules = {{ routing_rules_json|safe }};
|
|
var isRtl = language === 'ar';
|
|
var csrfToken = document.querySelector('[name=csrfmiddlewaretoken]') ? document.querySelector('[name=csrfmiddlewaretoken]').value : '';
|
|
if (!csrfToken) {
|
|
var metaTag = document.createElement('meta');
|
|
metaTag.name = 'csrfmiddlewaretoken';
|
|
document.head.appendChild(metaTag);
|
|
}
|
|
var getCsrf = function() {
|
|
var el = document.querySelector('input[name=csrfmiddlewaretoken]');
|
|
return el ? el.value : '';
|
|
};
|
|
|
|
var visibleQuestions = questionsData.filter(function(q) { return !q.is_conditional; });
|
|
var currentIndex = 0;
|
|
var answers = {};
|
|
var answerHistory = [];
|
|
var stepStartTime = Date.now();
|
|
var surveyStarted = false;
|
|
var isTransitioning = false;
|
|
var screenState = 'instructions';
|
|
var hospitalName = "{{ survey.survey_template.hospital.name|default:'Hospital'|escapejs }}";
|
|
var requiresConsent = {{ survey.survey_template.requires_consent|yesno:"true,false" }};
|
|
var instructionsText = isRtl
|
|
? "{{ survey.survey_template.instructions_ar|default:''|escapejs }}" || ''
|
|
: "{{ survey.survey_template.instructions_en|default:''|escapejs }}" || '';
|
|
var consentText = isRtl
|
|
? "{{ survey.survey_template.consent_text_ar|default:''|escapejs }}" || ''
|
|
: "{{ survey.survey_template.consent_text_en|default:''|escapejs }}" || '';
|
|
var todayDate = new Date().toISOString().split('T')[0];
|
|
instructionsText = instructionsText.replace(/Facility Name/g, hospitalName).replace(/\[Date\]/g, todayDate);
|
|
|
|
var container = document.getElementById('step-container');
|
|
var prevBtn = document.getElementById('prevBtn');
|
|
var nextBtn = document.getElementById('nextBtn');
|
|
var submitBtn = document.getElementById('submitBtn');
|
|
var finalSection = document.getElementById('final-section');
|
|
var navButtons = document.getElementById('nav-buttons');
|
|
var progressBar = document.getElementById('progressBar');
|
|
var progressText = document.getElementById('progressText');
|
|
|
|
function t(en, ar) {
|
|
return isRtl ? ar : en;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
function updateProgress() {
|
|
var total = visibleQuestions.length;
|
|
var current = currentIndex + 1;
|
|
var pct = Math.min((current / total) * 100, 100);
|
|
progressBar.style.width = pct + '%';
|
|
progressText.textContent = isRtl
|
|
? 'السؤال ' + current + ' من ' + total
|
|
: 'Question ' + current + ' of ' + total;
|
|
}
|
|
|
|
function updateContainerHeight() {
|
|
container.style.height = 'auto';
|
|
}
|
|
|
|
function renderQuestion(q, index) {
|
|
var isLast = index === visibleQuestions.length - 1;
|
|
var num = index + 1;
|
|
var html = '<div class="step-slide active" data-step="' + index + '">';
|
|
html += '<div class="fade-in">';
|
|
|
|
html += '<div class="flex items-start gap-3 mb-6">';
|
|
html += '<span class="font-bold text-navy text-2xl flex-shrink-0">' + num + '.</span>';
|
|
html += '<div>';
|
|
html += '<p class="font-bold text-lg leading-relaxed">';
|
|
html += (isRtl && q.text_ar) ? escapeHtml(q.text_ar) : escapeHtml(q.text);
|
|
if (q.is_required) html += '<span class="text-red-500 ms-1">*</span>';
|
|
html += '</p></div></div>';
|
|
|
|
html += '<div class="ms-7 md:ms-10">';
|
|
|
|
if (q.question_type === 'rating') {
|
|
html += '<div class="flex gap-4 justify-center flex-wrap">';
|
|
for (var i = 1; i <= 5; i++) {
|
|
html += '<button type="button" class="rating-star text-gray-300 focus:outline-none" data-value="' + i + '" onclick="window._selectRating(this)">';
|
|
html += '<svg xmlns="http://www.w3.org/2000/svg" width="42" height="42" 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>';
|
|
html += '</button>';
|
|
}
|
|
html += '</div>';
|
|
html += '<input type="hidden" id="answer_' + q.id + '" data-question-id="' + q.id + '">';
|
|
}
|
|
else if (q.question_type === 'nps') {
|
|
html += '<div class="nps-scale flex gap-2 justify-center flex-wrap">';
|
|
for (var i = 0; i <= 10; i++) {
|
|
html += '<button type="button" class="nps-button w-10 h-10 md:w-12 md:h-12 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="window._selectNPS(this)">';
|
|
html += i;
|
|
html += '</button>';
|
|
}
|
|
html += '</div>';
|
|
html += '<div class="flex justify-between text-xs text-gray-400 mt-2">';
|
|
html += '<span>' + t('Not likely', 'غير محتمل') + '</span>';
|
|
html += '<span>' + t('Very likely', 'محتمل جداً') + '</span>';
|
|
html += '</div>';
|
|
html += '<input type="hidden" id="answer_' + q.id + '" data-question-id="' + q.id + '">';
|
|
}
|
|
else if (q.question_type === 'likert') {
|
|
var likertLabels = isRtl
|
|
? ['1:أرفض بشدة', '2:أرفض', '3:محايد', '4:أوافق', '5:أوافق بشدة']
|
|
: ['1:Strongly Disagree', '2:Disagree', '3:Neutral', '4:Agree', '5:Strongly Agree'];
|
|
likertLabels.forEach(function(item) {
|
|
var parts = item.split(':');
|
|
html += '<label class="likert-option flex items-center cursor-pointer gap-3 py-2 hover:text-navy transition-colors text-base">';
|
|
html += '<input type="radio" name="answer_' + q.id + '" value="' + parts[0] + '" class="w-5 h-5 accent-navy" onchange="window._selectOption(this)">';
|
|
html += '<span>' + parts[1] + '</span>';
|
|
html += '</label>';
|
|
});
|
|
html += '<input type="hidden" id="answer_' + q.id + '" data-question-id="' + q.id + '">';
|
|
}
|
|
else if (q.question_type === 'yes_no') {
|
|
html += '<div class="flex gap-8">';
|
|
html += '<label class="flex items-center cursor-pointer gap-3 py-2">';
|
|
html += '<input type="radio" name="answer_' + q.id + '" value="yes" class="w-5 h-5 accent-navy" onchange="window._selectOption(this)">';
|
|
html += '<span class="text-lg">' + t('Yes', 'نعم') + '</span>';
|
|
html += '</label>';
|
|
html += '<label class="flex items-center cursor-pointer gap-3 py-2">';
|
|
html += '<input type="radio" name="answer_' + q.id + '" value="no" class="w-5 h-5 accent-navy" onchange="window._selectOption(this)">';
|
|
html += '<span class="text-lg">' + t('No', 'لا') + '</span>';
|
|
html += '</label>';
|
|
html += '</div>';
|
|
html += '<input type="hidden" id="answer_' + q.id + '" data-question-id="' + q.id + '">';
|
|
}
|
|
else if (q.question_type === 'multiple_choice') {
|
|
var choices = q.choices_json || [];
|
|
choices.forEach(function(choice) {
|
|
var label = (isRtl && choice.label_ar) ? choice.label_ar : choice.label;
|
|
html += '<label class="likert-option flex items-center cursor-pointer gap-3 py-2 hover:text-navy transition-colors text-base">';
|
|
html += '<input type="radio" name="answer_' + q.id + '" value="' + escapeAttr(choice.value) + '" class="w-5 h-5 accent-navy" onchange="window._selectOption(this)">';
|
|
html += '<span>' + escapeHtml(label) + '</span>';
|
|
html += '</label>';
|
|
});
|
|
html += '<input type="hidden" id="answer_' + q.id + '" data-question-id="' + q.id + '">';
|
|
}
|
|
else if (q.question_type === 'text') {
|
|
html += '<input type="text" id="answer_' + q.id + '" data-question-id="' + q.id + '" class="input-line w-full pb-2 text-lg bg-transparent text-[#2c3e50]" placeholder="' + t('Type your answer here...', 'أدخل إجابتك هنا') + '" oninput="window._onTextInput(this)">';
|
|
}
|
|
else if (q.question_type === 'textarea') {
|
|
html += '<textarea id="answer_' + q.id + '" data-question-id="' + q.id + '" class="input-line w-full pb-2 text-lg bg-transparent text-[#2c3e50] resize-none" rows="3" placeholder="' + t('Type your answer here...', 'أدخل إجابتك هنا') + '" oninput="window._onTextInput(this)"></textarea>';
|
|
}
|
|
|
|
html += '</div>';
|
|
html += '<div class="validation-error hidden ms-7 md:ms-10 mt-3 text-red-500 text-sm font-medium">';
|
|
html += t('This question is required', 'هذا السؤال مطلوب');
|
|
html += '</div>';
|
|
html += '</div></div>';
|
|
return html;
|
|
}
|
|
|
|
function escapeHtml(str) {
|
|
if (!str) return '';
|
|
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
}
|
|
function escapeAttr(str) {
|
|
if (!str) return '';
|
|
return String(str).replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''');
|
|
}
|
|
|
|
function getCurrentAnswer() {
|
|
var q = visibleQuestions[currentIndex];
|
|
if (!q) return null;
|
|
var el = document.getElementById('answer_' + q.id);
|
|
if (!el) return null;
|
|
if (el.tagName === 'INPUT' && el.type === 'hidden') {
|
|
var radios = document.querySelectorAll('input[name="answer_' + q.id + '"]');
|
|
for (var i = 0; i < radios.length; i++) {
|
|
if (radios[i].checked) return radios[i].value;
|
|
}
|
|
return el.value || null;
|
|
}
|
|
return el.value || null;
|
|
}
|
|
|
|
function isCurrentAnswered() {
|
|
var answer = getCurrentAnswer();
|
|
return answer !== null && String(answer).trim() !== '';
|
|
}
|
|
|
|
function evaluateRules(questionId, answerValue) {
|
|
var matchingRules = routingRules.filter(function(r) {
|
|
return r.source_question === String(questionId);
|
|
});
|
|
matchingRules.sort(function(a, b) { return (a.order || 0) - (b.order || 0); });
|
|
|
|
for (var i = 0; i < matchingRules.length; i++) {
|
|
var rule = matchingRules[i];
|
|
if (evaluateCondition(rule.operator, rule.value, answerValue)) {
|
|
return rule;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function evaluateCondition(operator, ruleValue, answerValue) {
|
|
if (operator === 'answered') return answerValue !== null && String(answerValue).trim() !== '';
|
|
if (operator === 'not_answered') return answerValue === null || String(answerValue).trim() === '';
|
|
if (answerValue === null || String(answerValue).trim() === '') return false;
|
|
var ansStr = String(answerValue).trim();
|
|
if (operator === 'equals') return ansStr === String(ruleValue);
|
|
if (operator === 'not_equals') return ansStr !== String(ruleValue);
|
|
if (operator === 'contains') return String(ruleValue).indexOf(ansStr) >= 0 || ansStr.indexOf(String(ruleValue)) >= 0;
|
|
if (operator === 'gt' || operator === 'lt') {
|
|
var aNum = parseFloat(ansStr);
|
|
var rNum = parseFloat(ruleValue);
|
|
if (isNaN(aNum) || isNaN(rNum)) return false;
|
|
return operator === 'gt' ? aNum > rNum : aNum < rNum;
|
|
}
|
|
if (operator === 'in_list') {
|
|
var list = Array.isArray(ruleValue) ? ruleValue : [ruleValue];
|
|
for (var j = 0; j < list.length; j++) {
|
|
if (ansStr === String(list[j])) return true;
|
|
}
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function showStep(newIndex, direction) {
|
|
if (isTransitioning) return;
|
|
if (newIndex < 0 || newIndex >= visibleQuestions.length) return;
|
|
isTransitioning = true;
|
|
|
|
var oldSlide = container.querySelector('.step-slide.active');
|
|
var newHtml = renderQuestion(visibleQuestions[newIndex], newIndex);
|
|
var temp = document.createElement('div');
|
|
temp.innerHTML = newHtml;
|
|
var newSlide = temp.firstElementChild;
|
|
|
|
if (direction === 'forward') {
|
|
newSlide.classList.remove('active');
|
|
newSlide.classList.add('enter-right');
|
|
} else {
|
|
newSlide.classList.remove('active');
|
|
newSlide.classList.add('enter-left');
|
|
}
|
|
container.appendChild(newSlide);
|
|
|
|
requestAnimationFrame(function() {
|
|
if (oldSlide) {
|
|
oldSlide.classList.remove('active');
|
|
oldSlide.classList.add(direction === 'forward' ? 'exit-left' : 'exit-right');
|
|
}
|
|
requestAnimationFrame(function() {
|
|
newSlide.classList.remove('enter-right', 'enter-left');
|
|
newSlide.classList.add('active');
|
|
updateContainerHeight();
|
|
});
|
|
});
|
|
|
|
setTimeout(function() {
|
|
if (oldSlide) oldSlide.remove();
|
|
isTransitioning = false;
|
|
updateContainerHeight();
|
|
}, 500);
|
|
|
|
currentIndex = newIndex;
|
|
updateNavButtons();
|
|
updateProgress();
|
|
stepStartTime = Date.now();
|
|
|
|
restoreAnswer(visibleQuestions[currentIndex].id);
|
|
}
|
|
|
|
function restoreAnswer(questionId) {
|
|
var answer = answers[questionId];
|
|
if (answer === undefined) return;
|
|
var hiddenEl = document.getElementById('answer_' + questionId);
|
|
if (!hiddenEl) return;
|
|
|
|
var q = visibleQuestions[currentIndex];
|
|
if (q.question_type === 'rating') {
|
|
hiddenEl.value = answer;
|
|
var stars = hiddenEl.parentElement.querySelectorAll('.rating-star');
|
|
for (var i = 0; i < stars.length; i++) {
|
|
var svg = stars[i].querySelector('svg');
|
|
if (i < parseInt(answer)) {
|
|
svg.setAttribute('fill', '#fbbf24');
|
|
svg.setAttribute('stroke', '#fbbf24');
|
|
stars[i].classList.add('active');
|
|
} else {
|
|
svg.setAttribute('fill', 'none');
|
|
svg.setAttribute('stroke', '#94a3b8');
|
|
stars[i].classList.remove('active');
|
|
}
|
|
}
|
|
} else if (q.question_type === 'nps') {
|
|
hiddenEl.value = answer;
|
|
var btns = hiddenEl.parentElement.querySelectorAll('.nps-button');
|
|
btns.forEach(function(b) {
|
|
b.classList.toggle('selected', b.getAttribute('data-value') === String(answer));
|
|
});
|
|
} else if (['likert', 'yes_no', 'multiple_choice'].indexOf(q.question_type) >= 0) {
|
|
var radios = document.querySelectorAll('input[name="answer_' + questionId + '"]');
|
|
radios.forEach(function(r) {
|
|
r.checked = r.value === String(answer);
|
|
if (r.checked && r.closest('.likert-option')) {
|
|
r.closest('.likert-option').classList.add('selected');
|
|
}
|
|
});
|
|
hiddenEl.value = answer;
|
|
} else {
|
|
hiddenEl.value = answer;
|
|
}
|
|
}
|
|
|
|
function updateNavButtons() {
|
|
prevBtn.disabled = currentIndex === 0;
|
|
var q = visibleQuestions[currentIndex];
|
|
var isLast = currentIndex === visibleQuestions.length - 1;
|
|
|
|
if (isLast && isCurrentAnswered()) {
|
|
finalSection.classList.remove('hidden');
|
|
navButtons.classList.add('hidden');
|
|
} else {
|
|
finalSection.classList.add('hidden');
|
|
navButtons.classList.remove('hidden');
|
|
}
|
|
|
|
nextBtn.innerHTML = t('Next <svg class="w-5 h-5 inline" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>', 'التالي <svg class="w-5 h-5 inline" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>');
|
|
}
|
|
|
|
function saveCurrentAnswer() {
|
|
var answer = getCurrentAnswer();
|
|
if (answer !== null) {
|
|
var q = visibleQuestions[currentIndex];
|
|
answers[q.id] = answer;
|
|
}
|
|
}
|
|
|
|
function goNext() {
|
|
var q = visibleQuestions[currentIndex];
|
|
if (q.is_required && !isCurrentAnswered()) {
|
|
var errEl = container.querySelector('.validation-error');
|
|
if (errEl) errEl.classList.remove('hidden');
|
|
return;
|
|
}
|
|
var errEl = container.querySelector('.validation-error');
|
|
if (errEl) errEl.classList.add('hidden');
|
|
|
|
saveCurrentAnswer();
|
|
trackSurveyStart();
|
|
|
|
var answer = getCurrentAnswer();
|
|
var matchedRule = evaluateRules(q.id, answer);
|
|
|
|
if (matchedRule) {
|
|
if (matchedRule.action === 'end_survey') {
|
|
submitSurvey(true);
|
|
return;
|
|
}
|
|
if (matchedRule.action === 'skip_to' && matchedRule.target_question) {
|
|
var targetIdx = visibleQuestions.findIndex(function(vq) { return vq.id === matchedRule.target_question; });
|
|
if (targetIdx >= 0 && targetIdx > currentIndex) {
|
|
showStep(targetIdx, 'forward');
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (currentIndex < visibleQuestions.length - 1) {
|
|
showStep(currentIndex + 1, 'forward');
|
|
} else {
|
|
updateNavButtons();
|
|
}
|
|
}
|
|
|
|
function goPrev() {
|
|
if (currentIndex > 0) {
|
|
saveCurrentAnswer();
|
|
showStep(currentIndex - 1, 'back');
|
|
}
|
|
}
|
|
|
|
function submitSurvey(routedEnd) {
|
|
saveCurrentAnswer();
|
|
var formData = new FormData();
|
|
formData.append('language', language);
|
|
|
|
for (var qId in answers) {
|
|
if (answers.hasOwnProperty(qId)) {
|
|
formData.append('question_' + qId, answers[qId]);
|
|
}
|
|
}
|
|
var comment = document.getElementById('survey_comment');
|
|
if (comment) formData.append('comment', comment.value);
|
|
|
|
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() {
|
|
var fallbackForm = document.createElement('form');
|
|
fallbackForm.method = 'POST';
|
|
fallbackForm.action = '/surveys/s/' + surveyToken + '/';
|
|
fallbackForm.innerHTML = '<input type="hidden" name="csrfmiddlewaretoken" value="' + getCsrf() + '"><input type="hidden" name="language" value="' + language + '">';
|
|
for (var qId in answers) {
|
|
if (answers.hasOwnProperty(qId)) {
|
|
fallbackForm.innerHTML += '<input type="hidden" name="question_' + qId + '" value="' + escapeAttr(answers[qId]) + '">';
|
|
}
|
|
}
|
|
var commentEl = document.getElementById('survey_comment');
|
|
if (commentEl && commentEl.value) {
|
|
fallbackForm.innerHTML += '<input type="hidden" name="comment" value="' + escapeAttr(commentEl.value) + '">';
|
|
}
|
|
document.body.appendChild(fallbackForm);
|
|
fallbackForm.submit();
|
|
});
|
|
}
|
|
|
|
window._selectRating = function(star) {
|
|
var value = parseInt(star.getAttribute('data-value'));
|
|
var container_ = star.parentElement;
|
|
var stars = container_.querySelectorAll('.rating-star');
|
|
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');
|
|
}
|
|
});
|
|
var hiddenInput = container_.parentElement.querySelector('input[type="hidden"]');
|
|
if (hiddenInput) hiddenInput.value = value;
|
|
updateNavButtons();
|
|
};
|
|
|
|
window._selectNPS = function(button) {
|
|
button.parentElement.querySelectorAll('.nps-button').forEach(function(b) { b.classList.remove('selected'); });
|
|
button.classList.add('selected');
|
|
var hiddenInput = button.parentElement.parentElement.querySelector('input[type="hidden"]');
|
|
if (hiddenInput) hiddenInput.value = button.getAttribute('data-value');
|
|
updateNavButtons();
|
|
};
|
|
|
|
window._selectOption = function(radio) {
|
|
var qContent = radio.closest('.step-slide');
|
|
if (qContent) qContent.querySelectorAll('.likert-option').forEach(function(opt) { opt.classList.remove('selected'); });
|
|
var label = radio.closest('.likert-option');
|
|
if (label) label.classList.add('selected');
|
|
var hiddenInput = radio.closest('.step-slide').querySelector('input[type="hidden"]');
|
|
if (hiddenInput) hiddenInput.value = radio.value;
|
|
updateNavButtons();
|
|
};
|
|
|
|
window._onTextInput = function(el) {
|
|
updateNavButtons();
|
|
};
|
|
|
|
nextBtn.addEventListener('click', goNext);
|
|
prevBtn.addEventListener('click', goPrev);
|
|
submitBtn.addEventListener('click', function() { submitSurvey(false); });
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
var activeEl = document.activeElement;
|
|
if (activeEl && activeEl.tagName === 'TEXTAREA') return;
|
|
e.preventDefault();
|
|
goNext();
|
|
}
|
|
});
|
|
|
|
if (visibleQuestions.length > 0) {
|
|
if (requiresConsent || instructionsText) {
|
|
showInstructions();
|
|
} else {
|
|
container.innerHTML = renderQuestion(visibleQuestions[0], 0);
|
|
updateNavButtons();
|
|
updateProgress();
|
|
updateContainerHeight();
|
|
}
|
|
}
|
|
|
|
function showInstructions() {
|
|
screenState = 'instructions';
|
|
prevBtn.style.display = 'none';
|
|
nextBtn.style.display = 'flex';
|
|
nextBtn.disabled = false;
|
|
nextBtn.innerHTML = isRtl
|
|
? 'التالي <svg class="w-5 h-5 inline" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>'
|
|
: 'OK <svg class="w-5 h-5 inline" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>';
|
|
finalSection.classList.add('hidden');
|
|
navButtons.classList.remove('hidden');
|
|
progressBar.style.width = '0%';
|
|
progressText.textContent = '';
|
|
|
|
var html = '<div class="step-slide active fade-in">';
|
|
html += '<div class="flex items-center gap-3 mb-6">';
|
|
html += '<svg class="w-8 h-8 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>';
|
|
html += '<h2 class="text-xl font-bold text-navy">' + (isRtl ? 'تعليمات الاستبيان' : 'Survey Instructions') + '</h2>';
|
|
html += '</div>';
|
|
html += '<div class="bg-blue-50 border border-blue-200 rounded-xl p-5 text-[#2c3e50] text-base leading-relaxed whitespace-pre-line">';
|
|
html += escapeHtml(instructionsText);
|
|
html += '</div>';
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
setTimeout(function() { updateContainerHeight(); }, 50);
|
|
}
|
|
|
|
function showConsent() {
|
|
screenState = 'consent';
|
|
prevBtn.style.display = 'flex';
|
|
prevBtn.disabled = false;
|
|
nextBtn.style.display = 'flex';
|
|
nextBtn.disabled = true;
|
|
nextBtn.innerHTML = isRtl
|
|
? 'أوافق ومتابعة <svg class="w-5 h-5 inline" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="15 18 9 12 15 6"/></svg>'
|
|
: 'I Agree <svg class="w-5 h-5 inline" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>';
|
|
|
|
var html = '<div class="step-slide active fade-in">';
|
|
html += '<div class="flex items-center gap-3 mb-6">';
|
|
html += '<svg class="w-8 h-8 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>';
|
|
html += '<h2 class="text-xl font-bold text-navy">' + (isRtl ? 'الموافقة على المشاركة' : 'Patient Consent') + '</h2>';
|
|
html += '</div>';
|
|
html += '<div class="bg-amber-50 border border-amber-200 rounded-xl p-5 text-[#2c3e50] text-base leading-relaxed mb-5">';
|
|
html += escapeHtml(consentText);
|
|
html += '</div>';
|
|
html += '<label class="flex items-start gap-3 cursor-pointer group">';
|
|
html += '<input type="checkbox" id="consent-checkbox" class="mt-1 w-5 h-5 accent-navy rounded" onchange="window._toggleConsent(this)">';
|
|
html += '<span class="text-base font-semibold text-[#2c3e50] group-hover:text-navy transition-colors">';
|
|
html += isRtl ? 'أوافق على الشروط المذكورة أعلاه' : 'I agree to the terms above';
|
|
html += '</span>';
|
|
html += '</label>';
|
|
html += '</div>';
|
|
container.innerHTML = html;
|
|
setTimeout(function() { updateContainerHeight(); }, 50);
|
|
}
|
|
|
|
window._toggleConsent = function(cb) {
|
|
nextBtn.disabled = !cb.checked;
|
|
};
|
|
|
|
nextBtn.removeEventListener('click', goNext);
|
|
nextBtn.addEventListener('click', function() {
|
|
if (screenState === 'instructions') {
|
|
if (requiresConsent) {
|
|
showConsent();
|
|
} else {
|
|
screenState = 'questions';
|
|
prevBtn.style.display = 'flex';
|
|
nextBtn.style.display = 'flex';
|
|
container.innerHTML = renderQuestion(visibleQuestions[0], 0);
|
|
updateNavButtons();
|
|
updateProgress();
|
|
updateContainerHeight();
|
|
}
|
|
} else if (screenState === 'consent') {
|
|
var cb = document.getElementById('consent-checkbox');
|
|
if (!cb || !cb.checked) return;
|
|
screenState = 'questions';
|
|
prevBtn.style.display = 'flex';
|
|
container.innerHTML = renderQuestion(visibleQuestions[0], 0);
|
|
updateNavButtons();
|
|
updateProgress();
|
|
updateContainerHeight();
|
|
} else {
|
|
goNext();
|
|
}
|
|
});
|
|
|
|
prevBtn.removeEventListener('click', goPrev);
|
|
prevBtn.addEventListener('click', function() {
|
|
if (screenState === 'consent') {
|
|
showInstructions();
|
|
} else if (screenState === 'questions' && currentIndex > 0) {
|
|
goPrev();
|
|
}
|
|
});
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|