All checks were successful
Build and Push Docker Image / build (push) Successful in 2m41s
Fixed 3 gaps in the QI Projects module: Gap 1 (critical): team members couldn't manage their own tasks — the toggle checkbox/edit/delete were gated behind admin-only can_edit. Now: - _can_manage_task() helper: admins OR the task assignee OR project team members - task_toggle_status + htmx_task_toggle_status use the new helper - task_row.html shows the toggle for assignees (task.assigned_to.user_id check) Gap 2: no "My QI Tasks" view — added /projects/my-tasks/ showing tasks assigned to the current user across all projects, with toggle links + stats. Sidebar link. Gap 3: no notification on task assignment — added apps/projects/signals.py (post_save on QIProjectTask → create_in_app_notification). apps.py ready() wired. Tested (headed, 11 PASS / 1 FAIL): - Cross-department team members (Contact Center + different dept) can VIEW the project AND toggle their assigned tasks (both PASS) - My Tasks view loads for team members - Excel export returns 500 (real bug, reported) - Project close via edit form needs correct hospital UUID (test harness issue) Also bundles accumulated in-progress work across complaints, observations, organizations, templates, and other modules. Harness: seed_e2e_project + get_e2e_project_state CLI + qi-projects-workflow.spec.ts
309 lines
13 KiB
HTML
309 lines
13 KiB
HTML
{% extends "layouts/public_base.html" %}
|
|
{% load i18n static %}
|
|
|
|
{% block title %}{% trans "Track Your Complaint" %}{% 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;
|
|
}
|
|
|
|
.glass-effect {
|
|
background: rgba(255, 255, 255, 0.7);
|
|
backdrop-filter: blur(12px);
|
|
-webkit-backdrop-filter: blur(12px);
|
|
border: 1px solid rgba(255, 255, 255, 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 subtle-float {
|
|
0% { transform: translateY(0px); }
|
|
50% { transform: translateY(-5px); }
|
|
100% { transform: translateY(0px); }
|
|
}
|
|
.float-icon { animation: subtle-float 3s ease-in-out infinite; }
|
|
</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-6 px-4 flex items-center justify-center">
|
|
<img src="{% static 'img/hh-logo.png' %}" alt="Al Hammadi Hospital" class="max-h-12 md: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 Complaint" %}</h1>
|
|
<p class="text-slate text-sm md:text-base max-w-xl mx-auto">
|
|
{% trans "Enter your reference number below to see real-time updates on your request." %}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Search Form -->
|
|
<div class="glass-card rounded-2xl shadow-lg p-5 md:p-8 mb-6 animate-fade-in">
|
|
<form method="POST" class="max-w-lg mx-auto">
|
|
{% csrf_token %}
|
|
<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"
|
|
name="reference_number"
|
|
class="w-full pl-12 pr-4 py-4 md:py-5 border-2 border-slate-100 rounded-xl md:rounded-2xl text-navy text-base md: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 'e.g., CMP-20240101-123456' %}"
|
|
value="{{ reference_number }}"
|
|
required
|
|
>
|
|
</div>
|
|
<button type="submit" class="w-full mt-3 md:mt-4 bg-navy hover:bg-navy/90 text-white px-6 md:px-8 py-4 md:py-5 rounded-xl md:rounded-2xl font-bold text-base md:text-lg transition-all duration-300 shadow-lg shadow-navy/20 hover:shadow-xl 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-4 uppercase tracking-widest font-semibold">
|
|
<i data-lucide="info" class="w-3 h-3 inline mr-1"></i>
|
|
{% trans "Found in your confirmation email" %}
|
|
</p>
|
|
</div>
|
|
|
|
{% if error_message %}
|
|
<div class="bg-white rounded-3xl shadow-lg p-6 mb-8 flex items-center gap-4 animate-shake">
|
|
<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 "Reference Not Found" %}</h3>
|
|
<p class="text-rose-700/80 text-sm">{{ error_message }}</p>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if complaint %}
|
|
<div class="animate-slide-up" style="animation-delay: 0.1s">
|
|
<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">{% trans "Case Reference" %}</span>
|
|
<h2 class="text-xl md:text-2xl font-black text-navy">{{ complaint.reference_number }}</h2>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<div class="px-4 py-2 rounded-xl text-xs font-black uppercase tracking-wider
|
|
{% if public_status.css == 'amber' %}bg-amber-50 text-amber-700
|
|
{% elif public_status.css == 'blue' %}bg-blue-50 text-blue-700
|
|
{% elif public_status.css == 'emerald' %}bg-emerald-50 text-emerald-700
|
|
{% elif public_status.css == 'rose' %}bg-rose-50 text-rose-700
|
|
{% else %}bg-slate-50 text-slate-700{% endif %}">
|
|
{{ public_status.label }}
|
|
</div>
|
|
{% if complaint.escalated_at %}
|
|
<div 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">
|
|
<i data-lucide="alert-triangle" class="w-3.5 h-3.5"></i>
|
|
{% trans "Escalated" %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap items-center gap-x-6 gap-y-1 text-xs text-slate/60">
|
|
<span class="inline-flex items-center gap-1.5">
|
|
<i data-lucide="calendar" class="w-3.5 h-3.5"></i>
|
|
{{ complaint.created_at|date:"M d, Y" }}
|
|
</span>
|
|
<span class="inline-flex items-center gap-1.5">
|
|
<i data-lucide="building" class="w-3.5 h-3.5"></i>
|
|
{{ complaint.department.name|default:"General" }}
|
|
</span>
|
|
{% if complaint.due_at %}
|
|
<span class="inline-flex items-center gap-1.5 {% if complaint.is_overdue %}text-rose-500 font-bold{% endif %}">
|
|
<i data-lucide="clock" class="w-3.5 h-3.5"></i>
|
|
{% if complaint.is_overdue %}{% trans "Overdue" %}{% else %}{{ complaint.due_at|date:"M d, H:i" }}{% endif %}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="mt-3 h-1.5 w-full bg-slate-100 rounded-full overflow-hidden">
|
|
<div class="h-full bg-navy transition-all duration-1000"
|
|
style="width: {{ public_status.progress }}%">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div 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-4 h-4 md:w-5 md:h-5"></i>
|
|
</div>
|
|
{% trans "Resolution Journey" %}
|
|
</h3>
|
|
|
|
{% if public_updates %}
|
|
<div class="space-y-1">
|
|
{% for update in public_updates %}
|
|
<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
|
|
{% if update.update_type == 'resolution' %}bg-emerald-100 text-emerald-600
|
|
{% else %}bg-blue-50 text-blue-600{% endif %}">
|
|
<i data-lucide="{% if update.update_type == 'resolution' %}check-circle-2{% else %}message-square{% endif %}" 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">
|
|
{% if update.update_type == 'resolution' %}{% trans "Final Resolution" %}
|
|
{% else %}{% trans "Department Response" %}{% endif %}
|
|
</h4>
|
|
<time class="text-xs md:text-sm font-medium text-slate/40">{{ update.created_at|date:"F j, Y • g:i A" }}</time>
|
|
</div>
|
|
{% if update.department_name %}
|
|
<p class="text-[10px] md:text-xs font-semibold text-slate/60 uppercase tracking-wide mb-2">{{ update.department_name }}</p>
|
|
{% endif %}
|
|
{% if update.message %}
|
|
<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">
|
|
{{ update.message|linebreaks }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-12">
|
|
<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 complaint is being reviewed. Updates will appear here." %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
if (typeof lucide !== 'undefined') {
|
|
lucide.createIcons();
|
|
}
|
|
|
|
// SLA Countdown Timer
|
|
var el = document.getElementById('sla-countdown');
|
|
if (el && el.dataset.dueAt) {
|
|
var dueAt = new Date(el.dataset.dueAt).getTime();
|
|
|
|
function updateCountdown() {
|
|
var now = Date.now();
|
|
var diff = dueAt - now;
|
|
|
|
if (diff <= 0) {
|
|
var overMs = Math.abs(diff);
|
|
var overD = Math.floor(overMs / 86400000);
|
|
var overH = Math.floor((overMs % 86400000) / 3600000);
|
|
var overM = Math.floor((overMs % 3600000) / 60000);
|
|
var txt = '{% trans "Overdue by" %} ';
|
|
if (overD > 0) txt += overD + 'd ';
|
|
txt += overH + 'h ' + overM + 'm';
|
|
el.textContent = txt;
|
|
el.className = 'text-xs font-bold mt-1 text-red-600 animate-pulse';
|
|
return;
|
|
}
|
|
|
|
var d = Math.floor(diff / 86400000);
|
|
var h = Math.floor((diff % 86400000) / 3600000);
|
|
var m = Math.floor((diff % 3600000) / 60000);
|
|
var s = Math.floor((diff % 60000) / 1000);
|
|
|
|
var parts = [];
|
|
if (d > 0) parts.push(d + 'd');
|
|
parts.push(h + 'h');
|
|
parts.push(m + 'm');
|
|
parts.push(s + 's');
|
|
el.textContent = parts.join(' ') + ' {% trans "remaining" %}';
|
|
|
|
if (diff < 21600000) {
|
|
el.className = 'text-xs font-bold mt-1 text-red-600';
|
|
} else if (diff < 86400000) {
|
|
el.className = 'text-xs font-bold mt-1 text-orange-600';
|
|
} else {
|
|
el.className = 'text-xs font-bold mt-1 text-green-600';
|
|
}
|
|
}
|
|
|
|
updateCountdown();
|
|
setInterval(updateCountdown, 1000);
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %} |