367 lines
14 KiB
HTML
367 lines
14 KiB
HTML
{% extends 'layouts/base.html' %}
|
|
|
|
{% load i18n %}
|
|
{% load static %}
|
|
|
|
{% block title %}{{ report.indicator_title }} - {% trans "KPI Report" %}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="p-6">
|
|
<!-- Header -->
|
|
<div class="flex flex-wrap justify-between items-start gap-4 mb-6">
|
|
<div>
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<a href="{% url 'analytics:kpi_report_list' %}" class="text-blue hover:text-navy">
|
|
<i data-lucide="arrow-left" class="w-5 h-5"></i>
|
|
</a>
|
|
<span class="px-2 py-1 text-xs font-bold rounded bg-navy text-white">{{ report.kpi_id }}</span>
|
|
<span class="text-slate">|</span>
|
|
<span class="text-slate">{{ report.report_period_display }}</span>
|
|
</div>
|
|
<h1 class="text-2xl font-bold text-navy">{{ report.indicator_title }}</h1>
|
|
<p class="text-slate mt-1">
|
|
<i data-lucide="building-2" class="w-4 h-4 inline mr-1"></i>
|
|
{{ report.hospital.name }}
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex gap-2">
|
|
<a href="{% url 'analytics:kpi_report_pdf' report.id %}"
|
|
class="btn-secondary flex items-center gap-2">
|
|
<i data-lucide="download" class="w-4 h-4"></i>
|
|
{% trans "Export PDF" %}
|
|
</a>
|
|
<form method="post" action="{% url 'analytics:kpi_report_regenerate' report.id %}"
|
|
onsubmit="return confirm('{% trans "Regenerate this report? Current data will be replaced." %}')">
|
|
{% csrf_token %}
|
|
<button type="submit" class="btn-secondary flex items-center gap-2">
|
|
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
|
|
{% trans "Regenerate" %}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Table (Excel-style) -->
|
|
<div class="card mb-6 overflow-x-auto">
|
|
<table class="w-full border-collapse min-w-[800px]">
|
|
<!-- Header Row -->
|
|
<thead>
|
|
<tr class="bg-navy text-white">
|
|
<th class="p-3 text-left text-sm font-semibold border border-slate-300">{% trans "KPI ID" %}</th>
|
|
<th class="p-3 text-left text-sm font-semibold border border-slate-300 w-64">{% trans "Indicator Title" %}</th>
|
|
<th class="p-3 text-left text-sm font-semibold border border-slate-300">{% trans "Numerator / Denominator" %}</th>
|
|
<th class="p-3 text-center text-sm font-semibold border border-slate-300 w-12">{% trans "Jan" %}</th>
|
|
<th class="p-3 text-center text-sm font-semibold border border-slate-300 w-12">{% trans "Feb" %}</th>
|
|
<th class="p-3 text-center text-sm font-semibold border border-slate-300 w-12">{% trans "Mar" %}</th>
|
|
<th class="p-3 text-center text-sm font-semibold border border-slate-300 w-12">{% trans "Apr" %}</th>
|
|
<th class="p-3 text-center text-sm font-semibold border border-slate-300 w-12">{% trans "May" %}</th>
|
|
<th class="p-3 text-center text-sm font-semibold border border-slate-300 w-12">{% trans "Jun" %}</th>
|
|
<th class="p-3 text-center text-sm font-semibold border border-slate-300 w-12">{% trans "Jul" %}</th>
|
|
<th class="p-3 text-center text-sm font-semibold border border-slate-300 w-12">{% trans "Aug" %}</th>
|
|
<th class="p-3 text-center text-sm font-semibold border border-slate-300 w-12">{% trans "Sep" %}</th>
|
|
<th class="p-3 text-center text-sm font-semibold border border-slate-300 w-12">{% trans "Oct" %}</th>
|
|
<th class="p-3 text-center text-sm font-semibold border border-slate-300 w-12">{% trans "Nov" %}</th>
|
|
<th class="p-3 text-center text-sm font-semibold border border-slate-300 w-12">{% trans "Dec" %}</th>
|
|
<th class="p-3 text-center text-sm font-semibold border border-slate-300 bg-blue">{% trans "TOTAL" %}</th>
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody>
|
|
<!-- Row 1: Numerator -->
|
|
<tr class="bg-white">
|
|
<td rowspan="3" class="p-3 border border-slate-300 text-center text-blue font-bold align-middle">
|
|
{{ report.kpi_id }}
|
|
</td>
|
|
<td rowspan="3" class="p-3 border border-slate-300 text-left font-bold text-navy align-middle">
|
|
{{ report.indicator_title }}
|
|
</td>
|
|
<td class="p-3 border border-slate-300 text-left bg-slate-50 text-sm">
|
|
{{ report.numerator_label }}
|
|
</td>
|
|
{% for m in monthly_data %}
|
|
<td class="p-3 border border-slate-300 text-center text-sm">{% if m %}{{ m.numerator }}{% else %}-{% endif %}</td>
|
|
{% endfor %}
|
|
<td class="p-3 border border-slate-300 text-center font-bold bg-slate-100">{{ report.total_numerator }}</td>
|
|
</tr>
|
|
|
|
<!-- Row 2: Denominator -->
|
|
<tr class="bg-white">
|
|
<td class="p-3 border border-slate-300 text-left bg-slate-50 text-sm">
|
|
{{ report.denominator_label }}
|
|
</td>
|
|
{% for m in monthly_data %}
|
|
<td class="p-3 border border-slate-300 text-center text-sm">{% if m %}{{ m.denominator }}{% else %}-{% endif %}</td>
|
|
{% endfor %}
|
|
<td class="p-3 border border-slate-300 text-center font-bold bg-slate-100">{{ report.total_denominator }}</td>
|
|
</tr>
|
|
|
|
<!-- Row 3: Result % -->
|
|
<tr class="bg-white font-semibold">
|
|
<td class="p-3 border border-slate-300 text-left text-navy bg-slate-100">
|
|
{% trans "Result (%)" %}
|
|
</td>
|
|
{% for m in monthly_data %}
|
|
<td class="p-3 border border-slate-300 text-center text-sm
|
|
{% if m and m.is_below_target %}text-red-600 bg-red-50{% elif m %}text-green-600{% endif %}">
|
|
{% if m %}{{ m.percentage }}%{% else %}-{% endif %}
|
|
</td>
|
|
{% endfor %}
|
|
<td class="p-3 border border-slate-300 text-center font-bold bg-navy text-white">
|
|
{{ report.overall_result }}%
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Metadata Badges -->
|
|
<div class="grid grid-cols-2 md:grid-cols-5 gap-2 text-xs bg-light p-4 rounded-lg border border-slate-200 mb-6">
|
|
<div>
|
|
<span class="font-semibold text-navy">{% trans "Category:" %}</span> {{ report.category }}
|
|
</div>
|
|
<div>
|
|
<span class="font-semibold text-navy">{% trans "Type:" %}</span> {{ report.kpi_type }}
|
|
</div>
|
|
<div>
|
|
<span class="font-semibold text-navy">{% trans "Risk:" %}</span>
|
|
<span class="px-1.5 py-0.5 rounded text-xs
|
|
{% if report.risk_level == 'High' %}bg-red-100 text-red-800
|
|
{% elif report.risk_level == 'Medium' %}bg-yellow-100 text-yellow-800
|
|
{% else %}bg-green-100 text-green-800{% endif %}">
|
|
{{ report.risk_level }}
|
|
</span>
|
|
</div>
|
|
<div>
|
|
<span class="font-semibold text-navy">{% trans "Data coll.:" %}</span> {{ report.data_collection_method }}
|
|
</div>
|
|
<div>
|
|
<span class="font-semibold text-navy">{% trans "Method:" %}</span> {{ report.data_collection_frequency }}
|
|
</div>
|
|
<div>
|
|
<span class="font-semibold text-navy">{% trans "Dimension:" %}</span> {{ report.dimension }}
|
|
</div>
|
|
<div>
|
|
<span class="font-semibold text-navy">{% trans "Gather freq.:" %}</span> {{ report.data_collection_frequency }}
|
|
</div>
|
|
<div>
|
|
<span class="font-semibold text-navy">{% trans "Reporting:" %}</span> {{ report.reporting_frequency }}
|
|
</div>
|
|
<div>
|
|
<span class="font-semibold text-navy">{% trans "Collector:" %}</span> {{ report.collector_name|default:"-" }}
|
|
</div>
|
|
<div>
|
|
<span class="font-semibold text-navy">{% trans "Analyzer:" %}</span> {{ report.analyzer_name|default:"-" }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charts -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
|
|
<!-- Trend Chart -->
|
|
<div class="card lg:col-span-2">
|
|
<h3 class="text-sm font-bold text-navy uppercase mb-4 flex items-center gap-2">
|
|
<i data-lucide="trending-up" class="w-4 h-4"></i>
|
|
{% trans "Monthly Performance Trend (%)" %}
|
|
<span class="text-slate font-normal">[{% trans "Target:" %} {{ report.target_percentage }}%]</span>
|
|
</h3>
|
|
<div id="trendChart" style="height: 300px;"></div>
|
|
</div>
|
|
|
|
<!-- Source Chart -->
|
|
<div class="card">
|
|
<h3 class="text-sm font-bold text-navy uppercase mb-4 flex items-center gap-2">
|
|
<i data-lucide="pie-chart" class="w-4 h-4"></i>
|
|
{% trans "Complaints by Source" %}
|
|
</h3>
|
|
<div id="sourceChart" style="height: 300px;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Department Breakdown -->
|
|
<div class="card mb-6">
|
|
<h3 class="text-sm font-bold text-navy uppercase mb-4 flex items-center gap-2">
|
|
<i data-lucide="building" class="w-4 h-4"></i>
|
|
{% trans "Department Breakdown" %}
|
|
</h3>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
|
{% for dept in department_breakdowns %}
|
|
<div class="border rounded-lg p-4
|
|
{% if dept.department_category == 'medical' %}border-blue-200 bg-blue-50
|
|
{% elif dept.department_category == 'nursing' %}border-slate-200 bg-slate-50
|
|
{% elif dept.department_category == 'admin' %}border-green-200 bg-green-50
|
|
{% else %}border-yellow-200 bg-yellow-50{% endif %}">
|
|
<h4 class="text-xs font-bold px-2 py-1 rounded mb-2 inline-block
|
|
{% if dept.department_category == 'medical' %}bg-blue text-white
|
|
{% elif dept.department_category == 'nursing' %}bg-slate text-white
|
|
{% elif dept.department_category == 'admin' %}bg-green-600 text-white
|
|
{% else %}bg-yellow-600 text-white{% endif %}">
|
|
{{ dept.get_department_category_display }}
|
|
</h4>
|
|
|
|
<div class="space-y-2 text-sm">
|
|
<div class="flex justify-between">
|
|
<span class="text-slate">{% trans "Complaints:" %}</span>
|
|
<span class="font-semibold">{{ dept.complaint_count }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span class="text-slate">{% trans "Resolved:" %}</span>
|
|
<span class="font-semibold">{{ dept.resolved_count }}</span>
|
|
</div>
|
|
{% if dept.avg_resolution_days %}
|
|
<div class="flex justify-between">
|
|
<span class="text-slate">{% trans "Avg Days:" %}</span>
|
|
<span class="font-semibold">{{ dept.avg_resolution_days }}</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
{% if dept.top_areas %}
|
|
<div class="mt-3 pt-3 border-t text-xs text-slate">
|
|
<p class="font-semibold mb-1">{% trans "Top Areas:" %}</p>
|
|
<p class="whitespace-pre-line">{{ dept.top_areas }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Report Info Footer -->
|
|
<div class="text-xs text-slate text-center">
|
|
<p>
|
|
{% trans "Generated" %}: {{ report.generated_at|default:"-" }}
|
|
{% if report.generated_by %}{% trans "by" %} {{ report.generated_by.get_full_name }}{% endif %}
|
|
| {% trans "Status" %}: {{ report.get_status_display }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts@3.45.0/dist/apexcharts.min.js"></script>
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Initialize Lucide icons
|
|
if (typeof lucide !== 'undefined') {
|
|
lucide.createIcons();
|
|
}
|
|
|
|
// Parse chart data safely
|
|
let trendData, sourceData;
|
|
try {
|
|
trendData = JSON.parse('{{ trend_chart_data_json|escapejs }}');
|
|
sourceData = JSON.parse('{{ source_chart_data_json|escapejs }}');
|
|
} catch (e) {
|
|
console.error('Error parsing chart data:', e);
|
|
return;
|
|
}
|
|
|
|
// Validate data
|
|
if (!trendData || !trendData.data || !Array.isArray(trendData.data)) {
|
|
console.error('Invalid trend data');
|
|
trendData = { data: [0,0,0,0,0,0,0,0,0,0,0,0], labels: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'], target: 95 };
|
|
}
|
|
|
|
if (!sourceData || !sourceData.data || !Array.isArray(sourceData.data)) {
|
|
console.error('Invalid source data');
|
|
sourceData = { data: [100], labels: ['No Data'] };
|
|
}
|
|
|
|
// Trend Chart
|
|
const trendChartEl = document.getElementById("trendChart");
|
|
if (trendChartEl) {
|
|
const trendOptions = {
|
|
series: [{
|
|
name: '{% trans "Performance %" %}',
|
|
data: trendData.data
|
|
}],
|
|
chart: {
|
|
type: 'line',
|
|
height: 300,
|
|
fontFamily: 'inherit',
|
|
toolbar: { show: false },
|
|
animations: { enabled: false }
|
|
},
|
|
stroke: {
|
|
curve: 'smooth',
|
|
width: 3
|
|
},
|
|
colors: ['#005696'],
|
|
xaxis: {
|
|
categories: trendData.labels,
|
|
labels: { style: { fontSize: '11px' } }
|
|
},
|
|
yaxis: {
|
|
max: 100,
|
|
min: 0,
|
|
labels: { formatter: function(val) { return val + '%'; } }
|
|
},
|
|
grid: {
|
|
borderColor: '#e2e8f0',
|
|
strokeDashArray: 4
|
|
},
|
|
annotations: trendData.target ? {
|
|
yaxis: [{
|
|
y: trendData.target,
|
|
borderColor: '#ef4444',
|
|
strokeDashArray: 4,
|
|
label: {
|
|
text: '{% trans "Target" %}: ' + trendData.target + '%',
|
|
style: { color: '#ef4444', background: '#fef2f2' }
|
|
}
|
|
}]
|
|
} : {},
|
|
tooltip: {
|
|
y: { formatter: function(val) { return val + '%'; } }
|
|
}
|
|
};
|
|
|
|
try {
|
|
const trendChart = new ApexCharts(trendChartEl, trendOptions);
|
|
trendChart.render();
|
|
} catch (e) {
|
|
console.error('Error rendering trend chart:', e);
|
|
}
|
|
}
|
|
|
|
// Source Chart
|
|
const sourceChartEl = document.getElementById("sourceChart");
|
|
if (sourceChartEl) {
|
|
const sourceOptions = {
|
|
series: sourceData.data,
|
|
labels: sourceData.labels,
|
|
chart: {
|
|
type: 'donut',
|
|
height: 300,
|
|
fontFamily: 'inherit',
|
|
animations: { enabled: false }
|
|
},
|
|
colors: ['#005696', '#007bbd', '#64748b', '#94a3b8', '#cbd5e1', '#e2e8f0'],
|
|
plotOptions: {
|
|
pie: {
|
|
donut: {
|
|
size: '65%'
|
|
}
|
|
}
|
|
},
|
|
legend: {
|
|
position: 'bottom',
|
|
fontSize: '11px'
|
|
},
|
|
tooltip: {
|
|
y: { formatter: function(val) { return val + '%'; } }
|
|
}
|
|
};
|
|
|
|
try {
|
|
const sourceChart = new ApexCharts(sourceChartEl, sourceOptions);
|
|
sourceChart.render();
|
|
} catch (e) {
|
|
console.error('Error rendering source chart:', e);
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
{% endblock %}
|