982 lines
44 KiB
HTML
982 lines
44 KiB
HTML
{% extends "layouts/public_base.html" %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}{% trans "Submit Feedback" %} - PX360{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
/* PX360 Theme */
|
|
.selection-card {
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
.selection-card:hover {
|
|
transform: translateY(-8px);
|
|
box-shadow: 0 20px 40px -10px rgba(0, 86, 150, 0.3);
|
|
}
|
|
|
|
.form-container.active {
|
|
animation: slideUp 0.4s ease-out;
|
|
}
|
|
|
|
@keyframes slideUp {
|
|
from { opacity: 0; transform: translateY(30px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
.severity-option {
|
|
transition: all 0.2s ease;
|
|
cursor: pointer;
|
|
}
|
|
.severity-option:hover {
|
|
transform: translateY(-2px);
|
|
}
|
|
.severity-option.selected {
|
|
border-color: #007bbd !important;
|
|
background-color: #eef6fb !important;
|
|
box-shadow: 0 4px 12px rgba(0, 123, 189, 0.2);
|
|
}
|
|
|
|
.file-upload-area {
|
|
transition: all 0.2s ease;
|
|
}
|
|
.file-upload-area:hover {
|
|
border-color: #007bbd;
|
|
background-color: #eef6fb;
|
|
}
|
|
|
|
/* Custom scrollbar for form content */
|
|
#formContent::-webkit-scrollbar {
|
|
width: 6px;
|
|
}
|
|
#formContent::-webkit-scrollbar-track {
|
|
background: #f1f5f9;
|
|
}
|
|
#formContent::-webkit-scrollbar-thumb {
|
|
background: #cbd5e1;
|
|
border-radius: 3px;
|
|
}
|
|
|
|
/* Complaint Form Styles */
|
|
.complaint-form {
|
|
max-width: 900px;
|
|
margin: 0 auto;
|
|
padding: 1rem 0;
|
|
}
|
|
|
|
.form-section {
|
|
background: white;
|
|
border-radius: 1.5rem;
|
|
padding: 2rem;
|
|
margin-bottom: 1.5rem;
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05);
|
|
border: 1px solid #e2e8f0;
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.form-section:hover {
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.form-section h3 {
|
|
color: #005696;
|
|
margin-bottom: 1.5rem;
|
|
padding-bottom: 1rem;
|
|
border-bottom: 2px solid #eef6fb;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
font-size: 1.25rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.form-control {
|
|
width: 100%;
|
|
padding: 0.875rem 1.125rem;
|
|
border: 2px solid #cbd5e1;
|
|
border-radius: 0.875rem;
|
|
font-size: 0.95rem;
|
|
transition: all 0.2s;
|
|
background: white;
|
|
color: #1e293b;
|
|
font-family: 'Inter', sans-serif;
|
|
}
|
|
|
|
.form-control:focus {
|
|
outline: none;
|
|
border-color: #007bbd;
|
|
box-shadow: 0 0 0 4px rgba(0, 123, 189, 0.1);
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.form-label {
|
|
display: block;
|
|
margin-bottom: 0.625rem;
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
.form-text {
|
|
display: block;
|
|
margin-top: 0.5rem;
|
|
font-size: 0.825rem;
|
|
color: #64748b;
|
|
}
|
|
|
|
.required-mark {
|
|
color: #ef4444;
|
|
}
|
|
|
|
.alert-box {
|
|
padding: 1.5rem 1.75rem;
|
|
border-radius: 1.25rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.alert-box.info {
|
|
background: linear-gradient(135deg, #eef6fb 0%, #e0f2fe 100%);
|
|
color: #005696;
|
|
border: 2px solid #bae6fd;
|
|
}
|
|
|
|
.location-hierarchy-help {
|
|
background: linear-gradient(135deg, #fffbeb 0%, #fef3c7 100%);
|
|
border-left: 4px solid #f59e0b;
|
|
padding: 1.25rem;
|
|
margin: 1.25rem 0;
|
|
border-radius: 1rem;
|
|
font-size: 0.9rem;
|
|
color: #4b5563;
|
|
}
|
|
|
|
[dir="rtl"] .location-hierarchy-help {
|
|
border-left: none;
|
|
border-right: 4px solid #f59e0b;
|
|
}
|
|
|
|
.btn-submit {
|
|
background: linear-gradient(135deg, #005696 0%, #007bbd 100%);
|
|
border: none;
|
|
padding: 1rem 2.5rem;
|
|
border-radius: 1rem;
|
|
color: white;
|
|
font-weight: 700;
|
|
font-size: 1.05rem;
|
|
cursor: pointer;
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
box-shadow: 0 8px 16px rgba(0, 86, 150, 0.25);
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
}
|
|
|
|
.btn-submit:hover {
|
|
transform: translateY(-3px);
|
|
box-shadow: 0 12px 24px rgba(0, 86, 150, 0.35);
|
|
}
|
|
|
|
.btn-submit:disabled {
|
|
background: #94a3b8;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.spinner {
|
|
display: inline-block;
|
|
width: 1.125rem;
|
|
height: 1.125rem;
|
|
border: 2px solid rgba(255,255,255,0.3);
|
|
border-radius: 50%;
|
|
border-top-color: white;
|
|
animation: spin 0.8s linear infinite;
|
|
}
|
|
|
|
@keyframes spin {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
|
|
.form-input {
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.form-input:focus {
|
|
transform: translateY(-1px);
|
|
}
|
|
|
|
.btn-hover {
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.btn-hover:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 10px 20px rgba(0, 86, 150, 0.2);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-6xl mx-auto">
|
|
<!-- Welcome Card -->
|
|
<div class="glass-card rounded-3xl shadow-2xl p-8 mb-8 text-center animate-fade-in">
|
|
<div class="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-blue to-navy rounded-2xl mb-4 shadow-lg">
|
|
<i data-lucide="heart-handshake" class="w-10 h-10 text-white"></i>
|
|
</div>
|
|
<h1 class="text-3xl font-bold text-navy mb-3">{% trans "We Value Your Feedback" %}</h1>
|
|
<p class="text-slate text-base max-w-xl mx-auto">
|
|
{% trans "Your feedback helps us improve our services and provide better care for everyone. Choose a category below to get started." %}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Selection Cards -->
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8" id="selectionCards">
|
|
<!-- Complaint Card -->
|
|
<div class="selection-card glass-card rounded-2xl shadow-xl border-2 border-transparent p-8 text-center cursor-pointer relative overflow-hidden group"
|
|
data-type="complaint">
|
|
<div class="absolute top-0 left-0 right-0 h-2 bg-gradient-to-r from-red-400 to-red-600"></div>
|
|
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-red-50 to-red-100 mx-auto mb-4 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
|
|
<i data-lucide="alert-circle" class="w-8 h-8 text-red-600"></i>
|
|
</div>
|
|
<h3 class="text-xl font-bold text-navy mb-3">{% trans "Complaint" %}</h3>
|
|
<p class="text-slate text-sm leading-relaxed">
|
|
{% trans "Report an issue with our services. We take all concerns seriously and will investigate thoroughly." %}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Observation Card -->
|
|
<div class="selection-card glass-card rounded-2xl shadow-xl border-2 border-transparent p-8 text-center cursor-pointer relative overflow-hidden group"
|
|
data-type="observation">
|
|
<div class="absolute top-0 left-0 right-0 h-2 bg-gradient-to-r from-purple-400 to-purple-600"></div>
|
|
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-purple-50 to-purple-100 mx-auto mb-4 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
|
|
<i data-lucide="eye" class="w-8 h-8 text-purple-600"></i>
|
|
</div>
|
|
<h3 class="text-xl font-bold text-navy mb-3">{% trans "Observation" %}</h3>
|
|
<p class="text-slate text-sm leading-relaxed">
|
|
{% trans "Help us improve safety by sharing what you've noticed. Anonymous submissions are welcome." %}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Inquiry Card -->
|
|
<div class="selection-card glass-card rounded-2xl shadow-xl border-2 border-transparent p-8 text-center cursor-pointer relative overflow-hidden group"
|
|
data-type="inquiry">
|
|
<div class="absolute top-0 left-0 right-0 h-2 bg-gradient-to-r from-blue-400 to-blue-600"></div>
|
|
<div class="w-16 h-16 rounded-2xl bg-gradient-to-br from-blue-50 to-blue-100 mx-auto mb-4 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
|
|
<i data-lucide="help-circle" class="w-8 h-8 text-blue-600"></i>
|
|
</div>
|
|
<h3 class="text-xl font-bold text-navy mb-3">{% trans "Inquiry" %}</h3>
|
|
<p class="text-slate text-sm leading-relaxed">
|
|
{% trans "Have questions? We're here to help with appointments, services, or general information." %}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Form Container -->
|
|
<div class="form-container glass-card rounded-3xl shadow-2xl p-8 mb-8 hidden animate-slide-up" id="publicFormContainer">
|
|
<!-- Back Button -->
|
|
<div class="mb-6">
|
|
<button class="inline-flex items-center gap-2 px-5 py-2.5 text-slate hover:text-navy transition font-semibold hover:bg-light rounded-xl"
|
|
onclick="resetToSelection()">
|
|
<i data-lucide="arrow-left" class="w-5 h-5"></i>
|
|
{% trans "Back to Selection" %}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Loading Spinner -->
|
|
<div class="loading-spinner text-center py-16 hidden" id="loadingSpinner">
|
|
<div class="inline-block w-14 h-14 border-4 border-slate-200 border-t-blue rounded-full animate-spin mb-4"></div>
|
|
<p class="text-slate text-sm font-medium">{% trans "Loading form..." %}</p>
|
|
</div>
|
|
|
|
<!-- Form Content -->
|
|
<div id="formContent" class="pr-2"></div>
|
|
</div>
|
|
|
|
<!-- Track Submission Links -->
|
|
<div class="glass-card rounded-2xl shadow-xl p-8 text-center">
|
|
<p class="text-slate mb-4 font-medium">{% trans "Already submitted something?" %}</p>
|
|
<div class="flex justify-center gap-3 flex-wrap">
|
|
<a href="{% url 'complaints:public_complaint_track' %}"
|
|
class="inline-flex items-center gap-2 px-6 py-3 bg-navy text-white rounded-xl hover:bg-blue transition font-semibold shadow-lg shadow-navy/30 btn-hover">
|
|
<i data-lucide="search" class="w-5 h-5"></i>
|
|
{% trans "Track Complaint" %}
|
|
</a>
|
|
<a href="{% url 'observations:observation_track' %}"
|
|
class="inline-flex items-center gap-2 px-6 py-3 bg-white text-navy border-2 border-navy/30 rounded-xl hover:bg-navy hover:text-white transition font-semibold btn-hover">
|
|
<i data-lucide="search" class="w-5 h-5"></i>
|
|
{% trans "Track Observation" %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Success Modal -->
|
|
<div id="successModal" class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
|
|
<div class="bg-white rounded-3xl shadow-2xl w-full max-w-sm p-8 text-center animate-slide-up">
|
|
<div class="inline-flex items-center justify-center w-20 h-20 bg-gradient-to-br from-emerald-400 to-emerald-600 rounded-full mb-6 shadow-xl">
|
|
<i data-lucide="check" class="w-10 h-10 text-white"></i>
|
|
</div>
|
|
<h3 class="text-2xl font-bold text-navy mb-3">{% trans "Submitted Successfully!" %}</h3>
|
|
<p class="text-slate text-sm mb-4">{% trans "Your submission has been received and will be reviewed." %}</p>
|
|
<div class="bg-light rounded-xl p-4 mb-6">
|
|
<p class="text-slate text-xs font-semibold mb-1">{% trans "Reference Number:" %}</p>
|
|
<span id="referenceNumber" class="text-2xl font-bold text-navy tracking-wide"></span>
|
|
</div>
|
|
<button onclick="closeSuccessModal()" class="w-full bg-gradient-to-r from-navy to-blue text-white px-6 py-3.5 rounded-xl font-semibold hover:shadow-xl transition btn-hover">
|
|
{% trans "Done" %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
lucide.createIcons();
|
|
let currentFormType = null;
|
|
|
|
// Card selection
|
|
document.querySelectorAll('.selection-card').forEach(function(card) {
|
|
card.addEventListener('click', function() {
|
|
const formType = this.getAttribute('data-type');
|
|
currentFormType = formType;
|
|
document.getElementById('selectionCards').style.display = 'none';
|
|
const formContainer = document.getElementById('publicFormContainer');
|
|
formContainer.classList.remove('hidden');
|
|
formContainer.classList.add('active');
|
|
loadForm(formType);
|
|
});
|
|
});
|
|
|
|
function loadForm(formType) {
|
|
document.getElementById('loadingSpinner').classList.remove('hidden');
|
|
document.getElementById('formContent').classList.add('hidden');
|
|
|
|
let hospitals = [];
|
|
fetch('/core/api/hospitals/')
|
|
.then(r => r.json())
|
|
.then(data => { hospitals = data.hospitals || []; });
|
|
|
|
if (formType === 'complaint') {
|
|
fetch('{% url "complaints:public_complaint_submit" %}')
|
|
.then(r => r.text())
|
|
.then(html => {
|
|
const parser = new DOMParser();
|
|
const doc = parser.parseFromString(html, 'text/html');
|
|
const formHtml = doc.querySelector('.complaint-form')?.innerHTML || '';
|
|
document.getElementById('formContent').innerHTML = formHtml;
|
|
|
|
// Create Lucide icons after form is loaded
|
|
setTimeout(() => {
|
|
lucide.createIcons();
|
|
|
|
// Initialize complaint form
|
|
initComplaintFormInLanding();
|
|
}, 100);
|
|
|
|
showForm();
|
|
})
|
|
.catch((error) => {
|
|
console.error('Error loading complaint form:', error);
|
|
document.getElementById('loadingSpinner').classList.add('hidden');
|
|
Swal.fire({ icon: 'error', title: '{% trans "Error" %}', text: '{% trans "Failed to load form." %}' });
|
|
});
|
|
} else if (formType === 'observation') {
|
|
fetchCategoriesAndRenderObservationForm();
|
|
} else if (formType === 'inquiry') {
|
|
renderInquiryForm(hospitals);
|
|
}
|
|
}
|
|
|
|
function fetchCategoriesAndRenderObservationForm() {
|
|
fetch('/core/api/observation-categories/')
|
|
.then(r => r.json())
|
|
.then(data => { renderObservationForm(data.categories || []); })
|
|
.catch(() => {
|
|
document.getElementById('loadingSpinner').classList.add('hidden');
|
|
Swal.fire({ icon: 'error', title: '{% trans "Error" %}', text: '{% trans "Failed to load form." %}' });
|
|
});
|
|
}
|
|
|
|
function renderObservationForm(categories) {
|
|
const lang = '{{ LANGUAGE_CODE }}';
|
|
let catOptions = '<option value="">{% trans "Select Category" %} ({% trans "optional" %})</option>';
|
|
categories.forEach(c => {
|
|
const name = lang === 'ar' ? c.name_ar : c.name_en;
|
|
catOptions += `<option value="${c.id}">${name}</option>`;
|
|
});
|
|
|
|
const formHtml = `
|
|
<form id="observationForm" enctype="multipart/form-data">
|
|
{% csrf_token %}
|
|
<div class="mb-6">
|
|
<h3 class="text-2xl font-bold text-navy mb-2 flex items-center gap-3">
|
|
<i data-lucide="eye" class="w-6 h-6 text-purple-600"></i>
|
|
{% trans "Report an Observation" %}
|
|
</h3>
|
|
<p class="text-slate text-sm">{% trans "Help us improve by reporting issues you notice. Your input is valuable." %}</p>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-5">
|
|
<div>
|
|
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Category" %}</label>
|
|
<select name="category" class="w-full px-4 py-3 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-purple-500/20 focus:border-purple-500 bg-white form-input">
|
|
${catOptions}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Severity" %} <span class="text-red-500">*</span></label>
|
|
<div class="flex gap-2">
|
|
<div class="severity-option flex-1 py-3 border border-slate-200 rounded-xl text-center text-sm font-semibold" data-value="low">{% trans "Low" %}</div>
|
|
<div class="severity-option flex-1 py-3 border-2 border-yellow-400 bg-yellow-50 rounded-xl text-center text-sm font-semibold selected" data-value="medium">{% trans "Medium" %}</div>
|
|
<div class="severity-option flex-1 py-3 border border-slate-200 rounded-xl text-center text-sm font-semibold" data-value="high">{% trans "High" %}</div>
|
|
<div class="severity-option flex-1 py-3 border border-slate-200 rounded-xl text-center text-sm font-semibold" data-value="critical">{% trans "Critical" %}</div>
|
|
</div>
|
|
<input type="hidden" name="severity" id="severityInput" value="medium">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-5">
|
|
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Title" %}</label>
|
|
<input type="text" name="title" maxlength="200" placeholder="{% trans 'Brief descriptive title' %}"
|
|
class="w-full px-4 py-3 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-purple-500/20 form-input">
|
|
</div>
|
|
|
|
<div class="mb-5">
|
|
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Description" %} <span class="text-red-500">*</span></label>
|
|
<textarea name="description" rows="5" required placeholder="{% trans 'Describe what you observed in detail...' %}"
|
|
class="w-full px-4 py-3 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-purple-500/20 resize-none form-input"></textarea>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-5">
|
|
<div>
|
|
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Location" %}</label>
|
|
<input type="text" name="location_text" maxlength="200" placeholder="{% trans 'Where did this occur?' %}"
|
|
class="w-full px-4 py-3 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-purple-500/20 form-input">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-semibold text-navy mb-2">{% trans "When" %}</label>
|
|
<input type="datetime-local" name="incident_datetime"
|
|
class="w-full px-4 py-3 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-purple-500/20 form-input">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-5">
|
|
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Attachments" %}</label>
|
|
<div class="file-upload-area border-2 border-dashed border-slate-300 rounded-xl p-6 text-center cursor-pointer bg-slate-50 hover:bg-blue/5 transition"
|
|
onclick="document.getElementById('obsFiles').click()">
|
|
<i data-lucide="upload-cloud" class="w-10 h-10 text-purple-600 mx-auto mb-2"></i>
|
|
<p class="text-navy text-sm font-semibold">{% trans "Click to upload files" %}</p>
|
|
<p class="text-slate text-xs mt-1">{% trans "Images, PDF, Word (max 10MB)" %}</p>
|
|
</div>
|
|
<input type="file" id="obsFiles" name="attachments" multiple accept=".jpg,.jpeg,.png,.pdf,.doc,.docx" class="hidden" onchange="updateObsFileList()">
|
|
<div id="obsFileList" class="mt-3 space-y-2"></div>
|
|
</div>
|
|
|
|
<div class="mb-6 p-5 bg-light rounded-xl">
|
|
<label class="block text-sm font-semibold text-navy mb-3">{% trans "Your Info" %} <span class="text-slate font-normal">({% trans "optional - leave blank for anonymous" %})</span></label>
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<input type="text" name="reporter_staff_id" placeholder="{% trans 'Staff ID' %}" class="px-4 py-2.5 border border-slate-200 rounded-lg text-navy text-sm form-input">
|
|
<input type="text" name="reporter_name" placeholder="{% trans 'Name' %}" class="px-4 py-2.5 border border-slate-200 rounded-lg text-navy text-sm form-input">
|
|
<input type="tel" name="reporter_phone" placeholder="{% trans 'Phone' %}" class="px-4 py-2.5 border border-slate-200 rounded-lg text-navy text-sm form-input">
|
|
<input type="email" name="reporter_email" placeholder="{% trans 'Email' %}" class="px-4 py-2.5 border border-slate-200 rounded-lg text-navy text-sm form-input">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="text-center pt-4 border-t border-slate-100">
|
|
<button type="submit" class="bg-gradient-to-r from-purple-600 to-purple-700 text-white px-8 py-3.5 rounded-xl font-semibold hover:shadow-xl transition inline-flex items-center gap-2 btn-hover" id="obsSubmitBtn">
|
|
<i data-lucide="send" class="w-5 h-5"></i> {% trans "Submit Observation" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
document.getElementById('formContent').innerHTML = formHtml;
|
|
lucide.createIcons();
|
|
|
|
document.querySelectorAll('.severity-option').forEach(opt => {
|
|
opt.addEventListener('click', function() {
|
|
document.querySelectorAll('.severity-option').forEach(o => {
|
|
o.classList.remove('selected');
|
|
o.style.borderColor = '';
|
|
o.style.backgroundColor = '';
|
|
});
|
|
this.classList.add('selected');
|
|
this.style.borderColor = '#007bbd';
|
|
this.style.backgroundColor = '#eef6fb';
|
|
document.getElementById('severityInput').value = this.dataset.value;
|
|
});
|
|
});
|
|
|
|
document.getElementById('observationForm').addEventListener('submit', handleObservationSubmit);
|
|
showForm();
|
|
}
|
|
|
|
function updateObsFileList() {
|
|
const input = document.getElementById('obsFiles');
|
|
const list = document.getElementById('obsFileList');
|
|
list.innerHTML = '';
|
|
if (input.files.length > 0) {
|
|
const div = document.createElement('div');
|
|
div.className = 'space-y-2';
|
|
for (let f of input.files) {
|
|
const size = (f.size / 1024 / 1024).toFixed(2);
|
|
div.innerHTML += `
|
|
<div class="flex items-center gap-3 text-sm text-navy p-3 bg-slate-50 rounded-xl">
|
|
<i data-lucide="file" class="w-5 h-5 text-slate-500"></i>
|
|
<span class="flex-1 truncate font-medium">${f.name}</span>
|
|
<span class="text-slate text-xs">${size} MB</span>
|
|
</div>
|
|
`;
|
|
}
|
|
list.appendChild(div);
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
function handleObservationSubmit(e) {
|
|
e.preventDefault();
|
|
const btn = document.getElementById('obsSubmitBtn');
|
|
const original = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="inline-block w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"></span>{% trans "Submitting..." %}';
|
|
|
|
fetch('/core/public/observation/submit/', {
|
|
method: 'POST',
|
|
body: new FormData(this),
|
|
headers: { 'X-CSRFToken': getCSRFToken() }
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
document.getElementById('referenceNumber').textContent = data.tracking_code;
|
|
document.getElementById('successModal').classList.remove('hidden');
|
|
this.reset();
|
|
} else {
|
|
Swal.fire({ icon: 'error', title: '{% trans "Error" %}', text: data.errors?.join('\\n') || '{% trans "Failed to submit" %}' });
|
|
}
|
|
})
|
|
.catch(() => Swal.fire({ icon: 'error', title: '{% trans "Error" %}', text: '{% trans "Failed to submit" %}' }))
|
|
.finally(() => { btn.disabled = false; btn.innerHTML = original; });
|
|
}
|
|
|
|
function renderInquiryForm(hospitals) {
|
|
let opts = '<option value="">{% trans "Select Hospital" %}</option>';
|
|
hospitals.forEach(h => opts += `<option value="${h.id}">${h.name}</option>`);
|
|
|
|
const formHtml = `
|
|
<form id="inquiryForm">
|
|
{% csrf_token %}
|
|
<div class="mb-6">
|
|
<h3 class="text-2xl font-bold text-navy mb-2 flex items-center gap-3">
|
|
<i data-lucide="help-circle" class="w-6 h-6 text-blue-600"></i>
|
|
{% trans "Inquiry Details" %}
|
|
</h3>
|
|
<p class="text-slate text-sm">{% trans "Ask a question. We'll respond within 24-48 hours." %}</p>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-5">
|
|
<div>
|
|
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Name" %} <span class="text-red-500">*</span></label>
|
|
<input type="text" name="name" required placeholder="{% trans 'Your full name' %}"
|
|
class="w-full px-4 py-3 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-blue/20 form-input">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Email Address" %} <span class="text-red-500">*</span></label>
|
|
<input type="email" name="email" required placeholder="your@email.com"
|
|
class="w-full px-4 py-3 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-blue/20 form-input">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-5">
|
|
<div>
|
|
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Phone Number" %} <span class="text-red-500">*</span></label>
|
|
<input type="tel" name="phone" maxlength="20" required placeholder="{% trans 'Your phone number' %}"
|
|
class="w-full px-4 py-3 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-blue/20 form-input">
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Hospital" %} <span class="text-red-500">*</span></label>
|
|
<select name="hospital" required class="w-full px-4 py-3 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-blue/20 bg-white form-input">
|
|
${opts}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-5">
|
|
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Category" %} <span class="text-red-500">*</span></label>
|
|
<select name="category" required class="w-full px-4 py-3 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-blue/20 bg-white form-input">
|
|
<option value="">{% trans "Select Category" %}</option>
|
|
<option value="general">{% trans "General Inquiry" %}</option>
|
|
<option value="services">{% trans "Services Information" %}</option>
|
|
<option value="appointments">{% trans "Appointments" %}</option>
|
|
<option value="billing">{% trans "Billing & Insurance" %}</option>
|
|
<option value="medical">{% trans "Medical Records" %}</option>
|
|
<option value="other">{% trans "Other" %}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-5">
|
|
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Subject" %} <span class="text-red-500">*</span></label>
|
|
<input type="text" name="subject" maxlength="200" required placeholder="{% trans 'Brief subject of your inquiry' %}"
|
|
class="w-full px-4 py-3 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-blue/20 form-input">
|
|
</div>
|
|
|
|
<div class="mb-6">
|
|
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Message" %} <span class="text-red-500">*</span></label>
|
|
<textarea name="message" rows="5" required placeholder="{% trans 'Please describe your inquiry in detail...' %}"
|
|
class="w-full px-4 py-3 border border-slate-200 rounded-xl text-navy focus:outline-none focus:ring-2 focus:ring-blue/20 resize-none form-input"></textarea>
|
|
</div>
|
|
|
|
<div class="text-center pt-4 border-t border-slate-100">
|
|
<button type="submit" class="bg-gradient-to-r from-blue-600 to-blue-700 text-white px-8 py-3.5 rounded-xl font-semibold hover:shadow-xl transition inline-flex items-center gap-2 btn-hover" id="inqSubmitBtn">
|
|
<i data-lucide="send" class="w-5 h-5"></i> {% trans "Submit Inquiry" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
document.getElementById('formContent').innerHTML = formHtml;
|
|
lucide.createIcons();
|
|
document.getElementById('inquiryForm').addEventListener('submit', handleInquirySubmit);
|
|
showForm();
|
|
}
|
|
|
|
function handleInquirySubmit(e) {
|
|
e.preventDefault();
|
|
const btn = document.getElementById('inqSubmitBtn');
|
|
const original = btn.innerHTML;
|
|
btn.disabled = true;
|
|
btn.innerHTML = '<span class="inline-block w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"></span>{% trans "Submitting..." %}';
|
|
|
|
fetch('{% url "core:public_inquiry_submit" %}', {
|
|
method: 'POST',
|
|
body: new FormData(this),
|
|
headers: { 'X-CSRFToken': getCSRFToken() }
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
document.getElementById('referenceNumber').textContent = data.reference_number;
|
|
document.getElementById('successModal').classList.remove('hidden');
|
|
this.reset();
|
|
} else {
|
|
Swal.fire({ icon: 'error', title: '{% trans "Error" %}', text: data.message || '{% trans "Failed" %}' });
|
|
}
|
|
})
|
|
.catch(() => Swal.fire({ icon: 'error', title: '{% trans "Error" %}', text: '{% trans "Failed" %}' }))
|
|
.finally(() => { btn.disabled = false; btn.innerHTML = original; });
|
|
}
|
|
|
|
function showForm() {
|
|
document.getElementById('loadingSpinner').classList.add('hidden');
|
|
document.getElementById('formContent').classList.remove('hidden');
|
|
}
|
|
|
|
function getCSRFToken() {
|
|
return document.cookie.split('; ').find(r => r.startsWith('csrftoken='))?.split('=')[1] ||
|
|
document.querySelector('[name="csrfmiddlewaretoken"]')?.value;
|
|
}
|
|
|
|
window.resetToSelection = function() {
|
|
document.getElementById('publicFormContainer').classList.add('hidden');
|
|
document.getElementById('formContent').innerHTML = '';
|
|
document.getElementById('selectionCards').style.display = 'grid';
|
|
currentFormType = null;
|
|
};
|
|
|
|
window.closeSuccessModal = function() {
|
|
document.getElementById('successModal').classList.add('hidden');
|
|
resetToSelection();
|
|
};
|
|
|
|
// Complaint form helper functions - defined at global scope
|
|
window.updateFileList = function() {
|
|
const input = document.getElementById('id_attachments');
|
|
const list = document.getElementById('fileList');
|
|
if (!input || !list) return;
|
|
|
|
list.innerHTML = '';
|
|
if (input.files.length > 0) {
|
|
const div = document.createElement('div');
|
|
div.className = 'space-y-2';
|
|
for (let f of input.files) {
|
|
const size = (f.size / 1024 / 1024).toFixed(2);
|
|
div.innerHTML += `
|
|
<div class="flex items-center gap-3 text-sm text-navy p-3 bg-slate-50 rounded-xl">
|
|
<i data-lucide="file" class="w-5 h-5 text-slate-500"></i>
|
|
<span class="flex-1 truncate font-medium">${f.name}</span>
|
|
<span class="text-slate text-xs">${size} MB</span>
|
|
</div>
|
|
`;
|
|
}
|
|
list.appendChild(div);
|
|
lucide.createIcons();
|
|
}
|
|
};
|
|
|
|
// Location hierarchy loading functions for complaint form
|
|
window.loadLocationsForForm = function() {
|
|
const locationSelect = document.getElementById('id_location');
|
|
if (!locationSelect) {
|
|
console.error('Location select not found');
|
|
return;
|
|
}
|
|
|
|
locationSelect.innerHTML = '<option value="">{% trans "Select Location" %}</option>';
|
|
|
|
fetch('/organizations/dropdowns/locations/')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
data.forEach(loc => {
|
|
const opt = document.createElement('option');
|
|
opt.value = loc.id;
|
|
opt.textContent = loc.name;
|
|
locationSelect.appendChild(opt);
|
|
});
|
|
})
|
|
.catch(err => console.error('Failed to load locations:', err));
|
|
};
|
|
|
|
window.loadMainSectionsForForm = function(locationId) {
|
|
const mainSectionSelect = document.getElementById('id_main_section');
|
|
const mainSectionContainer = document.getElementById('main_section_container');
|
|
const subsectionSelect = document.getElementById('id_subsection');
|
|
const subsectionContainer = document.getElementById('subsection_container');
|
|
|
|
if (!mainSectionSelect) return;
|
|
|
|
mainSectionSelect.innerHTML = '<option value="">{% trans "Select Main Section" %}</option>';
|
|
if (subsectionSelect) subsectionSelect.innerHTML = '<option value="">{% trans "Select Subsection" %}</option>';
|
|
|
|
if (!locationId) {
|
|
if (mainSectionContainer) mainSectionContainer.style.display = 'none';
|
|
if (subsectionContainer) subsectionContainer.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
fetch('/organizations/ajax/main-sections/?location_id=' + locationId)
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
const sections = data.sections || [];
|
|
sections.forEach(section => {
|
|
const opt = document.createElement('option');
|
|
opt.value = section.id;
|
|
opt.textContent = section.name;
|
|
mainSectionSelect.appendChild(opt);
|
|
});
|
|
if (mainSectionContainer) mainSectionContainer.style.display = sections.length ? 'block' : 'none';
|
|
if (subsectionContainer && sections.length === 0) subsectionContainer.style.display = 'none';
|
|
})
|
|
.catch(err => console.error('Failed to load main sections:', err));
|
|
};
|
|
|
|
window.loadSubsectionsForForm = function(locationId, mainSectionId) {
|
|
const subsectionSelect = document.getElementById('id_subsection');
|
|
const subsectionContainer = document.getElementById('subsection_container');
|
|
|
|
if (!subsectionSelect) return;
|
|
|
|
subsectionSelect.innerHTML = '<option value="">{% trans "Select Subsection" %}</option>';
|
|
|
|
if (!locationId || !mainSectionId) {
|
|
if (subsectionContainer) subsectionContainer.style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
fetch('/organizations/ajax/subsections/?location_id=' + locationId + '&main_section_id=' + mainSectionId)
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
const subsections = data.subsections || [];
|
|
subsections.forEach(sub => {
|
|
const opt = document.createElement('option');
|
|
opt.value = sub.id;
|
|
opt.textContent = sub.name;
|
|
subsectionSelect.appendChild(opt);
|
|
});
|
|
if (subsectionContainer) subsectionContainer.style.display = subsections.length ? 'block' : 'none';
|
|
})
|
|
.catch(err => console.error('Failed to load subsections:', err));
|
|
};
|
|
|
|
// Global variable to store all categories for taxonomy (legacy)
|
|
window.allComplaintCategories = [];
|
|
|
|
// Load taxonomy data
|
|
window.loadTaxonomy = function(hospitalId) {
|
|
fetch('{% url "complaints:api_load_categories" %}?hospital_id=' + (hospitalId || ''))
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
window.allComplaintCategories = data.categories || [];
|
|
const domainSelect = document.getElementById('id_domain');
|
|
if (!domainSelect) return;
|
|
domainSelect.innerHTML = '<option value="">{% trans "Select Domain" %}</option>';
|
|
window.allComplaintCategories.filter(c => c.level === 1).forEach(cat => {
|
|
const opt = document.createElement('option');
|
|
opt.value = cat.id;
|
|
opt.textContent = cat.name_en;
|
|
domainSelect.appendChild(opt);
|
|
});
|
|
})
|
|
.catch(err => console.error('Load taxonomy error:', err));
|
|
};
|
|
|
|
// Load categories for selected domain
|
|
window.loadCategories = function(domainId) {
|
|
const catSelect = document.getElementById('id_category');
|
|
const catContainer = document.getElementById('category_container');
|
|
if (!catSelect) return;
|
|
catSelect.innerHTML = '<option value="">{% trans "Select Category" %}</option>';
|
|
if (!domainId) {
|
|
if (catContainer) catContainer.style.display = 'none';
|
|
window.loadSubcategories('');
|
|
return;
|
|
}
|
|
const categories = window.allComplaintCategories.filter(c => c.level === 2 && c.parent_id == domainId);
|
|
categories.forEach(cat => {
|
|
const opt = document.createElement('option');
|
|
opt.value = cat.id;
|
|
opt.textContent = cat.name_en;
|
|
catSelect.appendChild(opt);
|
|
});
|
|
if (catContainer) catContainer.style.display = categories.length ? 'block' : 'none';
|
|
};
|
|
|
|
// Load subcategories for selected category
|
|
window.loadSubcategories = function(categoryId) {
|
|
const subSelect = document.getElementById('id_subcategory');
|
|
const subContainer = document.getElementById('subcategory_container');
|
|
if (!subSelect) return;
|
|
subSelect.innerHTML = '<option value="">{% trans "Select Subcategory" %}</option>';
|
|
if (!categoryId) {
|
|
if (subContainer) subContainer.style.display = 'none';
|
|
window.loadClassifications('');
|
|
return;
|
|
}
|
|
const subcategories = window.allComplaintCategories.filter(c => c.level === 3 && c.parent_id == categoryId);
|
|
subcategories.forEach(cat => {
|
|
const opt = document.createElement('option');
|
|
opt.value = cat.id;
|
|
opt.textContent = cat.name_en;
|
|
subSelect.appendChild(opt);
|
|
});
|
|
if (subContainer) subContainer.style.display = subcategories.length ? 'block' : 'none';
|
|
};
|
|
|
|
// Load classifications for selected subcategory
|
|
window.loadClassifications = function(subcategoryId) {
|
|
const clsSelect = document.getElementById('id_classification');
|
|
const clsContainer = document.getElementById('classification_container');
|
|
if (!clsSelect) return;
|
|
clsSelect.innerHTML = '<option value="">{% trans "Select Classification" %}</option>';
|
|
if (!subcategoryId) {
|
|
if (clsContainer) clsContainer.style.display = 'none';
|
|
return;
|
|
}
|
|
const classifications = window.allComplaintCategories.filter(c => c.level === 4 && c.parent_id == subcategoryId);
|
|
classifications.forEach(cat => {
|
|
const opt = document.createElement('option');
|
|
opt.value = cat.id;
|
|
opt.textContent = cat.name_en;
|
|
clsSelect.appendChild(opt);
|
|
});
|
|
if (clsContainer) clsContainer.style.display = classifications.length ? 'block' : 'none';
|
|
};
|
|
|
|
// Initialize complaint form when loaded in landing page
|
|
window.initComplaintFormInLanding = function() {
|
|
// Add file upload click handler
|
|
const uploadArea = document.querySelector('.border-dashed');
|
|
if (uploadArea) {
|
|
uploadArea.addEventListener('click', function() {
|
|
document.getElementById('id_attachments').click();
|
|
});
|
|
}
|
|
|
|
// Add file list update handler
|
|
const fileInput = document.getElementById('id_attachments');
|
|
if (fileInput) {
|
|
fileInput.addEventListener('change', window.updateFileList);
|
|
}
|
|
|
|
// Load locations on form init
|
|
window.loadLocationsForForm();
|
|
|
|
// Location change -> load main sections
|
|
const locationSelect = document.getElementById('id_location');
|
|
if (locationSelect) {
|
|
locationSelect.addEventListener('change', function() {
|
|
window.loadMainSectionsForForm(this.value);
|
|
});
|
|
}
|
|
|
|
// Main section change -> load subsections
|
|
const mainSectionSelect = document.getElementById('id_main_section');
|
|
if (mainSectionSelect) {
|
|
mainSectionSelect.addEventListener('change', function() {
|
|
const locationId = document.getElementById('id_location').value;
|
|
window.loadSubsectionsForForm(locationId, this.value);
|
|
});
|
|
}
|
|
|
|
// Add form submission handler
|
|
const complaintForm = document.getElementById('public_complaint_form');
|
|
if (complaintForm) {
|
|
complaintForm.addEventListener('submit', function(e) {
|
|
e.preventDefault();
|
|
|
|
const submitBtn = complaintForm.querySelector('button[type="submit"]');
|
|
const originalHTML = submitBtn.innerHTML;
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<span class="inline-block w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2"></span>{% trans "Submitting..." %}';
|
|
|
|
const formData = new FormData(complaintForm);
|
|
|
|
fetch('/complaints/public/submit/', {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-Requested-With': 'XMLHttpRequest',
|
|
'X-CSRFToken': getCSRFToken()
|
|
}
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
document.getElementById('referenceNumber').textContent = data.reference_number;
|
|
document.getElementById('successModal').classList.remove('hidden');
|
|
complaintForm.reset();
|
|
// Hide cascading dropdowns
|
|
const mainSectionContainer = document.getElementById('main_section_container');
|
|
const subsectionContainer = document.getElementById('subsection_container');
|
|
if (mainSectionContainer) mainSectionContainer.style.display = 'none';
|
|
if (subsectionContainer) subsectionContainer.style.display = 'none';
|
|
} else {
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: '{% trans "Error" %}',
|
|
text: data.errors ? data.errors.join('\n') : (data.message || '{% trans "Failed to submit. Please try again." %}')
|
|
});
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error('Submit error:', err);
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: '{% trans "Error" %}',
|
|
text: '{% trans "Failed to submit. Please try again." %}'
|
|
});
|
|
})
|
|
.finally(() => {
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerHTML = originalHTML;
|
|
});
|
|
});
|
|
|
|
// Initialize icons
|
|
lucide.createIcons();
|
|
}
|
|
};
|
|
});
|
|
</script>
|
|
{% endblock %}
|