1252 lines
38 KiB
HTML
1252 lines
38 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}{{ assessment.title }} - Risk Assessment{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<style>
|
|
.assessment-header {
|
|
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
|
color: white;
|
|
border-radius: 0.5rem;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.assessment-layout {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr;
|
|
gap: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.assessment-main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2rem;
|
|
}
|
|
|
|
.assessment-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;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.section-content {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.risk-level-badge {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.risk-critical { background: #dc3545; color: white; }
|
|
.risk-high { background: #fd7e14; color: white; }
|
|
.risk-medium { background: #ffc107; color: #212529; }
|
|
.risk-low { background: #28a745; color: white; }
|
|
|
|
.status-badge {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.status-draft { background: #6c757d; color: white; }
|
|
.status-pending { background: #ffc107; color: #212529; }
|
|
.status-approved { background: #28a745; color: white; }
|
|
.status-rejected { background: #dc3545; color: white; }
|
|
|
|
.info-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.risk-matrix {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.matrix-item {
|
|
text-align: center;
|
|
padding: 1rem;
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
border: 2px solid transparent;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.matrix-item:hover {
|
|
border-color: #dc3545;
|
|
background: #fff5f5;
|
|
}
|
|
|
|
.matrix-value {
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
color: #dc3545;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.matrix-label {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.risk-description {
|
|
background: #f8f9fa;
|
|
border-left: 4px solid #dc3545;
|
|
padding: 1rem;
|
|
border-radius: 0.375rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.mitigation-item {
|
|
display: flex;
|
|
align-items: start;
|
|
gap: 0.75rem;
|
|
padding: 1rem;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
margin-bottom: 1rem;
|
|
background: white;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.mitigation-item:hover {
|
|
border-color: #28a745;
|
|
background: #f8fff8;
|
|
}
|
|
|
|
.mitigation-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 0.375rem;
|
|
background: #28a745;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 1rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.mitigation-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.mitigation-title {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.mitigation-description {
|
|
color: #6c757d;
|
|
font-size: 0.875rem;
|
|
margin-bottom: 0.75rem;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.mitigation-meta {
|
|
display: flex;
|
|
gap: 1rem;
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.mitigation-status {
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.status-completed { background: #d4edda; color: #155724; }
|
|
.status-in-progress { background: #fff3cd; color: #856404; }
|
|
.status-pending { background: #f8d7da; color: #721c24; }
|
|
|
|
.progress-section {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.progress-header {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.progress-title {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.progress-percentage {
|
|
font-weight: bold;
|
|
color: #28a745;
|
|
}
|
|
|
|
.progress-bar-container {
|
|
height: 12px;
|
|
background: #e9ecef;
|
|
border-radius: 6px;
|
|
overflow: hidden;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.progress-bar-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #28a745, #20c997);
|
|
border-radius: 6px;
|
|
transition: width 0.3s ease;
|
|
}
|
|
|
|
.progress-details {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 1rem;
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.timeline-item {
|
|
display: flex;
|
|
align-items: start;
|
|
gap: 0.75rem;
|
|
padding: 1rem;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.timeline-item:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.timeline-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.timeline-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.timeline-title {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.timeline-description {
|
|
color: #6c757d;
|
|
font-size: 0.875rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.timeline-meta {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.alert-item {
|
|
display: flex;
|
|
align-items: start;
|
|
gap: 0.75rem;
|
|
padding: 1rem;
|
|
background: #fff3cd;
|
|
border: 1px solid #ffeaa7;
|
|
border-left: 4px solid #ffc107;
|
|
border-radius: 0.375rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.alert-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: #ffc107;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.875rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.alert-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.alert-title {
|
|
font-weight: 600;
|
|
color: #856404;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.alert-message {
|
|
color: #856404;
|
|
font-size: 0.875rem;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.alert-meta {
|
|
font-size: 0.75rem;
|
|
color: #856404;
|
|
}
|
|
|
|
.team-member {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.team-member:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.member-avatar {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background: #dc3545;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.member-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.member-name {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.member-role {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.member-contact {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.action-buttons {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.btn-action {
|
|
padding: 0.5rem 1rem;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.25rem;
|
|
background: white;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 0.875rem;
|
|
text-decoration: none;
|
|
color: #495057;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.chart-container {
|
|
height: 300px;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.chart-controls {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.chart-control {
|
|
padding: 0.25rem 0.75rem;
|
|
border: 1px solid #dee2e6;
|
|
background: white;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.chart-control.active {
|
|
background: #dc3545;
|
|
color: white;
|
|
border-color: #dc3545;
|
|
}
|
|
|
|
.documents-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.document-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 0.75rem;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.document-item:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.document-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 0.375rem;
|
|
background: #6c757d;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.875rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.document-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.document-name {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.document-meta {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
@media (max-width: 1200px) {
|
|
.assessment-layout {
|
|
grid-template-columns: 1fr;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.assessment-sidebar {
|
|
order: -1;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.assessment-header {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.info-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.risk-matrix {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.progress-details {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.action-buttons {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
}
|
|
|
|
@media print {
|
|
.assessment-sidebar, .action-buttons {
|
|
display: none !important;
|
|
}
|
|
|
|
.assessment-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:risk_assessment_list' %}">Risk Assessments</a></li>
|
|
<li class="breadcrumb-item active">{{ assessment.title|truncatechars:30 }}</li>
|
|
</ol>
|
|
<h1 class="page-header mb-0">
|
|
<i class="fas fa-shield-alt me-2"></i>Risk Assessment Details
|
|
</h1>
|
|
</div>
|
|
<div class="ms-auto">
|
|
<a href="{% url 'quality:risk_assessment_list' %}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-arrow-left me-1"></i>Back to List
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Assessment Header -->
|
|
<div class="assessment-header">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-8">
|
|
<h2 class="mb-2">{{ assessment.title }}</h2>
|
|
<p class="mb-3">{{ assessment.description|default:"No description provided" }}</p>
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
<span class="risk-level-badge risk-{{ assessment.risk_level|lower }}">
|
|
{{ assessment.get_risk_level_display }} Risk
|
|
</span>
|
|
<span class="status-badge status-{{ assessment.status|lower }}">
|
|
{{ assessment.get_status_display }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 text-md-end">
|
|
<div class="risk-matrix">
|
|
<div class="matrix-item">
|
|
<div class="matrix-value">{{ assessment.risk_score|default:"-" }}</div>
|
|
<div class="matrix-label">Risk Score</div>
|
|
</div>
|
|
<div class="matrix-item">
|
|
<div class="matrix-value">{{ assessment.residual_risk|default:"-" }}</div>
|
|
<div class="matrix-label">Residual Risk</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="action-buttons">
|
|
{% if assessment.can_edit %}
|
|
<a href="{% url 'quality:risk_assessment_edit' assessment.id %}" class="btn-action">
|
|
<i class="fas fa-edit me-1"></i>Edit Assessment
|
|
</a>
|
|
{% endif %}
|
|
|
|
{% if assessment.can_approve %}
|
|
<button type="button" class="btn-action" onclick="approveAssessment()">
|
|
<i class="fas fa-check me-1"></i>Approve
|
|
</button>
|
|
{% endif %}
|
|
|
|
<button type="button" class="btn-primary-action" onclick="addMitigation()">
|
|
<i class="fas fa-shield-alt me-1"></i>Add Mitigation
|
|
</button>
|
|
|
|
<button type="button" class="btn-action" onclick="scheduleReview()">
|
|
<i class="fas fa-calendar-plus me-1"></i>Schedule Review
|
|
</button>
|
|
|
|
<button type="button" class="btn-action" onclick="generateReport()">
|
|
<i class="fas fa-file-pdf me-1"></i>Generate Report
|
|
</button>
|
|
|
|
<button type="button" class="btn-action" onclick="exportData()">
|
|
<i class="fas fa-download me-1"></i>Export
|
|
</button>
|
|
|
|
<button type="button" class="btn-action" onclick="window.print()">
|
|
<i class="fas fa-print me-1"></i>Print
|
|
</button>
|
|
</div>
|
|
|
|
<div class="assessment-layout">
|
|
<!-- Main Content -->
|
|
<div class="assessment-main">
|
|
<!-- Assessment Information -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-info-circle"></i>
|
|
Assessment Information
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<div class="info-label">Assessment ID</div>
|
|
<div class="info-value">{{ assessment.assessment_id|default:assessment.id }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Department</div>
|
|
<div class="info-value">{{ assessment.department.name|default:"Not specified" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Assessor</div>
|
|
<div class="info-value">{{ assessment.assessor.get_full_name|default:"Not assigned" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Assessment Date</div>
|
|
<div class="info-value">{{ assessment.assessment_date|date:"M d, Y"|default:"Not set" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Next Review</div>
|
|
<div class="info-value">{{ assessment.next_review_date|date:"M d, Y"|default:"Not scheduled" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Priority</div>
|
|
<div class="info-value">{{ assessment.get_priority_display|default:"Not set" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Created</div>
|
|
<div class="info-value">{{ assessment.created_at|date:"M d, Y" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Last Updated</div>
|
|
<div class="info-value">{{ assessment.updated_at|date:"M d, Y" }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if assessment.description %}
|
|
<div class="risk-description">
|
|
<h6><i class="fas fa-file-alt me-2"></i>Description</h6>
|
|
<p class="mb-0">{{ assessment.description }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Risk Analysis -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-chart-line"></i>
|
|
Risk Analysis
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="risk-matrix">
|
|
<div class="matrix-item">
|
|
<div class="matrix-value">{{ assessment.likelihood|default:"-" }}</div>
|
|
<div class="matrix-label">Likelihood</div>
|
|
</div>
|
|
|
|
<div class="matrix-item">
|
|
<div class="matrix-value">{{ assessment.impact|default:"-" }}</div>
|
|
<div class="matrix-label">Impact</div>
|
|
</div>
|
|
|
|
<div class="matrix-item">
|
|
<div class="matrix-value">{{ assessment.risk_score|default:"-" }}</div>
|
|
<div class="matrix-label">Risk Score</div>
|
|
</div>
|
|
|
|
<div class="matrix-item">
|
|
<div class="matrix-value">{{ assessment.residual_risk|default:"-" }}</div>
|
|
<div class="matrix-label">Residual Risk</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if assessment.risk_factors %}
|
|
<div class="risk-description">
|
|
<h6><i class="fas fa-exclamation-triangle me-2"></i>Risk Factors</h6>
|
|
<p class="mb-0">{{ assessment.risk_factors }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if assessment.potential_consequences %}
|
|
<div class="risk-description">
|
|
<h6><i class="fas fa-warning me-2"></i>Potential Consequences</h6>
|
|
<p class="mb-0">{{ assessment.potential_consequences }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mitigation Actions -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-shield-alt"></i>
|
|
Mitigation Actions ({{ assessment.mitigation_actions.count }})
|
|
</div>
|
|
<div class="section-content">
|
|
{% if assessment.mitigation_progress %}
|
|
<div class="progress-section">
|
|
<div class="progress-header">
|
|
<div class="progress-title">Overall Mitigation Progress</div>
|
|
<div class="progress-percentage">{{ assessment.mitigation_progress|default:0 }}%</div>
|
|
</div>
|
|
<div class="progress-bar-container">
|
|
<div class="progress-bar-fill" style="width: {{ assessment.mitigation_progress|default:0 }}%;"></div>
|
|
</div>
|
|
<div class="progress-details">
|
|
<div>Completed: {{ assessment.completed_mitigations|default:0 }}</div>
|
|
<div>In Progress: {{ assessment.in_progress_mitigations|default:0 }}</div>
|
|
<div>Pending: {{ assessment.pending_mitigations|default:0 }}</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% for mitigation in assessment.mitigation_actions.all %}
|
|
<div class="mitigation-item">
|
|
<div class="mitigation-icon">
|
|
{% if mitigation.status == 'completed' %}
|
|
<i class="fas fa-check"></i>
|
|
{% elif mitigation.status == 'in_progress' %}
|
|
<i class="fas fa-clock"></i>
|
|
{% else %}
|
|
<i class="fas fa-hourglass-start"></i>
|
|
{% endif %}
|
|
</div>
|
|
<div class="mitigation-content">
|
|
<div class="mitigation-title">{{ mitigation.title }}</div>
|
|
<div class="mitigation-description">{{ mitigation.description|default:"No description provided" }}</div>
|
|
<div class="mitigation-meta">
|
|
<span class="mitigation-status status-{{ mitigation.status|lower }}">
|
|
{{ mitigation.get_status_display }}
|
|
</span>
|
|
<span>Due: {{ mitigation.due_date|date:"M d, Y"|default:"Not set" }}</span>
|
|
<span>Assigned: {{ mitigation.assigned_to.get_full_name|default:"Not assigned" }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center py-4 text-muted">
|
|
<i class="fas fa-shield-alt fa-2x mb-2"></i>
|
|
<p>No mitigation actions defined</p>
|
|
<button type="button" class="btn btn-outline-danger btn-sm" onclick="addMitigation()">
|
|
<i class="fas fa-plus me-1"></i>Add First Mitigation
|
|
</button>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Activity Timeline -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-history"></i>
|
|
Activity Timeline
|
|
</div>
|
|
<div class="section-content">
|
|
{% for activity in assessment.activities.all %}
|
|
<div class="timeline-item">
|
|
<div class="timeline-icon">
|
|
{% if activity.action == 'created' %}
|
|
<i class="fas fa-plus"></i>
|
|
{% elif activity.action == 'updated' %}
|
|
<i class="fas fa-edit"></i>
|
|
{% elif activity.action == 'approved' %}
|
|
<i class="fas fa-check"></i>
|
|
{% elif activity.action == 'reviewed' %}
|
|
<i class="fas fa-eye"></i>
|
|
{% else %}
|
|
<i class="fas fa-circle"></i>
|
|
{% endif %}
|
|
</div>
|
|
<div class="timeline-content">
|
|
<div class="timeline-title">{{ activity.get_action_display }}</div>
|
|
<div class="timeline-description">{{ activity.description|default:"No description" }}</div>
|
|
<div class="timeline-meta">
|
|
{{ activity.created_at|date:"M d, Y g:i A" }} by {{ activity.user.get_full_name|default:"System" }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center py-4 text-muted">
|
|
<i class="fas fa-history fa-2x mb-2"></i>
|
|
<p>No activity recorded</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="assessment-sidebar">
|
|
<!-- Risk Trend Chart -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-chart-area"></i>
|
|
Risk Trend
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="chart-controls">
|
|
<button type="button" class="chart-control active" onclick="updateChart('30d')">30D</button>
|
|
<button type="button" class="chart-control" onclick="updateChart('90d')">90D</button>
|
|
<button type="button" class="chart-control" onclick="updateChart('1y')">1Y</button>
|
|
</div>
|
|
<div class="chart-container">
|
|
<canvas id="riskTrendChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Active Alerts -->
|
|
{% if assessment.alerts.exists %}
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
Active Alerts ({{ assessment.alerts.count }})
|
|
</div>
|
|
<div class="section-content">
|
|
{% for alert in assessment.alerts.all %}
|
|
<div class="alert-item">
|
|
<div class="alert-icon">
|
|
<i class="fas fa-exclamation"></i>
|
|
</div>
|
|
<div class="alert-content">
|
|
<div class="alert-title">{{ alert.title }}</div>
|
|
<div class="alert-message">{{ alert.message|truncatechars:100 }}</div>
|
|
<div class="alert-meta">
|
|
{{ alert.created_at|date:"M d, Y" }} - {{ alert.get_severity_display }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Team Members -->
|
|
{% if assessment.team_members.exists %}
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-users"></i>
|
|
Team Members ({{ assessment.team_members.count }})
|
|
</div>
|
|
<div class="section-content">
|
|
{% for member in assessment.team_members.all %}
|
|
<div class="team-member">
|
|
<div class="member-avatar">
|
|
{{ member.first_name.0|upper }}{{ member.last_name.0|upper }}
|
|
</div>
|
|
<div class="member-info">
|
|
<div class="member-name">{{ member.get_full_name }}</div>
|
|
<div class="member-role">{{ member.profile.role|default:"Team Member" }}</div>
|
|
<div class="member-contact">{{ member.email }}</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Documents -->
|
|
{% if assessment.documents.exists %}
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-file-alt"></i>
|
|
Documents ({{ assessment.documents.count }})
|
|
</div>
|
|
<div class="section-content">
|
|
<ul class="documents-list">
|
|
{% for document in assessment.documents.all %}
|
|
<li class="document-item">
|
|
<div class="document-icon">
|
|
<i class="fas fa-file-pdf"></i>
|
|
</div>
|
|
<div class="document-info">
|
|
<div class="document-name">{{ document.name }}</div>
|
|
<div class="document-meta">
|
|
{{ document.file_size|filesizeformat }} - {{ document.uploaded_at|date:"M d, Y" }}
|
|
</div>
|
|
</div>
|
|
<a href="{{ document.file.url }}" target="_blank" class="btn btn-outline-secondary btn-sm">
|
|
<i class="fas fa-download"></i>
|
|
</a>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Quick Stats -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-chart-bar"></i>
|
|
Quick Statistics
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="info-grid">
|
|
<div class="info-item">
|
|
<div class="info-label">Days Since Created</div>
|
|
<div class="info-value">{{ assessment.days_since_created|default:0 }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Days Until Review</div>
|
|
<div class="info-value">{{ assessment.days_until_review|default:"N/A" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Mitigation Actions</div>
|
|
<div class="info-value">{{ assessment.mitigation_actions.count }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Completion Rate</div>
|
|
<div class="info-value">{{ assessment.mitigation_progress|default:0 }}%</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Add Mitigation Modal -->
|
|
<div class="modal fade" id="addMitigationModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-shield-alt me-2"></i>Add Mitigation Action
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="mitigation-form">
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div class="mb-3">
|
|
<label class="form-label">Title</label>
|
|
<input type="text" class="form-control" name="title" required>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label">Priority</label>
|
|
<select class="form-select" name="priority">
|
|
<option value="low">Low</option>
|
|
<option value="medium" selected>Medium</option>
|
|
<option value="high">High</option>
|
|
<option value="critical">Critical</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Description</label>
|
|
<textarea class="form-control" name="description" rows="3" required></textarea>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Assigned To</label>
|
|
<select class="form-select" name="assigned_to">
|
|
<option value="">Select team member</option>
|
|
{% for member in assessment.team_members.all %}
|
|
<option value="{{ member.id }}">{{ member.get_full_name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Due Date</label>
|
|
<input type="date" class="form-control" name="due_date">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Expected Outcome</label>
|
|
<textarea class="form-control" name="expected_outcome" rows="2"></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-danger" onclick="saveMitigation()">
|
|
<i class="fas fa-save me-1"></i>Add Mitigation
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
let riskTrendChart;
|
|
|
|
$(document).ready(function() {
|
|
initializeRiskTrendChart();
|
|
});
|
|
|
|
function initializeRiskTrendChart() {
|
|
const ctx = document.getElementById('riskTrendChart').getContext('2d');
|
|
riskTrendChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
|
|
datasets: [{
|
|
label: 'Risk Score',
|
|
data: [12, 15, 13, 11, 9, 8],
|
|
borderColor: '#dc3545',
|
|
backgroundColor: 'rgba(220, 53, 69, 0.1)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
max: 20
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateChart(period) {
|
|
$('.chart-control').removeClass('active');
|
|
$(`.chart-control:contains('${period.toUpperCase()}')`).addClass('active');
|
|
|
|
// Update chart data based on period
|
|
let newData, newLabels;
|
|
|
|
switch(period) {
|
|
case '30d':
|
|
newLabels = ['Week 1', 'Week 2', 'Week 3', 'Week 4'];
|
|
newData = [12, 11, 9, 8];
|
|
break;
|
|
case '90d':
|
|
newLabels = ['Month 1', 'Month 2', 'Month 3'];
|
|
newData = [15, 12, 8];
|
|
break;
|
|
case '1y':
|
|
newLabels = ['Q1', 'Q2', 'Q3', 'Q4'];
|
|
newData = [18, 15, 12, 8];
|
|
break;
|
|
}
|
|
|
|
riskTrendChart.data.labels = newLabels;
|
|
riskTrendChart.data.datasets[0].data = newData;
|
|
riskTrendChart.update();
|
|
}
|
|
|
|
function approveAssessment() {
|
|
if (confirm('Approve this risk assessment?')) {
|
|
fetch(`/quality/risk-assessments/{{ assessment.id }}/approve/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Assessment approved successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error approving assessment', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error approving assessment', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function addMitigation() {
|
|
new bootstrap.Modal(document.getElementById('addMitigationModal')).show();
|
|
}
|
|
|
|
function saveMitigation() {
|
|
const form = document.getElementById('mitigation-form');
|
|
const formData = new FormData(form);
|
|
|
|
fetch(`/quality/risk-assessments/{{ assessment.id }}/add-mitigation/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
},
|
|
body: formData
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Mitigation action added successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error adding mitigation action', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error adding mitigation action', 'danger');
|
|
});
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('addMitigationModal')).hide();
|
|
}
|
|
|
|
function scheduleReview() {
|
|
const reviewDate = prompt('Enter review date (YYYY-MM-DD):');
|
|
if (reviewDate) {
|
|
fetch(`/quality/risk-assessments/{{ assessment.id }}/schedule-review/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
review_date: reviewDate
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Review scheduled successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error scheduling review', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error scheduling review', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function generateReport() {
|
|
showAlert('Generating assessment report...', 'info');
|
|
window.open(`/quality/risk-assessments/{{ assessment.id }}/report/`, '_blank');
|
|
}
|
|
|
|
function exportData() {
|
|
window.open(`/quality/risk-assessments/{{ assessment.id }}/export/`, '_blank');
|
|
}
|
|
|
|
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 %}
|
|
|