340 lines
16 KiB
HTML
340 lines
16 KiB
HTML
{% load i18n %}
|
|
<section class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
|
|
<h3 class="text-xl font-bold text-navy mb-6">{% trans "Staff Explanations" %}</h3>
|
|
|
|
{% if explanations %}
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div></div>
|
|
{% if can_edit %}
|
|
<a href="{% url 'complaints:request_explanation_form' pk=complaint.id %}" class="px-4 py-2 bg-gradient-to-r from-navy to-blue text-white rounded-xl font-semibold hover:opacity-90 transition inline-flex items-center gap-2">
|
|
<i data-lucide="plus" class="w-4 h-4"></i> {% trans "Request More Explanations" %}
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
<div class="space-y-6">
|
|
{% for exp in explanations %}
|
|
<div class="bg-white border border-slate-200 rounded-xl p-5 {% if exp.escalated_to_manager %}border-l-4 border-l-orange-500{% endif %}">
|
|
<!-- Header: Staff Info + Status -->
|
|
<div class="flex justify-between items-start mb-4">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 bg-navy rounded-full flex items-center justify-center text-white font-bold text-sm">
|
|
{{ exp.staff.first_name|first }}{{ exp.staff.last_name|first }}
|
|
</div>
|
|
<div>
|
|
<h4 class="font-bold text-navy">{{ exp.staff }}</h4>
|
|
<div class="flex items-center gap-2 text-xs text-slate">
|
|
<span>{{ exp.staff.employee_id }}</span>
|
|
{% if exp.staff.department %}
|
|
<span>•</span>
|
|
<span>{{ exp.staff.department.name }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-col items-end gap-1">
|
|
<!-- Submission Status -->
|
|
<span class="px-3 py-1 {% if exp.is_used %}bg-green-100 text-green-600{% else %}bg-yellow-100 text-yellow-600{% endif %} rounded-lg text-xs font-bold">
|
|
{% if exp.is_used %}{% trans "Submitted" %}{% else %}{% trans "Pending" %}{% endif %}
|
|
</span>
|
|
<!-- Acceptance Status (only for submitted) -->
|
|
{% if exp.is_used %}
|
|
<span class="px-3 py-1 rounded-lg text-xs font-bold
|
|
{% if exp.acceptance_status == 'acceptable' %}bg-blue-100 text-blue-600
|
|
{% elif exp.acceptance_status == 'not_acceptable' %}bg-red-100 text-red-600
|
|
{% else %}bg-slate-100 text-slate-600{% endif %}">
|
|
{{ exp.get_acceptance_status_display }}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Escalation Notice -->
|
|
{% if exp.escalated_to_manager %}
|
|
<div class="bg-orange-50 border border-orange-200 rounded-lg p-3 mb-4">
|
|
<div class="flex items-center gap-2 text-orange-700 text-sm">
|
|
<i data-lucide="arrow-up-circle" class="w-4 h-4"></i>
|
|
<span class="font-semibold">{% trans "Escalated to Manager" %}</span>
|
|
<span class="text-slate">({{ exp.escalated_to_manager.staff.get_full_name }})</span>
|
|
</div>
|
|
{% if exp.acceptance_notes %}
|
|
<p class="text-xs text-slate mt-1 ml-6">{{ exp.acceptance_notes }}</p>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Explanation Content -->
|
|
{% if exp.is_used and exp.explanation %}
|
|
<div class="bg-slate-50 rounded-xl p-4 mb-4">
|
|
<p class="text-slate text-sm">{{ exp.explanation }}</p>
|
|
{% if exp.attachment_count > 0 %}
|
|
<div class="mt-3 pt-3 border-t border-slate-200 flex items-center gap-2 text-xs text-slate">
|
|
<i data-lucide="paperclip" class="w-4 h-4"></i>
|
|
<span>{{ exp.attachment_count }} {% trans "attachment(s)" %}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Admin Actions (for submitted explanations) -->
|
|
{% if can_edit and exp.is_used and not exp.escalated_to_manager %}
|
|
<div class="mt-4 pt-4 border-t border-slate-100">
|
|
{% if exp.acceptance_status == 'pending' %}
|
|
<div class="flex flex-wrap gap-2">
|
|
<button onclick="markExplanation('{{ exp.id|stringformat:"s" }}', 'acceptable')" class="px-4 py-2 bg-green-600 text-white rounded-lg text-sm font-semibold hover:bg-green-700 transition inline-flex items-center gap-2">
|
|
<i data-lucide="check-circle" class="w-4 h-4"></i> {% trans "Acceptable" %}
|
|
</button>
|
|
<button onclick="markExplanation('{{ exp.id|stringformat:"s" }}', 'not_acceptable')" class="px-4 py-2 bg-red-600 text-white rounded-lg text-sm font-semibold hover:bg-red-700 transition inline-flex items-center gap-2">
|
|
<i data-lucide="x-circle" class="w-4 h-4"></i> {% trans "Not Acceptable" %}
|
|
</button>
|
|
{% if exp.staff.report_to %}
|
|
<button type="button" data-escalate-explanation-id="{{ exp.id }}" class="escalate-btn px-4 py-2 bg-orange-500 text-white rounded-lg text-sm font-semibold hover:bg-orange-600 transition inline-flex items-center gap-2">
|
|
<i data-lucide="arrow-up-circle" class="w-4 h-4"></i> {% trans "Not Acceptable & Escalate" %}
|
|
</button>
|
|
{% else %}
|
|
<span class="px-4 py-2 bg-slate-100 text-slate-400 rounded-lg text-sm font-semibold inline-flex items-center gap-2 cursor-not-allowed" title="{% trans 'Cannot escalate: Staff has no manager assigned' %}">
|
|
<i data-lucide="arrow-up-circle" class="w-4 h-4"></i> {% trans "No Manager to Escalate" %}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
{% else %}
|
|
<div class="flex items-center gap-2 text-sm">
|
|
<span class="text-slate">{% trans "Reviewed by:" %}</span>
|
|
<span class="font-semibold text-navy">{{ exp.accepted_by.get_full_name }}</span>
|
|
<span class="text-slate">{% trans "on" %}</span>
|
|
<span class="font-semibold text-navy">{{ exp.accepted_at|date:"Y-m-d H:i" }}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Pending Actions -->
|
|
{% if can_edit and not exp.is_used %}
|
|
<div class="mt-4 flex gap-2">
|
|
<button onclick="resendExplanation('{{ exp.token }}')" class="px-4 py-2 bg-navy text-white rounded-lg text-sm font-semibold hover:bg-blue transition inline-flex items-center gap-2">
|
|
<i data-lucide="refresh-cw" class="w-4 h-4"></i> {% trans "Resend Link" %}
|
|
</button>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-12">
|
|
<i data-lucide="message-square" class="w-16 h-16 mx-auto text-slate-300 mb-4"></i>
|
|
<p class="text-slate mb-4">{% trans "No explanation requests sent yet" %}</p>
|
|
{% if can_edit %}
|
|
<a href="{% url 'complaints:request_explanation_form' pk=complaint.id %}" class="px-4 py-2 bg-gradient-to-r from-navy to-blue text-white rounded-xl font-semibold hover:opacity-90 transition inline-flex items-center gap-2">
|
|
<i data-lucide="plus" class="w-4 h-4"></i> {% trans "Request Explanation" %}
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<!-- Escalate Modal -->
|
|
<div id="escalateModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
|
|
<div class="bg-white rounded-2xl p-6 w-full max-w-md mx-4 shadow-2xl">
|
|
<div class="flex items-center gap-3 mb-4">
|
|
<div class="w-10 h-10 bg-orange-100 rounded-full flex items-center justify-center">
|
|
<i data-lucide="arrow-up-circle" class="w-5 h-5 text-orange-500"></i>
|
|
</div>
|
|
<h3 class="text-xl font-bold text-navy">{% trans "Escalate to Manager" %}</h3>
|
|
</div>
|
|
<p class="text-slate mb-4 text-sm">{% trans "This will mark the explanation as not acceptable and send a request to the staff's manager for an explanation." %}</p>
|
|
<form id="escalateForm" onsubmit="handleEscalate(event)">
|
|
{% csrf_token %}
|
|
<input type="hidden" id="escalateExplanationId" name="explanation_id">
|
|
<div class="mb-4">
|
|
<label class="block text-sm font-semibold text-slate mb-2">{% trans "Notes (Optional)" %}</label>
|
|
<textarea name="notes" id="escalateNotes" rows="3" class="w-full border border-slate-200 rounded-xl p-3 text-sm focus:ring-2 focus:ring-navy/20 outline-none" placeholder="{% trans 'Reason for escalation...' %}"></textarea>
|
|
</div>
|
|
<div class="flex gap-3">
|
|
<button type="button" onclick="closeEscalateModal()" class="flex-1 px-4 py-2 border border-slate-200 text-slate rounded-xl font-semibold hover:bg-slate-50 transition">
|
|
{% trans "Cancel" %}
|
|
</button>
|
|
<button type="submit" class="flex-1 px-4 py-2 bg-orange-500 text-white rounded-xl font-semibold hover:bg-orange-600 transition flex items-center justify-center gap-2">
|
|
<i data-lucide="arrow-up-circle" class="w-4 h-4"></i> {% trans "Escalate" %}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Store the explanation ID for escalation
|
|
let escalateExplanationId = null;
|
|
|
|
function resendExplanation(token) {
|
|
if (!confirm('{% trans "Are you sure you want to resend the explanation request?" %}')) {
|
|
return;
|
|
}
|
|
|
|
fetch('/complaints/{{ complaint.id }}/resend-explanation/', {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': getCookie('csrftoken'),
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (response.ok) {
|
|
alert('{% trans "Explanation request resent successfully!" %}');
|
|
} else {
|
|
alert('{% trans "Failed to resend explanation request. Please try again." %}');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('{% trans "An error occurred. Please try again." %}');
|
|
});
|
|
}
|
|
|
|
function markExplanation(explanationId, status) {
|
|
const actionText = status === 'acceptable' ? '{% trans "mark as acceptable" %}' : '{% trans "mark as not acceptable" %}';
|
|
if (!confirm('{% trans "Are you sure you want to" %} ' + actionText + '?')) {
|
|
return;
|
|
}
|
|
|
|
// Get CSRF token from the form's hidden input
|
|
const csrfToken = document.querySelector('#escalateForm input[name="csrfmiddlewaretoken"]')?.value || getCookie('csrftoken');
|
|
|
|
fetch(`/complaints/api/complaints/{{ complaint.id }}/review_explanation/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': csrfToken,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
explanation_id: explanationId,
|
|
acceptance_status: status
|
|
})
|
|
})
|
|
.then(response => {
|
|
if (response.ok) {
|
|
location.reload();
|
|
} else {
|
|
alert('{% trans "Failed to update explanation status. Please try again." %}');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('{% trans "An error occurred. Please try again." %}');
|
|
});
|
|
}
|
|
|
|
function showEscalateModal(explanationId) {
|
|
console.log('showEscalateModal called, raw explanationId:', explanationId, 'type:', typeof explanationId);
|
|
|
|
// Validate explanationId
|
|
if (!explanationId || explanationId === 'None' || explanationId === '' || explanationId === 'undefined') {
|
|
console.error('Invalid explanationId:', explanationId);
|
|
alert('{% trans "Error: Invalid explanation ID. Please refresh the page and try again." %}');
|
|
return;
|
|
}
|
|
|
|
// Store in module-level variable
|
|
escalateExplanationId = explanationId;
|
|
|
|
// Also store in hidden input as backup
|
|
const input = document.getElementById('escalateExplanationId');
|
|
if (input) {
|
|
input.value = explanationId;
|
|
}
|
|
|
|
console.log('Stored explanationId:', escalateExplanationId);
|
|
document.getElementById('escalateModal').classList.remove('hidden');
|
|
lucide.createIcons();
|
|
}
|
|
|
|
function closeEscalateModal() {
|
|
document.getElementById('escalateModal').classList.add('hidden');
|
|
document.getElementById('escalateNotes').value = '';
|
|
document.getElementById('escalateExplanationId').value = '';
|
|
escalateExplanationId = null;
|
|
}
|
|
|
|
function handleEscalate(event) {
|
|
event.preventDefault();
|
|
|
|
// Get explanationId from module variable first, then fallback to input
|
|
let explanationId = escalateExplanationId;
|
|
if (!explanationId) {
|
|
explanationId = document.getElementById('escalateExplanationId').value;
|
|
}
|
|
|
|
const notes = document.getElementById('escalateNotes').value;
|
|
// Get CSRF token from the form's hidden input
|
|
const csrfToken = document.querySelector('#escalateForm input[name="csrfmiddlewaretoken"]').value;
|
|
|
|
// Debug: Log values being sent
|
|
console.log('Escalating explanation:', { explanationId, notes });
|
|
|
|
if (!explanationId || explanationId === 'None') {
|
|
alert('{% trans "Error: No explanation selected. Please try again." %}');
|
|
closeEscalateModal();
|
|
return;
|
|
}
|
|
|
|
fetch(`/complaints/api/complaints/{{ complaint.id }}/escalate_explanation/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': csrfToken,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
explanation_id: escalateExplanationId,
|
|
acceptance_notes: notes
|
|
})
|
|
})
|
|
.then(response => {
|
|
if (response.ok) {
|
|
location.reload();
|
|
} else {
|
|
response.json().then(data => {
|
|
alert(data.error || '{% trans "Failed to escalate. Please try again." %}');
|
|
});
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error:', error);
|
|
alert('{% trans "An error occurred. Please try again." %}');
|
|
});
|
|
}
|
|
|
|
// Close modal when clicking outside
|
|
document.getElementById('escalateModal').addEventListener('click', function(e) {
|
|
if (e.target === this) {
|
|
closeEscalateModal();
|
|
}
|
|
});
|
|
|
|
// Add click event listeners to all escalate buttons (using data attributes)
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
document.querySelectorAll('.escalate-btn').forEach(function(btn) {
|
|
btn.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
const explanationId = this.getAttribute('data-escalate-explanation-id');
|
|
escalateExplanationId = explanationId;
|
|
console.log('Escalate button clicked, data-explanation-id:', explanationId);
|
|
showEscalateModal(explanationId);
|
|
});
|
|
});
|
|
});
|
|
|
|
// Helper function to get CSRF token
|
|
function getCookie(name) {
|
|
let cookieValue = null;
|
|
if (document.cookie && document.cookie !== '') {
|
|
const cookies = document.cookie.split(';');
|
|
for (let i = 0; i < cookies.length; i++) {
|
|
const cookie = cookies[i].trim();
|
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return cookieValue;
|
|
}
|
|
</script>
|