1513 lines
56 KiB
HTML
1513 lines
56 KiB
HTML
{% extends 'base.html' %}
|
||
{% load static %}
|
||
|
||
{% block title %}{% if measurement %}Edit{% else %}Create{% endif %} Quality Measurement{% endblock %}
|
||
|
||
{% block css %}
|
||
<link href="{% static 'plugins/select2/dist/css/select2.min.css' %}" rel="stylesheet" />
|
||
<link href="{% static 'plugins/bootstrap-datepicker/dist/css/bootstrap-datepicker.min.css' %}" rel="stylesheet" />
|
||
<style>
|
||
.form-header {
|
||
background: linear-gradient(135deg, #28a745 0%, #20c997 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;
|
||
}
|
||
|
||
.form-tabs {
|
||
border-bottom: 1px solid #dee2e6;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.nav-tabs .nav-link {
|
||
border: none;
|
||
border-bottom: 2px solid transparent;
|
||
color: #6c757d;
|
||
font-weight: 500;
|
||
padding: 1rem 1.5rem;
|
||
}
|
||
|
||
.nav-tabs .nav-link.active {
|
||
color: #28a745;
|
||
border-bottom-color: #28a745;
|
||
background: none;
|
||
}
|
||
|
||
.nav-tabs .nav-link:hover {
|
||
color: #28a745;
|
||
border-color: transparent;
|
||
}
|
||
|
||
.form-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||
gap: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.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 #ced4da;
|
||
border-radius: 0.375rem;
|
||
padding: 0.75rem;
|
||
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
|
||
}
|
||
|
||
.form-control:focus, .form-select:focus {
|
||
border-color: #28a745;
|
||
box-shadow: 0 0 0 0.2rem rgba(40, 167, 69, 0.25);
|
||
}
|
||
|
||
.input-group-text {
|
||
background: #f8f9fa;
|
||
border: 1px solid #ced4da;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.form-text {
|
||
font-size: 0.875rem;
|
||
color: #6c757d;
|
||
margin-top: 0.25rem;
|
||
}
|
||
|
||
.category-selector {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 0.75rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.category-option {
|
||
position: relative;
|
||
}
|
||
|
||
.category-option input[type="radio"] {
|
||
position: absolute;
|
||
opacity: 0;
|
||
}
|
||
|
||
.category-label {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 1rem;
|
||
border: 2px solid #dee2e6;
|
||
border-radius: 0.5rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
text-align: center;
|
||
}
|
||
|
||
.category-option input[type="radio"]:checked + .category-label {
|
||
border-color: #28a745;
|
||
background: #e8f5e8;
|
||
color: #2e7d32;
|
||
}
|
||
|
||
.category-icon {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.25rem;
|
||
color: white;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.category-clinical { background: #1976d2; }
|
||
.category-operational { background: #2e7d32; }
|
||
.category-financial { background: #f57c00; }
|
||
.category-safety { background: #d32f2f; }
|
||
.category-satisfaction { background: #7b1fa2; }
|
||
|
||
.category-name {
|
||
font-weight: 600;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.frequency-selector {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.frequency-option {
|
||
position: relative;
|
||
}
|
||
|
||
.frequency-option input[type="radio"] {
|
||
position: absolute;
|
||
opacity: 0;
|
||
}
|
||
|
||
.frequency-label {
|
||
display: block;
|
||
padding: 0.75rem;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 0.375rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
text-align: center;
|
||
font-size: 0.875rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.frequency-option input[type="radio"]:checked + .frequency-label {
|
||
border-color: #28a745;
|
||
background: #28a745;
|
||
color: white;
|
||
}
|
||
|
||
.target-settings {
|
||
background: #f8f9fa;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 0.5rem;
|
||
padding: 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.target-type-selector {
|
||
display: flex;
|
||
gap: 1rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.target-type {
|
||
flex: 1;
|
||
position: relative;
|
||
}
|
||
|
||
.target-type input[type="radio"] {
|
||
position: absolute;
|
||
opacity: 0;
|
||
}
|
||
|
||
.target-type-label {
|
||
display: block;
|
||
padding: 1rem;
|
||
border: 2px solid #dee2e6;
|
||
border-radius: 0.375rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
text-align: center;
|
||
}
|
||
|
||
.target-type input[type="radio"]:checked + .target-type-label {
|
||
border-color: #28a745;
|
||
background: #e8f5e8;
|
||
}
|
||
|
||
.target-direction {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.direction-option {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.5rem 1rem;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 0.375rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.direction-option.active {
|
||
border-color: #28a745;
|
||
background: #e8f5e8;
|
||
color: #2e7d32;
|
||
}
|
||
|
||
.calculation-method {
|
||
background: #f8f9fa;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 0.5rem;
|
||
padding: 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.method-options {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 1rem;
|
||
}
|
||
|
||
.method-option {
|
||
position: relative;
|
||
}
|
||
|
||
.method-option input[type="radio"] {
|
||
position: absolute;
|
||
opacity: 0;
|
||
}
|
||
|
||
.method-label {
|
||
display: block;
|
||
padding: 1rem;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 0.375rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.method-option input[type="radio"]:checked + .method-label {
|
||
border-color: #28a745;
|
||
background: #e8f5e8;
|
||
}
|
||
|
||
.method-title {
|
||
font-weight: 600;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.method-description {
|
||
font-size: 0.875rem;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.alert-configuration {
|
||
background: #fff3cd;
|
||
border: 1px solid #ffeaa7;
|
||
border-radius: 0.5rem;
|
||
padding: 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.alert-rules {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
.alert-rule {
|
||
display: grid;
|
||
grid-template-columns: 150px 100px 1fr auto;
|
||
gap: 0.5rem;
|
||
align-items: center;
|
||
padding: 0.75rem;
|
||
background: white;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 0.375rem;
|
||
}
|
||
|
||
.team-assignment {
|
||
background: #e3f2fd;
|
||
border: 1px solid #bbdefb;
|
||
border-radius: 0.5rem;
|
||
padding: 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.team-members {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.team-member {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
padding: 0.5rem 0.75rem;
|
||
background: white;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 0.375rem;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.member-avatar {
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
background: #28a745;
|
||
color: white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.remove-member {
|
||
width: 20px;
|
||
height: 20px;
|
||
border: none;
|
||
background: #dc3545;
|
||
color: white;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
font-size: 0.75rem;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
.action-group {
|
||
display: flex;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.auto-save-indicator {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
font-size: 0.875rem;
|
||
color: #6c757d;
|
||
}
|
||
|
||
.save-status {
|
||
width: 8px;
|
||
height: 8px;
|
||
border-radius: 50%;
|
||
background: #6c757d;
|
||
}
|
||
|
||
.save-status.saving {
|
||
background: #ffc107;
|
||
animation: pulse 1s infinite;
|
||
}
|
||
|
||
.save-status.saved {
|
||
background: #28a745;
|
||
}
|
||
|
||
.save-status.error {
|
||
background: #dc3545;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0% { opacity: 1; }
|
||
50% { opacity: 0.5; }
|
||
100% { opacity: 1; }
|
||
}
|
||
|
||
.preview-section {
|
||
background: #f8f9fa;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 0.5rem;
|
||
padding: 1.5rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.preview-card {
|
||
background: white;
|
||
border: 1px solid #dee2e6;
|
||
border-radius: 0.5rem;
|
||
padding: 1.5rem;
|
||
position: relative;
|
||
}
|
||
|
||
.preview-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
left: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 4px;
|
||
background: #28a745;
|
||
border-radius: 0.5rem 0 0 0.5rem;
|
||
}
|
||
|
||
.validation-errors {
|
||
background: #f8d7da;
|
||
border: 1px solid #f5c6cb;
|
||
border-radius: 0.5rem;
|
||
padding: 1rem;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.error-list {
|
||
list-style: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
}
|
||
|
||
.error-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
margin-bottom: 0.5rem;
|
||
font-size: 0.875rem;
|
||
color: #721c24;
|
||
}
|
||
|
||
.error-icon {
|
||
color: #dc3545;
|
||
}
|
||
|
||
@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-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.category-selector {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.frequency-selector {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
|
||
.target-type-selector {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.method-options {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.alert-rule {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.form-actions {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
}
|
||
|
||
.action-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:measurement_list' %}">Measurements</a></li>
|
||
<li class="breadcrumb-item active">{% if measurement %}Edit{% else %}Create{% endif %}</li>
|
||
</ol>
|
||
<h1 class="page-header mb-0">
|
||
<i class="fas fa-{% if measurement %}edit{% else %}plus{% endif %} me-2"></i>
|
||
{% if measurement %}Edit Measurement{% else %}Create Quality Measurement{% endif %}
|
||
</h1>
|
||
</div>
|
||
<div class="ms-auto">
|
||
<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>
|
||
|
||
<!-- Form Header -->
|
||
<div class="form-header">
|
||
<div class="row align-items-center">
|
||
<div class="col-md-8">
|
||
<h2 class="mb-2">
|
||
{% if measurement %}
|
||
Edit "{{ measurement.name }}"
|
||
{% else %}
|
||
Create New Quality Measurement
|
||
{% endif %}
|
||
</h2>
|
||
<p class="mb-0">
|
||
{% if measurement %}
|
||
Update the measurement configuration and settings below.
|
||
{% else %}
|
||
Define a new quality measurement to track performance metrics and targets.
|
||
{% endif %}
|
||
</p>
|
||
</div>
|
||
<div class="col-md-4 text-md-end">
|
||
<div class="auto-save-indicator">
|
||
<div class="save-status" id="save-status"></div>
|
||
<span id="save-text">Ready</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Validation Errors -->
|
||
{% if form.errors %}
|
||
<div class="validation-errors">
|
||
<h6 class="mb-3">
|
||
<i class="fas fa-exclamation-triangle me-2"></i>Please correct the following errors:
|
||
</h6>
|
||
<ul class="error-list">
|
||
{% for field, errors in form.errors.items %}
|
||
{% for error in errors %}
|
||
<li class="error-item">
|
||
<i class="fas fa-times-circle error-icon"></i>
|
||
<strong>{{ field|title }}:</strong> {{ error }}
|
||
</li>
|
||
{% endfor %}
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
{% endif %}
|
||
|
||
<form method="post" id="measurement-form" novalidate>
|
||
{% csrf_token %}
|
||
|
||
<div class="form-layout">
|
||
<!-- Main Form Content -->
|
||
<div class="form-main">
|
||
<!-- Basic Information -->
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<i class="fas fa-info-circle"></i>
|
||
Basic Information
|
||
</div>
|
||
<div class="section-content">
|
||
<div class="form-grid">
|
||
<div class="form-group">
|
||
<label class="form-label required">Measurement Name</label>
|
||
<input type="text" class="form-control" name="name"
|
||
value="{{ form.name.value|default:'' }}"
|
||
placeholder="Enter measurement name" required>
|
||
<div class="form-text">A clear, descriptive name for this measurement</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Measurement ID</label>
|
||
<input type="text" class="form-control" name="measurement_id"
|
||
value="{{ form.measurement_id.value|default:'' }}"
|
||
placeholder="Auto-generated if empty">
|
||
<div class="form-text">Unique identifier for this measurement</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Description</label>
|
||
<textarea class="form-control" name="description" rows="3"
|
||
placeholder="Describe what this measurement tracks and why it's important">{{ form.description.value|default:'' }}</textarea>
|
||
<div class="form-text">Detailed description of the measurement purpose and methodology</div>
|
||
</div>
|
||
|
||
<div class="form-grid">
|
||
<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 class="form-group">
|
||
<label class="form-label">Owner</label>
|
||
<select class="form-select" name="owner">
|
||
<option value="">Select Owner</option>
|
||
{% for user in users %}
|
||
<option value="{{ user.id }}" {% if form.owner.value == user.id %}selected{% endif %}>
|
||
{{ user.get_full_name }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Category Selection -->
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<i class="fas fa-tags"></i>
|
||
Category & Classification
|
||
</div>
|
||
<div class="section-content">
|
||
<div class="form-group">
|
||
<label class="form-label required">Measurement Category</label>
|
||
<div class="category-selector">
|
||
<div class="category-option">
|
||
<input type="radio" name="category" value="clinical" id="cat-clinical"
|
||
{% if form.category.value == 'clinical' %}checked{% endif %} required>
|
||
<label for="cat-clinical" class="category-label">
|
||
<div class="category-icon category-clinical">
|
||
<i class="fas fa-user-md"></i>
|
||
</div>
|
||
<div class="category-name">Clinical Quality</div>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="category-option">
|
||
<input type="radio" name="category" value="operational" id="cat-operational"
|
||
{% if form.category.value == 'operational' %}checked{% endif %}>
|
||
<label for="cat-operational" class="category-label">
|
||
<div class="category-icon category-operational">
|
||
<i class="fas fa-cogs"></i>
|
||
</div>
|
||
<div class="category-name">Operational</div>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="category-option">
|
||
<input type="radio" name="category" value="financial" id="cat-financial"
|
||
{% if form.category.value == 'financial' %}checked{% endif %}>
|
||
<label for="cat-financial" class="category-label">
|
||
<div class="category-icon category-financial">
|
||
<i class="fas fa-dollar-sign"></i>
|
||
</div>
|
||
<div class="category-name">Financial</div>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="category-option">
|
||
<input type="radio" name="category" value="safety" id="cat-safety"
|
||
{% if form.category.value == 'safety' %}checked{% endif %}>
|
||
<label for="cat-safety" class="category-label">
|
||
<div class="category-icon category-safety">
|
||
<i class="fas fa-shield-alt"></i>
|
||
</div>
|
||
<div class="category-name">Safety</div>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="category-option">
|
||
<input type="radio" name="category" value="satisfaction" id="cat-satisfaction"
|
||
{% if form.category.value == 'satisfaction' %}checked{% endif %}>
|
||
<label for="cat-satisfaction" class="category-label">
|
||
<div class="category-icon category-satisfaction">
|
||
<i class="fas fa-heart"></i>
|
||
</div>
|
||
<div class="category-name">Satisfaction</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label required">Measurement Frequency</label>
|
||
<div class="frequency-selector">
|
||
<div class="frequency-option">
|
||
<input type="radio" name="frequency" value="daily" id="freq-daily"
|
||
{% if form.frequency.value == 'daily' %}checked{% endif %} required>
|
||
<label for="freq-daily" class="frequency-label">Daily</label>
|
||
</div>
|
||
|
||
<div class="frequency-option">
|
||
<input type="radio" name="frequency" value="weekly" id="freq-weekly"
|
||
{% if form.frequency.value == 'weekly' %}checked{% endif %}>
|
||
<label for="freq-weekly" class="frequency-label">Weekly</label>
|
||
</div>
|
||
|
||
<div class="frequency-option">
|
||
<input type="radio" name="frequency" value="monthly" id="freq-monthly"
|
||
{% if form.frequency.value == 'monthly' %}checked{% endif %}>
|
||
<label for="freq-monthly" class="frequency-label">Monthly</label>
|
||
</div>
|
||
|
||
<div class="frequency-option">
|
||
<input type="radio" name="frequency" value="quarterly" id="freq-quarterly"
|
||
{% if form.frequency.value == 'quarterly' %}checked{% endif %}>
|
||
<label for="freq-quarterly" class="frequency-label">Quarterly</label>
|
||
</div>
|
||
|
||
<div class="frequency-option">
|
||
<input type="radio" name="frequency" value="annually" id="freq-annually"
|
||
{% if form.frequency.value == 'annually' %}checked{% endif %}>
|
||
<label for="freq-annually" class="frequency-label">Annually</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Measurement Configuration -->
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<i class="fas fa-sliders-h"></i>
|
||
Measurement Configuration
|
||
</div>
|
||
<div class="section-content">
|
||
<div class="form-grid">
|
||
<div class="form-group">
|
||
<label class="form-label">Unit of Measure</label>
|
||
<input type="text" class="form-control" name="unit_of_measure"
|
||
value="{{ form.unit_of_measure.value|default:'' }}"
|
||
placeholder="e.g., %, minutes, count, ratio">
|
||
<div class="form-text">The unit used to express measurement values</div>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Data Source</label>
|
||
<select class="form-select" name="data_source">
|
||
<option value="">Select Data Source</option>
|
||
<option value="manual" {% if form.data_source.value == 'manual' %}selected{% endif %}>Manual Entry</option>
|
||
<option value="ehr" {% if form.data_source.value == 'ehr' %}selected{% endif %}>Electronic Health Records</option>
|
||
<option value="lab" {% if form.data_source.value == 'lab' %}selected{% endif %}>Laboratory System</option>
|
||
<option value="billing" {% if form.data_source.value == 'billing' %}selected{% endif %}>Billing System</option>
|
||
<option value="survey" {% if form.data_source.value == 'survey' %}selected{% endif %}>Patient Surveys</option>
|
||
<option value="external" {% if form.data_source.value == 'external' %}selected{% endif %}>External System</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="calculation-method">
|
||
<h6 class="mb-3">
|
||
<i class="fas fa-calculator me-2"></i>Calculation Method
|
||
</h6>
|
||
|
||
<div class="method-options">
|
||
<div class="method-option">
|
||
<input type="radio" name="calculation_method" value="direct" id="calc-direct"
|
||
{% if form.calculation_method.value == 'direct' or not form.calculation_method.value %}checked{% endif %}>
|
||
<label for="calc-direct" class="method-label">
|
||
<div class="method-title">Direct Measurement</div>
|
||
<div class="method-description">Values are entered directly without calculation</div>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="method-option">
|
||
<input type="radio" name="calculation_method" value="percentage" id="calc-percentage"
|
||
{% if form.calculation_method.value == 'percentage' %}checked{% endif %}>
|
||
<label for="calc-percentage" class="method-label">
|
||
<div class="method-title">Percentage</div>
|
||
<div class="method-description">Calculate as (numerator/denominator) × 100</div>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="method-option">
|
||
<input type="radio" name="calculation_method" value="rate" id="calc-rate"
|
||
{% if form.calculation_method.value == 'rate' %}checked{% endif %}>
|
||
<label for="calc-rate" class="method-label">
|
||
<div class="method-title">Rate</div>
|
||
<div class="method-description">Calculate as events per time period</div>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="method-option">
|
||
<input type="radio" name="calculation_method" value="average" id="calc-average"
|
||
{% if form.calculation_method.value == 'average' %}checked{% endif %}>
|
||
<label for="calc-average" class="method-label">
|
||
<div class="method-title">Average</div>
|
||
<div class="method-description">Calculate mean of multiple values</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Target Settings -->
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<i class="fas fa-bullseye"></i>
|
||
Target Settings
|
||
</div>
|
||
<div class="section-content">
|
||
<div class="target-settings">
|
||
<h6 class="mb-3">
|
||
<i class="fas fa-target me-2"></i>Target Configuration
|
||
</h6>
|
||
|
||
<div class="target-type-selector">
|
||
<div class="target-type">
|
||
<input type="radio" name="target_type" value="fixed" id="target-fixed"
|
||
{% if form.target_type.value == 'fixed' or not form.target_type.value %}checked{% endif %}>
|
||
<label for="target-fixed" class="target-type-label">
|
||
<i class="fas fa-bullseye mb-2"></i>
|
||
<div>Fixed Target</div>
|
||
<small class="text-muted">Set a specific target value</small>
|
||
</label>
|
||
</div>
|
||
|
||
<div class="target-type">
|
||
<input type="radio" name="target_type" value="range" id="target-range"
|
||
{% if form.target_type.value == 'range' %}checked{% endif %}>
|
||
<label for="target-range" class="target-type-label">
|
||
<i class="fas fa-arrows-alt-h mb-2"></i>
|
||
<div>Target Range</div>
|
||
<small class="text-muted">Set minimum and maximum values</small>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-grid" id="target-inputs">
|
||
<div class="form-group">
|
||
<label class="form-label">Target Value</label>
|
||
<input type="number" class="form-control" name="target_value"
|
||
value="{{ form.target_value.value|default:'' }}"
|
||
placeholder="Enter target value" step="0.01">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Benchmark Value</label>
|
||
<input type="number" class="form-control" name="benchmark"
|
||
value="{{ form.benchmark.value|default:'' }}"
|
||
placeholder="Industry benchmark" step="0.01">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="target-direction">
|
||
<label class="form-label">Target Direction:</label>
|
||
<div class="direction-option" onclick="setDirection('higher')">
|
||
<i class="fas fa-arrow-up"></i>
|
||
<span>Higher is Better</span>
|
||
</div>
|
||
<div class="direction-option" onclick="setDirection('lower')">
|
||
<i class="fas fa-arrow-down"></i>
|
||
<span>Lower is Better</span>
|
||
</div>
|
||
<div class="direction-option active" onclick="setDirection('exact')">
|
||
<i class="fas fa-bullseye"></i>
|
||
<span>Exact Target</span>
|
||
</div>
|
||
</div>
|
||
<input type="hidden" name="target_direction" value="exact" id="target-direction-input">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Alert Configuration -->
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<i class="fas fa-bell"></i>
|
||
Alert Configuration
|
||
</div>
|
||
<div class="section-content">
|
||
<div class="alert-configuration">
|
||
<h6 class="mb-3">
|
||
<i class="fas fa-exclamation-triangle me-2"></i>Alert Rules
|
||
</h6>
|
||
|
||
<div class="alert-rules" id="alert-rules">
|
||
<div class="alert-rule">
|
||
<select class="form-select" name="alert_condition_1">
|
||
<option value="">Select Condition</option>
|
||
<option value="below_target">Below Target</option>
|
||
<option value="above_target">Above Target</option>
|
||
<option value="no_data">No Data Recorded</option>
|
||
<option value="trend_down">Declining Trend</option>
|
||
<option value="trend_up">Improving Trend</option>
|
||
</select>
|
||
|
||
<select class="form-select" name="alert_severity_1">
|
||
<option value="info">Info</option>
|
||
<option value="warning">Warning</option>
|
||
<option value="critical">Critical</option>
|
||
</select>
|
||
|
||
<input type="text" class="form-control" name="alert_message_1"
|
||
placeholder="Alert message">
|
||
|
||
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeAlertRule(this)">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="text-center mt-3">
|
||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addAlertRule()">
|
||
<i class="fas fa-plus me-1"></i>Add Alert Rule
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sidebar -->
|
||
<div class="form-sidebar">
|
||
<!-- Form Preview -->
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<i class="fas fa-eye"></i>
|
||
Preview
|
||
</div>
|
||
<div class="section-content">
|
||
<div class="preview-section">
|
||
<div class="preview-card" id="measurement-preview">
|
||
<h6 id="preview-name">Measurement Name</h6>
|
||
<p class="text-muted mb-2" id="preview-description">Description will appear here</p>
|
||
<div class="mb-2">
|
||
<span class="badge bg-primary" id="preview-category">Category</span>
|
||
<span class="badge bg-secondary" id="preview-frequency">Frequency</span>
|
||
</div>
|
||
<div class="small text-muted">
|
||
<div><strong>Department:</strong> <span id="preview-department">Not selected</span></div>
|
||
<div><strong>Target:</strong> <span id="preview-target">Not set</span></div>
|
||
<div><strong>Unit:</strong> <span id="preview-unit">Not specified</span></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Team Assignment -->
|
||
<div class="section-card">
|
||
<div class="section-header">
|
||
<i class="fas fa-users"></i>
|
||
Team Assignment
|
||
</div>
|
||
<div class="section-content">
|
||
<div class="team-assignment">
|
||
<h6 class="mb-3">
|
||
<i class="fas fa-user-friends me-2"></i>Assigned Team Members
|
||
</h6>
|
||
|
||
<div class="team-members" id="team-members">
|
||
<!-- Team members will be added here -->
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">Add Team Member</label>
|
||
<select class="form-select" id="team-member-select">
|
||
<option value="">Select team member</option>
|
||
{% for user in users %}
|
||
<option value="{{ user.id }}" data-name="{{ user.get_full_name }}">
|
||
{{ user.get_full_name }} - {{ user.email }}
|
||
</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<button type="button" class="btn btn-outline-primary btn-sm" onclick="addTeamMember()">
|
||
<i class="fas fa-plus me-1"></i>Add Member
|
||
</button>
|
||
</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>
|
||
|
||
<!-- 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="small">
|
||
<h6>Creating Effective Measurements:</h6>
|
||
<ul class="list-unstyled">
|
||
<li class="mb-2">
|
||
<i class="fas fa-lightbulb text-warning me-2"></i>
|
||
Use clear, specific names that describe what you're measuring
|
||
</li>
|
||
<li class="mb-2">
|
||
<i class="fas fa-lightbulb text-warning me-2"></i>
|
||
Set realistic but challenging targets based on historical data
|
||
</li>
|
||
<li class="mb-2">
|
||
<i class="fas fa-lightbulb text-warning me-2"></i>
|
||
Choose appropriate measurement frequency for your needs
|
||
</li>
|
||
<li class="mb-2">
|
||
<i class="fas fa-lightbulb text-warning me-2"></i>
|
||
Configure alerts to notify you of important changes
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Form Actions -->
|
||
<div class="form-actions">
|
||
<div class="auto-save-indicator">
|
||
<div class="save-status" id="save-status-bottom"></div>
|
||
<span id="save-text-bottom">Ready to save</span>
|
||
</div>
|
||
|
||
<div class="action-group">
|
||
<a href="{% url 'quality:measurement_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-success">
|
||
<i class="fas fa-check me-1"></i>
|
||
{% if measurement %}Update Measurement{% else %}Create Measurement{% endif %}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<!-- Template Selection 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 Measurement Template
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<div class="template-option" onclick="loadMeasurementTemplate('clinical')">
|
||
<i class="fas fa-user-md fa-2x mb-2 text-primary"></i>
|
||
<h6>Clinical Quality</h6>
|
||
<p class="small text-muted">Patient care and clinical outcome measurements</p>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="template-option" onclick="loadMeasurementTemplate('operational')">
|
||
<i class="fas fa-cogs fa-2x mb-2 text-success"></i>
|
||
<h6>Operational Efficiency</h6>
|
||
<p class="small text-muted">Process efficiency and operational metrics</p>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="template-option" onclick="loadMeasurementTemplate('safety')">
|
||
<i class="fas fa-shield-alt fa-2x mb-2 text-danger"></i>
|
||
<h6>Patient Safety</h6>
|
||
<p class="small text-muted">Safety incidents and prevention measures</p>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="template-option" onclick="loadMeasurementTemplate('satisfaction')">
|
||
<i class="fas fa-heart fa-2x mb-2 text-warning"></i>
|
||
<h6>Patient Satisfaction</h6>
|
||
<p class="small text-muted">Patient experience and satisfaction surveys</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block js %}
|
||
<script src="{% static 'plugins/select2/dist/js/select2.min.js' %}"></script>
|
||
<script src="{% static 'plugins/bootstrap-datepicker/dist/js/bootstrap-datepicker.min.js' %}"></script>
|
||
|
||
<script>
|
||
let teamMembers = [];
|
||
let autoSaveTimer = null;
|
||
|
||
$(document).ready(function() {
|
||
// Initialize Select2
|
||
$('.form-select').select2({
|
||
theme: 'bootstrap-5'
|
||
});
|
||
|
||
// Auto-save functionality
|
||
$('#measurement-form input, #measurement-form select, #measurement-form textarea').on('change keyup', function() {
|
||
clearTimeout(autoSaveTimer);
|
||
updateSaveStatus('saving');
|
||
|
||
autoSaveTimer = setTimeout(function() {
|
||
saveDraft();
|
||
}, 2000);
|
||
|
||
updatePreview();
|
||
});
|
||
|
||
// Initialize preview
|
||
updatePreview();
|
||
});
|
||
|
||
function updateSaveStatus(status) {
|
||
const statusElement = document.getElementById('save-status');
|
||
const textElement = document.getElementById('save-text');
|
||
const statusElementBottom = document.getElementById('save-status-bottom');
|
||
const textElementBottom = document.getElementById('save-text-bottom');
|
||
|
||
statusElement.className = `save-status ${status}`;
|
||
statusElementBottom.className = `save-status ${status}`;
|
||
|
||
switch(status) {
|
||
case 'saving':
|
||
textElement.textContent = 'Saving...';
|
||
textElementBottom.textContent = 'Saving...';
|
||
break;
|
||
case 'saved':
|
||
textElement.textContent = 'Saved';
|
||
textElementBottom.textContent = 'Saved';
|
||
break;
|
||
case 'error':
|
||
textElement.textContent = 'Error saving';
|
||
textElementBottom.textContent = 'Error saving';
|
||
break;
|
||
default:
|
||
textElement.textContent = 'Ready';
|
||
textElementBottom.textContent = 'Ready to save';
|
||
}
|
||
}
|
||
|
||
function updatePreview() {
|
||
const name = document.querySelector('[name="name"]').value || 'Measurement Name';
|
||
const description = document.querySelector('[name="description"]').value || 'Description will appear here';
|
||
const category = document.querySelector('[name="category"]:checked')?.value || 'Category';
|
||
const frequency = document.querySelector('[name="frequency"]:checked')?.value || 'Frequency';
|
||
const department = document.querySelector('[name="department"] option:checked')?.textContent || 'Not selected';
|
||
const target = document.querySelector('[name="target_value"]').value || 'Not set';
|
||
const unit = document.querySelector('[name="unit_of_measure"]').value || 'Not specified';
|
||
|
||
document.getElementById('preview-name').textContent = name;
|
||
document.getElementById('preview-description').textContent = description;
|
||
document.getElementById('preview-category').textContent = category.charAt(0).toUpperCase() + category.slice(1);
|
||
document.getElementById('preview-frequency').textContent = frequency.charAt(0).toUpperCase() + frequency.slice(1);
|
||
document.getElementById('preview-department').textContent = department;
|
||
document.getElementById('preview-target').textContent = target + (unit !== 'Not specified' ? ' ' + unit : '');
|
||
document.getElementById('preview-unit').textContent = unit;
|
||
}
|
||
|
||
function setDirection(direction) {
|
||
document.querySelectorAll('.direction-option').forEach(el => el.classList.remove('active'));
|
||
event.currentTarget.classList.add('active');
|
||
document.getElementById('target-direction-input').value = direction;
|
||
}
|
||
|
||
function addAlertRule() {
|
||
const rulesContainer = document.getElementById('alert-rules');
|
||
const ruleCount = rulesContainer.children.length + 1;
|
||
|
||
const ruleHtml = `
|
||
<div class="alert-rule">
|
||
<select class="form-select" name="alert_condition_${ruleCount}">
|
||
<option value="">Select Condition</option>
|
||
<option value="below_target">Below Target</option>
|
||
<option value="above_target">Above Target</option>
|
||
<option value="no_data">No Data Recorded</option>
|
||
<option value="trend_down">Declining Trend</option>
|
||
<option value="trend_up">Improving Trend</option>
|
||
</select>
|
||
|
||
<select class="form-select" name="alert_severity_${ruleCount}">
|
||
<option value="info">Info</option>
|
||
<option value="warning">Warning</option>
|
||
<option value="critical">Critical</option>
|
||
</select>
|
||
|
||
<input type="text" class="form-control" name="alert_message_${ruleCount}"
|
||
placeholder="Alert message">
|
||
|
||
<button type="button" class="btn btn-outline-danger btn-sm" onclick="removeAlertRule(this)">
|
||
<i class="fas fa-trash"></i>
|
||
</button>
|
||
</div>
|
||
`;
|
||
|
||
rulesContainer.insertAdjacentHTML('beforeend', ruleHtml);
|
||
}
|
||
|
||
function removeAlertRule(button) {
|
||
button.closest('.alert-rule').remove();
|
||
}
|
||
|
||
function addTeamMember() {
|
||
const select = document.getElementById('team-member-select');
|
||
const selectedOption = select.options[select.selectedIndex];
|
||
|
||
if (!selectedOption.value) {
|
||
showAlert('Please select a team member', 'warning');
|
||
return;
|
||
}
|
||
|
||
const memberId = selectedOption.value;
|
||
const memberName = selectedOption.dataset.name;
|
||
|
||
// Check if member is already added
|
||
if (teamMembers.includes(memberId)) {
|
||
showAlert('Team member already added', 'warning');
|
||
return;
|
||
}
|
||
|
||
teamMembers.push(memberId);
|
||
|
||
const memberHtml = `
|
||
<div class="team-member" data-member-id="${memberId}">
|
||
<div class="member-avatar">
|
||
${memberName.split(' ').map(n => n[0]).join('')}
|
||
</div>
|
||
<span>${memberName}</span>
|
||
<button type="button" class="remove-member" onclick="removeTeamMember('${memberId}')">
|
||
<i class="fas fa-times"></i>
|
||
</button>
|
||
<input type="hidden" name="team_members" value="${memberId}">
|
||
</div>
|
||
`;
|
||
|
||
document.getElementById('team-members').insertAdjacentHTML('beforeend', memberHtml);
|
||
select.selectedIndex = 0;
|
||
}
|
||
|
||
function removeTeamMember(memberId) {
|
||
teamMembers = teamMembers.filter(id => id !== memberId);
|
||
document.querySelector(`[data-member-id="${memberId}"]`).remove();
|
||
}
|
||
|
||
function saveDraft() {
|
||
const formData = new FormData(document.getElementById('measurement-form'));
|
||
formData.append('is_draft', '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) {
|
||
updateSaveStatus('saved');
|
||
} else {
|
||
updateSaveStatus('error');
|
||
}
|
||
})
|
||
.catch(error => {
|
||
updateSaveStatus('error');
|
||
});
|
||
}
|
||
|
||
function loadTemplate() {
|
||
new bootstrap.Modal(document.getElementById('templateModal')).show();
|
||
}
|
||
|
||
function loadMeasurementTemplate(type) {
|
||
const templates = {
|
||
clinical: {
|
||
name: 'Patient Readmission Rate',
|
||
description: 'Percentage of patients readmitted within 30 days of discharge',
|
||
category: 'clinical',
|
||
frequency: 'monthly',
|
||
unit_of_measure: '%',
|
||
target_value: '10',
|
||
calculation_method: 'percentage'
|
||
},
|
||
operational: {
|
||
name: 'Average Length of Stay',
|
||
description: 'Average number of days patients stay in the hospital',
|
||
category: 'operational',
|
||
frequency: 'monthly',
|
||
unit_of_measure: 'days',
|
||
target_value: '4.5',
|
||
calculation_method: 'average'
|
||
},
|
||
safety: {
|
||
name: 'Hospital-Acquired Infection Rate',
|
||
description: 'Rate of infections acquired during hospital stay',
|
||
category: 'safety',
|
||
frequency: 'monthly',
|
||
unit_of_measure: 'per 1000 patient days',
|
||
target_value: '2',
|
||
calculation_method: 'rate'
|
||
},
|
||
satisfaction: {
|
||
name: 'Patient Satisfaction Score',
|
||
description: 'Overall patient satisfaction rating from surveys',
|
||
category: 'satisfaction',
|
||
frequency: 'monthly',
|
||
unit_of_measure: 'score (1-10)',
|
||
target_value: '8.5',
|
||
calculation_method: 'average'
|
||
}
|
||
};
|
||
|
||
const template = templates[type];
|
||
if (template) {
|
||
Object.keys(template).forEach(key => {
|
||
const field = document.querySelector(`[name="${key}"]`);
|
||
if (field) {
|
||
if (field.type === 'radio') {
|
||
document.querySelector(`[name="${key}"][value="${template[key]}"]`).checked = true;
|
||
} else {
|
||
field.value = template[key];
|
||
}
|
||
}
|
||
});
|
||
|
||
updatePreview();
|
||
showAlert(`${type.charAt(0).toUpperCase() + type.slice(1)} template loaded`, 'success');
|
||
}
|
||
|
||
bootstrap.Modal.getInstance(document.getElementById('templateModal')).hide();
|
||
}
|
||
|
||
function saveTemplate() {
|
||
const templateName = prompt('Enter template name:');
|
||
if (templateName) {
|
||
// In real implementation, this would save the current form as a template
|
||
showAlert('Template saved successfully', 'success');
|
||
}
|
||
}
|
||
|
||
function validateForm() {
|
||
const form = document.getElementById('measurement-form');
|
||
const requiredFields = form.querySelectorAll('[required]');
|
||
let isValid = true;
|
||
let errors = [];
|
||
|
||
requiredFields.forEach(field => {
|
||
if (!field.value.trim()) {
|
||
isValid = false;
|
||
errors.push(`${field.name} 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('measurement-form').reset();
|
||
teamMembers = [];
|
||
document.getElementById('team-members').innerHTML = '';
|
||
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 %}
|
||
|