feat(inquiry): remove PX-side Response to Patient from UI
Per the workflow realignment, the department owns inquiry resolution end-to-end. The PX-side respond modal and its 'Response to Patient' tile button are removed; the 'Resolve' button in the 'Department has responded' banner now POSTs directly to inquiry_change_status (with a confirm dialog), which already enforces the contact_status gate. The inquiry_respond view and URL are kept for backward compatibility but no UI surfaces them. Two regression tests lock in the new behavior: - showRespondModal JS function and respondModal div are gone - Resolve form posts directly to inquiry_change_status with status=resolved
This commit is contained in:
parent
b0d77c21e5
commit
c8db98880a
@ -70,3 +70,79 @@ class TestInquiryDepartmentResponsePermission(TestCase):
|
||||
)
|
||||
assert response.status_code == 302, \
|
||||
f"dept_manager of other dept should be redirected (denied), got {response.status_code}"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestInquiryDetailUIRealignment(TestCase):
|
||||
"""The inquiry detail page no longer surfaces the PX-side 'Response to Patient'
|
||||
action. Resolution happens via direct POST to inquiry_change_status.
|
||||
|
||||
Locks in Task 5 of the workflow realignment: department owns resolution.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.hospital = Hospital.objects.create(name="Test Hospital UI", code="TH03")
|
||||
self.dept = Department.objects.create(name="Dept UI", hospital=self.hospital, code="UI")
|
||||
|
||||
_ensure_group("PX Admin")
|
||||
self.px_admin = User.objects.create_user(
|
||||
username="pxui", email="pxui@test", password="x", hospital=self.hospital
|
||||
)
|
||||
self.px_admin.groups.add(Group.objects.get(name="PX Admin"))
|
||||
|
||||
def _make_inquiry(self, **kwargs):
|
||||
defaults = dict(
|
||||
hospital=self.hospital,
|
||||
department=self.dept,
|
||||
subject="UI test",
|
||||
message="msg",
|
||||
status="in_progress",
|
||||
activated_at=kwargs.pop("activated_at", __import__("django.utils.timezone", fromlist=["now"]).now()),
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return Inquiry.objects.create(**defaults)
|
||||
|
||||
def test_response_to_patient_button_is_gone(self):
|
||||
"""The 'Response to Patient' tile button must not appear anywhere on the page."""
|
||||
inquiry = self._make_inquiry()
|
||||
self.client.force_login(self.px_admin)
|
||||
# PX admins are redirected to select-hospital unless the session has a
|
||||
# selected_hospital_id (HospitalSelectionMiddleware). Set it directly.
|
||||
session = self.client.session
|
||||
session["selected_hospital_id"] = str(self.hospital.id)
|
||||
session.save()
|
||||
response = self.client.get(reverse("inquiries:inquiry_detail", kwargs={"pk": inquiry.pk}))
|
||||
assert response.status_code == 200
|
||||
# The PX-side respond modal and its trigger are removed. The button label
|
||||
# may still appear inside {% comment %} blocks (inert), but the live
|
||||
# onclick="showRespondModal()" button in the actions tile is gone.
|
||||
content = response.content.decode()
|
||||
# showRespondModal JS function definition must be gone.
|
||||
assert "function showRespondModal" not in content, \
|
||||
"showRespondModal JS function should be removed"
|
||||
# The respondModal div must be gone.
|
||||
assert 'id="respondModal"' not in content, \
|
||||
"respondModal div should be removed"
|
||||
|
||||
def test_resolve_uses_direct_status_change_form(self):
|
||||
"""The Resolve button POSTs directly to inquiry_change_status (not a modal)."""
|
||||
from django.utils import timezone
|
||||
# Department has responded → "Department has responded" banner shows Resolve.
|
||||
inquiry = self._make_inquiry(
|
||||
sent_to_department=True,
|
||||
department_responded_at=timezone.now(),
|
||||
)
|
||||
self.client.force_login(self.px_admin)
|
||||
session = self.client.session
|
||||
session["selected_hospital_id"] = str(self.hospital.id)
|
||||
session.save()
|
||||
response = self.client.get(reverse("inquiries:inquiry_detail", kwargs={"pk": inquiry.pk}))
|
||||
assert response.status_code == 200
|
||||
content = response.content.decode()
|
||||
change_status_url = reverse("inquiries:inquiry_change_status", kwargs={"pk": inquiry.pk})
|
||||
# The Resolve button must be a real form posting to inquiry_change_status
|
||||
# with status=resolved — not a showRespondModal() trigger.
|
||||
assert f'action="{change_status_url}"' in content, \
|
||||
"Resolve must POST to inquiry_change_status"
|
||||
assert 'name="status" value="resolved"' in content, \
|
||||
"Resolve form must include hidden status=resolved input"
|
||||
|
||||
@ -213,7 +213,11 @@
|
||||
<button type="button" onclick="showRespondModal()" class="px-4 py-2 bg-green-600 text-white rounded-lg font-bold text-sm hover:bg-green-700 transition whitespace-nowrap">{% trans "Resolve" %}</button>
|
||||
</form> {% endcomment %}
|
||||
{% if can_respond and inquiry.activated_at %}
|
||||
<button type="button" onclick="showRespondModal()" class="px-4 py-2 bg-green-600 text-white rounded-lg font-bold text-sm hover:bg-green-700 transition whitespace-nowrap">{% trans "Resolve" %}</button>
|
||||
<form method="post" action="{% url 'inquiries:inquiry_change_status' inquiry.pk %}" class="inline shrink-0"
|
||||
onsubmit="return confirm('{% trans "Mark this inquiry as resolved?" %}')">
|
||||
{% csrf_token %}<input type="hidden" name="status" value="resolved">
|
||||
<button type="submit" class="px-4 py-2 bg-green-600 text-white rounded-lg font-bold text-sm hover:bg-green-700 transition whitespace-nowrap">{% trans "Resolve" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -623,10 +627,6 @@
|
||||
<i data-lucide="send" class="w-5 h-5 text-indigo-600"></i>
|
||||
<span class="text-[10px] font-bold text-indigo-700 uppercase">{% trans "Send to Department" %}</span>
|
||||
</button>
|
||||
<button onclick="showRespondModal()" class="col-span-2 p-3 border-navy bg-navy text-white rounded-xl hover:bg-blue transition flex items-center justify-center gap-2 group">
|
||||
<i data-lucide="message-square" class="w-5 h-5"></i>
|
||||
<span class="text-[10px] font-bold uppercase">{% trans "Response to Patient" %}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
@ -702,84 +702,7 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Respond Modal -->
|
||||
<div id="respondModal" style="display:none" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<div class="bg-white rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-y-auto">
|
||||
<div class="p-6 border-b border-slate-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-xl font-bold text-navy flex items-center gap-2">
|
||||
<i data-lucide="message-square" class="w-5 h-5"></i>
|
||||
{% trans "Response to Patient" %}
|
||||
</h3>
|
||||
<button type="button" onclick="closeModal('respondModal')" class="text-slate-400 hover:text-slate-600 transition">
|
||||
<i data-lucide="x" class="w-5 h-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<form method="post" action="{% url 'inquiries:inquiry_respond' inquiry.pk %}" id="respondForm">
|
||||
{% csrf_token %}
|
||||
<div class="p-6">
|
||||
{% if inquiry.short_description_en or inquiry.short_description_ar or inquiry.ai_brief_en or inquiry.ai_brief_ar %}
|
||||
<div class="bg-light/50 border border-slate-200 rounded-2xl p-4 mb-5">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i data-lucide="sparkles" class="w-4 h-4 text-navy"></i>
|
||||
<span class="text-sm font-bold text-navy">{% trans "AI Summary" %}</span>
|
||||
</div>
|
||||
{% if inquiry.short_description_en or inquiry.ai_brief_en %}
|
||||
<p class="text-sm text-slate-700 leading-relaxed">{% if inquiry.short_description_en %}{{ inquiry.short_description_en }}{% elif inquiry.ai_brief_en %}{{ inquiry.ai_brief_en }}{% endif %}</p>
|
||||
{% endif %}
|
||||
{% if inquiry.short_description_ar or inquiry.ai_brief_ar %}
|
||||
<p class="text-sm text-slate-700 leading-relaxed mt-2" dir="rtl">{% if inquiry.short_description_ar %}{{ inquiry.short_description_ar }}{% elif inquiry.ai_brief_ar %}{{ inquiry.ai_brief_ar }}{% endif %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mb-5">
|
||||
<button type="button" id="generateAiBtn" onclick="generateAIResponse()" class="w-full inline-flex items-center justify-center gap-2 px-4 py-3 bg-navy text-white rounded-xl font-bold hover:bg-blue transition text-sm shadow-lg">
|
||||
<i data-lucide="sparkles" class="w-4 h-4"></i>
|
||||
{% trans "Generate AI Response" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="aiSuggestions" class="hidden mb-5 space-y-3">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<i data-lucide="sparkles" class="w-4 h-4 text-navy"></i>
|
||||
<span class="text-sm font-semibold text-navy">{% trans "AI Generated Response (click to use)" %}</span>
|
||||
</div>
|
||||
<div id="aiSuggestionEn" onclick="useAISuggestion('en')" class="ai-suggestion-card">
|
||||
<p class="text-[10px] font-bold text-slate uppercase mb-1">English</p>
|
||||
<p class="text-sm text-slate-700" id="aiSuggestionEnText"></p>
|
||||
</div>
|
||||
<div id="aiSuggestionAr" onclick="useAISuggestion('ar')" class="ai-suggestion-card" dir="rtl" style="text-align: right;">
|
||||
<p class="text-[10px] font-bold text-slate uppercase mb-1">العربية</p>
|
||||
<p class="text-sm text-slate-700" id="aiSuggestionArText"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-semibold text-navy mb-2">{% trans "Your Response" %} <span class="text-red-500">*</span></label>
|
||||
<textarea name="response" id="responseText" rows="8"
|
||||
class="w-full px-4 py-3 border-2 border-slate-200 rounded-xl focus:outline-none focus:border-navy focus:ring-2 focus:ring-navy/20 resize-none text-sm"
|
||||
placeholder="{% trans 'Enter your response...' %}" required>{{ inquiry.response|default:'' }}</textarea>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-slate-400 mt-1">
|
||||
<i data-lucide="info" class="w-3 h-3 inline mr-1"></i>
|
||||
{% trans "The response will be sent to the inquirer via SMS and Email." %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-6 border-t border-slate-200 flex gap-3">
|
||||
<button type="submit" class="flex-1 px-4 py-2.5 bg-navy text-white rounded-xl font-semibold hover:bg-navy/90 transition text-sm inline-flex items-center justify-center gap-2">
|
||||
<i data-lucide="send" class="w-4 h-4"></i>
|
||||
{% trans "Response to Patient" %}
|
||||
</button>
|
||||
<button type="button" onclick="closeModal('respondModal')" class="px-4 py-2.5 bg-white border-2 border-slate-200 rounded-xl font-semibold text-slate-600 hover:bg-slate-50 transition text-sm">
|
||||
{% trans "Cancel" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Respond Modal (removed per workflow realignment: department owns resolution) -->
|
||||
|
||||
<!-- Assign Modal -->
|
||||
<div id="assignModal" style="display:none" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
@ -989,10 +912,6 @@ function closeModal(id) {
|
||||
document.getElementById(id).style.display = 'none';
|
||||
}
|
||||
|
||||
function showRespondModal() {
|
||||
document.getElementById('respondModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function showAssignModal() {
|
||||
document.getElementById('assignModal').style.display = 'flex';
|
||||
}
|
||||
@ -1072,52 +991,6 @@ function loadDeptContactsInline(deptId) {
|
||||
});
|
||||
}
|
||||
|
||||
function generateAIResponse() {
|
||||
const btn = document.getElementById('generateAiBtn');
|
||||
const suggestionsDiv = document.getElementById('aiSuggestions');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<span class="spinner"></span> {% trans "Generating..." %}';
|
||||
suggestionsDiv.classList.add('hidden');
|
||||
fetch('/complaints/api/inquiries/{{ inquiry.pk }}/generate_ai_response/', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': getCSRFToken() },
|
||||
credentials: 'same-origin'
|
||||
})
|
||||
.then(response => { if (!response.ok) return response.json().then(data => { throw new Error(data.error || 'Request failed'); }); return response.json(); })
|
||||
.then(data => {
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i data-lucide="sparkles" class="w-4 h-4"></i> {% trans "Generate AI Response" %}';
|
||||
lucide.createIcons();
|
||||
if (data.success) {
|
||||
document.getElementById('aiSuggestionEnText').textContent = data.response_en;
|
||||
document.getElementById('aiSuggestionArText').textContent = data.response_ar;
|
||||
suggestionsDiv.classList.remove('hidden');
|
||||
} else { alert(data.error || '{% trans "Failed to generate response" %}'); }
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i data-lucide="sparkles" class="w-4 h-4"></i> {% trans "Generate AI Response" %}';
|
||||
lucide.createIcons();
|
||||
alert(error.message || '{% trans "An error occurred while generating response" %}');
|
||||
});
|
||||
}
|
||||
|
||||
function useAISuggestion(lang) {
|
||||
var text = '';
|
||||
var card = null;
|
||||
if (lang === 'en') {
|
||||
text = document.getElementById('aiSuggestionEnText').textContent;
|
||||
card = document.getElementById('aiSuggestionEn');
|
||||
} else {
|
||||
text = document.getElementById('aiSuggestionArText').textContent;
|
||||
card = document.getElementById('aiSuggestionAr');
|
||||
}
|
||||
document.getElementById('responseText').value = text;
|
||||
card.classList.add('selected');
|
||||
setTimeout(() => card.classList.remove('selected'), 1500);
|
||||
}
|
||||
|
||||
function reanalyzeAI() {
|
||||
const btn = document.getElementById('reanalyzeBtn');
|
||||
const content = document.getElementById('aiAnalysisContent');
|
||||
@ -1163,7 +1036,6 @@ function escapeHtml(text) { const div = document.createElement('div'); div.textC
|
||||
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') {
|
||||
closeModal('respondModal');
|
||||
closeModal('assignModal');
|
||||
closeModal('sendToDeptModal');
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user