1396 lines
45 KiB
HTML
1396 lines
45 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block title %}{{ measurement.name }} - Quality Measurement{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<link href="{% static 'assets/plugins/chart.js/dist/Chart.min.css' %}" rel="stylesheet" />
|
|
<style>
|
|
.measurement-header {
|
|
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
|
|
color: white;
|
|
border-radius: 0.5rem;
|
|
padding: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.measurement-layout {
|
|
display: grid;
|
|
grid-template-columns: 2fr 1fr;
|
|
gap: 2rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.measurement-main {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2rem;
|
|
}
|
|
|
|
.measurement-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;
|
|
}
|
|
|
|
.measurement-info {
|
|
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;
|
|
}
|
|
|
|
.measurement-badges {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
flex-wrap: wrap;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.badge-custom {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 0.375rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.badge-clinical { background: #e3f2fd; color: #1976d2; }
|
|
.badge-operational { background: #e8f5e8; color: #2e7d32; }
|
|
.badge-financial { background: #fff3e0; color: #f57c00; }
|
|
.badge-safety { background: #ffebee; color: #d32f2f; }
|
|
.badge-satisfaction { background: #f3e5f5; color: #7b1fa2; }
|
|
|
|
.badge-active { background: #e8f5e8; color: #2e7d32; }
|
|
.badge-inactive { background: #f8f9fa; color: #6c757d; }
|
|
.badge-below-target { background: #fff3cd; color: #856404; }
|
|
.badge-overdue { background: #f8d7da; color: #721c24; }
|
|
|
|
.metrics-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: 1.5rem;
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.metric-card {
|
|
background: #f8f9fa;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
padding: 1.5rem;
|
|
text-align: center;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.metric-card::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 3px;
|
|
background: var(--metric-color);
|
|
}
|
|
|
|
.metric-icon {
|
|
width: 40px;
|
|
height: 40px;
|
|
border-radius: 50%;
|
|
background: var(--metric-color);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
margin: 0 auto 1rem;
|
|
font-size: 1rem;
|
|
}
|
|
|
|
.metric-value {
|
|
font-size: 2rem;
|
|
font-weight: bold;
|
|
color: #495057;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.metric-label {
|
|
color: #6c757d;
|
|
font-size: 0.875rem;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.metric-trend {
|
|
font-size: 0.75rem;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.trend-up { color: #28a745; }
|
|
.trend-down { color: #dc3545; }
|
|
.trend-stable { color: #6c757d; }
|
|
|
|
.chart-container {
|
|
position: relative;
|
|
height: 300px;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.chart-controls {
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 0.5rem;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.chart-control {
|
|
padding: 0.375rem 0.75rem;
|
|
border: 1px solid #dee2e6;
|
|
background: white;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
font-size: 0.875rem;
|
|
text-decoration: none;
|
|
color: #495057;
|
|
}
|
|
|
|
.chart-control:hover, .chart-control.active {
|
|
background: #28a745;
|
|
color: white;
|
|
border-color: #28a745;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.target-progress {
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.progress-header {
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.progress-title {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
}
|
|
|
|
.progress-percentage {
|
|
font-size: 1.25rem;
|
|
font-weight: bold;
|
|
color: #28a745;
|
|
}
|
|
|
|
.progress-bar-container {
|
|
position: relative;
|
|
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-marker {
|
|
position: absolute;
|
|
top: -2px;
|
|
bottom: -2px;
|
|
width: 2px;
|
|
background: #dc3545;
|
|
border-radius: 1px;
|
|
}
|
|
|
|
.progress-labels {
|
|
display: flex;
|
|
justify-content: between;
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.data-points-table {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.data-point-row {
|
|
display: grid;
|
|
grid-template-columns: 120px 1fr 100px 80px auto;
|
|
gap: 1rem;
|
|
align-items: center;
|
|
padding: 0.75rem;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.data-point-row:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.data-point-date {
|
|
font-size: 0.875rem;
|
|
color: #495057;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.data-point-value {
|
|
font-size: 1rem;
|
|
font-weight: 600;
|
|
color: #28a745;
|
|
}
|
|
|
|
.data-point-target {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.data-point-achievement {
|
|
font-size: 0.75rem;
|
|
padding: 0.25rem 0.5rem;
|
|
border-radius: 0.25rem;
|
|
font-weight: 600;
|
|
text-align: center;
|
|
}
|
|
|
|
.achievement-excellent { background: #e8f5e8; color: #2e7d32; }
|
|
.achievement-good { background: #e3f2fd; color: #1976d2; }
|
|
.achievement-fair { background: #fff3e0; color: #f57c00; }
|
|
.achievement-poor { background: #ffebee; color: #d32f2f; }
|
|
|
|
.data-point-actions {
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.action-btn {
|
|
width: 28px;
|
|
height: 28px;
|
|
border: 1px solid #dee2e6;
|
|
background: white;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.action-btn:hover {
|
|
border-color: #28a745;
|
|
color: #28a745;
|
|
}
|
|
|
|
.alerts-list {
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.alert-item {
|
|
display: flex;
|
|
align-items: start;
|
|
gap: 0.75rem;
|
|
padding: 1rem;
|
|
border-bottom: 1px solid #f0f0f0;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.alert-item:hover {
|
|
background: #f8f9fa;
|
|
}
|
|
|
|
.alert-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.875rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.alert-critical { background: #dc3545; color: white; }
|
|
.alert-warning { background: #ffc107; color: #212529; }
|
|
.alert-info { background: #17a2b8; color: white; }
|
|
|
|
.alert-content {
|
|
flex: 1;
|
|
}
|
|
|
|
.alert-title {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.alert-message {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.alert-meta {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
}
|
|
|
|
.alert-actions {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.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: #28a745;
|
|
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 {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.contact-btn {
|
|
width: 28px;
|
|
height: 28px;
|
|
border: 1px solid #dee2e6;
|
|
background: white;
|
|
border-radius: 0.25rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.contact-btn:hover {
|
|
border-color: #28a745;
|
|
color: #28a745;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.activity-timeline {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.timeline-item {
|
|
display: flex;
|
|
gap: 1rem;
|
|
margin-bottom: 1.5rem;
|
|
position: relative;
|
|
}
|
|
|
|
.timeline-item::before {
|
|
content: '';
|
|
position: absolute;
|
|
left: 15px;
|
|
top: 32px;
|
|
bottom: -24px;
|
|
width: 2px;
|
|
background: #dee2e6;
|
|
}
|
|
|
|
.timeline-item:last-child::before {
|
|
display: none;
|
|
}
|
|
|
|
.timeline-icon {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: #28a745;
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 0.875rem;
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
.timeline-content {
|
|
flex: 1;
|
|
background: #f8f9fa;
|
|
border-radius: 0.375rem;
|
|
padding: 1rem;
|
|
}
|
|
|
|
.timeline-title {
|
|
font-weight: 600;
|
|
color: #495057;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.timeline-description {
|
|
font-size: 0.875rem;
|
|
color: #6c757d;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
|
|
.timeline-meta {
|
|
font-size: 0.75rem;
|
|
color: #6c757d;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.measurement-actions {
|
|
background: white;
|
|
border: 1px solid #dee2e6;
|
|
border-radius: 0.5rem;
|
|
padding: 1.5rem;
|
|
display: flex;
|
|
justify-content: between;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
}
|
|
|
|
.action-group {
|
|
display: flex;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
@media (max-width: 1200px) {
|
|
.measurement-layout {
|
|
grid-template-columns: 1fr;
|
|
gap: 1.5rem;
|
|
}
|
|
|
|
.measurement-sidebar {
|
|
order: -1;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.measurement-header {
|
|
padding: 1.5rem;
|
|
}
|
|
|
|
.measurement-info {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.metrics-grid {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.data-point-row {
|
|
grid-template-columns: 1fr;
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.measurement-actions {
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
}
|
|
|
|
.action-group {
|
|
justify-content: center;
|
|
}
|
|
}
|
|
|
|
@media print {
|
|
.measurement-sidebar, .measurement-actions {
|
|
display: none !important;
|
|
}
|
|
|
|
.measurement-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:measurement_list' %}">Measurements</a></li>
|
|
<li class="breadcrumb-item active">{{ measurement.name|truncatechars:20 }}</li>
|
|
</ol>
|
|
<h1 class="page-header mb-0">
|
|
<i class="fas fa-chart-line me-2"></i>Measurement Details
|
|
</h1>
|
|
</div>
|
|
<div class="ms-auto">
|
|
<button type="button" class="btn btn-outline-secondary me-2" onclick="printMeasurement()">
|
|
<i class="fas fa-print me-1"></i>Print
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info me-2" onclick="exportMeasurement()">
|
|
<i class="fas fa-download me-1"></i>Export
|
|
</button>
|
|
{% if measurement.can_edit %}
|
|
<a href="{% url 'quality:measurement_edit' measurement.id %}" class="btn btn-primary">
|
|
<i class="fas fa-edit me-1"></i>Edit
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Measurement Header -->
|
|
<div class="measurement-header">
|
|
<div class="row align-items-center">
|
|
<div class="col-md-8">
|
|
<h2 class="mb-2">{{ measurement.name }}</h2>
|
|
<p class="mb-3">{{ measurement.description|default:"No description available" }}</p>
|
|
|
|
<div class="measurement-badges">
|
|
<span class="badge-custom badge-{{ measurement.category|lower }}">
|
|
<i class="fas fa-tag me-1"></i>{{ measurement.get_category_display }}
|
|
</span>
|
|
<span class="badge-custom badge-{{ measurement.status|lower }}">
|
|
<i class="fas fa-circle me-1"></i>{{ measurement.get_status_display }}
|
|
</span>
|
|
<span class="badge-custom" style="background: #e3f2fd; color: #1976d2;">
|
|
<i class="fas fa-calendar-alt me-1"></i>{{ measurement.get_frequency_display }}
|
|
</span>
|
|
{% if measurement.is_overdue %}
|
|
<span class="badge-custom badge-overdue">
|
|
<i class="fas fa-clock me-1"></i>Overdue
|
|
</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4 text-md-end">
|
|
<div class="measurement-info">
|
|
<div class="info-item">
|
|
<div class="info-label">Measurement ID</div>
|
|
<div class="info-value">{{ measurement.measurement_id|default:measurement.id }}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-label">Created</div>
|
|
<div class="info-value">{{ measurement.created_at|date:"M d, Y" }}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-label">Last Updated</div>
|
|
<div class="info-value">{{ measurement.updated_at|date:"M d, Y" }}</div>
|
|
</div>
|
|
<div class="info-item">
|
|
<div class="info-label">Next Due</div>
|
|
<div class="info-value">{{ measurement.next_measurement_date|date:"M d, Y"|default:"Not scheduled" }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Key Metrics -->
|
|
<div class="metrics-grid">
|
|
<div class="metric-card" style="--metric-color: #28a745;">
|
|
<div class="metric-icon">
|
|
<i class="fas fa-chart-line"></i>
|
|
</div>
|
|
<div class="metric-value">{{ measurement.current_value|default:"-" }}</div>
|
|
<div class="metric-label">Current Value</div>
|
|
<div class="metric-trend trend-{% if measurement.trend == 'up' %}up{% elif measurement.trend == 'down' %}down{% else %}stable{% endif %}">
|
|
<i class="fas fa-arrow-{% if measurement.trend == 'up' %}up{% elif measurement.trend == 'down' %}down{% else %}right{% endif %}"></i>
|
|
{{ measurement.trend_percentage|default:"0" }}%
|
|
</div>
|
|
</div>
|
|
|
|
<div class="metric-card" style="--metric-color: #17a2b8;">
|
|
<div class="metric-icon">
|
|
<i class="fas fa-bullseye"></i>
|
|
</div>
|
|
<div class="metric-value">{{ measurement.target_value|default:"-" }}</div>
|
|
<div class="metric-label">Target Value</div>
|
|
<div class="metric-trend">
|
|
Target for {{ measurement.target_period|default:"current period" }}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="metric-card" style="--metric-color: #ffc107;">
|
|
<div class="metric-icon">
|
|
<i class="fas fa-trophy"></i>
|
|
</div>
|
|
<div class="metric-value">{{ measurement.benchmark|default:"-" }}</div>
|
|
<div class="metric-label">Benchmark</div>
|
|
<div class="metric-trend">
|
|
Industry standard
|
|
</div>
|
|
</div>
|
|
|
|
<div class="metric-card" style="--metric-color: #6f42c1;">
|
|
<div class="metric-icon">
|
|
<i class="fas fa-percentage"></i>
|
|
</div>
|
|
<div class="metric-value">{{ measurement.achievement_percentage|default:0 }}%</div>
|
|
<div class="metric-label">Achievement</div>
|
|
<div class="metric-trend trend-{% if measurement.achievement_percentage >= 100 %}up{% elif measurement.achievement_percentage >= 80 %}stable{% else %}down{% endif %}">
|
|
{% if measurement.achievement_percentage >= 100 %}
|
|
<i class="fas fa-check"></i> Target achieved
|
|
{% elif measurement.achievement_percentage >= 80 %}
|
|
<i class="fas fa-minus"></i> Near target
|
|
{% else %}
|
|
<i class="fas fa-arrow-down"></i> Below target
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="measurement-layout">
|
|
<!-- Main Content -->
|
|
<div class="measurement-main">
|
|
<!-- Target Progress -->
|
|
{% if measurement.target_value %}
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-bullseye"></i>
|
|
Target Progress
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="target-progress">
|
|
<div class="progress-header">
|
|
<div class="progress-title">Progress to Target</div>
|
|
<div class="progress-percentage">{{ measurement.achievement_percentage|default:0 }}%</div>
|
|
</div>
|
|
<div class="progress-bar-container">
|
|
<div class="progress-bar-fill" style="width: {{ measurement.achievement_percentage|default:0 }}%;"></div>
|
|
<div class="progress-marker" style="left: 100%;"></div>
|
|
</div>
|
|
<div class="progress-labels">
|
|
<span>Current: {{ measurement.current_value|default:0 }}</span>
|
|
<span>Target: {{ measurement.target_value }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Trend Chart -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-chart-area"></i>
|
|
Trend Analysis
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="chart-controls">
|
|
<button type="button" class="chart-control active" onclick="setChartPeriod('7d')" data-period="7d">7 Days</button>
|
|
<button type="button" class="chart-control" onclick="setChartPeriod('30d')" data-period="30d">30 Days</button>
|
|
<button type="button" class="chart-control" onclick="setChartPeriod('90d')" data-period="90d">90 Days</button>
|
|
<button type="button" class="chart-control" onclick="setChartPeriod('1y')" data-period="1y">1 Year</button>
|
|
</div>
|
|
|
|
<div class="chart-container">
|
|
<canvas id="trendChart"></canvas>
|
|
</div>
|
|
|
|
<div class="text-center text-muted">
|
|
<small>Chart shows measurement values over time with target line and benchmark reference</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Data Points -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-table"></i>
|
|
Recent Data Points
|
|
<div class="ms-auto">
|
|
<button type="button" class="btn btn-sm btn-success" onclick="recordNewData()">
|
|
<i class="fas fa-plus me-1"></i>Record Data
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="section-content p-0">
|
|
<div class="data-points-table">
|
|
{% for data_point in measurement.recent_data_points %}
|
|
<div class="data-point-row">
|
|
<div class="data-point-date">{{ data_point.date|date:"M d, Y" }}</div>
|
|
<div class="data-point-value">{{ data_point.value }}</div>
|
|
<div class="data-point-target">{{ data_point.target|default:"-" }}</div>
|
|
<div class="data-point-achievement achievement-{% if data_point.achievement_percentage >= 100 %}excellent{% elif data_point.achievement_percentage >= 80 %}good{% elif data_point.achievement_percentage >= 60 %}fair{% else %}poor{% endif %}">
|
|
{{ data_point.achievement_percentage|default:0 }}%
|
|
</div>
|
|
<div class="data-point-actions">
|
|
<button type="button" class="action-btn" onclick="editDataPoint({{ data_point.id }})" title="Edit">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button type="button" class="action-btn" onclick="deleteDataPoint({{ data_point.id }})" title="Delete">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center py-4 text-muted">
|
|
<i class="fas fa-chart-line fa-2x mb-2"></i>
|
|
<p>No data points recorded yet</p>
|
|
<button type="button" class="btn btn-success btn-sm" onclick="recordNewData()">
|
|
<i class="fas fa-plus me-1"></i>Record First Data Point
|
|
</button>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</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">
|
|
<div class="activity-timeline">
|
|
{% for activity in measurement.recent_activities %}
|
|
<div class="timeline-item">
|
|
<div class="timeline-icon" style="background: {% if activity.type == 'data_recorded' %}#28a745{% elif activity.type == 'target_updated' %}#17a2b8{% elif activity.type == 'alert_triggered' %}#dc3545{% else %}#6c757d{% endif %};">
|
|
<i class="fas fa-{% if activity.type == 'data_recorded' %}plus{% elif activity.type == 'target_updated' %}bullseye{% elif activity.type == 'alert_triggered' %}exclamation{% else %}info{% endif %}"></i>
|
|
</div>
|
|
<div class="timeline-content">
|
|
<div class="timeline-title">{{ activity.title }}</div>
|
|
<div class="timeline-description">{{ activity.description }}</div>
|
|
<div class="timeline-meta">
|
|
<span><i class="fas fa-clock me-1"></i>{{ activity.created_at|timesince }} ago</span>
|
|
<span><i class="fas fa-user me-1"></i>{{ activity.user.get_full_name|default:"System" }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<div class="text-center py-4 text-muted">
|
|
<i class="fas fa-history fa-2x mb-2"></i>
|
|
<p>No recent activity</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Sidebar -->
|
|
<div class="measurement-sidebar">
|
|
<!-- 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-success btn-sm" onclick="recordNewData()">
|
|
<i class="fas fa-plus me-1"></i>Record Data
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm" onclick="updateTarget()">
|
|
<i class="fas fa-bullseye me-1"></i>Update Target
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info btn-sm" onclick="generateReport()">
|
|
<i class="fas fa-file-alt me-1"></i>Generate Report
|
|
</button>
|
|
<button type="button" class="btn btn-outline-warning btn-sm" onclick="setAlert()">
|
|
<i class="fas fa-bell me-1"></i>Set Alert
|
|
</button>
|
|
{% if measurement.can_deactivate %}
|
|
<button type="button" class="btn btn-outline-danger btn-sm" onclick="deactivateMeasurement()">
|
|
<i class="fas fa-pause me-1"></i>Deactivate
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Measurement Details -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-info-circle"></i>
|
|
Details
|
|
</div>
|
|
<div class="section-content">
|
|
<div class="measurement-info">
|
|
<div class="info-item">
|
|
<div class="info-label">Department</div>
|
|
<div class="info-value">{{ measurement.department.name|default:"Not assigned" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Owner</div>
|
|
<div class="info-value">{{ measurement.owner.get_full_name|default:"Not assigned" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Unit of Measure</div>
|
|
<div class="info-value">{{ measurement.unit_of_measure|default:"Not specified" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Data Source</div>
|
|
<div class="info-value">{{ measurement.data_source|default:"Manual entry" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Calculation Method</div>
|
|
<div class="info-value">{{ measurement.calculation_method|default:"Direct measurement" }}</div>
|
|
</div>
|
|
|
|
<div class="info-item">
|
|
<div class="info-label">Total Data Points</div>
|
|
<div class="info-value">{{ measurement.data_points.count }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Active Alerts -->
|
|
{% if measurement.active_alerts.exists %}
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-exclamation-triangle"></i>
|
|
Active Alerts ({{ measurement.active_alerts.count }})
|
|
</div>
|
|
<div class="section-content p-0">
|
|
<div class="alerts-list">
|
|
{% for alert in measurement.active_alerts.all %}
|
|
<div class="alert-item">
|
|
<div class="alert-icon alert-{{ alert.severity|lower }}">
|
|
<i class="fas fa-{% if alert.severity == 'critical' %}exclamation-triangle{% elif alert.severity == 'warning' %}exclamation{% else %}info{% endif %}"></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">
|
|
<i class="fas fa-clock me-1"></i>{{ alert.created_at|timesince }} ago
|
|
</div>
|
|
</div>
|
|
<div class="alert-actions">
|
|
<button type="button" class="action-btn" onclick="acknowledgeAlert({{ alert.id }})" title="Acknowledge">
|
|
<i class="fas fa-check"></i>
|
|
</button>
|
|
<button type="button" class="action-btn" onclick="dismissAlert({{ alert.id }})" title="Dismiss">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Team Members -->
|
|
{% if measurement.team_members.exists %}
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-users"></i>
|
|
Team Members
|
|
</div>
|
|
<div class="section-content p-0">
|
|
{% for member in measurement.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.role|default:"Team Member" }}</div>
|
|
</div>
|
|
<div class="member-contact">
|
|
<a href="mailto:{{ member.email }}" class="contact-btn" title="Email">
|
|
<i class="fas fa-envelope"></i>
|
|
</a>
|
|
{% if member.phone %}
|
|
<a href="tel:{{ member.phone }}" class="contact-btn" title="Phone">
|
|
<i class="fas fa-phone"></i>
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Related Measurements -->
|
|
{% if measurement.related_measurements.exists %}
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<i class="fas fa-link"></i>
|
|
Related Measurements
|
|
</div>
|
|
<div class="section-content">
|
|
{% for related in measurement.related_measurements.all|slice:":5" %}
|
|
<div class="mb-2">
|
|
<a href="{% url 'quality:measurement_detail' related.id %}" class="text-decoration-none">
|
|
<div class="d-flex align-items-center gap-2 p-2 border rounded hover-bg-light">
|
|
<i class="fas fa-chart-line text-success"></i>
|
|
<div class="flex-grow-1">
|
|
<div class="fw-semibold">{{ related.name|truncatechars:30 }}</div>
|
|
<small class="text-muted">{{ related.get_category_display }}</small>
|
|
</div>
|
|
<i class="fas fa-arrow-right text-muted"></i>
|
|
</div>
|
|
</a>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="measurement-actions">
|
|
<div>
|
|
<a href="{% url 'quality:measurement_list' %}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-arrow-left me-1"></i>Back to List
|
|
</a>
|
|
</div>
|
|
|
|
<div class="action-group">
|
|
<button type="button" class="btn btn-success" onclick="recordNewData()">
|
|
<i class="fas fa-plus me-1"></i>Record Data
|
|
</button>
|
|
{% if measurement.can_edit %}
|
|
<a href="{% url 'quality:measurement_edit' measurement.id %}" class="btn btn-primary">
|
|
<i class="fas fa-edit me-1"></i>Edit Measurement
|
|
</a>
|
|
{% endif %}
|
|
{% if measurement.can_delete %}
|
|
<a href="{% url 'quality:measurement_delete' measurement.id %}" class="btn btn-outline-danger">
|
|
<i class="fas fa-trash me-1"></i>Delete
|
|
</a>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Record Data Modal -->
|
|
<div class="modal fade" id="recordDataModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">
|
|
<i class="fas fa-plus me-2"></i>Record Measurement Data
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="mb-3">
|
|
<label class="form-label">Measurement Value</label>
|
|
<div class="input-group">
|
|
<input type="number" class="form-control" id="measurement-value"
|
|
placeholder="Enter value" step="0.01">
|
|
{% if measurement.unit_of_measure %}
|
|
<span class="input-group-text">{{ measurement.unit_of_measure }}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Measurement Date</label>
|
|
<input type="date" class="form-control" id="measurement-date"
|
|
value="{{ today|date:'Y-m-d' }}">
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="form-label">Notes</label>
|
|
<textarea class="form-control" id="measurement-notes" rows="3"
|
|
placeholder="Optional notes about this measurement..."></textarea>
|
|
</div>
|
|
|
|
{% if measurement.target_value %}
|
|
<div class="alert alert-info">
|
|
<i class="fas fa-bullseye me-2"></i>
|
|
<strong>Target:</strong> {{ measurement.target_value }} {{ measurement.unit_of_measure|default:"" }}
|
|
</div>
|
|
{% endif %}
|
|
</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-success" onclick="saveMeasurementData()">
|
|
<i class="fas fa-save me-1"></i>Record Data
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="{% static 'assets/plugins/chart.js/dist/Chart.min.js' %}"></script>
|
|
|
|
<script>
|
|
let trendChart = null;
|
|
|
|
$(document).ready(function() {
|
|
initializeTrendChart();
|
|
});
|
|
|
|
function initializeTrendChart() {
|
|
const ctx = document.getElementById('trendChart').getContext('2d');
|
|
|
|
// Sample data - in real implementation, this would come from the backend
|
|
const chartData = {
|
|
labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
|
|
datasets: [{
|
|
label: 'Actual Values',
|
|
data: [65, 59, 80, 81, 56, 75],
|
|
borderColor: '#28a745',
|
|
backgroundColor: 'rgba(40, 167, 69, 0.1)',
|
|
tension: 0.4,
|
|
fill: true
|
|
}, {
|
|
label: 'Target',
|
|
data: [70, 70, 70, 70, 70, 70],
|
|
borderColor: '#dc3545',
|
|
backgroundColor: 'transparent',
|
|
borderDash: [5, 5],
|
|
pointRadius: 0
|
|
}, {
|
|
label: 'Benchmark',
|
|
data: [60, 60, 60, 60, 60, 60],
|
|
borderColor: '#ffc107',
|
|
backgroundColor: 'transparent',
|
|
borderDash: [10, 5],
|
|
pointRadius: 0
|
|
}]
|
|
};
|
|
|
|
trendChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: chartData,
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'top',
|
|
},
|
|
title: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
grid: {
|
|
color: '#e9ecef'
|
|
}
|
|
},
|
|
x: {
|
|
grid: {
|
|
color: '#e9ecef'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function setChartPeriod(period) {
|
|
$('.chart-control').removeClass('active');
|
|
$(`[data-period="${period}"]`).addClass('active');
|
|
|
|
// In real implementation, this would fetch new data for the period
|
|
showAlert(`Chart updated for ${period} period`, 'info');
|
|
}
|
|
|
|
function recordNewData() {
|
|
new bootstrap.Modal(document.getElementById('recordDataModal')).show();
|
|
}
|
|
|
|
function saveMeasurementData() {
|
|
const value = document.getElementById('measurement-value').value;
|
|
const date = document.getElementById('measurement-date').value;
|
|
const notes = document.getElementById('measurement-notes').value;
|
|
|
|
if (!value) {
|
|
showAlert('Please enter a measurement value', 'warning');
|
|
return;
|
|
}
|
|
|
|
fetch(`/quality/measurements/{{ measurement.id }}/record-data/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
value: value,
|
|
date: date,
|
|
notes: notes
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Measurement data recorded successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error recording measurement data', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error recording measurement data', 'danger');
|
|
});
|
|
|
|
bootstrap.Modal.getInstance(document.getElementById('recordDataModal')).hide();
|
|
}
|
|
|
|
function updateTarget() {
|
|
const newTarget = prompt('Enter new target value:', '{{ measurement.target_value|default:"" }}');
|
|
if (newTarget && !isNaN(newTarget)) {
|
|
fetch(`/quality/measurements/{{ measurement.id }}/update-target/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({
|
|
target_value: parseFloat(newTarget)
|
|
})
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Target updated successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error updating target', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error updating target', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function generateReport() {
|
|
window.open(`/quality/measurements/{{ measurement.id }}/report/`, '_blank');
|
|
}
|
|
|
|
function setAlert() {
|
|
// In real implementation, this would open a modal to configure alerts
|
|
showAlert('Alert configuration would open here', 'info');
|
|
}
|
|
|
|
function deactivateMeasurement() {
|
|
if (confirm('Are you sure you want to deactivate this measurement?')) {
|
|
fetch(`/quality/measurements/{{ measurement.id }}/deactivate/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Measurement deactivated successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error deactivating measurement', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error deactivating measurement', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function editDataPoint(dataPointId) {
|
|
// In real implementation, this would open a modal to edit the data point
|
|
showAlert('Data point editing would open here', 'info');
|
|
}
|
|
|
|
function deleteDataPoint(dataPointId) {
|
|
if (confirm('Are you sure you want to delete this data point?')) {
|
|
fetch(`/quality/measurements/data-points/${dataPointId}/delete/`, {
|
|
method: 'DELETE',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Data point deleted successfully', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error deleting data point', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error deleting data point', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function acknowledgeAlert(alertId) {
|
|
fetch(`/quality/alerts/${alertId}/acknowledge/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Alert acknowledged', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error acknowledging alert', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error acknowledging alert', 'danger');
|
|
});
|
|
}
|
|
|
|
function dismissAlert(alertId) {
|
|
if (confirm('Are you sure you want to dismiss this alert?')) {
|
|
fetch(`/quality/alerts/${alertId}/dismiss/`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
showAlert('Alert dismissed', 'success');
|
|
setTimeout(() => location.reload(), 1500);
|
|
} else {
|
|
showAlert('Error dismissing alert', 'danger');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
showAlert('Error dismissing alert', 'danger');
|
|
});
|
|
}
|
|
}
|
|
|
|
function printMeasurement() {
|
|
window.print();
|
|
}
|
|
|
|
function exportMeasurement() {
|
|
window.open(`/quality/measurements/{{ measurement.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 %}
|
|
|