Marwan Alwali 0a037d3d9d update
2025-09-01 11:26:11 +03:00

1389 lines
50 KiB
HTML

{% extends 'base.html' %}
{% load static %}
{% block title %}{% if finding %}Edit Finding{% else %}New Finding{% endif %} - Quality Management{% endblock %}
{% block extra_css %}
<link href="{% static 'assets/plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
<link href="{% static 'assets/plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
<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;
}
.form-section {
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;
}
.form-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 1rem;
}
.form-row.single {
grid-template-columns: 1fr;
}
.form-group {
margin-bottom: 1rem;
}
.form-label {
font-weight: 600;
color: #495057;
margin-bottom: 0.5rem;
}
.form-label.required::after {
content: ' *';
color: #dc3545;
}
.form-control, .form-select {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 0.75rem;
transition: all 0.2s;
}
.form-control:focus, .form-select:focus {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
.form-text {
font-size: 0.875rem;
color: #6c757d;
margin-top: 0.25rem;
}
.severity-selector {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0.5rem;
}
.severity-option {
padding: 1rem;
border: 2px solid #dee2e6;
border-radius: 0.375rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
background: white;
}
.severity-option:hover {
border-color: #dc3545;
}
.severity-option.selected {
border-color: #dc3545;
background: #fff5f5;
color: #dc3545;
}
.severity-low.selected { border-color: #28a745; background: #e8f5e8; color: #2e7d32; }
.severity-medium.selected { border-color: #ffc107; background: #fff3e0; color: #f57c00; }
.severity-high.selected { border-color: #fd7e14; background: #ffebee; color: #d32f2f; }
.severity-critical.selected { border-color: #dc3545; background: #f8d7da; color: #721c24; }
.category-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 0.5rem;
}
.category-option {
padding: 0.75rem;
border: 2px solid #dee2e6;
border-radius: 0.375rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
background: white;
font-size: 0.875rem;
}
.category-option:hover {
border-color: #dc3545;
}
.category-option.selected {
border-color: #dc3545;
background: #fff5f5;
color: #dc3545;
}
.category-clinical.selected { border-color: #1976d2; background: #e3f2fd; color: #1976d2; }
.category-safety.selected { border-color: #d32f2f; background: #ffebee; color: #d32f2f; }
.category-compliance.selected { border-color: #7b1fa2; background: #f3e5f5; color: #7b1fa2; }
.category-process.selected { border-color: #2e7d32; background: #e8f5e8; color: #2e7d32; }
.category-documentation.selected { border-color: #f57c00; background: #fff3e0; color: #f57c00; }
.location-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 0.75rem;
}
.location-option {
padding: 0.75rem;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
text-align: center;
cursor: pointer;
transition: all 0.2s;
background: white;
font-size: 0.875rem;
}
.location-option:hover {
border-color: #dc3545;
background: #f8f9fa;
}
.location-option.selected {
border-color: #dc3545;
background: #fff5f5;
color: #dc3545;
}
.risk-assessment {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1rem;
background: #f8f9fa;
}
.risk-factors {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 0.5rem;
margin-top: 0.75rem;
}
.risk-factor {
display: flex;
align-items: center;
gap: 0.5rem;
}
.risk-factor input[type="checkbox"] {
margin: 0;
}
.risk-factor label {
margin: 0;
font-size: 0.875rem;
color: #495057;
cursor: pointer;
}
.evidence-upload {
border: 2px dashed #dee2e6;
border-radius: 0.375rem;
padding: 2rem;
text-align: center;
transition: all 0.2s;
cursor: pointer;
}
.evidence-upload:hover {
border-color: #dc3545;
background: #fff5f5;
}
.evidence-upload.dragover {
border-color: #dc3545;
background: #fff5f5;
}
.upload-icon {
font-size: 2rem;
color: #dc3545;
margin-bottom: 1rem;
}
.upload-text {
color: #495057;
margin-bottom: 0.5rem;
}
.upload-hint {
font-size: 0.875rem;
color: #6c757d;
}
.uploaded-files {
margin-top: 1rem;
}
.file-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
margin-bottom: 0.5rem;
background: white;
}
.file-icon {
width: 32px;
height: 32px;
border-radius: 0.25rem;
background: #dc3545;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.875rem;
}
.file-info {
flex: 1;
}
.file-name {
font-weight: 600;
color: #495057;
margin-bottom: 0.25rem;
}
.file-meta {
font-size: 0.75rem;
color: #6c757d;
}
.file-remove {
width: 24px;
height: 24px;
border: none;
background: #dc3545;
color: white;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
}
.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;
}
.btn-group {
display: flex;
gap: 0.5rem;
}
.auto-save-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
color: #6c757d;
font-size: 0.875rem;
}
.auto-save-indicator.saving {
color: #ffc107;
}
.auto-save-indicator.saved {
color: #28a745;
}
.validation-errors {
background: #f8d7da;
border: 1px solid #f5c6cb;
border-radius: 0.375rem;
padding: 1rem;
margin-bottom: 1rem;
}
.validation-errors ul {
margin: 0;
padding-left: 1.5rem;
}
.validation-errors li {
color: #721c24;
}
.impact-assessment {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
}
.impact-item {
text-align: center;
padding: 1rem;
border: 1px solid #dee2e6;
border-radius: 0.375rem;
background: white;
}
.impact-icon {
font-size: 1.5rem;
color: #dc3545;
margin-bottom: 0.5rem;
}
.impact-label {
font-size: 0.875rem;
color: #495057;
font-weight: 600;
margin-bottom: 0.5rem;
}
.impact-rating {
display: flex;
justify-content: center;
gap: 0.25rem;
}
.rating-star {
width: 16px;
height: 16px;
color: #dee2e6;
cursor: pointer;
transition: color 0.2s;
}
.rating-star.active {
color: #ffc107;
}
.timeline-preview {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
padding: 1rem;
background: #f8f9fa;
}
.timeline-item {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.75rem;
}
.timeline-item:last-child {
margin-bottom: 0;
}
.timeline-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: #dee2e6;
flex-shrink: 0;
}
.timeline-dot.active {
background: #dc3545;
}
.timeline-text {
font-size: 0.875rem;
color: #495057;
}
@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;
}
.form-row {
grid-template-columns: 1fr;
}
.severity-selector {
grid-template-columns: repeat(2, 1fr);
}
.category-selector {
grid-template-columns: 1fr;
}
.location-selector {
grid-template-columns: 1fr;
}
.form-actions {
flex-direction: column;
align-items: stretch;
}
.btn-group {
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:finding_list' %}">Findings</a></li>
<li class="breadcrumb-item active">{% if finding %}Edit{% else %}New{% endif %} Finding</li>
</ol>
<h1 class="page-header mb-0">
<i class="fas fa-search me-2"></i>{% if finding %}Edit Finding{% else %}Report New Finding{% endif %}
</h1>
</div>
<div class="ms-auto">
<a href="{% url 'quality:finding_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to Findings
</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 finding %}{{ finding.title }}{% else %}Report Quality Finding{% endif %}</h2>
<p class="mb-0">{% if finding %}Update finding details and resolution progress{% else %}Document quality issues, safety concerns, and improvement opportunities{% endif %}</p>
</div>
<div class="col-md-4 text-md-end">
<div class="auto-save-indicator" id="auto-save-status">
<i class="fas fa-save"></i>
<span>Auto-save enabled</span>
</div>
</div>
</div>
</div>
<form method="post" id="finding-form" enctype="multipart/form-data">
{% csrf_token %}
<!-- Validation Errors -->
{% if form.errors %}
<div class="validation-errors">
<h6><i class="fas fa-exclamation-triangle me-2"></i>Please correct the following errors:</h6>
<ul>
{% for field, errors in form.errors.items %}
{% for error in errors %}
<li>{{ field|title }}: {{ error }}</li>
{% endfor %}
{% endfor %}
</ul>
</div>
{% endif %}
<div class="form-layout">
<!-- Main Form Content -->
<div class="form-main">
<!-- Basic Information -->
<div class="form-section">
<div class="section-header">
<i class="fas fa-info-circle"></i>
Basic Information
</div>
<div class="section-content">
<div class="form-row single">
<div class="form-group">
<label class="form-label required">Finding Title</label>
<input type="text" class="form-control" name="title"
value="{{ form.title.value|default:'' }}"
placeholder="Brief, descriptive title of the finding..." required>
<div class="form-text">A clear, concise title that summarizes the finding</div>
</div>
</div>
<div class="form-row single">
<div class="form-group">
<label class="form-label required">Description</label>
<textarea class="form-control" name="description" rows="4"
placeholder="Detailed description of the finding, including what was observed, when, and where..." required>{{ form.description.value|default:'' }}</textarea>
<div class="form-text">Provide comprehensive details about the finding</div>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Finding ID</label>
<input type="text" class="form-control" name="finding_id"
value="{{ form.finding_id.value|default:'' }}"
placeholder="Auto-generated if left blank">
<div class="form-text">Unique identifier for tracking</div>
</div>
<div class="form-group">
<label class="form-label required">Department</label>
<select class="form-select" name="department" required>
<option value="">Select Department</option>
{% for dept in departments %}
<option value="{{ dept.id }}" {% if form.department.value == dept.id %}selected{% endif %}>
{{ dept.name }}
</option>
{% endfor %}
</select>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Date Identified</label>
<input type="date" class="form-control" name="date_identified"
value="{{ form.date_identified.value|date:'Y-m-d'|default:'' }}">
</div>
<div class="form-group">
<label class="form-label">Due Date</label>
<input type="date" class="form-control" name="due_date"
value="{{ form.due_date.value|date:'Y-m-d'|default:'' }}">
</div>
</div>
</div>
</div>
<!-- Classification -->
<div class="form-section">
<div class="section-header">
<i class="fas fa-tags"></i>
Classification
</div>
<div class="section-content">
<div class="form-group">
<label class="form-label required">Severity Level</label>
<div class="severity-selector">
<div class="severity-option severity-low" data-severity="low">
<i class="fas fa-arrow-down mb-2"></i>
<div><strong>Low</strong></div>
<div style="font-size: 0.75rem;">Minor impact</div>
</div>
<div class="severity-option severity-medium" data-severity="medium">
<i class="fas fa-minus mb-2"></i>
<div><strong>Medium</strong></div>
<div style="font-size: 0.75rem;">Moderate impact</div>
</div>
<div class="severity-option severity-high" data-severity="high">
<i class="fas fa-arrow-up mb-2"></i>
<div><strong>High</strong></div>
<div style="font-size: 0.75rem;">Significant impact</div>
</div>
<div class="severity-option severity-critical" data-severity="critical">
<i class="fas fa-exclamation-triangle mb-2"></i>
<div><strong>Critical</strong></div>
<div style="font-size: 0.75rem;">Immediate action</div>
</div>
</div>
<input type="hidden" name="severity" id="severity-input" value="{{ form.severity.value|default:'medium' }}">
</div>
<div class="form-group">
<label class="form-label required">Category</label>
<div class="category-selector">
<div class="category-option category-clinical" data-category="clinical">
<i class="fas fa-user-md mb-1"></i>
<div>Clinical</div>
</div>
<div class="category-option category-safety" data-category="safety">
<i class="fas fa-shield-alt mb-1"></i>
<div>Safety</div>
</div>
<div class="category-option category-compliance" data-category="compliance">
<i class="fas fa-gavel mb-1"></i>
<div>Compliance</div>
</div>
<div class="category-option category-process" data-category="process">
<i class="fas fa-cogs mb-1"></i>
<div>Process</div>
</div>
<div class="category-option category-documentation" data-category="documentation">
<i class="fas fa-file-alt mb-1"></i>
<div>Documentation</div>
</div>
</div>
<input type="hidden" name="category" id="category-input" value="{{ form.category.value|default:'clinical' }}">
</div>
</div>
</div>
<!-- Location & Context -->
<div class="form-section">
<div class="section-header">
<i class="fas fa-map-marker-alt"></i>
Location & Context
</div>
<div class="section-content">
<div class="form-group">
<label class="form-label">Location</label>
<div class="location-selector">
<div class="location-option" data-location="Emergency Department">
<i class="fas fa-ambulance mb-1"></i>
<div>Emergency Dept</div>
</div>
<div class="location-option" data-location="Operating Theatre">
<i class="fas fa-procedures mb-1"></i>
<div>Operating Theatre</div>
</div>
<div class="location-option" data-location="ICU">
<i class="fas fa-heartbeat mb-1"></i>
<div>ICU</div>
</div>
<div class="location-option" data-location="General Ward">
<i class="fas fa-bed mb-1"></i>
<div>General Ward</div>
</div>
<div class="location-option" data-location="Laboratory">
<i class="fas fa-flask mb-1"></i>
<div>Laboratory</div>
</div>
<div class="location-option" data-location="Pharmacy">
<i class="fas fa-pills mb-1"></i>
<div>Pharmacy</div>
</div>
<div class="location-option" data-location="Radiology">
<i class="fas fa-x-ray mb-1"></i>
<div>Radiology</div>
</div>
<div class="location-option" data-location="Other">
<i class="fas fa-building mb-1"></i>
<div>Other</div>
</div>
</div>
<input type="text" class="form-control mt-2" name="location"
value="{{ form.location.value|default:'' }}"
placeholder="Specify location or select from above">
</div>
<div class="form-row">
<div class="form-group">
<label class="form-label">Circumstances</label>
<textarea class="form-control" name="circumstances" rows="3"
placeholder="Describe the circumstances surrounding the finding...">{{ form.circumstances.value|default:'' }}</textarea>
</div>
</div>
</div>
</div>
<!-- Root Cause Analysis -->
<div class="form-section">
<div class="section-header">
<i class="fas fa-search-plus"></i>
Root Cause Analysis
</div>
<div class="section-content">
<div class="form-group">
<label class="form-label">Immediate Cause</label>
<textarea class="form-control" name="immediate_cause" rows="3"
placeholder="What directly caused this finding?">{{ form.immediate_cause.value|default:'' }}</textarea>
</div>
<div class="form-group">
<label class="form-label">Root Cause</label>
<textarea class="form-control" name="root_cause" rows="3"
placeholder="What is the underlying root cause?">{{ form.root_cause.value|default:'' }}</textarea>
</div>
<div class="form-group">
<label class="form-label">Contributing Factors</label>
<div class="risk-factors">
<div class="risk-factor">
<input type="checkbox" id="factor-communication" name="risk_factors" value="communication">
<label for="factor-communication">Communication Issues</label>
</div>
<div class="risk-factor">
<input type="checkbox" id="factor-training" name="risk_factors" value="training">
<label for="factor-training">Training Deficiency</label>
</div>
<div class="risk-factor">
<input type="checkbox" id="factor-equipment" name="risk_factors" value="equipment">
<label for="factor-equipment">Equipment Issues</label>
</div>
<div class="risk-factor">
<input type="checkbox" id="factor-process" name="risk_factors" value="process">
<label for="factor-process">Process Breakdown</label>
</div>
<div class="risk-factor">
<input type="checkbox" id="factor-workload" name="risk_factors" value="workload">
<label for="factor-workload">Workload Issues</label>
</div>
<div class="risk-factor">
<input type="checkbox" id="factor-environment" name="risk_factors" value="environment">
<label for="factor-environment">Environmental Factors</label>
</div>
</div>
</div>
</div>
</div>
<!-- Evidence & Documentation -->
<div class="form-section">
<div class="section-header">
<i class="fas fa-camera"></i>
Evidence & Documentation
</div>
<div class="section-content">
<div class="form-group">
<label class="form-label">Upload Evidence</label>
<div class="evidence-upload" id="evidence-upload">
<div class="upload-icon">
<i class="fas fa-cloud-upload-alt"></i>
</div>
<div class="upload-text">
<strong>Click to upload</strong> or drag and drop files here
</div>
<div class="upload-hint">
Photos, documents, reports (Max 10MB each)
</div>
<input type="file" id="evidence-files" multiple accept="image/*,.pdf,.doc,.docx" style="display: none;">
</div>
<div class="uploaded-files" id="uploaded-files"></div>
</div>
<div class="form-group">
<label class="form-label">Additional Notes</label>
<textarea class="form-control" name="additional_notes" rows="4"
placeholder="Any additional information, observations, or context...">{{ form.additional_notes.value|default:'' }}</textarea>
</div>
</div>
</div>
</div>
<!-- Sidebar -->
<div class="form-sidebar">
<!-- Assignment -->
<div class="form-section">
<div class="section-header">
<i class="fas fa-user-check"></i>
Assignment
</div>
<div class="section-content">
<div class="form-group">
<label class="form-label">Assign To</label>
<select class="form-select" name="assigned_to">
<option value="">Select User</option>
{% for user in assignable_users %}
<option value="{{ user.id }}" {% if form.assigned_to.value == user.id %}selected{% endif %}>
{{ user.get_full_name }}
</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<label class="form-label">Priority</label>
<select class="form-select" name="priority">
<option value="low" {% if form.priority.value == 'low' %}selected{% endif %}>Low Priority</option>
<option value="medium" {% if form.priority.value == 'medium' %}selected{% endif %}>Medium Priority</option>
<option value="high" {% if form.priority.value == 'high' %}selected{% endif %}>High Priority</option>
<option value="urgent" {% if form.priority.value == 'urgent' %}selected{% endif %}>Urgent</option>
</select>
</div>
<div class="form-group">
<label class="form-label">Status</label>
<select class="form-select" name="status">
<option value="open" {% if form.status.value == 'open' %}selected{% endif %}>Open</option>
<option value="investigating" {% if form.status.value == 'investigating' %}selected{% endif %}>Investigating</option>
<option value="resolved" {% if form.status.value == 'resolved' %}selected{% endif %}>Resolved</option>
<option value="closed" {% if form.status.value == 'closed' %}selected{% endif %}>Closed</option>
</select>
</div>
</div>
</div>
<!-- Impact Assessment -->
<div class="form-section">
<div class="section-header">
<i class="fas fa-chart-line"></i>
Impact Assessment
</div>
<div class="section-content">
<div class="impact-assessment">
<div class="impact-item">
<div class="impact-icon">
<i class="fas fa-users"></i>
</div>
<div class="impact-label">Patient Impact</div>
<div class="impact-rating" data-rating="patient_impact">
<i class="fas fa-star rating-star" data-value="1"></i>
<i class="fas fa-star rating-star" data-value="2"></i>
<i class="fas fa-star rating-star" data-value="3"></i>
<i class="fas fa-star rating-star" data-value="4"></i>
<i class="fas fa-star rating-star" data-value="5"></i>
</div>
</div>
<div class="impact-item">
<div class="impact-icon">
<i class="fas fa-dollar-sign"></i>
</div>
<div class="impact-label">Financial Impact</div>
<div class="impact-rating" data-rating="financial_impact">
<i class="fas fa-star rating-star" data-value="1"></i>
<i class="fas fa-star rating-star" data-value="2"></i>
<i class="fas fa-star rating-star" data-value="3"></i>
<i class="fas fa-star rating-star" data-value="4"></i>
<i class="fas fa-star rating-star" data-value="5"></i>
</div>
</div>
<div class="impact-item">
<div class="impact-icon">
<i class="fas fa-building"></i>
</div>
<div class="impact-label">Operational Impact</div>
<div class="impact-rating" data-rating="operational_impact">
<i class="fas fa-star rating-star" data-value="1"></i>
<i class="fas fa-star rating-star" data-value="2"></i>
<i class="fas fa-star rating-star" data-value="3"></i>
<i class="fas fa-star rating-star" data-value="4"></i>
<i class="fas fa-star rating-star" data-value="5"></i>
</div>
</div>
<div class="impact-item">
<div class="impact-icon">
<i class="fas fa-balance-scale"></i>
</div>
<div class="impact-label">Regulatory Impact</div>
<div class="impact-rating" data-rating="regulatory_impact">
<i class="fas fa-star rating-star" data-value="1"></i>
<i class="fas fa-star rating-star" data-value="2"></i>
<i class="fas fa-star rating-star" data-value="3"></i>
<i class="fas fa-star rating-star" data-value="4"></i>
<i class="fas fa-star rating-star" data-value="5"></i>
</div>
</div>
</div>
<input type="hidden" name="patient_impact" id="patient_impact" value="{{ form.patient_impact.value|default:1 }}">
<input type="hidden" name="financial_impact" id="financial_impact" value="{{ form.financial_impact.value|default:1 }}">
<input type="hidden" name="operational_impact" id="operational_impact" value="{{ form.operational_impact.value|default:1 }}">
<input type="hidden" name="regulatory_impact" id="regulatory_impact" value="{{ form.regulatory_impact.value|default:1 }}">
</div>
</div>
<!-- Resolution Timeline -->
<div class="form-section">
<div class="section-header">
<i class="fas fa-timeline"></i>
Resolution Timeline
</div>
<div class="section-content">
<div class="timeline-preview">
<div class="timeline-item">
<div class="timeline-dot active"></div>
<div class="timeline-text">Finding Reported</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-text">Investigation Started</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-text">Root Cause Identified</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-text">Corrective Actions</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-text">Resolution Verified</div>
</div>
</div>
{% if finding %}
<div class="mt-3">
<label class="form-label">Resolution Progress</label>
<div class="progress-container">
<div class="progress-header">
<span class="progress-percentage" id="progress-value">{{ form.resolution_progress.value|default:0 }}%</span>
</div>
<input type="range" class="form-range" name="resolution_progress"
value="{{ form.resolution_progress.value|default:0 }}"
min="0" max="100" step="5" id="progress-slider">
</div>
</div>
{% endif %}
</div>
</div>
<!-- Tags & Keywords -->
<div class="form-section">
<div class="section-header">
<i class="fas fa-tags"></i>
Tags & Keywords
</div>
<div class="section-content">
<div class="form-group">
<label class="form-label">Tags</label>
<input type="text" class="form-control" name="tags"
value="{{ form.tags.value|default:'' }}"
placeholder="Enter tags separated by commas">
<div class="form-text">Use tags to categorize and search findings</div>
</div>
<div class="form-group">
<label class="form-label">Keywords</label>
<input type="text" class="form-control" name="keywords"
value="{{ form.keywords.value|default:'' }}"
placeholder="Key terms for searching">
</div>
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="form-actions">
<div class="auto-save-indicator" id="auto-save-indicator">
<i class="fas fa-save"></i>
<span>All changes saved</span>
</div>
<div class="btn-group">
<a href="{% url 'quality:finding_list' %}" class="btn btn-outline-secondary">
<i class="fas fa-times me-1"></i>Cancel
</a>
<button type="button" class="btn btn-outline-info" onclick="saveDraft()">
<i class="fas fa-save me-1"></i>Save Draft
</button>
<button type="submit" class="btn btn-danger">
<i class="fas fa-check me-1"></i>{% if finding %}Update Finding{% else %}Report Finding{% endif %}
</button>
</div>
</div>
</form>
</div>
{% endblock %}
{% block extra_js %}
<script src="{% static 'assets/plugins/select2/dist/js/select2.min.js' %}"></script>
<script src="{% static 'assets/plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
<script>
let autoSaveTimer;
let uploadedFiles = [];
$(document).ready(function() {
// Initialize form
initializeForm();
// Set up auto-save
setupAutoSave();
// Initialize selectors
initializeSeveritySelector();
initializeCategorySelector();
initializeLocationSelector();
initializeImpactRatings();
// Initialize file upload
initializeFileUpload();
// Set initial values
setInitialValues();
});
function initializeForm() {
// Initialize Select2 for dropdowns
$('.form-select').select2({
width: '100%',
placeholder: function() {
return $(this).data('placeholder') || 'Select an option...';
}
});
// Initialize date pickers
$('input[type="date"]').datepicker({
format: 'yyyy-mm-dd',
autoclose: true,
todayHighlight: true
});
// Initialize progress slider
$('#progress-slider').on('input', function() {
const value = $(this).val();
$('#progress-value').text(value + '%');
});
}
function setupAutoSave() {
// Auto-save form data every 30 seconds
$('#finding-form input, #finding-form textarea, #finding-form select').on('input change', function() {
clearTimeout(autoSaveTimer);
$('#auto-save-indicator').removeClass('saved').addClass('saving');
$('#auto-save-indicator span').text('Saving...');
autoSaveTimer = setTimeout(function() {
saveDraft();
}, 2000);
});
}
function initializeSeveritySelector() {
$('.severity-option').click(function() {
$('.severity-option').removeClass('selected');
$(this).addClass('selected');
$('#severity-input').val($(this).data('severity'));
});
}
function initializeCategorySelector() {
$('.category-option').click(function() {
$('.category-option').removeClass('selected');
$(this).addClass('selected');
$('#category-input').val($(this).data('category'));
});
}
function initializeLocationSelector() {
$('.location-option').click(function() {
$('.location-option').removeClass('selected');
$(this).addClass('selected');
$('input[name="location"]').val($(this).data('location'));
});
}
function initializeImpactRatings() {
$('.impact-rating').each(function() {
const ratingType = $(this).data('rating');
const currentValue = $(`#${ratingType}`).val() || 1;
// Set initial rating
$(this).find('.rating-star').each(function(index) {
if (index < currentValue) {
$(this).addClass('active');
}
});
// Handle rating clicks
$(this).find('.rating-star').click(function() {
const value = $(this).data('value');
const container = $(this).closest('.impact-rating');
const ratingType = container.data('rating');
// Update visual rating
container.find('.rating-star').removeClass('active');
container.find('.rating-star').each(function(index) {
if (index < value) {
$(this).addClass('active');
}
});
// Update hidden input
$(`#${ratingType}`).val(value);
});
});
}
function initializeFileUpload() {
const uploadArea = document.getElementById('evidence-upload');
const fileInput = document.getElementById('evidence-files');
// Click to upload
uploadArea.addEventListener('click', () => {
fileInput.click();
});
// Drag and drop
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const files = Array.from(e.dataTransfer.files);
handleFileUpload(files);
});
// File input change
fileInput.addEventListener('change', (e) => {
const files = Array.from(e.target.files);
handleFileUpload(files);
});
}
function handleFileUpload(files) {
files.forEach(file => {
if (file.size > 10 * 1024 * 1024) { // 10MB limit
showAlert(`File ${file.name} is too large (max 10MB)`, 'warning');
return;
}
const fileId = Date.now() + Math.random();
uploadedFiles.push({
id: fileId,
file: file,
name: file.name,
size: file.size,
type: file.type
});
addFileToDisplay(fileId, file);
});
}
function addFileToDisplay(fileId, file) {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.innerHTML = `
<div class="file-icon">
<i class="fas fa-file-${getFileIcon(file.type)}"></i>
</div>
<div class="file-info">
<div class="file-name">${file.name}</div>
<div class="file-meta">${formatFileSize(file.size)}${file.type}</div>
</div>
<button type="button" class="file-remove" onclick="removeFile(${fileId})">
<i class="fas fa-times"></i>
</button>
`;
document.getElementById('uploaded-files').appendChild(fileItem);
}
function removeFile(fileId) {
uploadedFiles = uploadedFiles.filter(f => f.id !== fileId);
// Remove from display
const fileItems = document.querySelectorAll('.file-item');
fileItems.forEach(item => {
const removeBtn = item.querySelector('.file-remove');
if (removeBtn && removeBtn.onclick.toString().includes(fileId)) {
item.remove();
}
});
}
function getFileIcon(mimeType) {
if (mimeType.startsWith('image/')) return 'image';
if (mimeType.includes('pdf')) return 'pdf';
if (mimeType.includes('word') || mimeType.includes('document')) return 'word';
if (mimeType.includes('excel') || mimeType.includes('spreadsheet')) return 'excel';
return 'alt';
}
function formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
function setInitialValues() {
// Set initial severity
const initialSeverity = $('#severity-input').val();
$(`.severity-option[data-severity="${initialSeverity}"]`).addClass('selected');
// Set initial category
const initialCategory = $('#category-input').val();
$(`.category-option[data-category="${initialCategory}"]`).addClass('selected');
// Set initial location if exists
const initialLocation = $('input[name="location"]').val();
if (initialLocation) {
$(`.location-option[data-location="${initialLocation}"]`).addClass('selected');
}
}
function saveDraft() {
const formData = new FormData($('#finding-form')[0]);
formData.append('save_draft', 'true');
// Add uploaded files
uploadedFiles.forEach(fileObj => {
formData.append('evidence_files', fileObj.file);
});
fetch(window.location.href, {
method: 'POST',
headers: {
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
$('#auto-save-indicator').removeClass('saving').addClass('saved');
$('#auto-save-indicator span').text('Draft saved');
setTimeout(() => {
$('#auto-save-indicator').removeClass('saved');
$('#auto-save-indicator span').text('Auto-save enabled');
}, 3000);
}
})
.catch(error => {
console.error('Auto-save error:', error);
$('#auto-save-indicator').removeClass('saving');
$('#auto-save-indicator span').text('Save failed');
});
}
// Form validation
$('#finding-form').on('submit', function(e) {
let isValid = true;
const errors = [];
// Validate required fields
if (!$('input[name="title"]').val().trim()) {
errors.push('Finding title is required');
isValid = false;
}
if (!$('textarea[name="description"]').val().trim()) {
errors.push('Finding description is required');
isValid = false;
}
if (!$('select[name="department"]').val()) {
errors.push('Department selection is required');
isValid = false;
}
if (!$('#severity-input').val()) {
errors.push('Severity level is required');
isValid = false;
}
if (!$('#category-input').val()) {
errors.push('Category selection is required');
isValid = false;
}
// Validate date range
const dateIdentified = new Date($('input[name="date_identified"]').val());
const dueDate = new Date($('input[name="due_date"]').val());
if (dateIdentified && dueDate && dateIdentified >= dueDate) {
errors.push('Due date must be after the date identified');
isValid = false;
}
if (!isValid) {
e.preventDefault();
showAlert('Please correct the following errors:\n' + errors.join('\n'), 'danger');
} else {
// Add uploaded files to form
const formData = new FormData(this);
uploadedFiles.forEach(fileObj => {
formData.append('evidence_files', fileObj.file);
});
}
});
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 %}