433 lines
22 KiB
HTML
433 lines
22 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load i18n %}
|
|
{% load static %}
|
|
|
|
{% block title %}{{ observation.tracking_code }} - {% trans "Observation Detail" %} - PX360{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.timeline {
|
|
position: relative;
|
|
padding-left: 30px;
|
|
}
|
|
.timeline::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 8px;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 2px;
|
|
background: #e2e8f0;
|
|
}
|
|
.timeline-item {
|
|
position: relative;
|
|
padding-bottom: 24px;
|
|
}
|
|
.timeline-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: -26px;
|
|
top: 5px;
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 50%;
|
|
background: #fff;
|
|
border: 3px solid #005696;
|
|
z-index: 1;
|
|
}
|
|
.timeline-item.status_change::before { border-color: #f97316; }
|
|
.timeline-item.note::before { border-color: #22c55e; }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<!-- Back Link -->
|
|
<div class="mb-6">
|
|
<a href="{% url 'observations:observation_list' %}" class="inline-flex items-center gap-2 text-slate hover:text-navy transition font-medium">
|
|
<i data-lucide="arrow-left" class="w-4 h-4"></i> {% trans "Observations" %}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Observation Header -->
|
|
<div class="bg-white rounded-2xl shadow-sm border border-slate-100 p-6 mb-6">
|
|
<div class="flex flex-col lg:flex-row lg:items-start lg:justify-between gap-4">
|
|
<div class="flex-1">
|
|
<div class="flex flex-wrap items-center gap-3 mb-3">
|
|
<span class="font-mono text-lg font-bold text-blue tracking-wide">{{ observation.tracking_code }}</span>
|
|
<span class="px-2.5 py-1 rounded-full text-[10px] font-bold uppercase
|
|
{% if observation.status == 'new' %}bg-yellow-100 text-yellow-700
|
|
{% elif observation.status == 'triaged' %}bg-sky-100 text-sky-700
|
|
{% elif observation.status == 'assigned' %}bg-indigo-100 text-indigo-700
|
|
{% elif observation.status == 'in_progress' %}bg-blue-100 text-blue-700
|
|
{% elif observation.status == 'resolved' %}bg-green-100 text-green-700
|
|
{% elif observation.status == 'closed' %}bg-slate-100 text-slate-600
|
|
{% elif observation.status == 'rejected' %}bg-red-100 text-red-700
|
|
{% elif observation.status == 'duplicate' %}bg-slate-100 text-slate-600
|
|
{% else %}bg-slate-100 text-slate-600{% endif %}">
|
|
{{ observation.get_status_display }}
|
|
</span>
|
|
<span class="px-2.5 py-1 rounded-full text-[10px] font-bold uppercase
|
|
{% if observation.severity == 'low' %}bg-green-100 text-green-700
|
|
{% elif observation.severity == 'medium' %}bg-yellow-100 text-yellow-700
|
|
{% elif observation.severity == 'high' %}bg-orange-100 text-orange-700
|
|
{% elif observation.severity == 'critical' %}bg-red-500 text-white
|
|
{% else %}bg-slate-100 text-slate-600{% endif %}">
|
|
{{ observation.get_severity_display }}
|
|
</span>
|
|
{% if observation.is_anonymous %}
|
|
<span class="px-2.5 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-500 italic">
|
|
<i data-lucide="eye-off" class="w-3 h-3 inline me-1"></i>{% trans "Anonymous" %}
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
<h1 class="text-2xl font-bold text-navy">
|
|
{% if observation.title %}
|
|
{{ observation.title }}
|
|
{% else %}
|
|
{{ observation.description|truncatewords:10 }}
|
|
{% endif %}
|
|
</h1>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-3">
|
|
{% if can_convert and not observation.action_id %}
|
|
<a href="{% url 'observations:observation_convert_to_action' observation.id %}"
|
|
class="bg-green-500 text-white px-4 py-2.5 rounded-xl text-sm font-semibold hover:bg-green-600 transition inline-flex items-center gap-2">
|
|
<i data-lucide="arrow-right-circle" class="w-4 h-4"></i> {% trans "Convert to Action" %}
|
|
</a>
|
|
{% endif %}
|
|
<a href="{% url 'rca:rca_create' %}?related_model=observation&related_id={{ observation.pk }}"
|
|
class="bg-purple-600 text-white px-4 py-2.5 rounded-xl text-sm font-semibold hover:bg-purple-700 transition inline-flex items-center gap-2">
|
|
<i data-lucide="search" class="w-4 h-4"></i> {% trans "Initiate RCA" %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex flex-col lg:flex-row gap-6">
|
|
<!-- Main Content -->
|
|
<div class="flex-1 space-y-6">
|
|
<!-- Description Card -->
|
|
<section class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
|
|
<h3 class="font-bold text-navy mb-4 flex items-center gap-2">
|
|
<i data-lucide="file-text" class="w-5 h-5 text-blue"></i>
|
|
{% trans "Description" %}
|
|
</h3>
|
|
<div class="bg-light/40 p-4 rounded-xl border-l-4 border-blue">
|
|
<p class="text-sm leading-relaxed text-slate" style="white-space: pre-wrap;">{{ observation.description }}</p>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- AI Analysis Panel -->
|
|
{% include "observations/partials/ai_panel.html" %}
|
|
|
|
<!-- Details Card -->
|
|
<section class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
|
|
<h3 class="font-bold text-navy mb-4 flex items-center gap-2">
|
|
<i data-lucide="info" class="w-5 h-5 text-blue"></i>
|
|
{% trans "Details" %}
|
|
</h3>
|
|
<div class="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<p class="text-xs font-semibold text-slate uppercase tracking-wide mb-1">{% trans "Category" %}</p>
|
|
<p class="text-sm font-medium text-gray-800">{{ observation.category.get_localized_name|default:_("Not specified") }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-slate uppercase tracking-wide mb-1">{% trans "Location" %}</p>
|
|
<p class="text-sm font-medium text-gray-800">{{ observation.location_text|default:_("Not specified") }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-slate uppercase tracking-wide mb-1">{% trans "Incident Date/Time" %}</p>
|
|
<p class="text-sm font-medium text-gray-800">{{ observation.incident_datetime|date:"M d, Y H:i" }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-slate uppercase tracking-wide mb-1">{% trans "Submitted" %}</p>
|
|
<p class="text-sm font-medium text-gray-800">{{ observation.created_at|date:"M d, Y H:i" }}</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs font-semibold text-slate uppercase tracking-wide mb-1">{% trans "Last Updated" %}</p>
|
|
<p class="text-sm font-medium text-gray-800">{{ observation.updated_at|date:"M d, Y H:i" }}</p>
|
|
</div>
|
|
{% if observation.triaged_at %}
|
|
<div>
|
|
<p class="text-xs font-semibold text-slate uppercase tracking-wide mb-1">{% trans "Triaged" %}</p>
|
|
<p class="text-sm font-medium text-gray-800">
|
|
{{ observation.triaged_at|date:"M d, Y H:i" }}
|
|
{% if observation.triaged_by %}({{ observation.triaged_by.get_full_name }}){% endif %}
|
|
</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Reporter Information -->
|
|
<section class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
|
|
<h3 class="font-bold text-navy mb-4 flex items-center gap-2">
|
|
<i data-lucide="user" class="w-5 h-5 text-blue"></i>
|
|
{% trans "Reporter Information" %}
|
|
</h3>
|
|
{% if observation.is_anonymous %}
|
|
<div class="text-center py-4">
|
|
<div class="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-3">
|
|
<i data-lucide="eye-off" class="w-6 h-6 text-gray-400"></i>
|
|
</div>
|
|
<p class="text-slate text-sm">{% trans "This observation was submitted anonymously" %}</p>
|
|
</div>
|
|
{% else %}
|
|
<div class="grid grid-cols-2 gap-4">
|
|
{% if observation.reporter_staff_id %}
|
|
<div>
|
|
<p class="text-xs font-semibold text-slate uppercase tracking-wide mb-1">{% trans "Staff ID" %}</p>
|
|
<p class="text-sm font-medium text-gray-800">{{ observation.reporter_staff_id }}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if observation.reporter_name %}
|
|
<div>
|
|
<p class="text-xs font-semibold text-slate uppercase tracking-wide mb-1">{% trans "Name" %}</p>
|
|
<p class="text-sm font-medium text-gray-800">{{ observation.reporter_name }}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if observation.reporter_phone %}
|
|
<div>
|
|
<p class="text-xs font-semibold text-slate uppercase tracking-wide mb-1">{% trans "Phone" %}</p>
|
|
<p class="text-sm font-medium text-gray-800">{{ observation.reporter_phone }}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if observation.reporter_email %}
|
|
<div>
|
|
<p class="text-xs font-semibold text-slate uppercase tracking-wide mb-1">{% trans "Email" %}</p>
|
|
<p class="text-sm font-medium text-gray-800">{{ observation.reporter_email }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
|
|
<!-- Attachments -->
|
|
{% if attachments %}
|
|
<section class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
|
|
<h3 class="font-bold text-navy mb-4 flex items-center gap-2">
|
|
<i data-lucide="paperclip" class="w-5 h-5 text-blue"></i>
|
|
{% trans "Attachments" %} ({{ attachments.count }})
|
|
</h3>
|
|
<div class="space-y-3">
|
|
{% for attachment in attachments %}
|
|
<div class="flex items-center justify-between p-4 bg-slate-50 border border-slate-100 rounded-xl">
|
|
<div class="flex items-center gap-3">
|
|
<i data-lucide="file" class="w-5 h-5 text-blue"></i>
|
|
<div>
|
|
<div class="font-semibold text-gray-800">{{ attachment.filename }}</div>
|
|
<div class="text-xs text-slate">{{ attachment.file_type }} - {{ attachment.file_size|filesizeformat }}</div>
|
|
</div>
|
|
</div>
|
|
<a href="{{ attachment.file.url }}" target="_blank" class="p-2 rounded-lg bg-navy text-white hover:bg-blue transition">
|
|
<i data-lucide="download" class="w-4 h-4"></i>
|
|
</a>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
|
|
<!-- Timeline -->
|
|
<section class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
|
|
<h3 class="font-bold text-navy mb-4 flex items-center gap-2">
|
|
<i data-lucide="clock" class="w-5 h-5 text-blue"></i>
|
|
{% trans "Timeline" %}
|
|
</h3>
|
|
{% if timeline %}
|
|
<div class="timeline">
|
|
{% for item in timeline %}
|
|
<div class="timeline-item {{ item.type }}">
|
|
<div class="bg-slate-50 rounded-xl p-4 border border-slate-100">
|
|
<div class="flex justify-between items-start mb-2">
|
|
<div class="flex items-center gap-2">
|
|
<span class="px-2.5 py-1 rounded-lg text-xs font-semibold bg-navy text-white">
|
|
{% if item.type == 'status_change' %}{% trans "Status Changed" %}
|
|
{% elif item.type == 'note' %}{% trans "Note" %}{% endif %}
|
|
</span>
|
|
{% if item.item.created_by %}
|
|
<span class="text-xs text-slate">{% trans "by" %} {{ item.item.created_by.get_full_name }}</span>
|
|
{% elif item.item.changed_by %}
|
|
<span class="text-xs text-slate">{% trans "by" %} {{ item.item.changed_by.get_full_name }}</span>
|
|
{% endif %}
|
|
</div>
|
|
<span class="text-xs text-slate">{{ item.created_at|date:"M d, Y H:i" }}</span>
|
|
</div>
|
|
{% if item.type == 'status_change' %}
|
|
<div class="flex items-center gap-2">
|
|
{% if item.item.from_status %}
|
|
<span class="px-2 py-0.5 rounded text-xs font-medium bg-slate-100 text-slate-600">{{ item.item.from_status }}</span>
|
|
<i data-lucide="arrow-right" class="w-3 h-3 text-slate"></i>
|
|
{% endif %}
|
|
<span class="px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-600">{{ item.item.to_status }}</span>
|
|
</div>
|
|
{% elif item.type == 'note' %}
|
|
<p class="text-gray-700 text-sm mt-1">{{ item.item.note }}</p>
|
|
{% endif %}
|
|
{% if item.item.comment %}
|
|
<p class="text-gray-600 text-sm mt-2">{{ item.item.comment }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% else %}
|
|
<div class="text-center py-8">
|
|
<i data-lucide="clock" class="w-12 h-12 mx-auto mb-3 text-gray-300"></i>
|
|
<p class="text-slate text-sm">{% trans "No timeline entries yet" %}</p>
|
|
</div>
|
|
{% endif %}
|
|
</section>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="lg:w-80 space-y-4">
|
|
<!-- Assignment Info -->
|
|
<section class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
|
|
<div class="px-4 py-3 border-b border-slate-100">
|
|
<h3 class="font-bold text-navy flex items-center gap-2">
|
|
<i data-lucide="users" class="w-4 h-4"></i> {% trans "Assignment" %}
|
|
</h3>
|
|
</div>
|
|
<div class="p-4 space-y-4">
|
|
<div>
|
|
<div class="text-xs font-semibold text-slate uppercase tracking-wide mb-1">{% trans "Department" %}</div>
|
|
<div class="text-sm font-medium text-gray-800">{{ observation.assigned_department.name|default:_("Not assigned") }}</div>
|
|
</div>
|
|
<div>
|
|
<div class="text-xs font-semibold text-slate uppercase tracking-wide mb-1">{% trans "Assigned To" %}</div>
|
|
<div class="text-sm font-medium text-gray-800">{{ observation.assigned_to.get_full_name|default:_("Not assigned") }}</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Linked Action -->
|
|
{% if px_action %}
|
|
<section class="bg-navy rounded-2xl p-5 shadow-lg text-white">
|
|
<h3 class="font-bold flex items-center gap-2 mb-3">
|
|
<i data-lucide="link" class="w-4 h-4"></i> {% trans "Linked PX Action" %}
|
|
</h3>
|
|
<p class="text-sm opacity-90 mb-3">{{ px_action.title|truncatewords:10 }}</p>
|
|
<a href="{% url 'actions:action_detail' px_action.id %}" class="inline-flex items-center gap-2 px-4 py-2 bg-white text-navy rounded-xl font-semibold text-sm hover:bg-gray-100 transition">
|
|
<i data-lucide="arrow-right" class="w-4 h-4"></i> {% trans "View Action" %}
|
|
</a>
|
|
</section>
|
|
{% endif %}
|
|
|
|
<!-- RCA Section -->
|
|
{% if linked_rcas %}
|
|
<section class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
|
|
<div class="px-4 py-3 border-b border-slate-100 bg-purple-50">
|
|
<h3 class="font-bold text-purple-700 flex items-center gap-2">
|
|
<i data-lucide="search" class="w-4 h-4"></i> {% trans "Root Cause Analyses" %}
|
|
</h3>
|
|
</div>
|
|
<div class="p-4 space-y-3">
|
|
{% for rca in linked_rcas %}
|
|
<a href="{% url 'rca:rca_detail' pk=rca.pk %}" class="block p-3 rounded-lg border border-slate-200 hover:border-purple-300 transition group">
|
|
<p class="text-sm font-bold text-navy group-hover:text-purple-700 truncate">{{ rca.title }}</p>
|
|
<div class="flex items-center gap-2 mt-1">
|
|
<span class="px-2 py-0.5 rounded-full text-[10px] font-bold uppercase
|
|
{% if rca.status == 'draft' %}bg-slate-100 text-slate-600
|
|
{% elif rca.status == 'in_progress' %}bg-blue-100 text-blue-700
|
|
{% elif rca.status == 'review' %}bg-yellow-100 text-yellow-700
|
|
{% elif rca.status == 'approved' %}bg-green-100 text-green-700
|
|
{% elif rca.status == 'closed' %}bg-gray-100 text-gray-600{% endif %}">
|
|
{{ rca.get_status_display }}
|
|
</span>
|
|
<span class="text-[10px] text-slate">{{ rca.created_at|date:"M d, Y" }}</span>
|
|
</div>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
|
|
<!-- Triage Form -->
|
|
{% if can_triage %}
|
|
<section class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
|
|
<div class="px-4 py-3 bg-navy text-white">
|
|
<h3 class="font-bold flex items-center gap-2">
|
|
<i data-lucide="sliders-horizontal" class="w-4 h-4"></i> {% trans "Triage" %}
|
|
</h3>
|
|
</div>
|
|
<div class="p-4">
|
|
<form method="post" action="{% url 'observations:observation_triage' observation.id %}">
|
|
{% csrf_token %}
|
|
<div class="mb-3">
|
|
<label class="block text-xs font-semibold text-slate uppercase tracking-wide mb-2">{% trans "Department" %}</label>
|
|
{{ triage_form.assigned_department }}
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="block text-xs font-semibold text-slate uppercase tracking-wide mb-2">{% trans "Assign To" %}</label>
|
|
{{ triage_form.assigned_to }}
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="block text-xs font-semibold text-slate uppercase tracking-wide mb-2">{% trans "Status" %}</label>
|
|
{{ triage_form.status }}
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="block text-xs font-semibold text-slate uppercase tracking-wide mb-2">{% trans "Note" %}</label>
|
|
{{ triage_form.note }}
|
|
</div>
|
|
<button type="submit" class="w-full bg-navy text-white px-4 py-2.5 rounded-xl font-semibold hover:bg-blue transition flex items-center justify-center gap-2">
|
|
<i data-lucide="check-circle" class="w-4 h-4"></i> {% trans "Update" %}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
|
|
<!-- Add Note -->
|
|
<section class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
|
|
<div class="px-4 py-3 bg-green-500 text-white">
|
|
<h3 class="font-bold flex items-center gap-2">
|
|
<i data-lucide="message-square" class="w-4 h-4"></i> {% trans "Add Note" %}
|
|
</h3>
|
|
</div>
|
|
<div class="p-4">
|
|
<form method="post" action="{% url 'observations:observation_add_note' observation.id %}">
|
|
{% csrf_token %}
|
|
<div class="mb-3">
|
|
{{ note_form.note }}
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
{{ note_form.is_internal }}
|
|
<span class="text-xs font-medium text-slate">{% trans "Internal note (not visible to public)" %}</span>
|
|
</label>
|
|
</div>
|
|
<button type="submit" class="w-full bg-green-500 text-white px-4 py-2.5 rounded-xl font-semibold hover:bg-green-600 transition flex items-center justify-center gap-2">
|
|
<i data-lucide="plus-circle" class="w-4 h-4"></i> {% trans "Add Note" %}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
|
|
<!-- Quick Status Change -->
|
|
<section class="bg-white rounded-2xl shadow-sm border border-slate-100 overflow-hidden">
|
|
<div class="px-4 py-3 bg-orange-500 text-white">
|
|
<h3 class="font-bold flex items-center gap-2">
|
|
<i data-lucide="refresh-cw" class="w-4 h-4"></i> {% trans "Quick Status Change" %}
|
|
</h3>
|
|
</div>
|
|
<div class="p-4">
|
|
<form method="post" action="{% url 'observations:observation_change_status' observation.id %}">
|
|
{% csrf_token %}
|
|
<div class="mb-3">
|
|
{{ status_form.status }}
|
|
</div>
|
|
<div class="mb-3">
|
|
{{ status_form.comment }}
|
|
</div>
|
|
<button type="submit" class="w-full bg-orange-500 text-white px-4 py-2.5 rounded-xl font-semibold hover:bg-orange-600 transition flex items-center justify-center gap-2">
|
|
<i data-lucide="check" class="w-4 h-4"></i> {% trans "Change Status" %}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|