1348 lines
46 KiB
HTML
1348 lines
46 KiB
HTML
{% load i18n %}
|
|
{% load static %}
|
|
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>{{ report.indicator_title }} - {% trans "KPI Report" %}</title>
|
|
|
|
<!-- Tailwind CSS (for PDF preview only) -->
|
|
<link rel="stylesheet" href="{% static 'dist/css/tailwind.css' %}">
|
|
|
|
<!-- Lucide Icons -->
|
|
<script src="https://unpkg.com/lucide@latest"></script>
|
|
|
|
<!-- ApexCharts -->
|
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
|
|
|
<!-- html2pdf.js -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
|
|
|
<style>
|
|
/* @page rules for professional PDF layout */
|
|
@page {
|
|
size: A4 landscape;
|
|
margin: 15mm;
|
|
|
|
@top-left {
|
|
content: "Al Hammadi Hospital - KPI Report";
|
|
font-size: 8pt;
|
|
color: #005696;
|
|
font-weight: 600;
|
|
}
|
|
|
|
@top-right {
|
|
content: "{{ report.kpi_id }}";
|
|
font-size: 8pt;
|
|
color: #64748b;
|
|
}
|
|
|
|
@bottom-center {
|
|
content: "Page " counter(page) " of " counter(pages);
|
|
font-size: 8pt;
|
|
color: #64748b;
|
|
}
|
|
|
|
@bottom-left {
|
|
content: "Generated: {% now 'M d, Y H:i' %}";
|
|
font-size: 7pt;
|
|
color: #94a3b8;
|
|
}
|
|
|
|
@bottom-right {
|
|
content: "Confidential - Internal Use Only";
|
|
font-size: 7pt;
|
|
color: #94a3b8;
|
|
font-style: italic;
|
|
}
|
|
}
|
|
|
|
/* Base styles that work without Tailwind */
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
font-size: 10pt;
|
|
margin: 0;
|
|
padding: 0;
|
|
background: white;
|
|
color: #334155;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
/* Utility classes that match Tailwind */
|
|
.bg-white { background-color: white; }
|
|
.bg-navy { background-color: #005696; }
|
|
.bg-blue { background-color: #007bbd; }
|
|
.bg-light { background-color: #eef6fb; }
|
|
.text-white { color: white; }
|
|
.text-navy { color: #005696; }
|
|
.text-slate { color: #64748b; }
|
|
.text-slate-800 { color: #1e293b; }
|
|
.font-bold { font-weight: 700; }
|
|
.font-semibold { font-weight: 600; }
|
|
.text-2xl { font-size: 1.5rem; }
|
|
.text-3xl { font-size: 1.875rem; }
|
|
.text-sm { font-size: 0.875rem; }
|
|
.text-xs { font-size: 0.75rem; }
|
|
.text-xl { font-size: 1.25rem; }
|
|
.text-lg { font-size: 1.125rem; }
|
|
.font-black { font-weight: 900; }
|
|
.rounded-lg { border-radius: 0.5rem; }
|
|
.rounded-xl { border-radius: 0.75rem; }
|
|
.rounded-2xl { border-radius: 1rem; }
|
|
.rounded-full { border-radius: 9999px; }
|
|
.border { border: 1px solid #e2e8f0; }
|
|
.shadow-sm { box-shadow: 0 1px 2px rgba(0,0,0,0.05); }
|
|
.shadow-lg { box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); }
|
|
.p-2 { padding: 0.5rem; }
|
|
.p-3 { padding: 0.75rem; }
|
|
.p-4 { padding: 1rem; }
|
|
.p-5 { padding: 1.25rem; }
|
|
.p-6 { padding: 1.5rem; }
|
|
.p-8 { padding: 2rem; }
|
|
.px-2 { padding-left: 0.5rem; padding-right: 0.5rem; }
|
|
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; }
|
|
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
|
.px-5 { padding-left: 1.25rem; padding-right: 1.25rem; }
|
|
.py-1 { padding-top: 0.25rem; padding-bottom: 0.25rem; }
|
|
.py-1_5 { padding-top: 0.375rem; padding-bottom: 0.375rem; }
|
|
.py-2 { padding-top: 0.5rem; padding-bottom: 0.5rem; }
|
|
.py-2_5 { padding-top: 0.625rem; padding-bottom: 0.625rem; }
|
|
.py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; }
|
|
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
|
|
.m-0 { margin: 0; }
|
|
.mb-1 { margin-bottom: 0.25rem; }
|
|
.mb-2 { margin-bottom: 0.5rem; }
|
|
.mb-3 { margin-bottom: 0.75rem; }
|
|
.mb-4 { margin-bottom: 1rem; }
|
|
.mb-6 { margin-bottom: 1.5rem; }
|
|
.mb-8 { margin-bottom: 2rem; }
|
|
.mt-1 { margin-top: 0.25rem; }
|
|
.mt-2 { margin-top: 0.5rem; }
|
|
.mt-3 { margin-top: 0.75rem; }
|
|
.mt-4 { margin-top: 1rem; }
|
|
.mt-6 { margin-top: 1.5rem; }
|
|
.mt-8 { margin-top: 2rem; }
|
|
.mt-12 { margin-top: 3rem; }
|
|
.mr-1 { margin-right: 0.25rem; }
|
|
.mr-2 { margin-right: 0.5rem; }
|
|
.ml-1 { margin-left: 0.25rem; }
|
|
.ml-2 { margin-left: 0.5rem; }
|
|
.mx-auto { margin-left: auto; margin-right: auto; }
|
|
.gap-2 { gap: 0.5rem; }
|
|
.gap-3 { gap: 0.75rem; }
|
|
.gap-4 { gap: 1rem; }
|
|
.gap-6 { gap: 1.5rem; }
|
|
.flex { display: flex; }
|
|
.inline-flex { display: inline-flex; }
|
|
.inline { display: inline; }
|
|
.inline-block { display: inline-block; }
|
|
.block { display: block; }
|
|
.hidden { display: none; }
|
|
.grid { display: grid; }
|
|
.grid-cols-2 { grid-template-columns: repeat(2, 1fr); }
|
|
.grid-cols-3 { grid-template-columns: repeat(3, 1fr); }
|
|
.grid-cols-4 { grid-template-columns: repeat(4, 1fr); }
|
|
.grid-cols-5 { grid-template-columns: repeat(5, 1fr); }
|
|
.w-full { width: 100%; }
|
|
.w-3 { width: 0.75rem; }
|
|
.w-4 { width: 1rem; }
|
|
.w-5 { width: 1.25rem; }
|
|
.w-8 { width: 2rem; }
|
|
.h-3 { height: 0.75rem; }
|
|
.h-4 { height: 1rem; }
|
|
.h-5 { height: 1.25rem; }
|
|
.h-8 { height: 2rem; }
|
|
.h-10 { height: 2.5rem; }
|
|
.min-h-220 { min-height: 220px; }
|
|
.text-center { text-align: center; }
|
|
.text-left { text-align: left; }
|
|
.text-right { text-align: right; }
|
|
.justify-between { justify-content: space-between; }
|
|
.justify-center { justify-content: center; }
|
|
.items-start { align-items: flex-start; }
|
|
.items-center { align-items: center; }
|
|
.flex-wrap { flex-wrap: wrap; }
|
|
.flex-1 { flex: 1; }
|
|
.flex-col { flex-direction: column; }
|
|
.relative { position: relative; }
|
|
.absolute { position: absolute; }
|
|
.fixed { position: fixed; }
|
|
.top-4 { top: 1rem; }
|
|
.right-4 { right: 1rem; }
|
|
.z-50 { z-index: 50; }
|
|
.uppercase { text-transform: uppercase; }
|
|
.italic { font-style: italic; }
|
|
.whitespace-pre-line { white-space: pre-line; }
|
|
.line-clamp-2 {
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
overflow: hidden;
|
|
}
|
|
.border-collapse { border-collapse: collapse; }
|
|
.border-b { border-bottom: 1px solid #e2e8f0; }
|
|
.border-b-2 { border-bottom: 2px solid; }
|
|
.border-t { border-top: 1px solid #e2e8f0; }
|
|
.border-t-2 { border-top: 2px solid; }
|
|
.border-l { border-left: 1px solid #e2e8f0; }
|
|
.border-slate-200 { border-color: #e2e8f0; }
|
|
.border-slate-300 { border-color: #cbd5e1; }
|
|
.border-slate-400 { border-color: #94a3b8; }
|
|
.border-navy { border-color: #005696; }
|
|
.border-green-200 { border-color: #bbf7d0; }
|
|
.border-red-200 { border-color: #fecaca; }
|
|
.border-blue-200 { border-color: #bfdbfe; }
|
|
.border-yellow-200 { border-color: #fde68a; }
|
|
.bg-green-50 { background-color: #f0fdf4; }
|
|
.bg-green-100 { background-color: #dcfce7; }
|
|
.bg-green-200 { background-color: #bbf7d0; }
|
|
.bg-green-600 { background-color: #16a34a; }
|
|
.bg-green-700 { background-color: #15803d; }
|
|
.bg-green-800 { background-color: #166534; }
|
|
.bg-red-50 { background-color: #fef2f2; }
|
|
.bg-red-100 { background-color: #fee2e2; }
|
|
.bg-red-200 { background-color: #fecaca; }
|
|
.bg-red-500 { background-color: #ef4444; }
|
|
.bg-red-600 { background-color: #dc2626; }
|
|
.bg-yellow-50 { background-color: #fefce8; }
|
|
.bg-yellow-100 { background-color: #fef9c3; }
|
|
.bg-yellow-200 { background-color: #fde68a; }
|
|
.bg-yellow-600 { background-color: #ca8a04; }
|
|
.bg-yellow-800 { background-color: #854d0e; }
|
|
.bg-blue-50 { background-color: #eff6ff; }
|
|
.bg-blue-100 { background-color: #dbeafe; }
|
|
.bg-blue-200 { background-color: #bfdbfe; }
|
|
.bg-slate-50 { background-color: #f8fafc; }
|
|
.bg-slate-100 { background-color: #f1f5f9; }
|
|
.bg-slate-200 { background-color: #e2e8f0; }
|
|
.bg-slate-300 { background-color: #cbd5e1; }
|
|
.bg-slate-400 { background-color: #94a3b8; }
|
|
.bg-slate-600 { background-color: #475569; }
|
|
.text-green-600 { color: #16a34a; }
|
|
.text-green-700 { color: #15803d; }
|
|
.text-green-800 { color: #166534; }
|
|
.text-red-500 { color: #ef4444; }
|
|
.text-red-600 { color: #dc2626; }
|
|
.text-red-800 { color: #991b1b; }
|
|
.text-yellow-600 { color: #ca8a04; }
|
|
.text-yellow-800 { color: #854d0e; }
|
|
.text-blue { color: #007bbd; }
|
|
.text-blue-600 { color: #2563eb; }
|
|
.text-blue-700 { color: #1d4ed8; }
|
|
.text-blue-800 { color: #1e40af; }
|
|
.hover\:opacity-90:hover { opacity: 0.9; }
|
|
.hover\:bg-blue:hover { background-color: #007bbd; }
|
|
.hover\:bg-slate-600:hover { background-color: #475569; }
|
|
.hover\:bg-slate-50:hover { background-color: #f8fafc; }
|
|
.hover\:-translate-y-1:hover { transform: translateY(-0.25rem); }
|
|
.hover\:shadow-lg:hover { box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1); }
|
|
.hover\:underline:hover { text-decoration: underline; }
|
|
.transition { transition: all 0.2s; }
|
|
.transition-all { transition: all 0.2s; }
|
|
.transition-colors { transition: color 0.2s, background-color 0.2s, border-color 0.2s; }
|
|
.cursor-pointer { cursor: pointer; }
|
|
.cursor-not-allowed { cursor: not-allowed; }
|
|
.overflow-hidden { overflow: hidden; }
|
|
.overflow-x-auto { overflow-x: auto; }
|
|
.overflow-visible { overflow: visible; }
|
|
.list-disc { list-style-type: disc; }
|
|
.list-inside { list-style-position: inside; }
|
|
.space-y-1 > * + * { margin-top: 0.25rem; }
|
|
.space-y-2 > * + * { margin-top: 0.5rem; }
|
|
.space-y-4 > * + * { margin-top: 1rem; }
|
|
.pt-1 { padding-top: 0.25rem; }
|
|
.pt-3 { padding-top: 0.75rem; }
|
|
.pt-4 { padding-top: 1rem; }
|
|
.pb-1 { padding-bottom: 0.25rem; }
|
|
.pb-2 { padding-bottom: 0.5rem; }
|
|
.pb-4 { padding-bottom: 1rem; }
|
|
.pb-8 { padding-bottom: 2rem; }
|
|
.pl-4 { padding-left: 1rem; }
|
|
.pr-4 { padding-right: 1rem; }
|
|
.tracking-wider { letter-spacing: 0.05em; }
|
|
.tracking-tight { letter-spacing: -0.025em; }
|
|
.leading-tight { line-height: 1.25; }
|
|
|
|
/* Animation */
|
|
.animate-spin { animation: spin 1s linear infinite; }
|
|
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
|
|
|
|
/* Print styles */
|
|
@media print {
|
|
.no-print { display: none !important; }
|
|
body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
|
|
}
|
|
|
|
.excel-border { border: 1px solid #94a3b8; }
|
|
|
|
.page-break { page-break-after: always; }
|
|
|
|
.no-break { page-break-inside: avoid; }
|
|
|
|
.whitespace-nowrap { white-space: nowrap; }
|
|
|
|
.table-compact { font-size: 7pt; }
|
|
|
|
.table-compact th,
|
|
.table-compact td { vertical-align: middle; }
|
|
|
|
/* Chart container styles */
|
|
#trendChart, #sourceChart {
|
|
max-width: 100% !important;
|
|
overflow: visible !important;
|
|
position: relative;
|
|
}
|
|
|
|
/* Ensure charts fit within PDF */
|
|
.apexcharts-canvas {
|
|
max-width: 100% !important;
|
|
}
|
|
|
|
/* Static chart images for PDF */
|
|
.chart-static-image {
|
|
max-width: 100% !important;
|
|
height: auto !important;
|
|
display: block;
|
|
background: white;
|
|
}
|
|
|
|
/* Ensure proper sizing for PDF capture */
|
|
.apexcharts-svg {
|
|
max-width: 100% !important;
|
|
}
|
|
|
|
#trendChart, #sourceChart {
|
|
min-height: 220px;
|
|
background: white;
|
|
}
|
|
|
|
/* Prevent horizontal overflow */
|
|
#report-content {
|
|
overflow-x: hidden !important;
|
|
}
|
|
|
|
/* Ensure proper sizing for PDF capture */
|
|
.apexcharts-svg {
|
|
max-width: 100% !important;
|
|
}
|
|
|
|
/* Executive Report Styling - Subdued & Professional */
|
|
.executive-header {
|
|
background: white;
|
|
border-bottom: 2px solid #005696;
|
|
padding: 24px 0;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.executive-summary {
|
|
background: white;
|
|
border: 1px solid #e2e8f0;
|
|
padding: 24px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.summary-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(4, 1fr);
|
|
gap: 24px;
|
|
margin-top: 20px;
|
|
}
|
|
|
|
.summary-card {
|
|
background: white;
|
|
border: 1px solid #e2e8f0;
|
|
padding: 20px;
|
|
text-align: center;
|
|
}
|
|
|
|
.summary-value {
|
|
font-size: 22pt;
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.summary-label {
|
|
font-size: 8pt;
|
|
color: #64748b;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.status-text {
|
|
font-size: 10pt;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.status-ontarget { color: #005696; }
|
|
.status-below { color: #64748b; }
|
|
.status-critical { color: #334155; }
|
|
|
|
.progress-bar {
|
|
width: 100%;
|
|
height: 4px;
|
|
background: #e2e8f0;
|
|
margin-top: 12px;
|
|
}
|
|
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: #005696;
|
|
}
|
|
|
|
/* Minimalist Table Styling */
|
|
.table-container {
|
|
background: white;
|
|
border: 1px solid #e2e8f0;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.table-header {
|
|
background: #f8fafc;
|
|
color: #334155;
|
|
padding: 12px 16px;
|
|
font-size: 10pt;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.data-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 8pt;
|
|
}
|
|
|
|
.data-table th {
|
|
background: #f8fafc;
|
|
color: #475569;
|
|
font-weight: 600;
|
|
text-align: left;
|
|
padding: 10px 8px;
|
|
border-bottom: 1px solid #cbd5e1;
|
|
text-transform: uppercase;
|
|
font-size: 7pt;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.data-table td {
|
|
padding: 8px;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
/* Minimal Chart Container Styling */
|
|
.chart-section {
|
|
background: white;
|
|
border: 1px solid #e2e8f0;
|
|
padding: 20px;
|
|
margin-bottom: 24px;
|
|
}
|
|
|
|
.chart-title {
|
|
font-size: 11pt;
|
|
font-weight: 600;
|
|
color: #334155;
|
|
margin-bottom: 16px;
|
|
padding-bottom: 8px;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
/* Minimal Section Headers */
|
|
.section-header {
|
|
font-size: 12pt;
|
|
font-weight: 600;
|
|
color: #334155;
|
|
margin-bottom: 16px;
|
|
padding-bottom: 8px;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.subsection-header {
|
|
font-size: 10pt;
|
|
font-weight: 600;
|
|
color: #475569;
|
|
margin-bottom: 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
/* Minimal Breakdown Cards */
|
|
.breakdown-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 12px;
|
|
}
|
|
|
|
.breakdown-card {
|
|
background: white;
|
|
border: 1px solid #e2e8f0;
|
|
padding: 12px;
|
|
min-height: 100px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.breakdown-title {
|
|
font-size: 8pt;
|
|
font-weight: 600;
|
|
color: #64748b;
|
|
text-transform: uppercase;
|
|
letter-spacing: 1px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.breakdown-value {
|
|
font-size: 16pt;
|
|
font-weight: 600;
|
|
color: #1e293b;
|
|
}
|
|
|
|
.breakdown-percent {
|
|
font-size: 9pt;
|
|
color: #64748b;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
/* Minimal AI Analysis Styling */
|
|
.ai-section {
|
|
background: white;
|
|
border: 1px solid #e2e8f0;
|
|
padding: 24px;
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.ai-header {
|
|
font-size: 11pt;
|
|
font-weight: 600;
|
|
color: #334155;
|
|
margin-bottom: 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding-bottom: 8px;
|
|
border-bottom: 1px solid #e2e8f0;
|
|
}
|
|
|
|
.ai-content {
|
|
font-size: 10pt;
|
|
line-height: 1.6;
|
|
color: #334155;
|
|
}
|
|
|
|
.ai-content h4 {
|
|
font-size: 10pt;
|
|
font-weight: 600;
|
|
color: #475569;
|
|
margin-top: 16px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.ai-content ul {
|
|
margin: 8px 0;
|
|
padding-left: 20px;
|
|
}
|
|
|
|
.ai-content li {
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
/* Minimal Report Footer */
|
|
.report-footer {
|
|
margin-top: 40px;
|
|
padding-top: 20px;
|
|
border-top: 1px solid #e2e8f0;
|
|
text-align: center;
|
|
font-size: 8pt;
|
|
color: #64748b;
|
|
}
|
|
|
|
.report-footer-logo {
|
|
height: 20px;
|
|
margin-bottom: 8px;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* Minimal Metadata styling */
|
|
.metadata-row {
|
|
display: flex;
|
|
gap: 24px;
|
|
flex-wrap: wrap;
|
|
font-size: 9pt;
|
|
color: #64748b;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.metadata-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.metadata-item strong {
|
|
color: #475569;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Risk text (no badge) */
|
|
.risk-text {
|
|
font-weight: 600;
|
|
}
|
|
|
|
.risk-low { color: #475569; }
|
|
.risk-medium { color: #64748b; }
|
|
.risk-high { color: #334155; }
|
|
</style>
|
|
</head>
|
|
<body class="bg-white text-slate-800">
|
|
<!-- PDF Controls -->
|
|
<div class="no-print fixed top-4 right-4 z-50 flex gap-2">
|
|
<button onclick="generatePDF()" class="bg-navy text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-blue transition-colors shadow-lg">
|
|
<i data-lucide="download" class="w-4 h-4"></i>
|
|
{% trans "Download PDF" %}
|
|
</button>
|
|
<button onclick="window.print()" class="bg-slate text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-slate-600 transition-colors shadow-lg">
|
|
<i data-lucide="printer" class="w-4 h-4"></i>
|
|
{% trans "Print" %}
|
|
</button>
|
|
<a href="{% url 'analytics:kpi_report_detail' report.id %}" class="bg-white text-navy border border-navy px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-light transition-colors shadow-lg">
|
|
<i data-lucide="arrow-left" class="w-4 h-4"></i>
|
|
{% trans "Back" %}
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Report Container -->
|
|
<div id="report-content" class="w-full mx-auto p-6">
|
|
|
|
<!-- Executive Header with Logo -->
|
|
<div class="executive-header no-break">
|
|
<div class="flex justify-between items-start">
|
|
<div class="flex items-center gap-4">
|
|
<img src="{% static 'img/hh-logo.png' %}" alt="Al Hammadi Hospital" class="h-14 w-auto object-contain">
|
|
<div>
|
|
<h1 style="font-size: 20pt; font-weight: 700; margin: 0; letter-spacing: -0.5px;">{% trans "Key Performance Indicator Report" %}</h1>
|
|
<p style="font-size: 10pt; opacity: 0.9; margin: 4px 0 0 0;">{{ report.hospital.name }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="text-right">
|
|
<div style="font-size: 16pt; font-weight: 700; margin-bottom: 4px;">{{ report.kpi_id }}</div>
|
|
<p style="font-size: 10pt; opacity: 0.9;">{{ report.report_period_display }}</p>
|
|
<p style="font-size: 9pt; opacity: 0.8; margin-top: 4px;">{% trans "Generated:" %} {% now "M d, Y" %}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Executive Summary Section -->
|
|
<div class="executive-summary no-break">
|
|
<div class="flex justify-between items-start mb-4">
|
|
<div>
|
|
<h2 style="font-size: 14pt; font-weight: 700; color: #1e293b; margin: 0 0 8px 0;">{{ report.indicator_title }}</h2>
|
|
<div class="metadata-row" style="margin: 0;">
|
|
<div class="metadata-item">
|
|
<strong>{% trans "Category:" %}</strong> {{ report.category }}
|
|
</div>
|
|
<div class="metadata-item">
|
|
<strong>{% trans "Type:" %}</strong> {{ report.kpi_type }}
|
|
</div>
|
|
<div class="metadata-item">
|
|
<strong>{% trans "Risk Level:" %}</strong>
|
|
<span class="risk-text risk-{{ report.risk_level|lower }}">{{ report.risk_level }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="text-right">
|
|
{% if report.overall_result >= report.target_percentage %}
|
|
<span class="status-text status-ontarget">{% trans "On Target" %}</span>
|
|
{% elif report.overall_result >= report.threshold_percentage %}
|
|
<span class="status-text status-below">{% trans "Below Target" %}</span>
|
|
{% else %}
|
|
<span class="status-text status-critical">{% trans "Critical" %}</span>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="summary-grid">
|
|
<div class="summary-card">
|
|
<div class="summary-value">{{ report.overall_result }}%</div>
|
|
<div class="summary-label">{% trans "Overall Result" %}</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="summary-value">{{ report.target_percentage }}%</div>
|
|
<div class="summary-label">{% trans "Target" %}</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="summary-value">{{ report.total_numerator }}</div>
|
|
<div class="summary-label">{{ report.numerator_label }}</div>
|
|
</div>
|
|
<div class="summary-card">
|
|
<div class="summary-value">{{ report.total_denominator }}</div>
|
|
<div class="summary-label">{{ report.denominator_label }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div style="margin-top: 16px;">
|
|
<div style="font-size: 9pt; color: #64748b; margin-bottom: 4px;">{% trans "Progress vs Target" %}</div>
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" style="width: {{ report.overall_result|default:0 }}%;"></div>
|
|
</div>
|
|
<div style="display: flex; justify-content: space-between; font-size: 8pt; color: #94a3b8; margin-top: 4px;">
|
|
<span>0%</span>
|
|
<span>{% trans "Threshold:" %} {{ report.threshold_percentage }}%</span>
|
|
<span>{% trans "Target:" %} {{ report.target_percentage }}%</span>
|
|
<span>100%</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main Data Table -->
|
|
<div class="table-container no-break">
|
|
<div class="table-header">
|
|
<i data-lucide="table" style="width: 16px; height: 16px;"></i>
|
|
{% trans "Monthly Performance Data" %}
|
|
</div>
|
|
<table class="data-table">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 60px;">{% trans "KPI" %}</th>
|
|
<th style="width: 140px;">{% trans "Indicator" %}</th>
|
|
<th style="width: 100px;">{% trans "Measure" %}</th>
|
|
<th>{% trans "Jan" %}</th>
|
|
<th>{% trans "Feb" %}</th>
|
|
<th>{% trans "Mar" %}</th>
|
|
<th>{% trans "Apr" %}</th>
|
|
<th>{% trans "May" %}</th>
|
|
<th>{% trans "Jun" %}</th>
|
|
<th>{% trans "Jul" %}</th>
|
|
<th>{% trans "Aug" %}</th>
|
|
<th>{% trans "Sep" %}</th>
|
|
<th>{% trans "Oct" %}</th>
|
|
<th>{% trans "Nov" %}</th>
|
|
<th>{% trans "Dec" %}</th>
|
|
<th>{% trans "TOTAL" %}</th>
|
|
<th>{% trans "Target" %}</th>
|
|
<th>{% trans "Threshold" %}</th>
|
|
</tr>
|
|
</thead>
|
|
|
|
<tbody>
|
|
<!-- Numerator Row -->
|
|
<tr>
|
|
<td rowspan="3" style="text-align: center; font-weight: 600; background: #f8fafc;">
|
|
{{ report.kpi_id }}
|
|
</td>
|
|
<td rowspan="3" style="font-weight: 600; max-width: 140px; overflow: hidden; text-overflow: ellipsis;">
|
|
{{ report.indicator_title }}
|
|
</td>
|
|
<td style="background: #f8fafc; font-size: 7pt;">
|
|
{{ report.numerator_label }}
|
|
</td>
|
|
{% for m in monthly_data %}
|
|
<td style="text-align: center;">{% if m %}{{ m.numerator }}{% else %}-{% endif %}</td>
|
|
{% endfor %}
|
|
<td style="text-align: center; font-weight: 600; background: #f8fafc;">{{ report.total_numerator }}</td>
|
|
<td rowspan="3" style="text-align: center; font-weight: 600; background: #f8fafc;">{{ report.target_percentage }}%</td>
|
|
<td rowspan="3" style="text-align: center; font-weight: 600; background: #f8fafc;">{{ report.threshold_percentage }}%</td>
|
|
</tr>
|
|
|
|
<!-- Denominator Row -->
|
|
<tr>
|
|
<td style="background: #f8fafc; font-size: 7pt;">
|
|
{{ report.denominator_label }}
|
|
</td>
|
|
{% for m in monthly_data %}
|
|
<td style="text-align: center;">{% if m %}{{ m.denominator }}{% else %}-{% endif %}</td>
|
|
{% endfor %}
|
|
<td style="text-align: center; font-weight: 600; background: #f8fafc;">{{ report.total_denominator }}</td>
|
|
</tr>
|
|
|
|
<!-- Result Row -->
|
|
<tr style="font-weight: 600;">
|
|
<td style="background: #f8fafc;">
|
|
{% trans "Result (%)" %}
|
|
</td>
|
|
{% for m in monthly_data %}
|
|
<td style="text-align: center;">
|
|
{% if m %}{{ m.percentage }}%{% else %}-{% endif %}
|
|
</td>
|
|
{% endfor %}
|
|
<td style="text-align: center; font-weight: 600;">
|
|
{{ report.overall_result }}%
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Metadata Section -->
|
|
<div class="executive-summary no-break" style="margin-top: 20px;">
|
|
<div class="subsection-header">
|
|
<i data-lucide="info" style="width: 16px; height: 16px;"></i>
|
|
{% trans "Report Metadata" %}
|
|
</div>
|
|
<div class="metadata-row">
|
|
<div class="metadata-item">
|
|
<strong>{% trans "Category:" %}</strong> {{ report.category }}
|
|
</div>
|
|
<div class="metadata-item">
|
|
<strong>{% trans "Type:" %}</strong> {{ report.kpi_type }}
|
|
</div>
|
|
<div class="metadata-item">
|
|
<strong>{% trans "Dimension:" %}</strong> {{ report.dimension }}
|
|
</div>
|
|
<div class="metadata-item">
|
|
<strong>{% trans "Collection:" %}</strong> {{ report.data_collection_method }}
|
|
</div>
|
|
<div class="metadata-item">
|
|
<strong>{% trans "Frequency:" %}</strong> {{ report.data_collection_frequency }}
|
|
</div>
|
|
<div class="metadata-item">
|
|
<strong>{% trans "Reporting:" %}</strong> {{ report.reporting_frequency }}
|
|
</div>
|
|
<div class="metadata-item">
|
|
<strong>{% trans "Collector:" %}</strong> {{ report.collector_name|default:"-" }}
|
|
</div>
|
|
<div class="metadata-item">
|
|
<strong>{% trans "Analyzer:" %}</strong> {{ report.analyzer_name|default:"-" }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charts Section -->
|
|
<div class="section-header" style="margin-top: 24px;">
|
|
<i data-lucide="bar-chart-2" style="width: 20px; height: 20px;"></i>
|
|
{% trans "Performance Analytics" %}
|
|
</div>
|
|
|
|
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: 24px; margin-bottom: 24px;">
|
|
<!-- Trend Chart -->
|
|
<div class="chart-section no-break">
|
|
<div class="chart-title">
|
|
<i data-lucide="trending-up" style="width: 18px; height: 18px;"></i>
|
|
{% trans "Monthly Performance Trend" %}
|
|
</div>
|
|
<div style="font-size: 9pt; color: #64748b; margin-bottom: 12px;">
|
|
{% trans "Target:" %} <strong>{{ report.target_percentage }}%</strong> |
|
|
{% trans "Threshold:" %} <strong>{{ report.threshold_percentage }}%</strong>
|
|
</div>
|
|
<div id="trendChart" style="height: 240px; width: 100%;"></div>
|
|
</div>
|
|
|
|
<!-- Source Chart -->
|
|
<div class="chart-section no-break">
|
|
<div class="chart-title" style="justify-content: center;">
|
|
<i data-lucide="pie-chart" style="width: 18px; height: 18px;"></i>
|
|
{% trans "Distribution by Source" %}
|
|
</div>
|
|
<div id="sourceChart" style="height: 240px; width: 100%;"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Department Breakdown -->
|
|
{% if department_breakdowns %}
|
|
<div class="no-break" style="margin-bottom: 24px;">
|
|
<div class="section-header">
|
|
<i data-lucide="building" style="width: 20px; height: 20px;"></i>
|
|
{% trans "Department Performance Breakdown" %}
|
|
</div>
|
|
|
|
<div class="breakdown-grid">
|
|
{% for dept in department_breakdowns %}
|
|
<div class="breakdown-card no-break">
|
|
<div class="breakdown-title">{{ dept.get_department_category_display }}</div>
|
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-top: 8px;">
|
|
<div>
|
|
<div class="breakdown-value">{{ dept.complaint_count }}</div>
|
|
<div class="breakdown-percent">{% trans "complaints" %}</div>
|
|
</div>
|
|
<div style="text-align: right;">
|
|
<div style="font-size: 12pt; font-weight: 600;">{{ dept.resolved_count }}</div>
|
|
<div style="font-size: 7pt; color: #64748b;">{% trans "resolved" %}</div>
|
|
</div>
|
|
</div>
|
|
{% if dept.avg_resolution_days %}
|
|
<div style="margin-top: 8px; padding-top: 8px; border-top: 1px solid #e2e8f0; font-size: 8pt; color: #64748b;">
|
|
<strong>{% trans "Avg:" %}</strong> {{ dept.avg_resolution_days }} {% trans "days" %}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Location Breakdown -->
|
|
{% if location_breakdowns %}
|
|
<div class="no-break" style="margin-bottom: 24px;">
|
|
<div class="section-header">
|
|
<i data-lucide="map-pin" style="width: 20px; height: 20px;"></i>
|
|
{% trans "Location Distribution" %}
|
|
</div>
|
|
|
|
<div class="breakdown-grid">
|
|
{% for loc in location_breakdowns %}
|
|
<div class="breakdown-card no-break text-center">
|
|
<div class="breakdown-title">{{ loc.location_type }}</div>
|
|
<div style="margin-top: 8px;">
|
|
<div class="breakdown-value">{{ loc.complaint_count }}</div>
|
|
<div class="breakdown-percent">{{ loc.percentage }}%</div>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- AI Analysis Section -->
|
|
{% if report.ai_analysis %}
|
|
<div class="page-break"></div>
|
|
<div class="ai-section no-break">
|
|
<div class="ai-header">
|
|
<i data-lucide="brain" style="width: 20px; height: 20px;"></i>
|
|
{% trans "AI-Powered Performance Analysis" %}
|
|
</div>
|
|
|
|
{% with analysis=report.ai_analysis %}
|
|
|
|
<!-- Executive Summary -->
|
|
{% if analysis.executive_summary %}
|
|
<div style="background: white; border: 1px solid #e2e8f0; padding: 16px; margin-bottom: 16px;">
|
|
<h4 style="font-size: 10pt; font-weight: 600; color: #334155; margin: 0 0 8px 0;">
|
|
{% trans "Executive Summary" %}
|
|
</h4>
|
|
<p class="ai-content">{{ analysis.executive_summary }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
|
|
<!-- Performance Analysis -->
|
|
{% if analysis.performance_analysis %}
|
|
<div style="background: white; border: 1px solid #e2e8f0; padding: 16px;">
|
|
<h4 style="font-size: 10pt; font-weight: 600; color: #334155; margin: 0 0 8px 0;">
|
|
{% trans "Performance Analysis" %}
|
|
</h4>
|
|
<p class="ai-content">{{ analysis.performance_analysis }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Comparison to Target -->
|
|
{% if analysis.comparison_to_target %}
|
|
<div style="background: white; border: 1px solid #e2e8f0; padding: 16px;">
|
|
<h4 style="font-size: 10pt; font-weight: 600; color: #334155; margin: 0 0 8px 0;">
|
|
{% trans "Comparison to Target" %}
|
|
</h4>
|
|
<p class="ai-content">{{ analysis.comparison_to_target }}</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Key Findings -->
|
|
{% if analysis.key_findings %}
|
|
<div style="background: white; border: 1px solid #e2e8f0; padding: 16px;">
|
|
<h4 style="font-size: 10pt; font-weight: 600; color: #334155; margin: 0 0 8px 0;">
|
|
{% trans "Key Findings" %}
|
|
</h4>
|
|
<ul class="ai-content">
|
|
{% for finding in analysis.key_findings %}
|
|
<li>{{ finding }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Reasons for Delays -->
|
|
{% if analysis.reasons_for_delays %}
|
|
<div style="background: white; border: 1px solid #e2e8f0; padding: 16px;">
|
|
<h4 style="font-size: 10pt; font-weight: 600; color: #334155; margin: 0 0 8px 0;">
|
|
{% trans "Reasons for Delays" %}
|
|
</h4>
|
|
<ul class="ai-content">
|
|
{% for reason in analysis.reasons_for_delays %}
|
|
<li>{{ reason }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Resolution Time Analysis -->
|
|
{% if analysis.resolution_time_analysis %}
|
|
<div class="mb-4">
|
|
<h4 class="text-sm font-bold text-navy mb-1">{% trans "Resolution Time Breakdown" %}</h4>
|
|
<div class="grid grid-cols-4 gap-2 text-center text-sm">
|
|
{% if analysis.resolution_time_analysis.within_24h %}
|
|
<div class="p-2 bg-green-50 border border-green-200 rounded">
|
|
<p class="text-xs text-slate">{% trans "Within 24h" %}</p>
|
|
<p class="font-bold text-green-600">{{ analysis.resolution_time_analysis.within_24h.count }}</p>
|
|
<p class="text-xs text-green-600">{{ analysis.resolution_time_analysis.within_24h.percentage }}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if analysis.resolution_time_analysis.within_48h %}
|
|
<div class="p-2 bg-blue-50 border border-blue-200 rounded">
|
|
<p class="text-xs text-slate">{% trans "Within 48h" %}</p>
|
|
<p class="font-bold text-blue-600">{{ analysis.resolution_time_analysis.within_48h.count }}</p>
|
|
<p class="text-xs text-blue-600">{{ analysis.resolution_time_analysis.within_48h.percentage }}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if analysis.resolution_time_analysis.within_72h %}
|
|
<div class="p-2 bg-yellow-50 border border-yellow-200 rounded">
|
|
<p class="text-xs text-slate">{% trans "Within 72h" %}</p>
|
|
<p class="font-bold text-yellow-600">{{ analysis.resolution_time_analysis.within_72h.count }}</p>
|
|
<p class="text-xs text-yellow-600">{{ analysis.resolution_time_analysis.within_72h.percentage }}</p>
|
|
</div>
|
|
{% endif %}
|
|
{% if analysis.resolution_time_analysis.over_72h %}
|
|
<div class="p-2 bg-red-50 border border-red-200 rounded">
|
|
<p class="text-xs text-slate">{% trans "Over 72h" %}</p>
|
|
<p class="font-bold text-red-600">{{ analysis.resolution_time_analysis.over_72h.count }}</p>
|
|
<p class="text-xs text-red-600">{{ analysis.resolution_time_analysis.over_72h.percentage }}</p>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Recommendations -->
|
|
{% if analysis.recommendations %}
|
|
<div class="mb-4 p-3 bg-green-50 border border-green-200 rounded">
|
|
<h4 class="text-sm font-bold text-green-800 mb-1">{% trans "Recommendations" %}</h4>
|
|
<ul class="list-disc list-inside text-sm text-green-700">
|
|
{% for rec in analysis.recommendations %}
|
|
<li>{{ rec }}</li>
|
|
{% endfor %}
|
|
</ul>
|
|
</div>
|
|
{% endif %}
|
|
{% endwith %}
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Review and Approval Section -->
|
|
<div class="page-break"></div>
|
|
<div class="mt-12 pt-8 border-t-2 border-slate-300">
|
|
<h3 class="text-lg font-bold text-navy mb-6">{% trans "Review and Approval" %}</h3>
|
|
|
|
<div class="grid grid-cols-3 gap-6">
|
|
<!-- Reviewed By -->
|
|
<div class="border-t border-slate-400 pt-4">
|
|
<p class="text-sm font-bold text-navy mb-8">{% trans "Reviewed By:" %}</p>
|
|
<div class="mt-12">
|
|
<p class="text-sm text-slate border-b border-slate-300 pb-1 mb-2"> </p>
|
|
<p class="text-xs text-slate">{% trans "Name & Signature" %}</p>
|
|
<p class="text-xs text-slate mt-2">{% trans "Date:" %} _______________</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reviewed and Approved By 1 -->
|
|
<div class="border-t border-slate-400 pt-4">
|
|
<p class="text-sm font-bold text-navy mb-8">{% trans "Reviewed and Approved By:" %}</p>
|
|
<div class="mt-12">
|
|
<p class="text-sm text-slate border-b border-slate-300 pb-1 mb-2"> </p>
|
|
<p class="text-xs text-slate">{% trans "Name & Signature" %}</p>
|
|
<p class="text-xs text-slate mt-2">{% trans "Date:" %} _______________</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reviewed and Approved By 2 -->
|
|
<div class="border-t border-slate-400 pt-4">
|
|
<p class="text-sm font-bold text-navy mb-8">{% trans "Reviewed and Approved By:" %}</p>
|
|
<div class="mt-12">
|
|
<p class="text-sm text-slate border-b border-slate-300 pb-1 mb-2"> </p>
|
|
<p class="text-xs text-slate">{% trans "Name & Signature" %}</p>
|
|
<p class="text-xs text-slate mt-2">{% trans "Date:" %} _______________</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Professional Footer -->
|
|
<div class="report-footer">
|
|
<img src="{% static 'img/hh-logo.png' %}" alt="Al Hammadi Hospital" class="report-footer-logo">
|
|
<p style="font-weight: 600; color: #005696; margin-bottom: 4px;">
|
|
Al Hammadi Hospital - {% trans "Key Performance Indicator Report" %}
|
|
</p>
|
|
<p>
|
|
{% trans "Generated by PX360 Patient Experience Management System" %} |
|
|
{{ report.report_period_display }}
|
|
</p>
|
|
<p style="margin-top: 4px;">
|
|
© {% now "Y" %} Al Hammadi Hospitals. {% trans "Confidential - For Internal Use Only" %}
|
|
</p>
|
|
<p style="margin-top: 3px; font-size: 10px; color: #999;">
|
|
Powered by <a href="https://tenhal.sa" style="color: #005696;">tenhal.sa</a>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Initialize Lucide icons
|
|
if (typeof lucide !== 'undefined') {
|
|
lucide.createIcons();
|
|
}
|
|
|
|
// Parse chart data safely
|
|
let trendData, sourceData;
|
|
try {
|
|
trendData = {{ trend_chart_data_json|safe }};
|
|
sourceData = {{ source_chart_data_json|safe }};
|
|
} catch (e) {
|
|
console.error('Error parsing chart data:', e);
|
|
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 };
|
|
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: 220,
|
|
width: '100%',
|
|
toolbar: { show: false },
|
|
animations: { enabled: false }
|
|
},
|
|
stroke: {
|
|
curve: 'smooth',
|
|
width: 3
|
|
},
|
|
colors: ['#005696'],
|
|
xaxis: {
|
|
categories: trendData.labels,
|
|
labels: { style: { fontSize: '10px' } }
|
|
},
|
|
yaxis: {
|
|
max: 100,
|
|
labels: { formatter: function(val) { return val + '%'; }, style: { fontSize: '10px' } }
|
|
},
|
|
grid: {
|
|
borderColor: '#e2e8f0',
|
|
strokeDashArray: 4
|
|
},
|
|
annotations: trendData.target ? {
|
|
yaxis: [
|
|
{
|
|
y: trendData.target,
|
|
borderColor: '#22c55e',
|
|
strokeDashArray: 4,
|
|
label: {
|
|
text: '{% trans "Target" %}: ' + trendData.target + '%',
|
|
style: { color: '#22c55e', background: '#f0fdf4', fontSize: '10px' }
|
|
}
|
|
},
|
|
{
|
|
y: trendData.threshold,
|
|
borderColor: '#ef4444',
|
|
strokeDashArray: 6,
|
|
label: {
|
|
text: '{% trans "Threshold" %}: ' + trendData.threshold + '%',
|
|
style: { color: '#ef4444', background: '#fef2f2', fontSize: '10px' }
|
|
}
|
|
}
|
|
]
|
|
} : {},
|
|
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: 220,
|
|
width: '100%',
|
|
animations: { enabled: false }
|
|
},
|
|
colors: ['#005696', '#007bbd', '#64748b', '#94a3b8', '#cbd5e1'],
|
|
legend: {
|
|
position: 'bottom',
|
|
fontSize: '10px'
|
|
},
|
|
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);
|
|
}
|
|
}
|
|
|
|
// PDF Generation
|
|
async function generatePDF() {
|
|
const element = document.getElementById('report-content');
|
|
const btn = document.querySelector('button[onclick="generatePDF()"]');
|
|
const originalText = btn.innerHTML;
|
|
|
|
// Show loading
|
|
btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> {% trans "Generating..." %}';
|
|
btn.disabled = true;
|
|
lucide.createIcons();
|
|
|
|
try {
|
|
// Wait for charts to fully render
|
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
|
|
// Convert charts to images before PDF generation
|
|
await convertChartsToImages();
|
|
|
|
const opt = {
|
|
margin: [10, 10, 10, 10],
|
|
filename: '{{ report.kpi_id }}_{{ report.year }}_{{ report.month }}_{{ report.hospital.name|slugify }}.pdf',
|
|
image: { type: 'jpeg', quality: 0.95 },
|
|
html2canvas: {
|
|
scale: 1.5,
|
|
useCORS: false, // Disable CORS to avoid tainted canvas issues
|
|
logging: false,
|
|
allowTaint: false, // Don't allow tainted canvas
|
|
letterRendering: true,
|
|
foreignObjectRendering: false, // Disable foreignObject rendering which causes issues
|
|
onclone: function(clonedDoc) {
|
|
// Hide any remaining SVGs in the clone, only show our static images
|
|
clonedDoc.querySelectorAll('.apexcharts-svg, .apexcharts-canvas').forEach(el => {
|
|
el.style.display = 'none';
|
|
});
|
|
}
|
|
},
|
|
jsPDF: {
|
|
unit: 'mm',
|
|
format: 'a4',
|
|
orientation: 'landscape'
|
|
}
|
|
};
|
|
|
|
await html2pdf().set(opt).from(element).save();
|
|
|
|
} catch (error) {
|
|
console.error('PDF generation error:', error);
|
|
alert('{% trans "Error generating PDF. Please try again." %}');
|
|
} finally {
|
|
// Restore charts after PDF generation (success or failure)
|
|
restoreChartsFromImages();
|
|
btn.innerHTML = originalText;
|
|
btn.disabled = false;
|
|
lucide.createIcons();
|
|
}
|
|
}
|
|
|
|
// Convert ApexCharts to static images for PDF capture
|
|
async function convertChartsToImages() {
|
|
const chartContainers = ['trendChart', 'sourceChart'];
|
|
|
|
for (const containerId of chartContainers) {
|
|
const container = document.getElementById(containerId);
|
|
if (!container) continue;
|
|
|
|
try {
|
|
// Find the SVG element (ApexCharts renders SVG)
|
|
const svg = container.querySelector('.apexcharts-svg') || container.querySelector('svg');
|
|
|
|
if (svg) {
|
|
// Clone the SVG to modify it for export
|
|
const svgClone = svg.cloneNode(true);
|
|
|
|
// Get dimensions
|
|
const rect = svg.getBoundingClientRect();
|
|
const width = rect.width || 600;
|
|
const height = rect.height || 220;
|
|
|
|
// Ensure SVG has proper attributes for standalone rendering
|
|
svgClone.setAttribute('width', width);
|
|
svgClone.setAttribute('height', height);
|
|
svgClone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
svgClone.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
|
|
|
// Inline any computed styles by copying them
|
|
const computedStyles = window.getComputedStyle(svg);
|
|
svgClone.style.fontFamily = computedStyles.fontFamily;
|
|
|
|
// Convert SVG to XML string
|
|
const svgData = new XMLSerializer().serializeToString(svgClone);
|
|
|
|
// Create SVG data URL directly (no canvas needed!)
|
|
// Use TextEncoder for proper UTF-8 handling
|
|
const utf8Bytes = new TextEncoder().encode(svgData);
|
|
const binaryString = Array.from(utf8Bytes).map(b => String.fromCharCode(b)).join('');
|
|
const svgBase64 = btoa(binaryString);
|
|
const dataUrl = 'data:image/svg+xml;base64,' + svgBase64;
|
|
|
|
// Create image element
|
|
const imgElement = document.createElement('img');
|
|
imgElement.src = dataUrl;
|
|
imgElement.style.width = width + 'px';
|
|
imgElement.style.height = height + 'px';
|
|
imgElement.style.maxWidth = '100%';
|
|
imgElement.className = 'chart-static-image';
|
|
imgElement.dataset.originalContainer = containerId;
|
|
|
|
// Wait for image to load
|
|
await new Promise((resolve) => {
|
|
imgElement.onload = resolve;
|
|
imgElement.onerror = resolve; // Continue on error
|
|
});
|
|
|
|
// Hide original chart and show image
|
|
svg.style.visibility = 'hidden';
|
|
container.appendChild(imgElement);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error converting ${containerId} to image:`, error);
|
|
// Continue without converting this chart
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restore charts from images back to interactive charts
|
|
function restoreChartsFromImages() {
|
|
// Remove static images
|
|
document.querySelectorAll('.chart-static-image').forEach(img => {
|
|
img.remove();
|
|
});
|
|
|
|
// Show original SVGs
|
|
document.querySelectorAll('#trendChart svg, #sourceChart svg').forEach(svg => {
|
|
svg.style.visibility = '';
|
|
});
|
|
|
|
// Reset container positions
|
|
document.querySelectorAll('#trendChart, #sourceChart').forEach(container => {
|
|
container.style.position = '';
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|