HH/templates/complaints/partials/pdf_summary_panel.html
ismail 7369d08012
All checks were successful
Build and Push Docker Image / build (push) Successful in 4m14s
feat: unified reference numbers + feedback modules QA audit
Reference numbers (unified scheme PREFIX-YYYYMM-HOSP-NNNN, e.g. CMP-202606-HHN-0001):
- new ReferenceSequence model + generate_reference() helper (apps/core)
- Complaint/Inquiry/Observation/Appreciation/Suggestion emit unified refs via save()
- prefix-based auto-routing in public track API (CMP/INQ/OBS trackable; APR/SGT internal-only)
- removed legacy CMP-/INQ- generators in ui_views, integrations, px_sources
- migrations: core.0003_referencesequence, appreciation.0006, feedback.0008, observations.0012
- unit tests (format, sanitization, monthly reset, 40-thread concurrency)

QA audit:
- isolated E2E hospital sandbox mirroring HH-N + 10 role users (create_e2e_isolated_env)
- feedback-modules-audit.spec.ts + audit helper (headed, run-to-completion)
- reports/feedback-modules-qa-report.md

Also bundles accumulated in-progress work across complaints, observations,
organizations, templates, and other modules.
2026-06-14 14:29:23 +03:00

441 lines
22 KiB
HTML

