368 lines
19 KiB
HTML
368 lines
19 KiB
HTML
{% extends "layouts/base.html" %}
|
|
{% load i18n %}
|
|
|
|
{% block title %}Standards Compliance - PX360{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="max-w-[1400px] mx-auto">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-gray-800">{% trans "Standards Compliance" %}</h1>
|
|
<p class="text-sm text-gray-500 mt-1">{% trans "CBAHI & MOH accreditation standards tracking" %}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{% if no_data %}
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-12 text-center">
|
|
<div class="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
<i data-lucide="shield-check" class="w-8 h-8 text-gray-400"></i>
|
|
</div>
|
|
<h3 class="text-lg font-semibold text-gray-700 mb-2">{% trans "No Standards Data" %}</h3>
|
|
<p class="text-gray-500">{% trans "No compliance records found. Import standards data using the management command." %}</p>
|
|
</div>
|
|
{% else %}
|
|
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-4 mb-6">
|
|
<form method="get" class="flex flex-wrap gap-4 items-end">
|
|
<div class="flex-1 min-w-[200px]">
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">{% trans "Hospital" %}</label>
|
|
<select name="hospital" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500" data-tomselect>
|
|
{% for h in hospitals %}
|
|
<option value="{{ h.id }}" {% if selected_hospital == h.id|stringformat:'s' %}selected{% endif %}>{{ h.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="w-48">
|
|
<label class="block text-xs font-semibold text-gray-600 mb-1">{% trans "Source" %}</label>
|
|
<select name="source" class="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
|
<option value="">{% trans "All Sources" %}</option>
|
|
{% for s in sources %}
|
|
<option value="{{ s.code }}" {% if selected_source == s.code %}selected{% endif %}>{{ s.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<button type="submit" class="px-4 py-2 bg-navy text-white text-sm font-medium rounded-lg hover:bg-navy/90 transition">
|
|
{% trans "Apply" %}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
|
|
<i data-lucide="list-checks" class="w-5 h-5 text-blue-600"></i>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs text-gray-500">{% trans "Total Standards" %}</p>
|
|
<p class="text-xl font-bold text-gray-800">{{ summary.total }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center">
|
|
<i data-lucide="check-circle-2" class="w-5 h-5 text-emerald-600"></i>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs text-gray-500">{% trans "Met" %}</p>
|
|
<p class="text-xl font-bold text-emerald-600">{{ summary.met_pct }}%</p>
|
|
<p class="text-xs text-gray-400">{{ summary.met }} {{ "standard"|pluralize }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 bg-amber-100 rounded-lg flex items-center justify-center">
|
|
<i data-lucide="alert-circle" class="w-5 h-5 text-amber-600"></i>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs text-gray-500">{% trans "Partially Met" %}</p>
|
|
<p class="text-xl font-bold text-amber-600">{{ summary.partially_met_pct }}%</p>
|
|
<p class="text-xs text-gray-400">{{ summary.partially_met }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-5">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-10 h-10 bg-red-100 rounded-lg flex items-center justify-center">
|
|
<i data-lucide="x-circle" class="w-5 h-5 text-red-600"></i>
|
|
</div>
|
|
<div>
|
|
<p class="text-xs text-gray-500">{% trans "Not Met" %}</p>
|
|
<p class="text-xl font-bold text-red-600">{{ summary.not_met_pct }}%</p>
|
|
<p class="text-xs text-gray-400">{{ summary.not_met }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if score_gauge %}
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-4">{% trans "Overall Score" %} (MOH Framework)</h3>
|
|
<div id="score-gauge"></div>
|
|
<div class="text-center mt-2">
|
|
<span class="text-2xl font-bold text-gray-800">{{ score_gauge.score }}</span>
|
|
<span class="text-gray-500"> / {{ score_gauge.max_score }}</span>
|
|
<span class="text-sm ml-2 font-semibold {% if score_gauge.pct >= 80 %}text-emerald-600{% elif score_gauge.pct >= 60 %}text-amber-600{% else %}text-red-600{% endif %}">({{ score_gauge.pct }}%)</span>
|
|
</div>
|
|
</div>
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-4">{% trans "Status Distribution" %}</h3>
|
|
<div id="status-donut"></div>
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-4">{% trans "Status Distribution" %}</h3>
|
|
<div id="status-donut"></div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 mb-6">
|
|
<h3 class="text-sm font-semibold text-gray-700 mb-4">{% trans "Compliance by Category" %}</h3>
|
|
<div id="category-chart"></div>
|
|
</div>
|
|
|
|
{% if category_breakdown %}
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6 overflow-hidden">
|
|
<div class="px-6 py-4 border-b border-gray-200">
|
|
<h3 class="text-sm font-semibold text-gray-700">{% trans "Category Breakdown" %}</h3>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-4 py-3 text-right text-xs font-semibold text-gray-600">{% trans "Category" %}</th>
|
|
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-600">{% trans "Total" %}</th>
|
|
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-600">{% trans "Met" %}</th>
|
|
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-600">{% trans "Partially Met" %}</th>
|
|
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-600">{% trans "Not Met" %}</th>
|
|
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-600">{% trans "Compliance %" %}</th>
|
|
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100">
|
|
{% for cat in category_breakdown %}
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-4 py-3 font-medium text-gray-800">{{ cat.name_ar|default:cat.name }}</td>
|
|
<td class="px-4 py-3 text-center">{{ cat.total }}</td>
|
|
<td class="px-4 py-3 text-center text-emerald-600 font-medium">{{ cat.met }}</td>
|
|
<td class="px-4 py-3 text-center text-amber-600 font-medium">{{ cat.partially_met }}</td>
|
|
<td class="px-4 py-3 text-center text-red-600 font-medium">{{ cat.not_met }}</td>
|
|
<td class="px-4 py-3 text-center">
|
|
<div class="flex items-center justify-center gap-2">
|
|
<div class="w-20 bg-gray-200 rounded-full h-2">
|
|
<div class="h-2 rounded-full {% if cat.met_pct >= 80 %}bg-emerald-500{% elif cat.met_pct >= 60 %}bg-amber-500{% else %}bg-red-500{% endif %}" style="width: {{ cat.met_pct }}%"></div>
|
|
</div>
|
|
<span class="text-xs font-medium">{{ cat.met_pct }}%</span>
|
|
</div>
|
|
</td>
|
|
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if corrective_actions %}
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6 overflow-hidden">
|
|
<div class="px-6 py-4 border-b border-gray-200 flex items-center gap-2">
|
|
<i data-lucide="wrench" class="w-4 h-4 text-amber-600"></i>
|
|
<h3 class="text-sm font-semibold text-gray-700">{% trans "Corrective Actions" %} ({{ corrective_actions|length }})</h3>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-4 py-3 text-right text-xs font-semibold text-gray-600">{% trans "Standard" %}</th>
|
|
<th class="px-4 py-3 text-right text-xs font-semibold text-gray-600">{% trans "Category" %}</th>
|
|
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-600">{% trans "Status" %}</th>
|
|
<th class="px-4 py-3 text-right text-xs font-semibold text-gray-600">{% trans "Corrective Action" %}</th>
|
|
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-600">{% trans "Priority" %}</th>
|
|
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-600">{% trans "Target Date" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100">
|
|
{% for ca in corrective_actions %}
|
|
<tr class="hover:bg-gray-50">
|
|
<td class="px-4 py-3">
|
|
<div class="font-medium text-gray-800 text-xs">{{ ca.code }}</div>
|
|
<div class="text-xs text-gray-500 mt-0.5 max-w-xs truncate">{{ ca.title }}</div>
|
|
</td>
|
|
<td class="px-4 py-3 text-xs text-gray-600">{{ ca.category }}</td>
|
|
<td class="px-4 py-3 text-center">
|
|
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full
|
|
{% if ca.status == 'Met' %}bg-emerald-100 text-emerald-700
|
|
{% elif ca.status == 'Partially Met' %}bg-amber-100 text-amber-700
|
|
{% elif ca.status == 'Not Met' %}bg-red-100 text-red-700
|
|
{% else %}bg-gray-100 text-gray-700{% endif %}">
|
|
{{ ca.status_ar|default:ca.status }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-xs text-gray-700 max-w-md">{{ ca.corrective_action }}</td>
|
|
<td class="px-4 py-3 text-center">
|
|
{% if ca.priority %}
|
|
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full
|
|
{% if ca.priority == 'High' %}bg-red-100 text-red-700
|
|
{% elif ca.priority == 'Medium' %}bg-amber-100 text-amber-700
|
|
{% else %}bg-blue-100 text-blue-700{% endif %}">
|
|
{{ ca.priority }}
|
|
</span>
|
|
{% endif %}
|
|
</td>
|
|
<td class="px-4 py-3 text-center text-xs text-gray-600">{{ ca.target_date }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if standards_table %}
|
|
<div class="bg-white rounded-xl shadow-sm border border-gray-200 mb-6 overflow-hidden">
|
|
<div class="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
|
|
<div class="flex items-center gap-2">
|
|
<i data-lucide="shield-check" class="w-4 h-4 text-blue-600"></i>
|
|
<h3 class="text-sm font-semibold text-gray-700">{% trans "All Standards" %} ({{ standards_table|length }})</h3>
|
|
</div>
|
|
</div>
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-sm" id="standards-table">
|
|
<thead class="bg-gray-50">
|
|
<tr>
|
|
<th class="px-4 py-3 text-right text-xs font-semibold text-gray-600">{% trans "Code" %}</th>
|
|
<th class="px-4 py-3 text-right text-xs font-semibold text-gray-600">{% trans "Standard" %}</th>
|
|
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-600">{% trans "Category" %}</th>
|
|
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-600">{% trans "Status" %}</th>
|
|
<th class="px-4 py-3 text-center text-xs font-semibold text-gray-600">{% trans "Assessment" %}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100">
|
|
{% for s in standards_table %}
|
|
<tr class="hover:bg-gray-50 cursor-pointer" onclick="toggleDetail(this)">
|
|
<td class="px-4 py-3 text-xs font-mono text-gray-600">{{ s.code }}</td>
|
|
<td class="px-4 py-3 text-xs text-gray-800 max-w-sm">{{ s.title }}</td>
|
|
<td class="px-4 py-3 text-center text-xs text-gray-600">{{ s.category }}</td>
|
|
<td class="px-4 py-3 text-center">
|
|
<span class="inline-flex px-2 py-0.5 text-xs font-medium rounded-full
|
|
{% if s.status_raw == 'met' %}bg-emerald-100 text-emerald-700
|
|
{% elif s.status_raw == 'partially_met' %}bg-amber-100 text-amber-700
|
|
{% elif s.status_raw == 'not_met' %}bg-red-100 text-red-700
|
|
{% elif s.status_raw == 'not_applicable' %}bg-gray-100 text-gray-500
|
|
{% else %}bg-gray-100 text-gray-700{% endif %}">
|
|
{{ s.status_ar|default:s.status }}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-center text-xs text-gray-500">{{ s.assessment_method }}</td>
|
|
</tr>
|
|
{% if s.recommendations %}
|
|
<tr class="hidden detail-row">
|
|
<td colspan="5" class="px-6 py-3 bg-gray-50">
|
|
<div class="text-xs">
|
|
<span class="font-semibold text-gray-700">{% trans "Recommendations:" %}</span>
|
|
<span class="text-gray-600 ml-1">{{ s.recommendations }}</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
{% endif %}
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% endif %}
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script>
|
|
function toggleDetail(row) {
|
|
const detail = row.nextElementSibling;
|
|
if (detail && detail.classList.contains('detail-row')) {
|
|
detail.classList.toggle('hidden');
|
|
}
|
|
}
|
|
|
|
(function() {
|
|
const chartData = JSON.parse('{{ chart_data_json|escapejs }}');
|
|
const statusChart = chartData.status_distribution;
|
|
const categoryChart = chartData.category_compliance;
|
|
const scoreGauge = chartData.score_gauge;
|
|
|
|
if (statusChart) {
|
|
const opts = {
|
|
chart: { type: 'donut', height: 280, fontFamily: 'inherit' },
|
|
labels: statusChart.labels_ar,
|
|
series: statusChart.series,
|
|
colors: statusChart.colors,
|
|
legend: { position: 'bottom', fontSize: '12px' },
|
|
dataLabels: { enabled: true, style: { fontSize: '12px' } },
|
|
plotOptions: {
|
|
pie: {
|
|
donut: {
|
|
size: '65%',
|
|
labels: {
|
|
show: true,
|
|
total: {
|
|
show: true,
|
|
label: '{% trans "Total" %}',
|
|
fontSize: '14px',
|
|
fontWeight: 600,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
new ApexCharts(document.querySelector('#status-donut'), opts).render();
|
|
}
|
|
|
|
if (categoryChart && categoryChart.categories.length > 0) {
|
|
const opts = {
|
|
chart: { type: 'bar', height: 300, stacked: true, fontFamily: 'inherit', toolbar: { show: false } },
|
|
series: categoryChart.series,
|
|
xaxis: { categories: categoryChart.categories },
|
|
colors: ['#10b981', '#f59e0b', '#ef4444'],
|
|
legend: { position: 'top', fontSize: '12px' },
|
|
plotOptions: { bar: { horizontal: true, borderRadius: 4 } },
|
|
yaxis: { labels: { style: { fontSize: '11px' } } },
|
|
tooltip: { shared: true, intersect: false },
|
|
};
|
|
new ApexCharts(document.querySelector('#category-chart'), opts).render();
|
|
}
|
|
|
|
if (scoreGauge) {
|
|
const opts = {
|
|
chart: { type: 'radialBar', height: 220, fontFamily: 'inherit' },
|
|
series: [scoreGauge.pct],
|
|
colors: [scoreGauge.pct >= 80 ? '#10b981' : scoreGauge.pct >= 60 ? '#f59e0b' : '#ef4444'],
|
|
plotOptions: {
|
|
radialBar: {
|
|
startAngle: -135,
|
|
endAngle: 135,
|
|
hollow: { size: '60%' },
|
|
dataLabels: {
|
|
name: { show: false },
|
|
value: {
|
|
show: true,
|
|
fontSize: '28px',
|
|
fontWeight: 700,
|
|
formatter: function(val) { return val + '%'; },
|
|
}
|
|
}
|
|
}
|
|
},
|
|
};
|
|
new ApexCharts(document.querySelector('#score-gauge'), opts).render();
|
|
}
|
|
})();
|
|
</script>
|
|
{% endblock %}
|