1242 lines
46 KiB
HTML
1242 lines
46 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}{% if assessment %}Edit{% else %}Create{% endif %} Risk Assessment{% endblock %}
|
|
|
|
{% block css %}
|
|
<style>
|
|
.form-header {
|
|
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
|
color: white;
|
|
border-radius: 0.5rem;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.form-layout {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr;
|
|
gap: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.form-main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2rem;
|
|
}
|
|
|
|
.form-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-matrix-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(2, 1fr);
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.matrix-input-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.matrix-input {
|
|
text-align: center;
|
|
font-size: 1.25rem;
|
|
font-weight: bold;
|
|
color: #dc3545;
|
|
border: 2px solid #dee2e6;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.matrix-input:focus {
|
|
border-color: #dc3545;
|
|
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
|
}
|
|
|
|
.matrix-label {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
text-align: center;
|
|
}
|
|
|
|
.risk-calculator {
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.calculator-title {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 1rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.risk-score-display {
|
|
text-align: center;
|
|
padding: 1rem;
|
|
background: white;
|
|
border-radius: 0.375rem;
|
|
border: 2px solid #dee2e6;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.risk-score-value {
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
color: #dc3545;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.risk-score-label {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.risk-level-indicator {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
text-align: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.risk-critical { background: #dc3545; color: white; }
|
|
.risk-high { background: #fd7e14; color: white; }
|
|
.risk-medium { background: #ffc107; color: #212529; }
|
|
.risk-low { background: #28a745; color: white; }
|
|
|
|
.mitigation-section {
|
|
margin-top: 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-inputs {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr 1fr;
|
|
gap: 0.75rem;
|
|
margin-bottom: 0.75rem;
|
|
}
|
|
|
|
.mitigation-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.btn-remove-mitigation {
|
|
padding: 0.25rem 0.5rem;
|
|
border: 1px solid #dc3545;
|
|
background: white;
|
|
color: #dc3545;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
.btn-remove-mitigation:hover {
|
|
background: #dc3545;
|
|
color: white;
|
|
}
|
|
|
|
.form-tabs {
|
|
display: flex;
|
|
background: #f8f9fa;
|
|
border-radius: 0.5rem 0.5rem 0 0;
|
|
border: 1px solid #dee2e6;
|
|
border-bottom: none;
|
|
}
|
|
|
|
.tab-button {
|
|
flex: 1;
|
|
padding: 1rem;
|
|
border: none;
|
|
background: transparent;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-weight: 600;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.tab-button.active {
|
|
background: white;
|
|
color: #dc3545;
|
|
border-bottom: 2px solid #dc3545;
|
|
}
|
|
|
|
.tab-content {
|
|
display: none;
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0 0 0.5rem 0.5rem;
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
|
|
.auto-save-indicator {
|
|
position: fixed;
|
|
top: 20px;
|
|
right: 20px;
|
|
background: #28a745;
|
|
color: white;
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 0.25rem;
|
|
font-size: 0.875rem;
|
|
z-index: 1050;
|
|
opacity: 0;
|
|
transition: opacity 0.3s;
|
|
}
|
|
|
|
.auto-save-indicator.show {
|
|
opacity: 1;
|
|
}
|
|
|
|
.form-preview {
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.preview-title {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.75rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.preview-content {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.help-section {
|
|
background: #e3f2fd;
|
|
border-left: 4px solid #2196f3;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.help-title {
|
|
font-weight: 600;
|
|
color: #1976d2;
|
|
margin-bottom: 0.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.help-content {
|
|
font-size: 0.875rem;
|
|
color: #1976d2;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.help-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
}
|
|
|
|
.help-item {
|
|
display: flex;
|
|
align-items: start;
|
|
gap: 0.5rem;
|
|
margin-bottom: 0.5rem;
|
|
font-size: 0.875rem;
|
|
color: #1976d2;
|
|
}
|
|
|
|
.help-icon {
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 50%;
|
|
background: #2196f3;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.75rem;
|
|
flex-shrink: 0;
|
|
margin-top: 0.125rem;
|
|
}
|
|
|
|
.form-actions {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
padding: 1.5rem;
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.alternative-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.btn-alternative {
|
|
padding: 0.5rem 1rem;
|
|
border: 1px solid #dee2e6;
|
|
background: white;
|
|
color: #6c757d;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 0.875rem;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn-alternative:hover {
|
|
border-color: #dc3545;
|
|
color: #dc3545;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.primary-actions {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
@media (max-width: 1200px) {
|
|
.form-layout {
|
|
grid-template-columns: 1fr;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.form-sidebar {
|
|
order: -1;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.form-header {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.risk-matrix-grid {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.mitigation-inputs {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.form-actions {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.alternative-actions, .primary-actions {
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
@media print {
|
|
.form-sidebar, .form-actions {
|
|
display: none !important;
|
|
}
|
|
|
|
.form-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">{% if assessment %}Edit{% else %}Create{% endif %}</li>
|
|
</ol>
|
|
<h1 class="page-header mb-0">
|
|
<i class="fas fa-shield-alt me-2"></i>{% if assessment %}Edit{% else %}Create{% endif %} Risk Assessment
|
|
</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>
|
|
|
|
<!-- Form Header -->
|
|
<div class="form-header">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-8">
|
|
<h2 class="mb-2">
|
|
{% if assessment %}
|
|
Edit Risk Assessment: {{ assessment.title }}
|
|
{% else %}
|
|
Create New Risk Assessment
|
|
{% endif %}
|
|
</h2>
|
|
<p class="mb-0">Complete all sections to create a comprehensive risk assessment for your organization.</p>
|
|
</div>
|
|
<div class="col-md-4 text-md-end">
|
|
<div class="risk-score-display" id="risk-score-preview">
|
|
<div class="risk-score-value" id="calculated-score">-</div>
|
|
<div class="risk-score-label">Risk Score</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Auto-save Indicator -->
|
|
<div class="auto-save-indicator" id="auto-save-indicator">
|
|
<i class="fas fa-check me-1"></i>Changes saved
|
|
</div>
|
|
|
|
<form method="post" id="assessment-form" enctype="multipart/form-data">
|
|
{% csrf_token %}
|
|
|
|
<div class="form-layout">
|
|
<!-- Main Form Content -->
|
|
<div class="form-main">
|
|
<!-- Basic Information -->
|
|
<div class="section-card">
|
|
<div class="form-tabs">
|
|
<button type="button" class="tab-button active" onclick="switchTab('basic')">
|
|
<i class="fas fa-info-circle me-1"></i>Basic Information
|
|
</button>
|
|
<button type="button" class="tab-button" onclick="switchTab('risk')">
|
|
<i class="fas fa-chart-line me-1"></i>Risk Analysis
|
|
</button>
|
|
<button type="button" class="tab-button" onclick="switchTab('mitigation')">
|
|
<i class="fas fa-shield-alt me-1"></i>Mitigation
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Basic Information Tab -->
|
|
<div class="tab-content active" id="basic-tab">
|
|
<div class="row">
|
|
<div class="col-md-8">
|
|
<div class="mb-3">
|
|
<label class="form-label">Assessment Title <span class="text-danger">*</span></label>
|
|
<input type="text" class="form-control" name="title"
|
|
value="{{ assessment.title|default:'' }}" required>
|
|
<div class="form-text">Provide a clear, descriptive title for this risk assessment</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label">Assessment ID</label>
|
|
<input type="text" class="form-control" name="assessment_id"
|
|
value="{{ assessment.assessment_id|default:'' }}" placeholder="Auto-generated">
|
|
<div class="form-text">Leave blank for auto-generation</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Description <span class="text-danger">*</span></label>
|
|
<textarea class="form-control" name="description" rows="4" required>{{ assessment.description|default:'' }}</textarea>
|
|
<div class="form-text">Detailed description of the risk being assessed</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Department <span class="text-danger">*</span></label>
|
|
<select class="form-select" name="department" required>
|
|
<option value="">Select Department</option>
|
|
{% for dept in departments %}
|
|
<option value="{{ dept.id }}" {% if assessment.department_id == dept.id %}selected{% endif %}>
|
|
{{ dept.name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Assessor <span class="text-danger">*</span></label>
|
|
<select class="form-select" name="assessor" required>
|
|
<option value="">Select Assessor</option>
|
|
{% for user in assessors %}
|
|
<option value="{{ user.id }}" {% if assessment.assessor_id == user.id %}selected{% endif %}>
|
|
{{ user.get_full_name }}
|
|
</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label">Assessment Date</label>
|
|
<input type="date" class="form-control" name="assessment_date"
|
|
value="{{ assessment.assessment_date|date:'Y-m-d'|default:'' }}">
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="mb-3">
|
|
<label class="form-label">Next Review Date</label>
|
|
<input type="date" class="form-control" name="next_review_date"
|
|
value="{{ assessment.next_review_date|date:'Y-m-d'|default:'' }}">
|
|
</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" {% if assessment.priority == 'low' %}selected{% endif %}>Low</option>
|
|
<option value="medium" {% if assessment.priority == 'medium' %}selected{% endif %}>Medium</option>
|
|
<option value="high" {% if assessment.priority == 'high' %}selected{% endif %}>High</option>
|
|
<option value="critical" {% if assessment.priority == 'critical' %}selected{% endif %}>Critical</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Risk Factors</label>
|
|
<textarea class="form-control" name="risk_factors" rows="3">{{ assessment.risk_factors|default:'' }}</textarea>
|
|
<div class="form-text">Identify and describe the key factors contributing to this risk</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Potential Consequences</label>
|
|
<textarea class="form-control" name="potential_consequences" rows="3">{{ assessment.potential_consequences|default:'' }}</textarea>
|
|
<div class="form-text">Describe what could happen if this risk materializes</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Risk Analysis Tab -->
|
|
<div class="tab-content" id="risk-tab">
|
|
<div class="risk-calculator">
|
|
<div class="calculator-title">
|
|
<i class="fas fa-calculator"></i>Risk Matrix Calculator
|
|
</div>
|
|
|
|
<div class="risk-matrix-grid">
|
|
<div class="matrix-input-group">
|
|
<input type="number" class="form-control matrix-input" name="likelihood"
|
|
value="{{ assessment.likelihood|default:'' }}" min="1" max="5"
|
|
placeholder="1-5" onchange="calculateRiskScore()">
|
|
<div class="matrix-label">Likelihood</div>
|
|
<div class="form-text">1 = Very Unlikely, 5 = Very Likely</div>
|
|
</div>
|
|
|
|
<div class="matrix-input-group">
|
|
<input type="number" class="form-control matrix-input" name="impact"
|
|
value="{{ assessment.impact|default:'' }}" min="1" max="5"
|
|
placeholder="1-5" onchange="calculateRiskScore()">
|
|
<div class="matrix-label">Impact</div>
|
|
<div class="form-text">1 = Minimal, 5 = Catastrophic</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="risk-score-display">
|
|
<div class="risk-score-value" id="risk-score-value">-</div>
|
|
<div class="risk-score-label">Calculated Risk Score</div>
|
|
</div>
|
|
|
|
<div class="risk-level-indicator" id="risk-level-indicator">
|
|
Risk Level: Not Calculated
|
|
</div>
|
|
|
|
<input type="hidden" name="risk_score" id="risk-score-input">
|
|
<input type="hidden" name="risk_level" id="risk-level-input">
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Current Controls</label>
|
|
<textarea class="form-control" name="current_controls" rows="4">{{ assessment.current_controls|default:'' }}</textarea>
|
|
<div class="form-text">Existing measures in place to manage this risk</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Control Effectiveness</label>
|
|
<select class="form-select" name="control_effectiveness">
|
|
<option value="">Select Effectiveness</option>
|
|
<option value="poor" {% if assessment.control_effectiveness == 'poor' %}selected{% endif %}>Poor</option>
|
|
<option value="fair" {% if assessment.control_effectiveness == 'fair' %}selected{% endif %}>Fair</option>
|
|
<option value="good" {% if assessment.control_effectiveness == 'good' %}selected{% endif %}>Good</option>
|
|
<option value="excellent" {% if assessment.control_effectiveness == 'excellent' %}selected{% endif %}>Excellent</option>
|
|
</select>
|
|
<div class="form-text">How effective are the current controls?</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Residual Risk Score</label>
|
|
<input type="number" class="form-control" name="residual_risk"
|
|
value="{{ assessment.residual_risk|default:'' }}" min="1" max="25">
|
|
<div class="form-text">Risk score after considering current controls</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mitigation Tab -->
|
|
<div class="tab-content" id="mitigation-tab">
|
|
<div class="mitigation-section">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6><i class="fas fa-shield-alt me-2"></i>Mitigation Actions</h6>
|
|
<button type="button" class="btn btn-outline-danger btn-sm" onclick="addMitigation()">
|
|
<i class="fas fa-plus me-1"></i>Add Mitigation
|
|
</button>
|
|
</div>
|
|
|
|
<div id="mitigation-container">
|
|
{% for mitigation in assessment.mitigation_actions.all %}
|
|
<div class="mitigation-item">
|
|
<div class="mitigation-icon">
|
|
<i class="fas fa-shield-alt"></i>
|
|
</div>
|
|
<div class="mitigation-content">
|
|
<div class="mitigation-inputs">
|
|
<input type="text" class="form-control" name="mitigation_title[]"
|
|
value="{{ mitigation.title }}" placeholder="Mitigation title">
|
|
<select class="form-select" name="mitigation_priority[]">
|
|
<option value="low" {% if mitigation.priority == 'low' %}selected{% endif %}>Low</option>
|
|
<option value="medium" {% if mitigation.priority == 'medium' %}selected{% endif %}>Medium</option>
|
|
<option value="high" {% if mitigation.priority == 'high' %}selected{% endif %}>High</option>
|
|
<option value="critical" {% if mitigation.priority == 'critical' %}selected{% endif %}>Critical</option>
|
|
</select>
|
|
<input type="date" class="form-control" name="mitigation_due_date[]"
|
|
value="{{ mitigation.due_date|date:'Y-m-d' }}">
|
|
</div>
|
|
<textarea class="form-control mb-2" name="mitigation_description[]" rows="2"
|
|
placeholder="Describe the mitigation action...">{{ mitigation.description }}</textarea>
|
|
<div class="mitigation-actions">
|
|
<button type="button" class="btn-remove-mitigation" onclick="removeMitigation(this)">
|
|
<i class="fas fa-trash me-1"></i>Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<div class="text-center py-3" id="no-mitigations" {% if assessment.mitigation_actions.exists %}style="display: none;"{% endif %}>
|
|
<div class="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>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mt-4">
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Review Frequency</label>
|
|
<select class="form-select" name="review_frequency">
|
|
<option value="">Select Frequency</option>
|
|
<option value="monthly" {% if assessment.review_frequency == 'monthly' %}selected{% endif %}>Monthly</option>
|
|
<option value="quarterly" {% if assessment.review_frequency == 'quarterly' %}selected{% endif %}>Quarterly</option>
|
|
<option value="semi-annually" {% if assessment.review_frequency == 'semi-annually' %}selected{% endif %}>Semi-Annually</option>
|
|
<option value="annually" {% if assessment.review_frequency == 'annually' %}selected{% endif %}>Annually</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="mb-3">
|
|
<label class="form-label">Monitoring Method</label>
|
|
<select class="form-select" name="monitoring_method">
|
|
<option value="">Select Method</option>
|
|
<option value="manual" {% if assessment.monitoring_method == 'manual' %}selected{% endif %}>Manual Review</option>
|
|
<option value="automated" {% if assessment.monitoring_method == 'automated' %}selected{% endif %}>Automated Monitoring</option>
|
|
<option value="hybrid" {% if assessment.monitoring_method == 'hybrid' %}selected{% endif %}>Hybrid Approach</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="form-sidebar">
|
|
<!-- Live Preview -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-eye"></i>
|
|
Live Preview
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="form-preview">
|
|
<div class="preview-title">
|
|
<i class="fas fa-file-alt"></i>Assessment Preview
|
|
</div>
|
|
<div class="preview-content" id="preview-content">
|
|
<p class="text-muted">Start filling the form to see a preview...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Help & Tips -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-question-circle"></i>
|
|
Help & Tips
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="help-section">
|
|
<div class="help-title">
|
|
<i class="fas fa-lightbulb"></i>Risk Assessment Tips
|
|
</div>
|
|
<ul class="help-list">
|
|
<li class="help-item">
|
|
<div class="help-icon">1</div>
|
|
<span>Use clear, specific language in your risk description</span>
|
|
</li>
|
|
<li class="help-item">
|
|
<div class="help-icon">2</div>
|
|
<span>Consider both likelihood and impact when scoring</span>
|
|
</li>
|
|
<li class="help-item">
|
|
<div class="help-icon">3</div>
|
|
<span>Include multiple mitigation strategies for high risks</span>
|
|
</li>
|
|
<li class="help-item">
|
|
<div class="help-icon">4</div>
|
|
<span>Schedule regular reviews to keep assessments current</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<div class="help-section">
|
|
<div class="help-title">
|
|
<i class="fas fa-chart-line"></i>Risk Scoring Guide
|
|
</div>
|
|
<div class="help-content">
|
|
<p><strong>Likelihood Scale:</strong></p>
|
|
<p>1 = Very Unlikely (0-5%)<br>
|
|
2 = Unlikely (6-25%)<br>
|
|
3 = Possible (26-50%)<br>
|
|
4 = Likely (51-75%)<br>
|
|
5 = Very Likely (76-100%)</p>
|
|
|
|
<p><strong>Impact Scale:</strong></p>
|
|
<p>1 = Minimal<br>
|
|
2 = Minor<br>
|
|
3 = Moderate<br>
|
|
4 = Major<br>
|
|
5 = Catastrophic</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Quick Actions -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-bolt"></i>
|
|
Quick Actions
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="d-grid gap-2">
|
|
<button type="button" class="btn btn-outline-info btn-sm" onclick="loadTemplate()">
|
|
<i class="fas fa-file-import me-1"></i>Load Template
|
|
</button>
|
|
<button type="button" class="btn btn-outline-success btn-sm" onclick="saveTemplate()">
|
|
<i class="fas fa-save me-1"></i>Save as Template
|
|
</button>
|
|
<button type="button" class="btn btn-outline-warning btn-sm" onclick="validateForm()">
|
|
<i class="fas fa-check-circle me-1"></i>Validate Form
|
|
</button>
|
|
<button type="button" class="btn btn-outline-secondary btn-sm" onclick="resetForm()">
|
|
<i class="fas fa-undo me-1"></i>Reset Form
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Form Actions -->
|
|
<div class="form-actions">
|
|
<div class="alternative-actions">
|
|
<button type="button" class="btn-alternative" onclick="saveDraft()">
|
|
<i class="fas fa-save me-1"></i>Save Draft
|
|
</button>
|
|
<button type="button" class="btn-alternative" onclick="previewAssessment()">
|
|
<i class="fas fa-eye me-1"></i>Preview
|
|
</button>
|
|
<a href="{% url 'quality:risk_assessment_list' %}" class="btn-alternative">
|
|
<i class="fas fa-times me-1"></i>Cancel
|
|
</a>
|
|
</div>
|
|
|
|
<div class="primary-actions">
|
|
<button type="submit" name="action" value="save" class="btn btn-outline-danger">
|
|
<i class="fas fa-save me-1"></i>Save Assessment
|
|
</button>
|
|
<button type="submit" name="action" value="save_and_submit" class="btn btn-danger">
|
|
<i class="fas fa-paper-plane me-1"></i>Save & Submit for Review
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
|
|
<!-- Load Template Modal -->
|
|
<div class="modal fade" id="templateModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-file-import me-2"></i>Load Assessment Template
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row">
|
|
{% for template in templates %}
|
|
<div class="col-md-6 mb-3">
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h6 class="card-title">{{ template.name }}</h6>
|
|
<p class="card-text">{{ template.description|truncatechars:100 }}</p>
|
|
<button type="button" class="btn btn-outline-danger btn-sm"
|
|
onclick="applyTemplate({{ template.id }})">
|
|
<i class="fas fa-download me-1"></i>Use Template
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="col-12 text-center py-4">
|
|
<div class="text-muted">
|
|
<i class="fas fa-file-alt fa-2x mb-2"></i>
|
|
<p>No templates available</p>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</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>Close
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block js %}
|
|
<script>
|
|
let autoSaveTimer;
|
|
let mitigationCount = {{ assessment.mitigation_actions.count|default:0 }};
|
|
|
|
$(document).ready(function() {
|
|
// Initialize form
|
|
calculateRiskScore();
|
|
updatePreview();
|
|
|
|
// Auto-save functionality
|
|
$('#assessment-form input, #assessment-form textarea, #assessment-form select').on('input change', function() {
|
|
clearTimeout(autoSaveTimer);
|
|
autoSaveTimer = setTimeout(autoSave, 2000);
|
|
updatePreview();
|
|
});
|
|
|
|
// Calculate risk score when likelihood or impact changes
|
|
$('input[name="likelihood"], input[name="impact"]').on('input', calculateRiskScore);
|
|
});
|
|
|
|
function switchTab(tabName) {
|
|
$('.tab-button').removeClass('active');
|
|
$('.tab-content').removeClass('active');
|
|
|
|
$(`.tab-button:contains('${tabName.charAt(0).toUpperCase() + tabName.slice(1)}')`).addClass('active');
|
|
$(`#${tabName}-tab`).addClass('active');
|
|
}
|
|
|
|
function calculateRiskScore() {
|
|
const likelihood = parseInt($('input[name="likelihood"]').val()) || 0;
|
|
const impact = parseInt($('input[name="impact"]').val()) || 0;
|
|
|
|
if (likelihood > 0 && impact > 0) {
|
|
const riskScore = likelihood * impact;
|
|
let riskLevel, riskClass;
|
|
|
|
if (riskScore >= 20) {
|
|
riskLevel = 'Critical';
|
|
riskClass = 'risk-critical';
|
|
} else if (riskScore >= 15) {
|
|
riskLevel = 'High';
|
|
riskClass = 'risk-high';
|
|
} else if (riskScore >= 8) {
|
|
riskLevel = 'Medium';
|
|
riskClass = 'risk-medium';
|
|
} else {
|
|
riskLevel = 'Low';
|
|
riskClass = 'risk-low';
|
|
}
|
|
|
|
$('#risk-score-value, #calculated-score').text(riskScore);
|
|
$('#risk-level-indicator').text(`Risk Level: ${riskLevel}`).removeClass().addClass(`risk-level-indicator ${riskClass}`);
|
|
|
|
$('#risk-score-input').val(riskScore);
|
|
$('#risk-level-input').val(riskLevel.toLowerCase());
|
|
} else {
|
|
$('#risk-score-value, #calculated-score').text('-');
|
|
$('#risk-level-indicator').text('Risk Level: Not Calculated').removeClass().addClass('risk-level-indicator');
|
|
|
|
$('#risk-score-input').val('');
|
|
$('#risk-level-input').val('');
|
|
}
|
|
}
|
|
|
|
function addMitigation() {
|
|
mitigationCount++;
|
|
const mitigationHtml = `
|
|
<div class="mitigation-item">
|
|
<div class="mitigation-icon">
|
|
<i class="fas fa-shield-alt"></i>
|
|
</div>
|
|
<div class="mitigation-content">
|
|
<div class="mitigation-inputs">
|
|
<input type="text" class="form-control" name="mitigation_title[]"
|
|
placeholder="Mitigation title">
|
|
<select class="form-select" name="mitigation_priority[]">
|
|
<option value="low">Low</option>
|
|
<option value="medium" selected>Medium</option>
|
|
<option value="high">High</option>
|
|
<option value="critical">Critical</option>
|
|
</select>
|
|
<input type="date" class="form-control" name="mitigation_due_date[]">
|
|
</div>
|
|
<textarea class="form-control mb-2" name="mitigation_description[]" rows="2"
|
|
placeholder="Describe the mitigation action..."></textarea>
|
|
<div class="mitigation-actions">
|
|
<button type="button" class="btn-remove-mitigation" onclick="removeMitigation(this)">
|
|
<i class="fas fa-trash me-1"></i>Remove
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
$('#mitigation-container').append(mitigationHtml);
|
|
$('#no-mitigations').hide();
|
|
}
|
|
|
|
function removeMitigation(button) {
|
|
$(button).closest('.mitigation-item').remove();
|
|
|
|
if ($('#mitigation-container .mitigation-item').length === 0) {
|
|
$('#no-mitigations').show();
|
|
}
|
|
}
|
|
|
|
function updatePreview() {
|
|
const title = $('input[name="title"]').val();
|
|
const description = $('textarea[name="description"]').val();
|
|
const department = $('select[name="department"] option:selected').text();
|
|
const riskScore = $('#risk-score-input').val();
|
|
const riskLevel = $('#risk-level-input').val();
|
|
|
|
let previewHtml = '';
|
|
|
|
if (title) {
|
|
previewHtml += `<p><strong>Title:</strong> ${title}</p>`;
|
|
}
|
|
|
|
if (description) {
|
|
previewHtml += `<p><strong>Description:</strong> ${description.substring(0, 100)}${description.length > 100 ? '...' : ''}</p>`;
|
|
}
|
|
|
|
if (department && department !== 'Select Department') {
|
|
previewHtml += `<p><strong>Department:</strong> ${department}</p>`;
|
|
}
|
|
|
|
if (riskScore && riskLevel) {
|
|
previewHtml += `<p><strong>Risk:</strong> ${riskLevel} (Score: ${riskScore})</p>`;
|
|
}
|
|
|
|
if (!previewHtml) {
|
|
previewHtml = '<p class="text-muted">Start filling the form to see a preview...</p>';
|
|
}
|
|
|
|
$('#preview-content').html(previewHtml);
|
|
}
|
|
|
|
function autoSave() {
|
|
const formData = new FormData(document.getElementById('assessment-form'));
|
|
formData.append('auto_save', 'true');
|
|
|
|
fetch(window.location.href, {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAutoSaveIndicator();
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.log('Auto-save failed:', error);
|
|
});
|
|
}
|
|
|
|
function showAutoSaveIndicator() {
|
|
$('#auto-save-indicator').addClass('show');
|
|
setTimeout(() => {
|
|
$('#auto-save-indicator').removeClass('show');
|
|
}, 2000);
|
|
}
|
|
|
|
function saveDraft() {
|
|
const form = document.getElementById('assessment-form');
|
|
const actionInput = document.createElement('input');
|
|
actionInput.type = 'hidden';
|
|
actionInput.name = 'action';
|
|
actionInput.value = 'draft';
|
|
form.appendChild(actionInput);
|
|
form.submit();
|
|
}
|
|
|
|
function previewAssessment() {
|
|
const formData = new FormData(document.getElementById('assessment-form'));
|
|
|
|
// Open preview in new window
|
|
const previewWindow = window.open('', '_blank');
|
|
previewWindow.document.write('<html><head><title>Assessment Preview</title></head><body><h1>Loading preview...</h1></body></html>');
|
|
|
|
fetch('/quality/risk-assessments/preview/', {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.text())
|
|
.then(html => {
|
|
previewWindow.document.open();
|
|
previewWindow.document.write(html);
|
|
previewWindow.document.close();
|
|
})
|
|
.catch(error => {
|
|
previewWindow.document.write('<h1>Error loading preview</h1>');
|
|
});
|
|
}
|
|
|
|
function loadTemplate() {
|
|
new bootstrap.Modal(document.getElementById('templateModal')).show();
|
|
}
|
|
|
|
function applyTemplate(templateId) {
|
|
fetch(`/quality/risk-assessments/templates/${templateId}/`, {
|
|
method: 'GET',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Populate form with template data
|
|
Object.keys(data.template).forEach(key => {
|
|
const field = document.querySelector(`[name="${key}"]`);
|
|
if (field) {
|
|
field.value = data.template[key];
|
|
}
|
|
});
|
|
|
|
showAlert('Template loaded successfully', 'success');
|
|
updatePreview();
|
|
calculateRiskScore();
|
|
} else {
|
|
showAlert('Error loading template', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error loading template', 'danger');
|
|
});
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('templateModal')).hide();
|
|
}
|
|
|
|
function saveTemplate() {
|
|
const templateName = prompt('Enter template name:');
|
|
if (templateName) {
|
|
const formData = new FormData(document.getElementById('assessment-form'));
|
|
formData.append('template_name', templateName);
|
|
|
|
fetch('/quality/risk-assessments/save-template/', {
|
|
method: 'POST',
|
|
body: formData,
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Template saved successfully', 'success');
|
|
} else {
|
|
showAlert('Error saving template', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error saving template', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function validateForm() {
|
|
const form = document.getElementById('assessment-form');
|
|
const requiredFields = form.querySelectorAll('[required]');
|
|
let isValid = true;
|
|
let errors = [];
|
|
|
|
requiredFields.forEach(field => {
|
|
if (!field.value.trim()) {
|
|
isValid = false;
|
|
errors.push(`${field.previousElementSibling.textContent.replace(' *', '')} is required`);
|
|
field.classList.add('is-invalid');
|
|
} else {
|
|
field.classList.remove('is-invalid');
|
|
}
|
|
});
|
|
|
|
if (isValid) {
|
|
showAlert('Form validation passed', 'success');
|
|
} else {
|
|
showAlert(`Validation failed: ${errors.join(', ')}`, 'danger');
|
|
}
|
|
}
|
|
|
|
function resetForm() {
|
|
if (confirm('Are you sure you want to reset the form? All unsaved changes will be lost.')) {
|
|
document.getElementById('assessment-form').reset();
|
|
$('#mitigation-container').empty();
|
|
$('#no-mitigations').show();
|
|
calculateRiskScore();
|
|
updatePreview();
|
|
showAlert('Form reset successfully', 'info');
|
|
}
|
|
}
|
|
|
|
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 %}
|
|
|