531 lines
24 KiB
HTML
531 lines
24 KiB
HTML
{% extends "base.html" %}
|
|
{% load static %}
|
|
|
|
{% block title %}{{ object.name }} - Quality Indicator - {{ block.super }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid">
|
|
<!-- Page Header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h1 class="h3 mb-1">
|
|
<i class="fas fa-chart-line me-2"></i>{{ object.name }}
|
|
</h1>
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb mb-0">
|
|
<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:quality_indicator_list' %}">Indicators</a></li>
|
|
<li class="breadcrumb-item active">{{ object.name|truncatechars:30 }}</li>
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
<div class="btn-group">
|
|
<a href="{% url 'quality:quality_indicator_list' %}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-arrow-left me-2"></i>Back to List
|
|
</a>
|
|
<a href="{% url 'quality:quality_indicator_update' object.pk %}" class="btn btn-primary">
|
|
<i class="fas fa-edit me-2"></i>Edit
|
|
</a>
|
|
<div class="btn-group">
|
|
<button type="button" class="btn btn-outline-info dropdown-toggle" data-bs-toggle="dropdown">
|
|
<i class="fas fa-ellipsis-v"></i>
|
|
</button>
|
|
<ul class="dropdown-menu">
|
|
<li><a class="dropdown-item" href="{% url 'quality:quality_measurement_create' %}?indicator={{ object.pk }}">
|
|
<i class="fas fa-plus me-2"></i>Add Measurement
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="exportData()">
|
|
<i class="fas fa-download me-2"></i>Export Data
|
|
</a></li>
|
|
<li><a class="dropdown-item" href="#" onclick="printIndicator()">
|
|
<i class="fas fa-print me-2"></i>Print
|
|
</a></li>
|
|
<li><hr class="dropdown-divider"></li>
|
|
<li><a class="dropdown-item text-danger" href="{% url 'quality:quality_indicator_delete' object.pk %}">
|
|
<i class="fas fa-trash me-2"></i>Delete
|
|
</a></li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<!-- Main Content -->
|
|
<div class="col-lg-8">
|
|
<!-- Indicator Overview -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-info-circle me-2"></i>Indicator Overview
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label text-muted">Description</label>
|
|
<p class="mb-0">{{ object.description }}</p>
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<label class="form-label text-muted">Category</label>
|
|
<p class="mb-0">
|
|
<span class="badge bg-primary">{{ object.get_category_display }}</span>
|
|
</p>
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<label class="form-label text-muted">Type</label>
|
|
<p class="mb-0">
|
|
<span class="badge bg-info">{{ object.get_type_display }}</span>
|
|
</p>
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<label class="form-label text-muted">Measurement Unit</label>
|
|
<p class="mb-0">{{ object.measurement_unit }}</p>
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<label class="form-label text-muted">Frequency</label>
|
|
<p class="mb-0">{{ object.get_frequency_display }}</p>
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<label class="form-label text-muted">Status</label>
|
|
<p class="mb-0">
|
|
{% if object.is_active %}
|
|
<span class="badge bg-success">Active</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">Inactive</span>
|
|
{% endif %}
|
|
{% if object.regulatory_requirement %}
|
|
<span class="badge bg-warning text-dark ms-1">Regulatory</span>
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
<div class="col-md-3 mb-3">
|
|
<label class="form-label text-muted">Current Status</label>
|
|
<p class="mb-0">
|
|
{% if object.current_status == 'within_target' %}
|
|
<span class="badge bg-success">Within Target</span>
|
|
{% elif object.current_status == 'warning' %}
|
|
<span class="badge bg-warning text-dark">Warning</span>
|
|
{% elif object.current_status == 'critical' %}
|
|
<span class="badge bg-danger">Critical</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">No Data</span>
|
|
{% endif %}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Targets and Thresholds -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-bullseye me-2"></i>Targets and Thresholds
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-4">
|
|
<div class="text-center p-3 border rounded" style="background: linear-gradient(135deg, #28a745 0%, #20c997 100%);">
|
|
<div class="text-white">
|
|
<h4 class="mb-1">{{ object.target_value }}</h4>
|
|
<small class="opacity-75">Target Value</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="text-center p-3 border rounded" style="background: linear-gradient(135deg, #ffc107 0%, #fd7e14 100%);">
|
|
<div class="text-white">
|
|
<h4 class="mb-1">{{ object.threshold_warning }}</h4>
|
|
<small class="opacity-75">Warning Threshold</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="text-center p-3 border rounded" style="background: linear-gradient(135deg, #dc3545 0%, #e83e8c 100%);">
|
|
<div class="text-white">
|
|
<h4 class="mb-1">{{ object.threshold_critical }}</h4>
|
|
<small class="opacity-75">Critical Threshold</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Calculation Method -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-calculator me-2"></i>Calculation Method
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label text-muted">Calculation Method</label>
|
|
<div class="bg-light p-3 rounded">
|
|
<pre class="mb-0">{{ object.calculation_method }}</pre>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label class="form-label text-muted">Data Source</label>
|
|
<p class="mb-0">{{ object.data_source }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Measurements -->
|
|
<div class="card">
|
|
<div class="card-header d-flex justify-content-between align-items-center">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-chart-bar me-2"></i>Recent Measurements
|
|
</h5>
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-outline-secondary" onclick="refreshMeasurements()">
|
|
<i class="fas fa-sync-alt"></i>
|
|
</button>
|
|
<a href="{% url 'quality:quality_measurement_create' %}?indicator={{ object.pk }}" class="btn btn-outline-primary">
|
|
<i class="fas fa-plus me-1"></i>Add Measurement
|
|
</a>
|
|
</div>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
{% if object.measurements.all %}
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Date</th>
|
|
<th>Value</th>
|
|
<th>Status</th>
|
|
<th>Numerator</th>
|
|
<th>Denominator</th>
|
|
<th>Verified By</th>
|
|
<th width="100">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for measurement in object.measurements.all|slice:":10" %}
|
|
<tr>
|
|
<td>{{ measurement.measurement_date|date:"M d, Y" }}</td>
|
|
<td>
|
|
<strong>{{ measurement.value }}</strong>
|
|
<small class="text-muted">{{ object.measurement_unit }}</small>
|
|
</td>
|
|
<td>
|
|
{% if measurement.status == 'within_target' %}
|
|
<span class="badge bg-success">Within Target</span>
|
|
{% elif measurement.status == 'warning' %}
|
|
<span class="badge bg-warning text-dark">Warning</span>
|
|
{% elif measurement.status == 'critical' %}
|
|
<span class="badge bg-danger">Critical</span>
|
|
{% elif measurement.status == 'improving' %}
|
|
<span class="badge bg-info">Improving</span>
|
|
{% elif measurement.status == 'declining' %}
|
|
<span class="badge bg-warning text-dark">Declining</span>
|
|
{% else %}
|
|
<span class="badge bg-secondary">{{ measurement.get_status_display }}</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>{{ measurement.numerator|default:"-" }}</td>
|
|
<td>{{ measurement.denominator|default:"-" }}</td>
|
|
<td>
|
|
{% if measurement.verified_by %}
|
|
<div class="d-flex align-items-center">
|
|
<div class="avatar-circle bg-success text-white me-2">
|
|
{{ measurement.verified_by.first_name.0 }}{{ measurement.verified_by.last_name.0 }}
|
|
</div>
|
|
<div>
|
|
<div class="fw-semibold">{{ measurement.verified_by.get_full_name }}</div>
|
|
<small class="text-muted">{{ measurement.verified_at|date:"M d, Y" }}</small>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<span class="text-muted">Not verified</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<div class="btn-group btn-group-sm">
|
|
<a href="{% url 'quality:quality_measurement_detail' measurement.pk %}"
|
|
class="btn btn-outline-primary" title="View Details">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
{% if not measurement.verified_by %}
|
|
<button class="btn btn-outline-success"
|
|
title="Verify"
|
|
onclick="verifyMeasurement({{ measurement.pk }})">
|
|
<i class="fas fa-check"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% if object.measurements.count > 10 %}
|
|
<div class="card-footer text-center">
|
|
<a href="{% url 'quality:quality_measurement_list' %}?indicator={{ object.pk }}" class="btn btn-outline-primary btn-sm">
|
|
<i class="fas fa-list me-2"></i>View All Measurements ({{ object.measurements.count }})
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="text-center py-5">
|
|
<i class="fas fa-chart-bar fa-3x text-muted mb-3"></i>
|
|
<h5 class="text-muted">No Measurements</h5>
|
|
<p class="text-muted mb-3">No measurements have been recorded for this indicator yet.</p>
|
|
<a href="{% url 'quality:quality_measurement_create' %}?indicator={{ object.pk }}" class="btn btn-primary">
|
|
<i class="fas fa-plus me-2"></i>Add First Measurement
|
|
</a>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="col-lg-4">
|
|
<!-- Quick Stats -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-chart-pie me-2"></i>Quick Stats
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-3">
|
|
<div class="col-6">
|
|
<div class="text-center">
|
|
<h4 class="text-primary mb-1">{{ object.measurements.count }}</h4>
|
|
<small class="text-muted">Total Measurements</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="text-center">
|
|
{% if object.latest_measurement %}
|
|
<h4 class="mb-1
|
|
{% if object.current_status == 'within_target' %}text-success
|
|
{% elif object.current_status == 'warning' %}text-warning
|
|
{% elif object.current_status == 'critical' %}text-danger
|
|
{% else %}text-muted{% endif %}">
|
|
{{ object.latest_measurement.value }}
|
|
</h4>
|
|
<small class="text-muted">Latest Value</small>
|
|
{% else %}
|
|
<h4 class="text-muted mb-1">-</h4>
|
|
<small class="text-muted">No Data</small>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="text-center">
|
|
<h4 class="text-success mb-1">{{ object.measurements.filter.status='within_target'.count }}</h4>
|
|
<small class="text-muted">Within Target</small>
|
|
</div>
|
|
</div>
|
|
<div class="col-6">
|
|
<div class="text-center">
|
|
<h4 class="text-warning mb-1">{{ object.measurements.filter.status='warning'.count }}</h4>
|
|
<small class="text-muted">Warning</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Responsibility -->
|
|
<div class="card mb-4">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-user-check me-2"></i>Responsibility
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% if object.responsible_department %}
|
|
<div class="mb-3">
|
|
<label class="form-label text-muted">Department</label>
|
|
<p class="mb-0">{{ object.responsible_department.name }}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if object.responsible_user %}
|
|
<div class="mb-3">
|
|
<label class="form-label text-muted">Responsible Person</label>
|
|
<div class="d-flex align-items-center">
|
|
<div class="avatar-circle bg-primary text-white me-3">
|
|
{{ object.responsible_user.first_name.0 }}{{ object.responsible_user.last_name.0 }}
|
|
</div>
|
|
<div>
|
|
<div class="fw-semibold">{{ object.responsible_user.get_full_name }}</div>
|
|
<small class="text-muted">{{ object.responsible_user.email }}</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
{% if not object.responsible_department and not object.responsible_user %}
|
|
<p class="text-muted mb-0">No specific responsibility assigned</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Metadata -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<h5 class="mb-0">
|
|
<i class="fas fa-info me-2"></i>Metadata
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-3">
|
|
<label class="form-label text-muted">Created</label>
|
|
<p class="mb-0">{{ object.created_at|date:"M d, Y g:i A" }}</p>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label text-muted">Last Updated</label>
|
|
<p class="mb-0">{{ object.updated_at|date:"M d, Y g:i A" }}</p>
|
|
</div>
|
|
{% if object.latest_measurement %}
|
|
<div class="mb-0">
|
|
<label class="form-label text-muted">Last Measurement</label>
|
|
<p class="mb-0">{{ object.latest_measurement.measurement_date|date:"M d, Y" }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Refresh measurements
|
|
function refreshMeasurements() {
|
|
location.reload();
|
|
}
|
|
|
|
// Verify measurement
|
|
function verifyMeasurement(measurementId) {
|
|
if (confirm('Verify this measurement? This action cannot be undone.')) {
|
|
fetch(`/quality/measurements/${measurementId}/verify/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
location.reload();
|
|
} else {
|
|
alert('Error verifying measurement: ' + data.error);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Export data
|
|
function exportData() {
|
|
window.location.href = `/quality/indicators/${{{ object.pk }}}/export/`;
|
|
}
|
|
|
|
// Print indicator
|
|
function printIndicator() {
|
|
window.print();
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.avatar-circle {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: bold;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
.card {
|
|
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
|
|
border: 1px solid rgba(0, 0, 0, 0.125);
|
|
}
|
|
|
|
.card-header {
|
|
background-color: rgba(13, 110, 253, 0.1);
|
|
border-bottom: 1px solid rgba(0, 0, 0, 0.125);
|
|
}
|
|
|
|
.btn {
|
|
border-radius: 0.375rem;
|
|
transition: all 0.15s ease-in-out;
|
|
}
|
|
|
|
.btn:hover:not(:disabled) {
|
|
transform: translateY(-1px);
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
.table th {
|
|
border-top: none;
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.table-hover tbody tr:hover {
|
|
background-color: rgba(13, 110, 253, 0.05);
|
|
}
|
|
|
|
.fw-semibold {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.badge {
|
|
font-size: 0.75rem;
|
|
}
|
|
|
|
pre {
|
|
white-space: pre-wrap;
|
|
word-wrap: break-word;
|
|
font-size: 0.875rem;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.d-flex.justify-content-between {
|
|
flex-direction: column;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.btn-group {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.card-body {
|
|
padding: 1rem;
|
|
}
|
|
|
|
.table-responsive {
|
|
font-size: 0.875rem;
|
|
}
|
|
}
|
|
|
|
@media print {
|
|
.btn, .btn-group, .dropdown {
|
|
display: none !important;
|
|
}
|
|
|
|
.card {
|
|
border: 1px solid #000 !important;
|
|
box-shadow: none !important;
|
|
}
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|