335 lines
20 KiB
HTML
335 lines
20 KiB
HTML
{% load i18n %}
|
||
{% load static %}
|
||
<!DOCTYPE html>
|
||
<html lang="{{ LANGUAGE_CODE|default:'en' }}" dir="{% if LANGUAGE_CODE == 'ar' %}rtl{% else %}ltr{% endif %}">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>{{ complaint.reference_number }} - {% trans "Collect Feedback" %}</title>
|
||
<link rel="stylesheet" href="{% static 'dist/css/tailwind.css' %}">
|
||
<link rel="stylesheet" href="{% static 'vendor/fonts/noto-kufi-arabic/arabic.css' %}">
|
||
<script src="https://unpkg.com/lucide@latest"></script>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||
body { font-family: 'Inter', 'Noto Kufi Arabic', sans-serif; }
|
||
[dir="rtl"] body, [dir="rtl"] input, [dir="rtl"] textarea, [dir="rtl"] select, [dir="rtl"] button {
|
||
font-family: 'Noto Kufi Arabic', sans-serif;
|
||
}
|
||
.btn-add {
|
||
display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1rem;
|
||
background: #eef6fb; color: #005696; border: 2px dashed #93c5fd; border-radius: 0.75rem;
|
||
font-weight: 600; cursor: pointer; transition: all 0.2s; font-size: 0.875rem;
|
||
}
|
||
.btn-add:hover { background: #dbeaf6; border-color: #005696; }
|
||
.btn-remove {
|
||
width: 2rem; height: 2rem; display: flex; align-items: center; justify-content: center;
|
||
background: #fee2e2; color: #dc2626; border: none; border-radius: 0.5rem; cursor: pointer;
|
||
font-size: 1.25rem; transition: all 0.2s; flex-shrink: 0;
|
||
}
|
||
.btn-remove:hover { background: #fecaca; }
|
||
</style>
|
||
</head>
|
||
<body class="bg-slate-50 min-h-screen py-8 px-4">
|
||
<div class="w-full max-w-3xl mx-auto">
|
||
|
||
{% if complaint.department %}
|
||
<a href="{% url 'organizations:department_detail' complaint.department.id %}" class="inline-flex items-center gap-2 text-sm text-slate hover:text-navy transition mb-4">← {% trans "Back to Department" %}</a>
|
||
{% endif %}
|
||
|
||
<!-- Logo -->
|
||
<div class="text-center mb-6">
|
||
<img src="{% static 'img/hh-logo.png' %}" alt="Al Hammadi Hospital" class="h-16 w-auto mx-auto inline-block">
|
||
</div>
|
||
|
||
<!-- Complaint Card -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6 mb-6">
|
||
<div class="flex items-center gap-2 mb-1">
|
||
<span class="text-sm font-bold text-navy">{{ complaint.reference_number }}</span>
|
||
</div>
|
||
<h1 class="text-xl font-bold text-navy mb-4">{{ complaint.title }}</h1>
|
||
<div class="border-t border-slate-100 pt-4">
|
||
<p class="text-xs font-bold text-slate uppercase mb-2">{% trans "Description" %}</p>
|
||
<p class="text-base leading-relaxed text-slate">{{ complaint.description }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
{% if success %}
|
||
<!-- Success State -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-8 text-center">
|
||
<div class="w-16 h-16 mx-auto bg-green-100 rounded-full flex items-center justify-center mb-4">
|
||
<svg class="w-8 h-8 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>
|
||
</div>
|
||
<h2 class="text-lg font-bold text-navy mb-1">{% trans "Feedback Requests Sent" %}</h2>
|
||
<p class="text-sm text-slate">{{ success }}</p>
|
||
<p class="text-xs text-slate/60 mt-3">{% trans "You will be notified when all staff members respond." %}</p>
|
||
</div>
|
||
{% else %}
|
||
<!-- Feedback Form -->
|
||
<div class="bg-white rounded-2xl shadow-sm border border-slate-200 p-6">
|
||
{% if error %}
|
||
<div class="bg-red-50 border border-red-200 rounded-xl p-4 mb-4">
|
||
<p class="text-sm text-red-700 font-medium">{{ error }}</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<form method="post" class="space-y-6">
|
||
{% csrf_token %}
|
||
|
||
<div>
|
||
<p class="text-sm font-semibold text-navy mb-2">{% trans "Select staff and write their questions" %}</p>
|
||
<p class="text-xs text-slate mb-4">{% trans "Each staff member receives their own questions. You can write different questions for each person." %}</p>
|
||
|
||
{% if not accused_staff %}
|
||
<!-- Empty state: no PX-team pre-selected staff -->
|
||
<div class="bg-blue-50 border border-blue-200 rounded-xl p-3 mb-4 flex items-start gap-2">
|
||
<i data-lucide="info" class="w-4 h-4 text-blue-600 mt-0.5 shrink-0"></i>
|
||
<p class="text-sm text-blue-700">{% trans "No staff pre-selected by PX-team. Search and add staff members to investigate." %}</p>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<!-- Staff search box -->
|
||
<div class="bg-slate-50 rounded-xl p-4 mb-4">
|
||
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Add staff member" %}</label>
|
||
<div class="relative">
|
||
<input type="text" id="staffSearchInput" autocomplete="off"
|
||
class="w-full px-4 py-2 pl-10 border-2 border-slate-200 rounded-xl text-sm focus:outline-none focus:border-navy"
|
||
placeholder="{% trans 'Search by name or employee ID...' %}">
|
||
<i data-lucide="search" class="w-4 h-4 absolute left-3 top-3 text-slate/60 pointer-events-none"></i>
|
||
<div id="staffSearchResults" class="hidden absolute z-10 mt-1 w-full bg-white border border-slate-200 rounded-xl shadow-lg max-h-72 overflow-auto"></div>
|
||
</div>
|
||
<p class="text-xs text-slate/60 mt-2">{% trans "Search returns active staff from the complaint's hospital." %}</p>
|
||
</div>
|
||
|
||
<!-- Staff cards (pre-existing + dynamically added) -->
|
||
<div id="staffCardsContainer" class="space-y-4">
|
||
{% for s in accused_staff %}
|
||
<div class="staff-card border-2 border-slate-200 rounded-xl p-4" data-staff-id="{{ s.staff_id }}">
|
||
<label class="flex items-center gap-3 cursor-pointer mb-3">
|
||
<input type="checkbox" name="accused_staff[]" value="{{ s.staff_id }}" class="include-staff w-5 h-5 rounded accent-navy" onchange="toggleStaffCard(this)">
|
||
<span class="font-semibold text-sm text-slate">{{ s.staff.get_full_name }}</span>
|
||
{% if s.staff.job_title %}<span class="text-xs text-slate/60">{{ s.staff.job_title }}</span>{% endif %}
|
||
<button type="button" class="btn-remove-staff ml-auto text-slate/60 hover:text-red-600 transition" onclick="removeStaffCard(this)" title="{% trans 'Remove' %}">
|
||
<i data-lucide="x" class="w-4 h-4"></i>
|
||
</button>
|
||
</label>
|
||
<div class="questions-block pl-8">
|
||
<div class="questions-container space-y-2" id="q-{{ s.staff_id }}">
|
||
<div class="question-row flex items-start gap-2">
|
||
<span class="q-num mt-2 text-sm font-bold text-slate/40">1.</span>
|
||
<select name="question_types__{{ s.staff_id }}[]" class="px-2 py-2 border-2 border-slate-200 rounded-lg text-xs text-slate focus:outline-none focus:border-navy shrink-0" onchange="syncQuestionType(this)">
|
||
<option value="text">{% trans "Text" %}</option>
|
||
<option value="yes_no">{% trans "Yes/No" %}</option>
|
||
</select>
|
||
<input type="text" name="questions__{{ s.staff_id }}[]" class="q-input w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:outline-none focus:border-navy" placeholder="{% trans 'Enter a question...' %}" onkeydown="handleQuestionKeydown(event, '{{ s.staff_id }}')" required>
|
||
<button type="button" class="btn-remove mt-1" onclick="removeQuestion(this)" title="{% trans 'Remove' %}">×</button>
|
||
</div>
|
||
</div>
|
||
<button type="button" onclick="addQuestion('{{ s.staff_id }}')" class="btn-add mt-2">
|
||
+ {% trans "Add Question" %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
{% endfor %}
|
||
</div>
|
||
</div>
|
||
|
||
<button type="submit" onclick="return confirmSend(event)" class="w-full flex items-center justify-center gap-2 px-4 py-3 bg-navy text-white rounded-xl font-bold text-sm hover:bg-blue transition">
|
||
<i data-lucide="send" class="w-4 h-4"></i>
|
||
{% trans "Send Feedback Requests" %}
|
||
</button>
|
||
</form>
|
||
</div>
|
||
{% endif %}
|
||
|
||
</div>
|
||
|
||
<script>
|
||
lucide.createIcons();
|
||
|
||
// Apply initial visual state for unchecked staff cards (dim the question blocks)
|
||
document.querySelectorAll('.include-staff').forEach(function (cb) {
|
||
if (!cb.checked) toggleStaffCard(cb);
|
||
});
|
||
|
||
const qCounts = {};
|
||
document.querySelectorAll('.questions-container').forEach(function (c) {
|
||
qCounts[c.id] = c.children.length;
|
||
});
|
||
|
||
function toggleStaffCard(checkbox) {
|
||
var card = checkbox.closest('.staff-card');
|
||
var block = card.querySelector('.questions-block');
|
||
block.style.opacity = checkbox.checked ? '1' : '0.4';
|
||
block.style.pointerEvents = checkbox.checked ? 'auto' : 'none';
|
||
block.querySelectorAll('input[type="text"]').forEach(function (i) { i.required = checkbox.checked; });
|
||
}
|
||
|
||
function confirmSend(event) {
|
||
var checked = document.querySelectorAll('.include-staff:checked');
|
||
var totalQuestions = document.querySelectorAll('.q-input').length;
|
||
if (checked.length === 0) {
|
||
alert('{% trans "Please select at least one staff member." %}');
|
||
event.preventDefault();
|
||
return false;
|
||
}
|
||
var names = [];
|
||
checked.forEach(function (cb) {
|
||
var nameEl = cb.closest('.staff-card').querySelector('.font-semibold');
|
||
if (nameEl) names.push(nameEl.textContent.trim());
|
||
});
|
||
return confirm(
|
||
'{% trans "Send feedback requests to the following staff?" %}\n\n' +
|
||
names.join('\n') + '\n\n' +
|
||
'{% trans "Each will receive an email and SMS with their questions. This action cannot be undone." %}'
|
||
);
|
||
}
|
||
|
||
function addQuestion(staffId) {
|
||
var container = document.getElementById('q-' + staffId);
|
||
if (!container) return;
|
||
qCounts[container.id] = (qCounts[container.id] || 0) + 1;
|
||
var row = document.createElement('div');
|
||
row.className = 'question-row flex items-start gap-2';
|
||
row.innerHTML = '<span class="q-num mt-2 text-sm font-bold text-slate/40">' + qCounts[container.id] + '.</span>' +
|
||
'<select name="question_types__' + staffId + '[]" class="px-2 py-2 border-2 border-slate-200 rounded-lg text-xs text-slate focus:outline-none focus:border-navy shrink-0" onchange="syncQuestionType(this)">' +
|
||
'<option value="text">Text</option><option value="yes_no">Yes/No</option></select>' +
|
||
'<input type="text" name="questions__' + staffId + '[]" class="q-input w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:outline-none focus:border-navy" placeholder="Enter a question..." onkeydown="handleQuestionKeydown(event, \'' + staffId + '\')" required>' +
|
||
'<button type="button" class="btn-remove mt-1" onclick="removeQuestion(this)" title="Remove">×</button>';
|
||
container.appendChild(row);
|
||
var inputs = row.querySelectorAll('input[type="text"]');
|
||
if (inputs.length > 0) inputs[0].focus();
|
||
}
|
||
|
||
function syncQuestionType(select) {
|
||
var input = select.parentElement.querySelector('.q-input');
|
||
if (!input) return;
|
||
if (select.value === 'yes_no') {
|
||
input.placeholder = 'Enter a yes/no question...';
|
||
} else {
|
||
input.placeholder = 'Enter a question...';
|
||
}
|
||
}
|
||
|
||
function handleQuestionKeydown(event, staffId) {
|
||
if (event.key === 'Enter') { event.preventDefault(); addQuestion(staffId); }
|
||
}
|
||
|
||
function removeQuestion(btn) {
|
||
var container = btn.closest('.questions-container');
|
||
if (container && container.children.length > 1) {
|
||
btn.closest('.question-row').remove();
|
||
renumber(container);
|
||
}
|
||
}
|
||
|
||
function renumber(container) {
|
||
var rows = container.querySelectorAll('.question-row');
|
||
rows.forEach(function (row, i) { row.querySelector('.q-num').textContent = (i + 1) + '.'; });
|
||
qCounts[container.id] = rows.length;
|
||
}
|
||
|
||
// ─── Staff search + dynamic card creation ───
|
||
var searchTimer = null;
|
||
var searchInput = document.getElementById('staffSearchInput');
|
||
var searchResults = document.getElementById('staffSearchResults');
|
||
|
||
function escapeHtml(s) {
|
||
return String(s).replace(/[&<>"']/g, function (c) {
|
||
return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[c];
|
||
});
|
||
}
|
||
|
||
if (searchInput) {
|
||
searchInput.addEventListener('input', function () {
|
||
clearTimeout(searchTimer);
|
||
var q = this.value.trim();
|
||
if (q.length < 2) { searchResults.classList.add('hidden'); return; }
|
||
searchTimer = setTimeout(function () { doStaffSearch(q); }, 300);
|
||
});
|
||
|
||
// Hide results when clicking outside
|
||
document.addEventListener('click', function (e) {
|
||
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
|
||
searchResults.classList.add('hidden');
|
||
}
|
||
});
|
||
}
|
||
|
||
function doStaffSearch(q) {
|
||
fetch('{{ search_url }}?q=' + encodeURIComponent(q))
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
if (!data.results || !data.results.length) {
|
||
searchResults.innerHTML = '<div class="p-3 text-sm text-slate/60 italic">{% trans "No matches." %}</div>';
|
||
} else {
|
||
searchResults.innerHTML = data.results.map(function (s) {
|
||
var alreadyInList = document.querySelector('.staff-card[data-staff-id="' + s.id + '"]');
|
||
var disabled = s.already_added || alreadyInList;
|
||
var onclickAttr = disabled ? '' : 'onclick="addStaffCard(\'' + s.id + '\', \'' + escapeHtml(s.name).replace(/'/g, "\\'") + '\', \'' + escapeHtml(s.department_name).replace(/'/g, "\\'") + '\')"';
|
||
return '<div class="px-3 py-2 flex items-center justify-between ' + (disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer hover:bg-slate-50') + '" ' + onclickAttr + '>' +
|
||
'<span class="text-sm">' + escapeHtml(s.name) + (s.employee_id ? ' <span class="text-xs text-slate/40">(' + escapeHtml(s.employee_id) + ')</span>' : '') + ' <span class="text-xs text-slate/60">' + escapeHtml(s.department_name) + '</span></span>' +
|
||
(disabled ? '<span class="text-[10px] font-bold text-slate/60">{% trans "ADDED" %}</span>' : '') +
|
||
'</div>';
|
||
}).join('');
|
||
}
|
||
searchResults.classList.remove('hidden');
|
||
})
|
||
.catch(function () {
|
||
searchResults.innerHTML = '<div class="p-3 text-sm text-red-600 italic">{% trans "Search failed. Please try again." %}</div>';
|
||
searchResults.classList.remove('hidden');
|
||
});
|
||
}
|
||
|
||
function addStaffCard(id, name, dept) {
|
||
// Avoid duplicates: bail if a card with this staff_id already exists
|
||
if (document.querySelector('.staff-card[data-staff-id="' + id + '"]')) return;
|
||
|
||
var container = document.getElementById('staffCardsContainer');
|
||
var card = document.createElement('div');
|
||
card.className = 'staff-card border-2 border-slate-200 rounded-xl p-4';
|
||
card.dataset.staffId = id;
|
||
card.innerHTML =
|
||
'<label class="flex items-center gap-3 cursor-pointer mb-3">' +
|
||
'<input type="checkbox" name="accused_staff[]" value="' + id + '" class="include-staff w-5 h-5 rounded accent-navy" checked onchange="toggleStaffCard(this)">' +
|
||
'<span class="font-semibold text-sm text-slate">' + escapeHtml(name) + '</span>' +
|
||
'<span class="text-xs text-slate/60">' + escapeHtml(dept) + '</span>' +
|
||
'<button type="button" class="btn-remove-staff ml-auto text-slate/60 hover:text-red-600 transition" onclick="removeStaffCard(this)" title="{% trans "Remove" %}">' +
|
||
'<i data-lucide="x" class="w-4 h-4"></i>' +
|
||
'</button>' +
|
||
'</label>' +
|
||
'<div class="questions-block pl-8">' +
|
||
'<div class="questions-container space-y-2" id="q-' + id + '">' +
|
||
'<div class="question-row flex items-start gap-2">' +
|
||
'<span class="q-num mt-2 text-sm font-bold text-slate/40">1.</span>' +
|
||
'<select name="question_types__' + id + '[]" class="px-2 py-2 border-2 border-slate-200 rounded-lg text-xs text-slate focus:outline-none focus:border-navy shrink-0" onchange="syncQuestionType(this)">' +
|
||
'<option value="text">{% trans "Text" %}</option><option value="yes_no">{% trans "Yes/No" %}</option>' +
|
||
'</select>' +
|
||
'<input type="text" name="questions__' + id + '[]" class="q-input w-full px-3 py-2 border-2 border-slate-200 rounded-lg text-sm focus:outline-none focus:border-navy" placeholder="{% trans "Enter a question..." %}" onkeydown="handleQuestionKeydown(event, \'' + id + '\')" required>' +
|
||
'<button type="button" class="btn-remove mt-1" onclick="removeQuestion(this)" title="{% trans "Remove" %}">×</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<button type="button" onclick="addQuestion(\'' + id + '\')" class="btn-add mt-2">+ {% trans "Add Question" %}</button>' +
|
||
'</div>';
|
||
container.appendChild(card);
|
||
|
||
// Reset search input + hide dropdown
|
||
searchInput.value = '';
|
||
searchResults.classList.add('hidden');
|
||
|
||
// Register q-count for the new card so addQuestion() works
|
||
qCounts['q-' + id] = 1;
|
||
|
||
// Re-render icons (for the × button)
|
||
lucide.createIcons();
|
||
}
|
||
|
||
function removeStaffCard(btn) {
|
||
if (confirm('{% trans "Remove this staff member from the investigation?" %}')) {
|
||
var card = btn.closest('.staff-card');
|
||
if (card) card.remove();
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|