383 lines
18 KiB
HTML
383 lines
18 KiB
HTML
{% extends 'layouts/base.html' %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}{{ page_title }} - PX360{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid py-4">
|
|
<!-- Header -->
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h4 class="mb-1">
|
|
<i class="fas fa-chart-pie text-primary me-2"></i>{% trans "Acknowledgement Dashboard" %}
|
|
</h4>
|
|
<p class="text-muted mb-0">{% trans "Track onboarding progress and user acknowledgements" %}</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<a href="{% url 'accounts:provisional-user-list' %}" class="btn btn-outline-primary">
|
|
<i class="fas fa-users me-1"></i>{% trans "Manage Users" %}
|
|
</a>
|
|
<a href="{% url 'accounts:acknowledgement-content-list' %}" class="btn btn-outline-secondary">
|
|
<i class="fas fa-file-alt me-1"></i>{% trans "Manage Content" %}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-3">
|
|
<div class="card bg-primary text-white h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title mb-0">{% trans "Total Users" %}</h6>
|
|
<h2 class="mb-0">{{ stats.completed_onboarding|add:stats.in_progress }}</h2>
|
|
</div>
|
|
<i class="fas fa-users fa-2x opacity-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-success text-white h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title mb-0">{% trans "Completed" %}</h6>
|
|
<h2 class="mb-0">{{ stats.completed_onboarding }}</h2>
|
|
<small>{{ stats.completion_rate }}% {% trans "completion rate" %}</small>
|
|
</div>
|
|
<i class="fas fa-check-circle fa-2x opacity-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-warning text-dark h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title mb-0">{% trans "In Progress" %}</h6>
|
|
<h2 class="mb-0">{{ stats.in_progress }}</h2>
|
|
<small>{% trans "awaiting completion" %}</small>
|
|
</div>
|
|
<i class="fas fa-clock fa-2x opacity-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="card bg-danger text-white h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h6 class="card-title mb-0">{% trans "Expired" %}</h6>
|
|
<h2 class="mb-0">{{ stats.expired_invitations }}</h2>
|
|
<small>{% trans "need renewal" %}</small>
|
|
</div>
|
|
<i class="fas fa-exclamation-triangle fa-2x opacity-50"></i>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charts Row -->
|
|
<div class="row g-4 mb-4">
|
|
<!-- Daily Activity Chart -->
|
|
<div class="col-lg-8">
|
|
<div class="card h-100">
|
|
<div class="card-header bg-white">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-chart-line text-primary me-2"></i>{% trans "Daily Acknowledgements (Last 30 Days)" %}
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<canvas id="dailyActivityChart" height="250"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Completion by Role -->
|
|
<div class="col-lg-4">
|
|
<div class="card h-100">
|
|
<div class="card-header bg-white">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-user-tag text-info me-2"></i>{% trans "Completion by Role" %}
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% for role_data in completion_by_role %}
|
|
<div class="mb-3">
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span>{{ role_data.role }}</span>
|
|
<span class="fw-bold">{{ role_data.rate }}%</span>
|
|
</div>
|
|
<div class="progress" style="height: 8px;">
|
|
<div class="progress-bar {% if role_data.rate >= 80 %}bg-success{% elif role_data.rate >= 50 %}bg-warning{% else %}bg-danger{% endif %}"
|
|
role="progressbar"
|
|
style="width: {{ role_data.rate }}%"
|
|
aria-valuenow="{{ role_data.rate }}"
|
|
aria-valuemin="0"
|
|
aria-valuemax="100">
|
|
</div>
|
|
</div>
|
|
<small class="text-muted">{{ role_data.completed }}/{{ role_data.total }} {% trans "completed" %}</small>
|
|
</div>
|
|
{% empty %}
|
|
<p class="text-muted text-center">{% trans "No data available" %}</p>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pending Users & Checklist Stats -->
|
|
<div class="row g-4 mb-4">
|
|
<!-- Pending Users -->
|
|
<div class="col-lg-6">
|
|
<div class="card h-100">
|
|
<div class="card-header bg-white d-flex justify-content-between align-items-center">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-user-clock text-warning me-2"></i>{% trans "Pending Users" %}
|
|
</h5>
|
|
<span class="badge bg-warning">{{ pending_users|length }}</span>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>{% trans "User" %}</th>
|
|
<th>{% trans "Role" %}</th>
|
|
<th>{% trans "Days Left" %}</th>
|
|
<th>{% trans "Action" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for user in pending_users %}
|
|
<tr {% if user.is_expiring_soon %}class="table-danger"{% endif %}>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-light rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
|
|
<i class="fas fa-user text-muted small"></i>
|
|
</div>
|
|
<div>
|
|
<div class="fw-semibold">{{ user.get_full_name|default:user.email }}</div>
|
|
<small class="text-muted">{{ user.email }}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{% if user.groups.first %}
|
|
<span class="badge bg-secondary">{{ user.groups.first.name }}</span>
|
|
{% else %}-{% endif %}
|
|
</td>
|
|
<td>
|
|
{% if user.days_remaining is not None %}
|
|
{% if user.is_expiring_soon %}
|
|
<span class="badge bg-danger">{{ user.days_remaining }} {% trans "days" %}</span>
|
|
{% else %}
|
|
<span class="badge bg-info">{{ user.days_remaining }} {% trans "days" %}</span>
|
|
{% endif %}
|
|
{% else %}-
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<a href="{% url 'accounts:provisional-user-progress' user.id %}" class="btn btn-sm btn-outline-primary">
|
|
<i class="fas fa-eye"></i>
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="4" class="text-center text-muted py-4">
|
|
<i class="fas fa-check-circle text-success fa-2x mb-2"></i>
|
|
<p class="mb-0">{% trans "No pending users!" %}</p>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Checklist Item Stats -->
|
|
<div class="col-lg-6">
|
|
<div class="card h-100">
|
|
<div class="card-header bg-white">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-tasks text-secondary me-2"></i>{% trans "Items Needing Attention" %}
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
{% for stat in checklist_stats %}
|
|
<div class="d-flex align-items-center mb-3 p-2 bg-light rounded">
|
|
<div class="flex-grow-1">
|
|
<div class="fw-semibold">{{ stat.item.text_en|truncatechars:50 }}</div>
|
|
<small class="text-muted">{{ stat.acknowledged_count }}/{{ stat.eligible_count }} {% trans "acknowledged" %}</small>
|
|
</div>
|
|
<div class="text-end ms-3">
|
|
<span class="badge {% if stat.completion_rate >= 80 %}bg-success{% elif stat.completion_rate >= 50 %}bg-warning{% else %}bg-danger{% endif %}">
|
|
{{ stat.completion_rate }}%
|
|
</span>
|
|
</div>
|
|
</div>
|
|
{% empty %}
|
|
<p class="text-muted text-center py-4">
|
|
<i class="fas fa-check-circle text-success fa-2x mb-2"></i><br>
|
|
{% trans "All items have good completion rates!" %}
|
|
</p>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Activations -->
|
|
<div class="card">
|
|
<div class="card-header bg-white">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-history text-success me-2"></i>{% trans "Recent Activations" %}
|
|
</h5>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>{% trans "User" %}</th>
|
|
<th>{% trans "Role" %}</th>
|
|
<th>{% trans "Hospital" %}</th>
|
|
<th>{% trans "Department" %}</th>
|
|
<th>{% trans "Completed At" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for user in recent_activations %}
|
|
<tr>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="bg-success bg-opacity-10 rounded-circle d-flex align-items-center justify-content-center me-2" style="width: 32px; height: 32px;">
|
|
<i class="fas fa-check text-success small"></i>
|
|
</div>
|
|
<div>
|
|
<div class="fw-semibold">{{ user.get_full_name }}</div>
|
|
<small class="text-muted">{{ user.email }}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{% if user.groups.first %}
|
|
<span class="badge bg-secondary">{{ user.groups.first.name }}</span>
|
|
{% else %}-{% endif %}
|
|
</td>
|
|
<td>{{ user.hospital.name|default:"-" }}</td>
|
|
<td>{{ user.department.name|default:"-" }}</td>
|
|
<td>
|
|
<span class="text-success">
|
|
<i class="fas fa-calendar-check me-1"></i>
|
|
{{ user.acknowledgement_completed_at|date:"M d, Y H:i" }}
|
|
</span>
|
|
</td>
|
|
</tr>
|
|
{% empty %}
|
|
<tr>
|
|
<td colspan="5" class="text-center text-muted py-4">
|
|
{% trans "No recent activations" %}
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if is_px_admin and completion_by_hospital %}
|
|
<!-- Hospital Stats (PX Admin only) -->
|
|
<div class="card mt-4">
|
|
<div class="card-header bg-white">
|
|
<h5 class="card-title mb-0">
|
|
<i class="fas fa-hospital text-primary me-2"></i>{% trans "Completion by Hospital" %}
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row">
|
|
{% for hosp_data in completion_by_hospital %}
|
|
<div class="col-md-4 mb-3">
|
|
<div class="border rounded p-3">
|
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
|
<h6 class="mb-0">{{ hosp_data.hospital.name }}</h6>
|
|
<span class="badge {% if hosp_data.rate >= 80 %}bg-success{% elif hosp_data.rate >= 50 %}bg-warning{% else %}bg-danger{% endif %}">
|
|
{{ hosp_data.rate }}%
|
|
</span>
|
|
</div>
|
|
<div class="progress mb-2" style="height: 6px;">
|
|
<div class="progress-bar {% if hosp_data.rate >= 80 %}bg-success{% elif hosp_data.rate >= 50 %}bg-warning{% else %}bg-danger{% endif %}"
|
|
role="progressbar"
|
|
style="width: {{ hosp_data.rate }}%">
|
|
</div>
|
|
</div>
|
|
<small class="text-muted">{{ hosp_data.completed }}/{{ hosp_data.total }} {% trans "users completed" %}</small>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Daily Activity Chart
|
|
const dailyCtx = document.getElementById('dailyActivityChart').getContext('2d');
|
|
const dailyData = {{ daily_data|safe }};
|
|
|
|
new Chart(dailyCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: dailyData.map(d => d.date),
|
|
datasets: [{
|
|
label: '{% trans "Acknowledgements" %}',
|
|
data: dailyData.map(d => d.count),
|
|
borderColor: '#0d6efd',
|
|
backgroundColor: 'rgba(13, 110, 253, 0.1)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
stepSize: 1
|
|
}
|
|
},
|
|
x: {
|
|
grid: {
|
|
display: false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
</script>
|
|
{% endblock %}
|