1341 lines
42 KiB
HTML
1341 lines
42 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}{{ finding.title }} - Quality Finding{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
|
<style>
|
|
.finding-header {
|
|
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
|
color: white;
|
|
border-radius: 0.5rem;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.finding-layout {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr;
|
|
gap: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.finding-main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2rem;
|
|
}
|
|
|
|
.finding-sidebar {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.section-card {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.section-header {
|
|
background: #f8f9fa;
|
|
border-bottom: 1px solid #dee2e6;
|
|
padding: 1rem 1.5rem;
|
|
font-weight: 600;
|
|
color: #495057;
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
}
|
|
|
|
.section-content {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.severity-badge {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 0.375rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.severity-critical { background: #f8d7da; color: #721c24; }
|
|
.severity-high { background: #ffebee; color: #d32f2f; }
|
|
.severity-medium { background: #fff3e0; color: #f57c00; }
|
|
.severity-low { background: #e8f5e8; color: #2e7d32; }
|
|
|
|
.status-badge {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 0.375rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.status-open { background: #ffebee; color: #d32f2f; }
|
|
.status-investigating { background: #fff3e0; color: #f57c00; }
|
|
.status-resolved { background: #e8f5e8; color: #2e7d32; }
|
|
.status-closed { background: #e9ecef; color: #6c757d; }
|
|
|
|
.category-badge {
|
|
padding: 0.375rem 0.75rem;
|
|
border-radius: 0.375rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.category-clinical { background: #e3f2fd; color: #1976d2; }
|
|
.category-safety { background: #ffebee; color: #d32f2f; }
|
|
.category-compliance { background: #f3e5f5; color: #7b1fa2; }
|
|
.category-process { background: #e8f5e8; color: #2e7d32; }
|
|
.category-documentation { background: #fff3e0; color: #f57c00; }
|
|
|
|
.progress-container {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.progress-header {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.progress-label {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.progress-percentage {
|
|
font-size: 1.25rem;
|
|
font-weight: bold;
|
|
color: #dc3545;
|
|
}
|
|
|
|
.progress-bar-container {
|
|
background: #e9ecef;
|
|
border-radius: 0.5rem;
|
|
height: 12px;
|
|
overflow: hidden;
|
|
position: relative;
|
|
}
|
|
|
|
.progress-bar {
|
|
background: linear-gradient(90deg, #dc3545, #c82333);
|
|
height: 100%;
|
|
transition: width 0.3s ease;
|
|
border-radius: 0.5rem;
|
|
}
|
|
|
|
.progress-milestones {
|
|
display: flex;
|
|
justify-content: between;
|
|
margin-top: 0.5rem;
|
|
}
|
|
|
|
.milestone {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
text-align: center;
|
|
flex: 1;
|
|
}
|
|
|
|
.milestone.completed {
|
|
color: #28a745;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.milestone.active {
|
|
color: #dc3545;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.info-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1rem;
|
|
}
|
|
|
|
.info-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.info-label {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.info-value {
|
|
font-size: 0.875rem;
|
|
color: #495057;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.timeline-container {
|
|
position: relative;
|
|
}
|
|
|
|
.timeline-item {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
position: relative;
|
|
}
|
|
|
|
.timeline-item:not(:last-child)::after {
|
|
content: '';
|
|
position: absolute;
|
|
left: 15px;
|
|
top: 40px;
|
|
bottom: -24px;
|
|
width: 2px;
|
|
background: #dee2e6;
|
|
}
|
|
|
|
.timeline-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: #dc3545;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.875rem;
|
|
flex-shrink: 0;
|
|
z-index: 1;
|
|
position: relative;
|
|
}
|
|
|
|
.timeline-icon.completed {
|
|
background: #28a745;
|
|
}
|
|
|
|
.timeline-icon.pending {
|
|
background: #6c757d;
|
|
}
|
|
|
|
.timeline-content {
|
|
flex: 1;
|
|
padding-top: 0.25rem;
|
|
}
|
|
|
|
.timeline-title {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.timeline-description {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.timeline-meta {
|
|
display: flex;
|
|
gap: 1rem;
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.action-item {
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.action-item:hover {
|
|
border-color: #dc3545;
|
|
}
|
|
|
|
.action-header {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: start;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.action-title {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.action-status {
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.action-pending { background: #fff3e0; color: #f57c00; }
|
|
.action-progress { background: #e3f2fd; color: #1976d2; }
|
|
.action-completed { background: #e8f5e8; color: #2e7d32; }
|
|
|
|
.action-description {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.action-meta {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.activity-item {
|
|
display: flex;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem 0;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
}
|
|
|
|
.activity-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.activity-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: #dc3545;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.activity-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.activity-text {
|
|
font-size: 0.875rem;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.activity-meta {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.document-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
margin-bottom: 0.75rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.document-item:hover {
|
|
border-color: #dc3545;
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.document-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 0.375rem;
|
|
background: #dc3545;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.document-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.document-name {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.document-meta {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn-action {
|
|
padding: 0.5rem 1rem;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
background: white;
|
|
color: #495057;
|
|
text-decoration: none;
|
|
transition: all 0.2s;
|
|
font-size: 0.875rem;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.btn-action:hover {
|
|
border-color: #dc3545;
|
|
color: #dc3545;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn-primary-action {
|
|
background: #dc3545;
|
|
border-color: #dc3545;
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary-action:hover {
|
|
background: #c82333;
|
|
border-color: #c82333;
|
|
color: white;
|
|
}
|
|
|
|
.stats-mini {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 1rem;
|
|
}
|
|
|
|
.stat-mini {
|
|
text-align: center;
|
|
padding: 1rem;
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
}
|
|
|
|
.stat-mini-number {
|
|
font-size: 1.5rem;
|
|
font-weight: bold;
|
|
color: #dc3545;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.stat-mini-label {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.risk-assessment {
|
|
border-left: 4px solid #dc3545;
|
|
background: #fff5f5;
|
|
padding: 1rem;
|
|
border-radius: 0 0.375rem 0.375rem 0;
|
|
}
|
|
|
|
.risk-level {
|
|
font-weight: 600;
|
|
color: #dc3545;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.risk-factors {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.risk-factors li {
|
|
padding: 0.25rem 0;
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.risk-factors li::before {
|
|
content: '•';
|
|
color: #dc3545;
|
|
margin-right: 0.5rem;
|
|
}
|
|
|
|
.comment-item {
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
background: white;
|
|
}
|
|
|
|
.comment-header {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.comment-author {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.comment-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: #dc3545;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.comment-name {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.comment-date {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.comment-text {
|
|
font-size: 0.875rem;
|
|
color: #495057;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
@media (max-width: 1200px) {
|
|
.finding-layout {
|
|
grid-template-columns: 1fr;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.finding-sidebar {
|
|
order: -1;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.finding-header {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.info-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.action-buttons {
|
|
flex-direction: column;
|
|
}
|
|
|
|
.stats-mini {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
@media print {
|
|
.finding-sidebar, .action-buttons {
|
|
display: none !important;
|
|
}
|
|
|
|
.finding-layout {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.section-header {
|
|
background: none;
|
|
border-bottom: 2px solid #000;
|
|
color: #000;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="content" class="app-content">
|
|
<!-- Page Header -->
|
|
<div class="d-flex align-items-center mb-3">
|
|
<div>
|
|
<ol class="breadcrumb">
|
|
<li class="breadcrumb-item"><a href="{% url 'core:dashboard' %}">Dashboard</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'quality:dashboard' %}">Quality</a></li>
|
|
<li class="breadcrumb-item"><a href="{% url 'quality:finding_list' %}">Findings</a></li>
|
|
<li class="breadcrumb-item active">{{ finding.title|truncatechars:30 }}</li>
|
|
</ol>
|
|
<h1 class="page-header mb-0">
|
|
<i class="fas fa-search me-2"></i>Finding Details
|
|
</h1>
|
|
</div>
|
|
<div class="ms-auto">
|
|
<div class="action-buttons">
|
|
<button type="button" class="btn-action" onclick="exportFinding()">
|
|
<i class="fas fa-download me-1"></i>Export
|
|
</button>
|
|
<button type="button" class="btn-action" onclick="generateReport()">
|
|
<i class="fas fa-file-alt me-1"></i>Report
|
|
</button>
|
|
{% if finding.can_edit %}
|
|
<a href="{% url 'quality:finding_edit' finding.id %}" class="btn-action">
|
|
<i class="fas fa-edit me-1"></i>Edit
|
|
</a>
|
|
{% endif %}
|
|
{% if finding.status == 'open' %}
|
|
<button type="button" class="btn-primary-action" onclick="startInvestigation()">
|
|
<i class="fas fa-search me-1"></i>Start Investigation
|
|
</button>
|
|
{% elif finding.status == 'investigating' %}
|
|
<button type="button" class="btn-primary-action" onclick="resolveFinding()">
|
|
<i class="fas fa-check me-1"></i>Resolve Finding
|
|
</button>
|
|
{% elif finding.status == 'resolved' %}
|
|
<button type="button" class="btn-primary-action" onclick="closeFinding()">
|
|
<i class="fas fa-times me-1"></i>Close Finding
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Finding Header -->
|
|
<div class="finding-header">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-8">
|
|
<h2 class="mb-2">{{ finding.title }}</h2>
|
|
<p class="mb-3">{{ finding.description }}</p>
|
|
<div class="d-flex align-items-center gap-3 flex-wrap">
|
|
<span class="severity-badge severity-{{ finding.severity|lower }}">
|
|
{{ finding.get_severity_display }} Severity
|
|
</span>
|
|
<span class="status-badge status-{{ finding.status }}">
|
|
{{ finding.get_status_display }}
|
|
</span>
|
|
<span class="category-badge category-{{ finding.category|lower }}">
|
|
{{ finding.get_category_display }}
|
|
</span>
|
|
<span class="badge bg-light text-dark">
|
|
<i class="fas fa-hashtag me-1"></i>{{ finding.finding_id|default:finding.id }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 text-md-end">
|
|
<div class="stats-mini">
|
|
<div class="stat-mini">
|
|
<div class="stat-mini-number">{{ finding.resolution_progress|default:0 }}%</div>
|
|
<div class="stat-mini-label">Resolved</div>
|
|
</div>
|
|
<div class="stat-mini">
|
|
<div class="stat-mini-number">{{ finding.corrective_actions.count|default:0 }}</div>
|
|
<div class="stat-mini-label">Actions</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="finding-layout">
|
|
<!-- Main Content -->
|
|
<div class="finding-main">
|
|
<!-- Progress Section -->
|
|
{% if finding.resolution_progress is not None %}
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-chart-line me-2"></i>Resolution Progress
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="progress-container">
|
|
<div class="progress-header">
|
|
<span class="progress-label">Overall Progress</span>
|
|
<span class="progress-percentage">{{ finding.resolution_progress }}%</span>
|
|
</div>
|
|
<div class="progress-bar-container">
|
|
<div class="progress-bar" style="width: {{ finding.resolution_progress }}%;"></div>
|
|
</div>
|
|
<div class="progress-milestones">
|
|
<div class="milestone {% if finding.resolution_progress >= 25 %}completed{% elif finding.resolution_progress >= 0 %}active{% endif %}">
|
|
Investigation
|
|
</div>
|
|
<div class="milestone {% if finding.resolution_progress >= 50 %}completed{% elif finding.resolution_progress >= 25 %}active{% endif %}">
|
|
Analysis
|
|
</div>
|
|
<div class="milestone {% if finding.resolution_progress >= 75 %}completed{% elif finding.resolution_progress >= 50 %}active{% endif %}">
|
|
Action Plan
|
|
</div>
|
|
<div class="milestone {% if finding.resolution_progress >= 100 %}completed{% elif finding.resolution_progress >= 75 %}active{% endif %}">
|
|
Resolution
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Finding Information -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-info-circle me-2"></i>Finding Information
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<div class="info-label">Reported By</div>
|
|
<div class="info-value">{{ finding.reported_by.get_full_name|default:"Anonymous" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Department</div>
|
|
<div class="info-value">{{ finding.department.name|default:"Not specified" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Date Identified</div>
|
|
<div class="info-value">{{ finding.date_identified|date:"M d, Y"|default:"Not set" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Due Date</div>
|
|
<div class="info-value">{{ finding.due_date|date:"M d, Y"|default:"Not set" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Assigned To</div>
|
|
<div class="info-value">{{ finding.assigned_to.get_full_name|default:"Unassigned" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Created</div>
|
|
<div class="info-value">{{ finding.created_at|date:"M d, Y g:i A" }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if finding.location %}
|
|
<div class="mt-3">
|
|
<div class="info-label">Location</div>
|
|
<div class="info-value">{{ finding.location }}</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if finding.root_cause %}
|
|
<div class="mt-3">
|
|
<div class="info-label">Root Cause Analysis</div>
|
|
<div class="info-value">{{ finding.root_cause }}</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Investigation Timeline -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-timeline me-2"></i>Investigation Timeline
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="timeline-container">
|
|
{% for event in finding.timeline_events.all %}
|
|
<div class="timeline-item">
|
|
<div class="timeline-icon {% if event.is_completed %}completed{% elif event.is_current %}active{% else %}pending{% endif %}">
|
|
<i class="fas fa-{{ event.icon|default:'circle' }}"></i>
|
|
</div>
|
|
<div class="timeline-content">
|
|
<div class="timeline-title">{{ event.title }}</div>
|
|
<div class="timeline-description">{{ event.description }}</div>
|
|
<div class="timeline-meta">
|
|
<span><i class="fas fa-calendar me-1"></i>{{ event.date|date:"M d, Y g:i A" }}</span>
|
|
{% if event.user %}
|
|
<span><i class="fas fa-user me-1"></i>{{ event.user.get_full_name }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center text-muted py-3">
|
|
<i class="fas fa-timeline fa-2x mb-2"></i>
|
|
<p>No timeline events recorded yet</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Corrective Actions -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-tools me-2"></i>Corrective Actions ({{ finding.corrective_actions.count }})
|
|
</div>
|
|
{% if finding.can_edit %}
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addCorrectiveAction()">
|
|
<i class="fas fa-plus me-1"></i>Add Action
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
<div class="section-content">
|
|
{% for action in finding.corrective_actions.all %}
|
|
<div class="action-item">
|
|
<div class="action-header">
|
|
<div class="action-title">{{ action.description }}</div>
|
|
<span class="action-status action-{{ action.status }}">
|
|
{{ action.get_status_display }}
|
|
</span>
|
|
</div>
|
|
{% if action.details %}
|
|
<div class="action-description">
|
|
{{ action.details }}
|
|
</div>
|
|
{% endif %}
|
|
<div class="action-meta">
|
|
<span>
|
|
<i class="fas fa-user me-1"></i>{{ action.assigned_to.get_full_name|default:"Unassigned" }}
|
|
</span>
|
|
<span>
|
|
<i class="fas fa-calendar me-1"></i>Due: {{ action.due_date|date:"M d, Y"|default:"No due date" }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center text-muted py-3">
|
|
<i class="fas fa-tools fa-2x mb-2"></i>
|
|
<p>No corrective actions defined yet</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comments & Notes -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-comments me-2"></i>Comments & Notes ({{ finding.comments.count }})
|
|
</div>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addComment()">
|
|
<i class="fas fa-plus me-1"></i>Add Comment
|
|
</button>
|
|
</div>
|
|
<div class="section-content">
|
|
{% for comment in finding.comments.all %}
|
|
<div class="comment-item">
|
|
<div class="comment-header">
|
|
<div class="comment-author">
|
|
<div class="comment-avatar">
|
|
{{ comment.user.first_name.0|upper }}{{ comment.user.last_name.0|upper }}
|
|
</div>
|
|
<div class="comment-name">{{ comment.user.get_full_name }}</div>
|
|
</div>
|
|
<div class="comment-date">{{ comment.created_at|timesince }} ago</div>
|
|
</div>
|
|
<div class="comment-text">{{ comment.text|linebreaks }}</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center text-muted py-3">
|
|
<i class="fas fa-comments fa-2x mb-2"></i>
|
|
<p>No comments yet</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="finding-sidebar">
|
|
<!-- Risk Assessment -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>Risk Assessment
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="risk-assessment">
|
|
<div class="risk-level">{{ finding.get_severity_display }} Risk Level</div>
|
|
{% if finding.risk_factors %}
|
|
<ul class="risk-factors">
|
|
{% for factor in finding.risk_factors_list %}
|
|
<li>{{ factor }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if finding.impact_assessment %}
|
|
<div class="mt-3">
|
|
<div class="info-label">Impact Assessment</div>
|
|
<div class="info-value">{{ finding.impact_assessment }}</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Activity -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-history me-2"></i>Recent Activity
|
|
</div>
|
|
<div class="section-content">
|
|
{% for activity in finding.activities.all|slice:":10" %}
|
|
<div class="activity-item">
|
|
<div class="activity-avatar">
|
|
{{ activity.user.first_name.0|upper }}{{ activity.user.last_name.0|upper }}
|
|
</div>
|
|
<div class="activity-content">
|
|
<div class="activity-text">{{ activity.description }}</div>
|
|
<div class="activity-meta">
|
|
{{ activity.created_at|timesince }} ago
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center text-muted py-3">
|
|
<i class="fas fa-history fa-2x mb-2"></i>
|
|
<p>No recent activity</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Related Documents -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<div>
|
|
<i class="fas fa-folder me-2"></i>Documents ({{ finding.documents.count }})
|
|
</div>
|
|
{% if finding.can_edit %}
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="uploadDocument()">
|
|
<i class="fas fa-upload"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
<div class="section-content">
|
|
{% for document in finding.documents.all %}
|
|
<div class="document-item">
|
|
<div class="document-icon">
|
|
<i class="fas fa-file-{{ document.get_icon }}"></i>
|
|
</div>
|
|
<div class="document-info">
|
|
<div class="document-name">{{ document.name }}</div>
|
|
<div class="document-meta">
|
|
{{ document.size|filesizeformat }} • {{ document.uploaded_at|date:"M d, Y" }}
|
|
</div>
|
|
</div>
|
|
<a href="{{ document.file.url }}" class="btn btn-outline-primary btn-sm" target="_blank">
|
|
<i class="fas fa-download"></i>
|
|
</a>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center text-muted py-3">
|
|
<i class="fas fa-folder-open fa-2x mb-2"></i>
|
|
<p>No documents uploaded</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Finding Statistics -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-chart-bar me-2"></i>Statistics
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<div class="info-label">Days Open</div>
|
|
<div class="info-value">{{ finding.days_open|default:0 }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Actions Completed</div>
|
|
<div class="info-value">{{ finding.completed_actions|default:0 }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Comments</div>
|
|
<div class="info-value">{{ finding.comments.count }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Last Updated</div>
|
|
<div class="info-value">{{ finding.updated_at|timesince }} ago</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comment Modal -->
|
|
<div class="modal fade" id="commentModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-comment me-2"></i>Add Comment
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Comment</label>
|
|
<textarea class="form-control" id="comment-text" rows="4"
|
|
placeholder="Add your comment or note..."></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>Cancel
|
|
</button>
|
|
<button type="button" class="btn btn-primary" onclick="saveComment()">
|
|
<i class="fas fa-comment me-1"></i>Add Comment
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Corrective Action Modal -->
|
|
<div class="modal fade" id="actionModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-tools me-2"></i>Add Corrective Action
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row">
|
|
<div class="col-md-12 mb-3">
|
|
<label class="form-label">Action Description</label>
|
|
<input type="text" class="form-control" id="action-description"
|
|
placeholder="Brief description of the corrective action">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Assign To</label>
|
|
<select class="form-select" id="action-assignee">
|
|
<option value="">Select User</option>
|
|
{% for user in assignable_users %}
|
|
<option value="{{ user.id }}">{{ user.get_full_name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label">Due Date</label>
|
|
<input type="date" class="form-control" id="action-due-date">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Detailed Plan</label>
|
|
<textarea class="form-control" id="action-details" rows="4"
|
|
placeholder="Detailed plan for implementing this corrective action..."></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>Cancel
|
|
</button>
|
|
<button type="button" class="btn btn-primary" onclick="saveCorrectiveAction()">
|
|
<i class="fas fa-tools me-1"></i>Add Action
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Document Upload Modal -->
|
|
<div class="modal fade" id="documentModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-upload me-2"></i>Upload Document
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="document-form" enctype="multipart/form-data">
|
|
<div class="mb-3">
|
|
<label class="form-label">Document Name</label>
|
|
<input type="text" class="form-control" id="document-name" required>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Document Type</label>
|
|
<select class="form-select" id="document-type">
|
|
<option value="evidence">Evidence</option>
|
|
<option value="report">Report</option>
|
|
<option value="photo">Photo</option>
|
|
<option value="other">Other</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">File</label>
|
|
<input type="file" class="form-control" id="document-file" required>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Description</label>
|
|
<textarea class="form-control" id="document-description" rows="3"></textarea>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-1"></i>Cancel
|
|
</button>
|
|
<button type="button" class="btn btn-primary" onclick="uploadDocumentFile()">
|
|
<i class="fas fa-upload me-1"></i>Upload
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
|
|
|
|
<script>
|
|
$(document).ready(function() {
|
|
// Initialize Select2
|
|
$('.form-select').select2({
|
|
width: '100%'
|
|
});
|
|
});
|
|
|
|
function startInvestigation() {
|
|
if (confirm('Start investigation for this finding?')) {
|
|
fetch(`/quality/findings/{{ finding.id }}/start-investigation/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Investigation started', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error starting investigation', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error starting investigation', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function resolveFinding() {
|
|
if (confirm('Mark this finding as resolved?')) {
|
|
fetch(`/quality/findings/{{ finding.id }}/resolve/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Finding resolved', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error resolving finding', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error resolving finding', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function closeFinding() {
|
|
if (confirm('Close this finding? This action cannot be undone.')) {
|
|
fetch(`/quality/findings/{{ finding.id }}/close/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Finding closed', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error closing finding', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error closing finding', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function generateReport() {
|
|
window.open(`/quality/findings/{{ finding.id }}/report/`, '_blank');
|
|
}
|
|
|
|
function exportFinding() {
|
|
window.open(`/quality/findings/{{ finding.id }}/export/`, '_blank');
|
|
}
|
|
|
|
function addComment() {
|
|
new bootstrap.Modal(document.getElementById('commentModal')).show();
|
|
}
|
|
|
|
function saveComment() {
|
|
const commentText = document.getElementById('comment-text').value;
|
|
|
|
if (!commentText.trim()) {
|
|
showAlert('Please enter a comment', 'warning');
|
|
return;
|
|
}
|
|
|
|
fetch(`/quality/findings/{{ finding.id }}/add-comment/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
text: commentText
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Comment added', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error adding comment', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error adding comment', 'danger');
|
|
});
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('commentModal')).hide();
|
|
}
|
|
|
|
function addCorrectiveAction() {
|
|
new bootstrap.Modal(document.getElementById('actionModal')).show();
|
|
}
|
|
|
|
function saveCorrectiveAction() {
|
|
const description = document.getElementById('action-description').value;
|
|
const assignee = document.getElementById('action-assignee').value;
|
|
const dueDate = document.getElementById('action-due-date').value;
|
|
const details = document.getElementById('action-details').value;
|
|
|
|
if (!description.trim()) {
|
|
showAlert('Please enter an action description', 'warning');
|
|
return;
|
|
}
|
|
|
|
fetch(`/quality/findings/{{ finding.id }}/add-action/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
description: description,
|
|
assignee: assignee,
|
|
due_date: dueDate,
|
|
details: details
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Corrective action added', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error adding corrective action', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error adding corrective action', 'danger');
|
|
});
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('actionModal')).hide();
|
|
}
|
|
|
|
function uploadDocument() {
|
|
new bootstrap.Modal(document.getElementById('documentModal')).show();
|
|
}
|
|
|
|
function uploadDocumentFile() {
|
|
const formData = new FormData();
|
|
formData.append('name', document.getElementById('document-name').value);
|
|
formData.append('type', document.getElementById('document-type').value);
|
|
formData.append('description', document.getElementById('document-description').value);
|
|
formData.append('file', document.getElementById('document-file').files[0]);
|
|
|
|
fetch(`/quality/findings/{{ finding.id }}/upload-document/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
},
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Document uploaded successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error uploading document', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error uploading document', 'danger');
|
|
});
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('documentModal')).hide();
|
|
}
|
|
|
|
function showAlert(message, type) {
|
|
const alertDiv = document.createElement('div');
|
|
alertDiv.className = `alert alert-${type} alert-dismissible fade show position-fixed`;
|
|
alertDiv.style.cssText = 'top: 20px; right: 20px; z-index: 1060; min-width: 300px;';
|
|
alertDiv.innerHTML = `
|
|
${message}
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
|
`;
|
|
|
|
document.body.appendChild(alertDiv);
|
|
|
|
setTimeout(() => {
|
|
if (alertDiv.parentNode) {
|
|
alertDiv.remove();
|
|
}
|
|
}, 5000);
|
|
}
|
|
</script>
|
|
{% endblock %}
|
|
|