{% load i18n %}
<section id="pdfSummaryContent" class="bg-white rounded-2xl p-6 shadow-sm border border-slate-100">
<div class="flex items-center justify-between mb-6">
<h3 class="text-xl font-bold text-navy flex items-center gap-2">
<i data-lucide="file-text" class="w-6 h-6"></i> {% trans "PDF Report" %}
</h3>
</div>
<p class="text-slate text-sm mb-6">
{% trans "Generate a complaint report PDF with AI-generated summaries." %}
</p>
<!-- State: checking -->
<div id="pdfSummaryChecking" class="text-center py-10">
<div class="inline-block w-8 h-8 border-[3px] border-navy/20 border-t-navy rounded-full animate-spin"></div>
<p class="text-slate text-sm mt-4">{% trans "Checking..." %}</p>
</div>
<!-- State: empty -->
<div id="pdfSummaryEmpty" class="hidden">
<button onclick="generateSummaries()" id="generateSummariesBtn"
class="inline-flex items-center gap-2.5 px-6 py-3 bg-gradient-to-r from-navy to-blue text-white rounded-xl font-bold hover:from-blue hover:to-navy transition shadow-lg text-sm w-full justify-center">
<i data-lucide="sparkles" class="w-4 h-4"></i>
إنشاء الملخص
</button>
</div>
<!-- State: loading_ai -->
<div id="pdfSummaryLoadingAi" class="text-center py-10 hidden">
<div class="inline-block w-8 h-8 border-[3px] border-navy/20 border-t-navy rounded-full animate-spin"></div>
<p class="text-slate text-sm mt-4">{% trans "Generating summaries..." %}</p>
<p class="text-slate text-xs mt-1">{% trans "This may take a moment" %}</p>
</div>
<!-- State: edit -->
<div id="pdfSummaryEdit" class="hidden">
<div class="mb-5">
<h4 class="text-sm font-bold text-navy mb-3 pb-2 border-b border-slate-200">معلومات الشكوى</h4>
<div class="grid grid-cols-2 gap-3 mb-4">
<div>
<label class="block text-xs font-semibold text-slate mb-1">الاسم</label>
<input id="fld_complainant_name" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div>
<label class="block text-xs font-semibold text-slate mb-1">جهة الادعاء</label>
<input id="fld_source_name" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div>
<label class="block text-xs font-semibold text-slate mb-1">تاريخ التقديم</label>
<input id="fld_submission_date" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div>
<label class="block text-xs font-semibold text-slate mb-1">تاريخ الحادثة</label>
<input id="fld_incident_date" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div class="col-span-2">
<label class="block text-xs font-semibold text-slate mb-1">القسم</label>
<input id="fld_department_name" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
</div>
<h4 class="text-sm font-bold text-navy mb-3 pb-2 border-b border-slate-200">بيانات الموظف</h4>
<div class="grid grid-cols-2 gap-3 mb-4">
<div>
<label class="block text-xs font-semibold text-slate mb-1">اسم الموظف</label>
<input id="fld_accused_staff_name" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div>
<label class="block text-xs font-semibold text-slate mb-1">الوظيفة</label>
<input id="fld_accused_staff_title" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div>
<label class="block text-xs font-semibold text-slate mb-1">تاريخ الإرسال</label>
<input id="fld_sent_to_dept_date" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div>
<label class="block text-xs font-semibold text-slate mb-1">تاريخ الرد</label>
<input id="fld_response_date" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
</div>
<h4 class="text-sm font-bold text-navy mb-3 pb-2 border-b border-slate-200">الملخصات</h4>
<div class="mb-3">
<label class="block text-xs font-semibold text-slate mb-1">مختصر الشكوى</label>
<textarea id="fld_content_summary" rows="5"
class="w-full border border-slate-200 rounded-xl p-3 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy resize-y"></textarea>
</div>
<div class="mb-5">
<label class="block text-xs font-semibold text-slate mb-1">مختصر الرد</label>
<textarea id="fld_dept_response_summary" rows="5"
class="w-full border border-slate-200 rounded-xl p-3 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy resize-y"></textarea>
</div>
<div class="flex items-center gap-3">
<button onclick="generatePdf()" id="generatePdfBtn"
class="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-navy to-blue text-white rounded-xl font-bold hover:from-blue hover:to-navy transition shadow-lg text-sm flex-1 justify-center">
<i data-lucide="file-text" class="w-4 h-4"></i>
إنشاء PDF
</button>
<button onclick="generateSummaries()"
class="inline-flex items-center gap-2 px-4 py-3 bg-slate-100 rounded-xl text-sm text-slate hover:bg-slate-200 transition font-semibold">
<i data-lucide="refresh-cw" class="w-3.5 h-3.5"></i>
إعادة إنشاء الملخص
</button>
</div>
</div>
</div>
<!-- State: loading_pdf -->
<div id="pdfSummaryLoadingPdf" class="text-center py-10 hidden">
<div class="inline-block w-8 h-8 border-[3px] border-navy/20 border-t-navy rounded-full animate-spin"></div>
<p class="text-slate text-sm mt-4">{% trans "Generating your PDF..." %}</p>
<p class="text-slate text-xs mt-1">{% trans "This may take a moment" %}</p>
</div>
<!-- State: ready -->
<div id="pdfSummaryReady" class="hidden">
<div class="bg-green-50 border border-green-200 rounded-xl p-4 flex items-start gap-3 mb-4">
<i data-lucide="check-circle" class="w-5 h-5 text-green-500 flex-shrink-0 mt-0.5"></i>
<div class="flex-1 min-w-0">
<p class="text-green-700 text-sm font-semibold">{% trans "PDF report ready" %}</p>
<p class="text-green-600 text-xs mt-0.5">{% trans "You can download or regenerate the report" %}</p>
</div>
</div>
<div class="border border-slate-200 rounded-xl overflow-hidden mb-4 bg-slate-50">
<iframe id="pdfSummaryIframe" class="w-full border-0" style="height:550px;"></iframe>
</div>
<div class="flex items-center gap-3">
<a id="pdfSummaryDownloadLink" href="#" download
class="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-navy to-blue text-white rounded-xl font-bold hover:from-blue hover:to-navy transition shadow-lg text-sm flex-1 justify-center">
<i data-lucide="download" class="w-4 h-4"></i>
تحميل PDF
</a>
<button onclick="toggleEditForm()" id="toggleEditBtn"
class="inline-flex items-center gap-2 px-4 py-3 bg-slate-100 rounded-xl text-sm text-slate hover:bg-slate-200 transition font-semibold">
<i data-lucide="edit" class="w-3.5 h-3.5"></i>
تعديل النص
</button>
</div>
<!-- Collapsible edit form in ready state -->
<div id="pdfSummaryReadyEdit" class="hidden mt-4 pt-4 border-t border-slate-200">
<div class="grid grid-cols-2 gap-3 mb-4">
<div>
<label class="block text-xs font-semibold text-slate mb-1">الاسم</label>
<input id="fld2_complainant_name" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div>
<label class="block text-xs font-semibold text-slate mb-1">جهة الادعاء</label>
<input id="fld2_source_name" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div>
<label class="block text-xs font-semibold text-slate mb-1">تاريخ التقديم</label>
<input id="fld2_submission_date" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div>
<label class="block text-xs font-semibold text-slate mb-1">تاريخ الحادثة</label>
<input id="fld2_incident_date" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div class="col-span-2">
<label class="block text-xs font-semibold text-slate mb-1">القسم</label>
<input id="fld2_department_name" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div>
<label class="block text-xs font-semibold text-slate mb-1">اسم الموظف</label>
<input id="fld2_accused_staff_name" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div>
<label class="block text-xs font-semibold text-slate mb-1">الوظيفة</label>
<input id="fld2_accused_staff_title" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div>
<label class="block text-xs font-semibold text-slate mb-1">تاريخ الإرسال</label>
<input id="fld2_sent_to_dept_date" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
<div>
<label class="block text-xs font-semibold text-slate mb-1">تاريخ الرد</label>
<input id="fld2_response_date" type="text"
class="w-full border border-slate-200 rounded-lg p-2.5 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy">
</div>
</div>
<div class="mb-3">
<label class="block text-xs font-semibold text-slate mb-1">مختصر الشكوى</label>
<textarea id="fld2_content_summary" rows="4"
class="w-full border border-slate-200 rounded-xl p-3 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy resize-y"></textarea>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold text-slate mb-1">مختصر الرد</label>
<textarea id="fld2_dept_response_summary" rows="4"
class="w-full border border-slate-200 rounded-xl p-3 text-sm text-slate focus:outline-none focus:ring-2 focus:ring-navy/20 focus:border-navy resize-y"></textarea>
</div>
<button onclick="generatePdfFromReady()" id="regeneratePdfBtn"
class="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-navy to-blue text-white rounded-xl font-bold hover:from-blue hover:to-navy transition shadow-lg text-sm w-full justify-center">
<i data-lucide="refresh-cw" class="w-4 h-4"></i>
تحديث PDF
</button>
</div>
</div>
<!-- State: error -->
<div id="pdfSummaryError" class="hidden">
<div class="bg-red-50 border border-red-200 rounded-xl p-4 flex items-start gap-3">
<i data-lucide="alert-circle" class="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5"></i>
<div>
<p class="text-red-700 text-sm font-semibold">{% trans "Generation Failed" %}</p>
<p id="pdfSummaryErrorMsg" class="text-red-600 text-xs mt-1"></p>
</div>
</div>
<div class="text-center mt-4">
<button onclick="clearError()"
class="inline-flex items-center gap-2 px-4 py-2 bg-slate-100 rounded-lg text-sm text-slate hover:bg-slate-200 transition">
<i data-lucide="arrow-left" class="w-3.5 h-3.5"></i>
{% trans "Back" %}
</button>
</div>
</div>
</section>
<script>
(function() {
let _pdfBlobUrl = null;
let _fileUrl = null;
const FIELD_IDS = [
'complainant_name', 'source_name', 'submission_date', 'incident_date',
'department_name', 'accused_staff_name', 'accused_staff_title',
'sent_to_dept_date', 'response_date', 'content_summary', 'dept_response_summary'
];
function populateFields(data, prefix) {
for (const id of FIELD_IDS) {
const el = document.getElementById(prefix + id);
if (el && data[id] !== undefined) el.value = data[id];
}
}
function collectFields(prefix) {
const data = {};
for (const id of FIELD_IDS) {
const el = document.getElementById(prefix + id);
if (el) data[id] = el.value;
}
return data;
}
function showState(state) {
document.getElementById('pdfSummaryChecking').classList.toggle('hidden', state !== 'checking');
document.getElementById('pdfSummaryEmpty').classList.toggle('hidden', state !== 'empty');
document.getElementById('pdfSummaryLoadingAi').classList.toggle('hidden', state !== 'loading_ai');
document.getElementById('pdfSummaryEdit').classList.toggle('hidden', state !== 'edit');
document.getElementById('pdfSummaryLoadingPdf').classList.toggle('hidden', state !== 'loading_pdf');
document.getElementById('pdfSummaryReady').classList.toggle('hidden', state !== 'ready');
document.getElementById('pdfSummaryError').classList.toggle('hidden', state !== 'error');
try { lucide.createIcons(); } catch(e) {}
}
function setError(msg) {
document.getElementById('pdfSummaryErrorMsg').textContent = msg;
showState('error');
}
window.clearError = function() {
document.getElementById('pdfSummaryEmpty').classList.remove('hidden');
document.getElementById('pdfSummaryError').classList.add('hidden');
};
window.toggleEditForm = function() {
const form = document.getElementById('pdfSummaryReadyEdit');
const isHidden = form.classList.contains('hidden');
form.classList.toggle('hidden');
document.getElementById('toggleEditBtn').innerHTML = isHidden
? '<i data-lucide="x" class="w-3.5 h-3.5"></i> إغلاق'
: '<i data-lucide="edit" class="w-3.5 h-3.5"></i> تعديل النص';
try { lucide.createIcons(); } catch(e) {}
};
// Check existing summary on load
fetch('/complaints/api/complaints/{{ complaint.id }}/summary_preview/', {
method: 'GET',
headers: { 'X-CSRFToken': getCSRFToken() },
credentials: 'same-origin',
})
.then(r => r.json())
.then(data => {
if (data.exists) {
populateFields(data, 'fld_');
populateFields(data, 'fld2_');
if (data.has_file) {
_fileUrl = data.file_url;
document.getElementById('pdfSummaryDownloadLink').href = data.file_url;
fetch(data.file_url)
.then(r => r.blob())
.then(blob => {
var blobUrl = URL.createObjectURL(blob);
document.getElementById('pdfSummaryIframe').src = blobUrl;
});
showState('ready');
} else {
showState('edit');
}
} else {
// Populate with computed DB values even without saved summary
populateFields(data, 'fld_');
populateFields(data, 'fld2_');
showState('empty');
}
})
.catch(() => {
showState('empty');
});
// Generate AI summaries (POST summary_preview)
window.generateSummaries = function() {
showState('loading_ai');
fetch('/complaints/api/complaints/{{ complaint.id }}/summary_preview/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken(),
},
credentials: 'same-origin',
body: JSON.stringify({ lang: 'ar' })
})
.then(r => {
if (!r.ok) return r.json().then(d => { throw new Error(d.error || 'Request failed'); });
return r.json();
})
.then(data => {
populateFields(data, 'fld_');
populateFields(data, 'fld2_');
showState('edit');
})
.catch(err => {
setError(err.message || '{% trans "An unexpected error occurred" %}');
});
};
// Generate PDF (POST generate_summary_pdf)
function doGeneratePdf(prefix) {
showState('loading_pdf');
if (_pdfBlobUrl) {
window.URL.revokeObjectURL(_pdfBlobUrl);
_pdfBlobUrl = null;
}
const body = {
lang: 'ar',
...collectFields(prefix),
};
fetch('/complaints/api/complaints/{{ complaint.id }}/generate_summary_pdf/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCSRFToken(),
},
credentials: 'same-origin',
body: JSON.stringify(body)
})
.then(response => {
if (!response.ok) {
return response.json().then(data => {
throw new Error(data.error || 'Request failed');
}).catch(e => {
if (e instanceof SyntaxError) {
throw new Error('Server returned an error (' + response.status + ')');
}
throw e;
});
}
const disposition = response.headers.get('Content-Disposition');
let filename = 'complaint_report.pdf';
if (disposition && disposition.indexOf('filename=') !== -1) {
const matches = disposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/);
if (matches && matches[1]) {
filename = matches[1].replace(/['"]/g, '');
}
}
return response.blob().then(blob => ({ blob, filename }));
})
.then(({ blob, filename }) => {
_pdfBlobUrl = window.URL.createObjectURL(blob);
document.getElementById('pdfSummaryIframe').src = _pdfBlobUrl;
document.getElementById('pdfSummaryDownloadLink').href = _pdfBlobUrl;
document.getElementById('pdfSummaryDownloadLink').download = filename;
const a = document.createElement('a');
a.href = _pdfBlobUrl;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// Sync the other form set
if (prefix === 'fld_') {
populateFields(collectFields('fld_'), 'fld2_');
} else {
populateFields(collectFields('fld2_'), 'fld_');
}
showState('ready');
})
.catch(err => {
setError(err.message || '{% trans "An unexpected error occurred" %}');
});
}
window.generatePdf = function() {
doGeneratePdf('fld_');
};
window.generatePdfFromReady = function() {
doGeneratePdf('fld2_');
};
})();
</script>