HH/templates/core/public_track.html
2026-07-05 14:49:45 +03:00

547 lines
26 KiB
HTML

{% extends "layouts/public_base.html" %}
{% load i18n static %}
{% block title %}{% trans "Track Your Submission" %} - PX360{% endblock %}
{% block extra_css %}
<style>
header.glass-card {
display: none !important;
}
.lang-switcher {
position: fixed;
top: 1rem;
right: 1rem;
z-index: 100;
display: flex;
gap: 0.5rem;
}
.lang-switcher a {
padding: 0.5rem 1rem;
border-radius: 0.75rem;
border: 2px solid rgba(255,255,255,0.3);
background: rgba(255,255,255,0.1);
backdrop-filter: blur(10px);
color: white;
font-weight: 600;
font-size: 0.875rem;
transition: all 0.2s ease;
text-decoration: none;
display: flex;
align-items: center;
gap: 0.5rem;
}
.lang-switcher a:hover {
background: rgba(255,255,255,0.2);
border-color: rgba(255,255,255,0.5);
}
.lang-switcher a.active {
background: white;
color: #005696;
border-color: white;
}
[dir="rtl"] .lang-switcher {
right: auto;
left: 1rem;
}
.selection-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
}
.selection-card:hover {
transform: translateY(-4px);
box-shadow: 0 12px 24px -6px rgba(0, 86, 150, 0.2);
}
.selection-card.active {
transform: translateY(-4px);
box-shadow: 0 12px 24px -6px rgba(0, 86, 150, 0.3);
}
.timeline-dot::after {
content: '';
position: absolute;
width: 2px;
height: 100%;
background: #e2e8f0;
left: 50%;
transform: translateX(-50%);
top: 24px;
z-index: 0;
}
.timeline-item:last-child .timeline-dot::after {
display: none;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-slide-up {
animation: slideUp 0.5s ease-out forwards;
}
@keyframes subtle-float {
0% { transform: translateY(0px); }
50% { transform: translateY(-5px); }
100% { transform: translateY(0px); }
}
.float-icon { animation: subtle-float 3s ease-in-out infinite; }
.spinner {
display: inline-block;
width: 1.5rem;
height: 1.5rem;
border: 3px 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); }
}
</style>
{% endblock %}
{% block content %}
<!-- Language Switcher -->
<div class="lang-switcher">
{% get_available_languages as LANGUAGES %}
{% get_current_language as LANGUAGE_CODE %}
{% for lang_code, lang_name in LANGUAGES %}
<a href="{% url 'core:set_language' %}?language={{ lang_code }}"
class="{% if LANGUAGE_CODE == lang_code %}active{% endif %}">
<span>{% if lang_code == 'en' %}🇬🇧{% elif lang_code == 'ar' %}🇸🇦{% endif %}</span>
<span>{{ lang_name }}</span>
</a>
{% endfor %}
</div>
<div class="max-w-4xl mx-auto px-4 py-6 md:py-12">
<!-- Logo Banner -->
<div class="rounded-2xl shadow-lg overflow-hidden mb-6 text-center animate-fade-in">
<div class="bg-white w-full py-8 px-6 flex items-center justify-center">
<img src="{% static 'img/hh-logo.png' %}" alt="Al Hammadi Hospital" class="max-h-16 w-auto object-contain">
</div>
<div class="bg-white p-4 md:p-8">
<h1 class="text-xl md:text-2xl font-bold text-navy mb-2">{% trans "Track Your Submission" %}</h1>
<p class="text-slate text-sm md:text-base max-w-xl mx-auto">
{% trans "Select a category below and enter your reference number to check the status." %}
</p>
</div>
</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 bg-white rounded-2xl shadow-xl border-2 border-transparent p-6 text-center relative overflow-hidden group"
data-type="complaint" onclick="selectType('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-14 h-14 rounded-2xl bg-gradient-to-br from-red-50 to-red-100 mx-auto mb-3 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<i data-lucide="alert-circle" class="w-7 h-7 text-red-600"></i>
</div>
<h3 class="text-lg font-bold text-navy mb-1">{% trans "Complaint" %}</h3>
<p class="text-slate text-xs">{% trans "e.g., CMP-20240101-123456" %}</p>
</div>
<!-- Inquiry Card -->
<div class="selection-card bg-white rounded-2xl shadow-xl border-2 border-transparent p-6 text-center relative overflow-hidden group"
data-type="inquiry" onclick="selectType('inquiry')">
<div class="absolute top-0 left-0 right-0 h-2 bg-gradient-to-r from-amber-400 to-amber-600"></div>
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-amber-50 to-amber-100 mx-auto mb-3 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<i data-lucide="help-circle" class="w-7 h-7 text-amber-600"></i>
</div>
<h3 class="text-lg font-bold text-navy mb-1">{% trans "Inquiry" %}</h3>
<p class="text-slate text-xs">{% trans "e.g., INQ-20260428-123456" %}</p>
</div>
<!-- Observation Card -->
<div class="selection-card bg-white rounded-2xl shadow-xl border-2 border-transparent p-6 text-center relative overflow-hidden group"
data-type="observation" onclick="selectType('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-14 h-14 rounded-2xl bg-gradient-to-br from-purple-50 to-purple-100 mx-auto mb-3 flex items-center justify-center group-hover:scale-110 transition-transform duration-300">
<i data-lucide="eye" class="w-7 h-7 text-purple-600"></i>
</div>
<h3 class="text-lg font-bold text-navy mb-1">{% trans "Observation" %}</h3>
<p class="text-slate text-xs">{% trans "e.g., OBS-ABC123" %}</p>
</div>
</div>
<!-- Search Form (hidden until type selected) -->
<div id="searchForm" class="glass-card rounded-3xl shadow-2xl p-8 mb-8 hidden">
<form id="trackForm" class="max-w-lg mx-auto">
{% csrf_token %}
<input type="hidden" id="trackType" name="type" value="">
<div class="relative group">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<i data-lucide="hash" class="w-5 h-5 text-slate/40 group-focus-within:text-blue transition-colors"></i>
</div>
<input type="text" id="referenceInput" name="reference"
class="w-full pl-12 pr-6 py-5 border-2 border-slate-100 rounded-2xl text-navy text-lg focus:ring-4 focus:ring-blue/10 focus:border-blue transition-all duration-300 bg-white/80 placeholder:text-slate/30"
placeholder="{% trans 'Enter your reference number' %}"
required>
</div>
<button type="submit" id="trackBtn" class="w-full mt-4 bg-navy hover:bg-navy/90 text-white px-8 py-5 rounded-2xl font-bold text-lg transition-all duration-300 shadow-lg shadow-navy/20 hover:shadow-xl hover:-translate-y-1 flex items-center justify-center gap-3">
<i data-lucide="crosshair" class="w-5 h-5"></i>
{% trans "Track Status" %}
</button>
</form>
<p class="text-center text-slate/50 text-xs mt-6 uppercase tracking-widest font-semibold">
<i data-lucide="info" class="w-3 h-3 inline mr-1"></i>
{% trans "Found in your confirmation email or SMS" %}
</p>
</div>
<!-- Error Message -->
<div id="errorBox" class="bg-white rounded-3xl shadow-lg p-6 mb-8 hidden flex items-center gap-4">
<div class="w-12 h-12 bg-rose-100 rounded-xl flex items-center justify-center text-rose-600 shrink-0">
<i data-lucide="alert-circle" class="w-6 h-6"></i>
</div>
<div>
<h3 class="font-bold text-rose-900">{% trans "Not Found" %}</h3>
<p id="errorText" class="text-rose-700/80 text-sm"></p>
</div>
</div>
<!-- Loading -->
<div id="loadingBox" class="text-center py-16 hidden">
<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 "Looking up your submission..." %}</p>
</div>
<!-- Results -->
<div id="resultsBox" class="hidden animate-slide-up">
<!-- Status Header (compact) -->
<div class="bg-white rounded-2xl shadow-lg p-4 md:p-6 mb-6">
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-3">
<div>
<span class="text-[10px] font-bold text-slate/40 uppercase tracking-wider block" id="resultRefLabel"></span>
<h2 class="text-xl md:text-2xl font-black text-navy" id="resultReference"></h2>
</div>
<div class="flex items-center gap-2">
<div id="resultStatusBadge" class="px-4 py-2 rounded-xl text-xs font-black uppercase tracking-wider"></div>
<div id="resultEscalated" class="px-3 py-2 rounded-xl text-[10px] font-bold uppercase tracking-wider bg-red-50 text-red-700 flex items-center gap-1 hidden">
<i data-lucide="alert-triangle" class="w-3.5 h-3.5"></i>
{% trans "Escalated" %}
</div>
</div>
</div>
<div id="resultInfoCards" class="flex flex-wrap items-center gap-x-6 gap-y-1 text-xs text-slate/60"></div>
<div class="mt-3 h-1.5 w-full bg-slate-100 rounded-full overflow-hidden">
<div id="resultProgressBar" class="h-full bg-navy transition-all duration-1000" style="width: 0%"></div>
</div>
</div>
<!-- Timeline -->
<div id="timelineBox" class="bg-white rounded-2xl shadow-lg p-5 md:p-8">
<h3 class="text-lg md:text-2xl font-bold text-navy mb-6 md:mb-10 flex items-center gap-3">
<div class="p-2 bg-navy text-white rounded-lg">
<i data-lucide="list-checks" class="w-5 h-5"></i>
</div>
{% trans "Resolution Journey" %}
</h3>
<div id="resultTimeline" class="space-y-1"></div>
<div id="resultNoTimeline" class="text-center py-12 hidden">
<div class="w-20 h-20 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4">
<i data-lucide="loader" class="w-8 h-8 text-slate/30 animate-spin"></i>
</div>
<p class="text-slate/60 font-medium">{% trans "Your submission is being reviewed. Updates will appear here." %}</p>
</div>
</div>
<!-- Response Section -->
<div id="responseBox" class="hidden mt-6 animate-slide-up">
<div class="bg-white rounded-3xl shadow-2xl p-8 md:p-10">
<h3 class="text-2xl font-bold text-navy mb-8 flex items-center gap-3">
<div class="p-2 bg-emerald-600 text-white rounded-lg">
<i data-lucide="message-circle" class="w-5 h-5"></i>
</div>
{% trans "Response" %}
</h3>
<div id="responseContent"></div>
<div id="satisfactionSection" class="hidden mt-6 pt-6 border-t border-slate-100">
<h4 class="text-sm font-bold text-navy mb-3 flex items-center gap-2">
<i data-lucide="smile" class="w-4 h-4"></i> {% trans "Was this resolution helpful?" %}
</h4>
<div id="satisfactionButtons" class="flex flex-wrap gap-3">
<button onclick="setSatisfaction('satisfied')" id="satBtnSatisfied"
class="inline-flex items-center gap-1.5 px-4 py-2.5 rounded-xl text-sm font-semibold border-2 border-slate-200 text-slate-600 hover:border-green-400 hover:bg-green-50 transition">
<i data-lucide="thumbs-up" class="w-4 h-4"></i> {% trans "Satisfied" %}
</button>
<button onclick="setSatisfaction('neutral')" id="satBtnNeutral"
class="inline-flex items-center gap-1.5 px-4 py-2.5 rounded-xl text-sm font-semibold border-2 border-slate-200 text-slate-600 hover:border-yellow-400 hover:bg-yellow-50 transition">
<i data-lucide="minus-circle" class="w-4 h-4"></i> {% trans "Neutral" %}
</button>
<button onclick="setSatisfaction('dissatisfied')" id="satBtnDissatisfied"
class="inline-flex items-center gap-1.5 px-4 py-2.5 rounded-xl text-sm font-semibold border-2 border-slate-200 text-slate-600 hover:border-red-400 hover:bg-red-50 transition">
<i data-lucide="thumbs-down" class="w-4 h-4"></i> {% trans "Dissatisfied" %}
</button>
</div>
<div id="satisfactionThankYou" class="hidden items-center gap-2 text-sm text-emerald-600 font-semibold">
<i data-lucide="check-circle" class="w-5 h-5"></i>
<span>{% trans "Thank you for your feedback!" %}</span>
</div>
</div>
</div>
</div>
<!-- Privacy Note -->
<div class="bg-white/80 backdrop-blur rounded-2xl p-5 mt-6 flex items-start gap-3 border border-white/50">
<i data-lucide="shield" class="w-5 h-5 text-navy mt-0.5 shrink-0"></i>
<p class="text-sm text-slate/60">{% trans "For privacy reasons, detailed notes and internal communications are not shown here." %}</p>
</div>
</div>
<!-- Back Link -->
<div class="text-center mt-8">
<a href="{% url 'core:public_submit_landing' %}" class="inline-flex items-center gap-2 px-6 py-3 bg-navy text-white rounded-xl font-bold text-sm hover:bg-blue transition shadow-lg">
<i data-lucide="arrow-left" class="w-4 h-4"></i>
{% trans "Submit New Feedback" %}
</a>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
if (typeof lucide !== 'undefined') lucide.createIcons();
var selectedType = null;
window.selectType = function(type) {
selectedType = type;
document.getElementById('trackType').value = type;
document.getElementById('searchForm').classList.remove('hidden');
document.getElementById('errorBox').classList.add('hidden');
document.getElementById('resultsBox').classList.add('hidden');
document.getElementById('loadingBox').classList.add('hidden');
var placeholders = {
complaint: "{% trans 'e.g., CMP-20240101-123456' %}",
inquiry: "{% trans 'e.g., INQ-20260428-123456' %}",
observation: "{% trans 'e.g., OBS-ABC123' %}"
};
document.getElementById('referenceInput').placeholder = placeholders[type] || '';
document.getElementById('referenceInput').focus();
document.querySelectorAll('.selection-card').forEach(function(card) {
var isActive = card.getAttribute('data-type') === type;
card.classList.toggle('active', isActive);
card.style.borderColor = isActive ? (type === 'complaint' ? '#ef4444' : type === 'inquiry' ? '#f59e0b' : '#a855f7') : 'transparent';
});
};
document.getElementById('trackForm').addEventListener('submit', function(e) {
e.preventDefault();
var reference = document.getElementById('referenceInput').value.trim();
if (!reference || !selectedType) return;
document.getElementById('errorBox').classList.add('hidden');
document.getElementById('resultsBox').classList.add('hidden');
document.getElementById('loadingBox').classList.remove('hidden');
var btn = document.getElementById('trackBtn');
var originalBtn = btn.innerHTML;
btn.disabled = true;
btn.innerHTML = '<span class="spinner"></span> {% trans "Searching..." %}';
var url = '{% url "core:public_track_api" %}?type=' + encodeURIComponent(selectedType) + '&reference=' + encodeURIComponent(reference);
fetch(url)
.then(function(r) { return r.json(); })
.then(function(data) {
btn.disabled = false;
btn.innerHTML = originalBtn;
document.getElementById('loadingBox').classList.add('hidden');
if (data.found && data.expired) {
document.getElementById('errorText').textContent = "{% trans 'This tracking link has expired. Tracking is available for 5 days after resolution. Please contact patient Experience team if you need assistance.' %}";
document.getElementById('errorBox').classList.remove('hidden');
document.getElementById('errorBox').style.display = 'flex';
lucide.createIcons();
} else if (data.found) {
renderResults(data);
} else {
document.getElementById('errorText').textContent = data.error || "{% trans 'No submission found with this reference number.' %}";
document.getElementById('errorBox').classList.remove('hidden');
document.getElementById('errorBox').style.display = 'flex';
lucide.createIcons();
}
})
.catch(function() {
btn.disabled = false;
btn.innerHTML = originalBtn;
document.getElementById('loadingBox').classList.add('hidden');
document.getElementById('errorText').textContent = "{% trans 'An error occurred. Please try again.' %}";
document.getElementById('errorBox').classList.remove('hidden');
document.getElementById('errorBox').style.display = 'flex';
lucide.createIcons();
});
});
function renderResults(data) {
var refLabels = {
complaint: "{% trans 'Case Reference' %}",
inquiry: "{% trans 'Inquiry Reference' %}",
observation: "{% trans 'Tracking Code' %}"
};
document.getElementById('resultRefLabel').textContent = refLabels[data.type] || "{% trans 'Reference' %}";
document.getElementById('resultReference').textContent = data.reference;
var colorMap = {
amber: 'bg-amber-50 text-amber-700 border-amber-200',
blue: 'bg-blue-50 text-blue-700 border-blue-200',
emerald: 'bg-emerald-50 text-emerald-700 border-emerald-200',
rose: 'bg-rose-50 text-rose-700 border-rose-200',
slate: 'bg-slate-50 text-slate-700 border-slate-200',
sky: 'bg-sky-50 text-sky-700 border-sky-200',
teal: 'bg-teal-50 text-teal-700 border-teal-200'
};
var badge = document.getElementById('resultStatusBadge');
badge.className = 'px-6 py-3 rounded-2xl text-sm font-black uppercase tracking-wider shadow-sm border-b-4 ' + (colorMap[data.status_color] || colorMap.slate);
badge.textContent = data.status_display;
var esc = document.getElementById('resultEscalated');
if (data.escalated) { esc.classList.remove('hidden'); } else { esc.classList.add('hidden'); }
document.getElementById('resultProgressBar').style.width = data.progress + '%';
var cardsHtml = '';
(data.info_cards || []).forEach(function(card) {
var iconColor = card.alert ? 'text-rose-500' : 'text-slate/60';
cardsHtml += '<span class="inline-flex items-center gap-1.5 ' + (card.alert ? 'text-rose-500 font-bold' : '') + '">' +
'<i data-lucide="' + card.icon + '" class="w-3.5 h-3.5 ' + iconColor + '"></i>' +
card.value +
'</span>';
});
document.getElementById('resultInfoCards').innerHTML = cardsHtml;
var timeline = data.timeline || [];
var timelineEl = document.getElementById('resultTimeline');
var noTimelineEl = document.getElementById('resultNoTimeline');
if (timeline.length > 0) {
noTimelineEl.classList.add('hidden');
var tlHtml = '';
timeline.forEach(function(item) {
var iconBg = item.type === 'resolution' ? 'bg-emerald-100 text-emerald-600' : 'bg-blue-50 text-blue-600';
tlHtml += '<div class="timeline-item flex gap-3 md:gap-6 pb-6 md:pb-10 relative">' +
'<div class="timeline-dot shrink-0 relative z-10">' +
'<div class="w-10 h-10 md:w-12 md:h-12 rounded-2xl flex items-center justify-center shadow-sm border-2 border-white ' + iconBg + '">' +
'<i data-lucide="' + item.icon + '" class="w-5 h-5 md:w-6 md:h-6"></i>' +
'</div>' +
'</div>' +
'<div class="flex-1 pt-1 min-w-0">' +
'<div class="flex flex-col md:flex-row md:items-center justify-between mb-2">' +
'<h4 class="font-black text-navy text-base md:text-lg">' + item.title + '</h4>' +
'<time class="text-xs md:text-sm font-medium text-slate/40">' + item.created_at + '</time>' +
'</div>';
if (item.department) {
tlHtml += '<p class="text-[10px] md:text-xs font-semibold text-slate/60 uppercase tracking-wide mb-2">' + item.department + '</p>';
}
if (item.comment) {
tlHtml += '<div class="bg-slate-50/50 rounded-xl md:rounded-2xl p-4 md:p-5 border border-slate-100 text-slate-700 leading-relaxed text-sm shadow-inner">' + item.comment.replace(/\n/g, '<br>') + '</div>';
}
tlHtml += '</div></div>';
});
timelineEl.innerHTML = tlHtml;
timelineEl.classList.remove('hidden');
} else {
timelineEl.innerHTML = '';
timelineEl.classList.add('hidden');
noTimelineEl.classList.remove('hidden');
}
var responseBox = document.getElementById('responseBox');
var responseContent = document.getElementById('responseContent');
var timelineBox = document.getElementById('timelineBox');
var resp = data.response || {};
if (resp.has_response && (resp.en || resp.ar)) {
var responseText = resp.ar || resp.en;
var isRtl = !!resp.ar;
var rHtml = '<div class="bg-emerald-50/50 rounded-2xl p-6 border border-emerald-100"' + (isRtl ? ' dir="rtl"' : '') + '>' +
'<div class="text-slate-700 leading-relaxed whitespace-pre-line"' + (isRtl ? ' style="text-align: right;"' : '') + '>' + escapeHtml(responseText) + '</div>' +
'</div>';
responseContent.innerHTML = rHtml;
responseBox.classList.remove('hidden');
timelineBox.classList.add('hidden');
} else {
responseContent.innerHTML = '';
responseBox.classList.add('hidden');
timelineBox.classList.remove('hidden');
}
var satSection = document.getElementById('satisfactionSection');
var satButtons = document.getElementById('satisfactionButtons');
var satThankYou = document.getElementById('satisfactionThankYou');
var isResolved = resp.has_response && resp.en;
if (isResolved && selectedType === 'complaint') {
satSection.classList.remove('hidden');
if (data.satisfaction_locked_by_patient) {
satButtons.classList.add('hidden');
satThankYou.classList.remove('hidden');
satThankYou.classList.add('flex');
} else {
satButtons.classList.remove('hidden');
satThankYou.classList.add('hidden');
satThankYou.classList.remove('flex');
}
} else {
satSection.classList.add('hidden');
}
document.getElementById('resultsBox').classList.remove('hidden');
lucide.createIcons();
document.getElementById('resultsBox').scrollIntoView({ behavior: 'smooth', block: 'start' });
}
function escapeHtml(text) {
var div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
window.setSatisfaction = function(value) {
var ref = document.getElementById('referenceInput').value.trim();
var btns = document.getElementById('satisfactionButtons');
var thankYou = document.getElementById('satisfactionThankYou');
fetch("{% url 'core:public_set_satisfaction' %}", {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'reference=' + encodeURIComponent(ref) + '&satisfaction=' + encodeURIComponent(value)
})
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
btns.classList.add('hidden');
thankYou.classList.remove('hidden');
thankYou.classList.add('flex');
lucide.createIcons();
}
});
};
var urlParams = new URLSearchParams(window.location.search);
var autoType = urlParams.get('type');
var autoRef = urlParams.get('reference');
if (autoType && autoRef) {
selectType(autoType);
document.getElementById('referenceInput').value = autoRef;
document.getElementById('trackForm').dispatchEvent(new Event('submit'));
}
});
</script>
{% endblock %}