fix(diagnosis): resolve print template placeholders and base64 logo rendering

- Support hospital, patient, doctor, and date placeholders matching template variables
- Convert local logo and signature paths to base64 Data URIs for mPDF
- Apply token replacement to report body in single and compiled prints
- Add interactive placeholder toolbar in diagnosis template settings view
- Add unit tests in tests/unit/DiagnosisTemplateTokenTest.php
This commit is contained in:
DevSoftTech Server 2026-10-03 02:45:22 +05:30
parent 48d11b20df
commit 414dc42c05
3 changed files with 519 additions and 54 deletions

View File

@ -1459,6 +1459,7 @@ class Diagnosis extends BaseController
$sql = "SELECT l.*, m.invoice_code, m.attach_id,
p.p_fname, p.p_rname, p.p_relative, p.p_code, p.gender, p.dob, p.age, p.age_in, p.age_in_month, p.estimate_dob,
p.mphone1, p.mphone2, p.add1, p.add2, p.city, p.district, p.state,
r.Title as repo_title, g.RepoGrp
FROM lab_request l
LEFT JOIN invoice_master m ON m.id = l.charge_id
@ -1523,6 +1524,13 @@ class Diagnosis extends BaseController
$doctorTokenData = $this->resolveMappedReportDoctorTokenData((int) ($row->lab_type ?? 0), $headRow);
$patientAddress = trim(($row->add1 ?? '') . ' ' . ($row->add2 ?? '') . ' ' . ($row->city ?? ''));
$patientPhone = trim((string) ($row->mphone1 ?? $row->mphone2 ?? ''));
$signatureUrl = (string) ($printSetting['signature_image'] ?? '');
if ($signatureUrl === '') {
$signatureUrl = (string) ($doctorTokenData['signature_image'] ?? '');
}
$tokens = $this->buildPdfTokens([
'invoice_code' => $row->invoice_code ?? '',
'patient_name' => $patientName,
@ -1531,13 +1539,15 @@ class Diagnosis extends BaseController
'age' => $ageText,
'gender' => $genderText,
'uhid' => $row->p_code ?? '',
'phone_no' => $patientPhone,
'patient_address'=> $patientAddress,
'collected_time' => $row->collected_time ?? '',
'reported_time' => $row->reported_time ?? '',
'report_title' => $repoTitle,
'doctor_name' => $doctorTokenData['doctor_name'] ?? '',
'doctor_education' => $doctorTokenData['doctor_education'] ?? '',
'technician_name' => $doctorTokenData['technician_name'] ?? '',
'signature_image_url' => (string) ($printSetting['signature_image'] ?? ''),
'signature_image_url' => $signatureUrl,
]);
// Render the optional patient info template (tokens resolved).
@ -1596,6 +1606,7 @@ class Diagnosis extends BaseController
. $autoHeaderHtml
. $autoFooterHtml, $tokens);
$suffixHtml = $this->applyPdfTokens(trim((string) ($printSetting['mpdf_suffix_html'] ?? '')), $tokens);
$pdfHtml = $this->applyPdfTokens($pdfHtml, $tokens);
$pdfHtml = mpdf_normalize_font_weight_css($pdfHtml);
$prefixHtml = mpdf_normalize_font_weight_css($prefixHtml);
$suffixHtml = mpdf_normalize_font_weight_css($suffixHtml);
@ -1877,6 +1888,7 @@ class Diagnosis extends BaseController
'doctor_name' => $legacyDoctorName,
'doctor_education' => $legacyDoctorEducation,
'technician_name' => $legacyTechnicianName,
'signature_image' => '',
];
$mappedDoctorId = $this->resolveMappedDoctorIdForLabType($labType);
@ -1891,6 +1903,7 @@ class Diagnosis extends BaseController
$mappedName = trim((string) ($mappedDoctor['name'] ?? ''));
$mappedEducation = trim((string) ($mappedDoctor['education'] ?? ''));
$mappedSign = trim((string) ($mappedDoctor['signature_image'] ?? ''));
if ($mappedName !== '') {
$out['doctor_name'] = $mappedName;
@ -1898,6 +1911,9 @@ class Diagnosis extends BaseController
if ($mappedEducation !== '') {
$out['doctor_education'] = $mappedEducation;
}
if ($mappedSign !== '') {
$out['signature_image'] = $mappedSign;
}
return $out;
}
@ -1951,7 +1967,7 @@ class Diagnosis extends BaseController
$fields = $this->db->getFieldNames('doctor_master') ?? [];
$select = ['id'];
foreach (['p_title', 'p_fname', 'p_lname', 'education', 'qualification', 'degree', 'speciality', 'specialty', 'designation'] as $field) {
foreach (['p_title', 'p_fname', 'p_lname', 'doc_sign', 'sign_image', 'signature', 'education', 'qualification', 'degree', 'speciality', 'specialty', 'designation'] as $field) {
if (in_array($field, $fields, true)) {
$select[] = $field;
}
@ -1985,63 +2001,233 @@ class Diagnosis extends BaseController
}
}
$docSign = '';
foreach (['doc_sign', 'sign_image', 'signature'] as $signCol) {
$val = trim((string) ($row[$signCol] ?? ''));
if ($val !== '') {
$docSign = $val;
break;
}
}
return [
'name' => $name,
'education' => $education,
'signature_image' => $docSign,
];
}
private function buildImageDataUriFromPath(string $absolutePath): string
{
$absolutePath = trim($absolutePath);
if ($absolutePath === '' || ! is_file($absolutePath) || ! is_readable($absolutePath)) {
return '';
}
$bytes = @file_get_contents($absolutePath);
if ($bytes === false || $bytes === '') {
return '';
}
$mime = 'image/png';
if (function_exists('mime_content_type')) {
$detected = @mime_content_type($absolutePath);
if (is_string($detected) && str_starts_with($detected, 'image/')) {
$mime = $detected;
}
}
return 'data:' . $mime . ';base64,' . base64_encode($bytes);
}
/**
* Build an associative array of {{token}} => value pairs for PDF template substitution.
* All string values are HTML-escaped so they are safe to embed inside HTML attributes and text.
*/
private function buildPdfTokens(array $data): array
protected function buildPdfTokens(array $data): array
{
$e = static function (string $v): string {
return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
};
$age = (string) ($data['age'] ?? '-');
$gender = (string) ($data['gender'] ?? '-');
$hospitalName = defined('H_Name') ? (string) constant('H_Name') : 'Hospital';
$hospitalAddress1 = defined('H_address_1') ? (string) constant('H_address_1') : '';
$hospitalPhone = defined('H_phone_No') ? (string) constant('H_phone_No') : '';
$hospitalLogoName = defined('H_logo') ? trim((string) constant('H_logo')) : '';
$hospitalLogoUrl = $this->resolvePdfAssetPath([
$hospitalName = (string) (defined('H_Name') ? constant('H_Name') : (function_exists('hospital_setting_value') ? hospital_setting_value('H_Name', 'Hospital') : 'Hospital'));
$hospitalAddress1 = (string) (defined('H_address_1') ? constant('H_address_1') : (function_exists('hospital_setting_value') ? hospital_setting_value('H_address_1', '') : ''));
$hospitalAddress2 = (string) (defined('H_address_2') ? constant('H_address_2') : (function_exists('hospital_setting_value') ? hospital_setting_value('H_address_2', '') : ''));
$hospitalPhone = (string) (defined('H_phone_No') ? constant('H_phone_No') : (function_exists('hospital_setting_value') ? hospital_setting_value('H_phone_No', '') : ''));
$hospitalEmail = (string) (defined('H_Email') ? constant('H_Email') : (function_exists('hospital_setting_value') ? hospital_setting_value('H_Email', '') : ''));
$hospitalLogoName = trim((string) (defined('H_logo') ? constant('H_logo') : (function_exists('hospital_setting_value') ? hospital_setting_value('H_logo', '') : '')));
$hospitalNabh = trim((string) (defined('H_NABH') ? constant('H_NABH') : (function_exists('hospital_setting_value') ? hospital_setting_value('H_NABH', '') : '')));
$hospitalNameHindi= (string) (defined('H_Name_Hindi') ? constant('H_Name_Hindi') : (function_exists('hospital_setting_value') ? hospital_setting_value('H_Name_Hindi', '') : ''));
$hospitalGst = (string) (defined('H_Med_GST') ? constant('H_Med_GST') : (function_exists('hospital_setting_value') ? hospital_setting_value('H_Med_GST', '') : ''));
$fullHospitalAddress = trim($hospitalAddress1 . ($hospitalAddress2 !== '' ? (', ' . $hospitalAddress2) : ''));
$hospitalLogoAbs = $this->resolvePdfAssetPath([
$hospitalLogoName !== '' ? 'assets/images/' . ltrim($hospitalLogoName, '/\\') : '',
$hospitalLogoName !== '' ? $hospitalLogoName : '',
'assets/images/hospital_logo_20260723_161110_75d8b3e0.png',
'assets/img/logo.png',
'assets/images/no_image.svg',
]);
$signatureImageUrl = $this->resolvePdfAssetPath([
$logoDataUri = $this->buildImageDataUriFromPath($hospitalLogoAbs);
$logoSrc = $logoDataUri !== '' ? $logoDataUri : $hospitalLogoAbs;
$logoHtml = $logoSrc !== '' ? '<img style="width: 100px; vertical-align: top;" src="' . $logoSrc . '" />' : '';
$signatureImageAbs = $this->resolvePdfAssetPath([
(string) ($data['signature_image_url'] ?? ''),
'assets/images/drPreetiSingh.jpg',
]);
$signDataUri = $this->buildImageDataUriFromPath($signatureImageAbs);
$signSrc = $signDataUri !== '' ? $signDataUri : $signatureImageAbs;
$signHtml = $signSrc !== '' ? '<img style="height: 40px; vertical-align: middle;" src="' . $signSrc . '" />' : '';
$patientName = (string) ($data['patient_name'] ?? '');
$relativeType = (string) ($data['relative_type'] ?? '');
$relativeName = (string) ($data['relative_name'] ?? '');
$relativeCombined = trim($relativeType . ' ' . $relativeName);
$age = (string) ($data['age'] ?? '-');
$gender = (string) ($data['gender'] ?? '-');
$ageSex = (string) ($data['age_sex'] ?? ($age . ' / ' . $gender));
$uhid = (string) ($data['uhid'] ?? '');
$invoiceCode = (string) ($data['invoice_code'] ?? '');
$phoneNo = (string) ($data['phone_no'] ?? '');
$patientAddr = (string) ($data['patient_address'] ?? '');
$reportTitle = (string) ($data['report_title'] ?? '');
$collectedTime= (string) ($data['collected_time'] ?? '');
$reportedTime = (string) ($data['reported_time'] ?? '');
$doctorName = (string) ($data['doctor_name'] ?? '');
$doctorEducation = (string) ($data['doctor_education'] ?? '');
$technicianName = (string) ($data['technician_name'] ?? '');
$nowDate = date('d-m-Y');
$nowDateTime = date('d-m-Y h:i A');
$nowDateTimeSec = date('d-m-Y h:i:s A');
return [
'{{invoice_code}}' => $e((string) ($data['invoice_code'] ?? '')),
'{{patient_name}}' => $e((string) ($data['patient_name'] ?? '')),
'{{relative_type}}' => $e((string) ($data['relative_type'] ?? '')),
'{{relative_name}}' => $e((string) ($data['relative_name'] ?? '')),
'{{relative}}' => $e(trim((string) ($data['relative_type'] ?? '') . ' ' . (string) ($data['relative_name'] ?? ''))),
'{{age}}' => $e($age),
'{{gender}}' => $e($gender),
'{{age_sex}}' => $e($age . ' / ' . $gender),
'{{uhid}}' => $e((string) ($data['uhid'] ?? '')),
'{{collected_time}}' => $e((string) ($data['collected_time'] ?? '')),
'{{reported_time}}' => $e((string) ($data['reported_time'] ?? '')),
'{{printed_time}}' => date('d-m-Y h:i A'),
'{{report_title}}' => $e((string) ($data['report_title'] ?? '')),
'{{doctor_name}}' => $e((string) ($data['doctor_name'] ?? '')),
'{{doctor_education}}' => $e((string) ($data['doctor_education'] ?? '')),
'{{technician_name}}' => $e((string) ($data['technician_name'] ?? '')),
'{{hospital_name}}' => $e($hospitalName),
// Long prefixes first to avoid partial replacement of nested placeholders
'assets/images/{{H_logo}}' => $logoSrc,
'assets/images/{{h_logo}}' => $logoSrc,
'assets/images/{{H_LOGO}}' => $logoSrc,
'assets/images/{{hospital_logo_url}}' => $logoSrc,
'assets/images/{{hospital_logo_name}}' => $logoSrc,
'assets/images/{{signature_image_url}}' => $signSrc,
// Hospital Placeholders
'{{H_Name}}' => $e($hospitalName),
'{{h_name}}' => $e($hospitalName),
'{{H_NAME}}' => $e($hospitalName),
'{{hospital_name}}' => $e($hospitalName),
'{{HOSPITAL_NAME}}' => $e($hospitalName),
'{{H_address_1}}' => $e($hospitalAddress1),
'{{h_address_1}}' => $e($hospitalAddress1),
'{{H_ADDRESS_1}}' => $e($hospitalAddress1),
'{{hospital_address_1}}' => $e($hospitalAddress1),
'{{hospital_phone}}' => $e($hospitalPhone),
'{{HOSPITAL_ADDRESS_1}}' => $e($hospitalAddress1),
'{{H_address_2}}' => $e($hospitalAddress2),
'{{h_address_2}}' => $e($hospitalAddress2),
'{{H_ADDRESS_2}}' => $e($hospitalAddress2),
'{{hospital_address_2}}' => $e($hospitalAddress2),
'{{HOSPITAL_ADDRESS_2}}' => $e($hospitalAddress2),
'{{hospital_address}}' => $e($fullHospitalAddress),
'{{HOSPITAL_ADDRESS}}' => $e($fullHospitalAddress),
'{{H_phone_No}}' => $e($hospitalPhone),
'{{H_phone_no}}' => $e($hospitalPhone),
'{{h_phone_no}}' => $e($hospitalPhone),
'{{H_PHONE_NO}}' => $e($hospitalPhone),
'{{hospital_phone}}' => $e($hospitalPhone),
'{{HOSPITAL_PHONE}}' => $e($hospitalPhone),
'{{H_Email}}' => $e($hospitalEmail),
'{{H_email}}' => $e($hospitalEmail),
'{{h_email}}' => $e($hospitalEmail),
'{{H_EMAIL}}' => $e($hospitalEmail),
'{{hospital_email}}' => $e($hospitalEmail),
'{{HOSPITAL_EMAIL}}' => $e($hospitalEmail),
'{{H_logo}}' => $logoSrc,
'{{h_logo}}' => $logoSrc,
'{{H_LOGO}}' => $logoSrc,
'{{H_logo_abs}}' => $hospitalLogoAbs,
'{{hospital_logo_url}}' => $logoSrc,
'{{hospital_logo_path}}' => $hospitalLogoAbs,
'{{hospital_logo_name}}' => $e($hospitalLogoName),
'{{hospital_logo_url}}' => $e($hospitalLogoUrl),
'{{hospital_logo_path}}' => $e($hospitalLogoUrl),
'{{signature_image_url}}' => $e($signatureImageUrl),
'{{signature_image_path}}' => $e($signatureImageUrl),
'{{hospital_logo_html}}' => $logoHtml,
'{{HOSPITAL_LOGO_HTML}}' => $logoHtml,
'{{H_NABH}}' => $e($hospitalNabh),
'{{H_Name_Hindi}}' => $e($hospitalNameHindi),
'{{H_Med_GST}}' => $e($hospitalGst),
// Patient Placeholders
'{{patient_name}}' => $e($patientName),
'{{PATIENT_NAME}}' => $e($patientName),
'{{pName}}' => $e($patientName),
'{{p_fname}}' => $e($patientName),
'{{uhid}}' => $e($uhid),
'{{UHID}}' => $e($uhid),
'{{uhid_no}}' => $e($uhid),
'{{p_code}}' => $e($uhid),
'{{invoice_code}}' => $e($invoiceCode),
'{{INVOICE_CODE}}' => $e($invoiceCode),
'{{invoice_no}}' => $e($invoiceCode),
'{{age}}' => $e($age),
'{{AGE}}' => $e($age),
'{{gender}}' => $e($gender),
'{{GENDER}}' => $e($gender),
'{{age_sex}}' => $e($ageSex),
'{{AGE_GENDER}}' => $e($ageSex),
'{{age_gender}}' => $e($ageSex),
'{{relative_type}}' => $e($relativeType),
'{{relative_name}}' => $e($relativeName),
'{{relative}}' => $e($relativeCombined),
'{{pRelative}}' => $e($relativeCombined),
'{{p_relative}}' => $e($relativeType),
'{{p_rname}}' => $e($relativeName),
'{{phoneno}}' => $e($phoneNo),
'{{phone_no}}' => $e($phoneNo),
'{{contact_no}}' => $e($phoneNo),
'{{p_address}}' => $e($patientAddr),
'{{patient_address}}' => $e($patientAddr),
'{{collected_time}}' => $e($collectedTime),
'{{COLLECTED_TIME}}' => $e($collectedTime),
'{{reported_time}}' => $e($reportedTime),
'{{REPORTED_TIME}}' => $e($reportedTime),
'{{report_title}}' => $e($reportTitle),
'{{REPORT_TITLE}}' => $e($reportTitle),
'{{report_name}}' => $e($reportTitle),
// Doctor & Sign Placeholders
'{{doctor_name}}' => $e($doctorName),
'{{dr_name}}' => $e($doctorName),
'{{DOCTORS}}' => $e($doctorName),
'{{doctor_education}}' => $e($doctorEducation),
'{{technician_name}}' => $e($technicianName),
'{{signature_image_url}}' => $signSrc,
'{{signature_image_path}}' => $signatureImageAbs,
'{{doctor_sign_html}}' => $signHtml,
'{{doctor_sign_img}}' => $signHtml,
'{{signature_html}}' => $signHtml,
// Date / Time Placeholders
'{{printed_time}}' => $nowDateTime,
'{{print_time}}' => $nowDateTimeSec,
'{{CURRENT_DATE}}' => $nowDate,
'{{current_date}}' => $nowDate,
'{{date}}' => $nowDate,
'{{CURRENT_DATETIME}}' => $nowDateTime,
'{{current_datetime}}' => $nowDateTime,
];
}
@ -2055,7 +2241,7 @@ class Diagnosis extends BaseController
}
if (preg_match('/^https?:\/\//i', $candidate) === 1 || str_starts_with($candidate, 'data:')) {
continue;
return $candidate;
}
if (preg_match('/^[A-Za-z]:[\\\\\/]/', $candidate) === 1) {
@ -2080,9 +2266,37 @@ class Diagnosis extends BaseController
/**
* Replace all {{token}} placeholders in $html using the token map returned by buildPdfTokens().
*/
private function applyPdfTokens(string $html, array $tokens): string
protected function applyPdfTokens(string $html, array $tokens): string
{
return str_replace(array_keys($tokens), array_values($tokens), $html);
if ($html === '') {
return '';
}
// Case-insensitive token replacement
$html = str_ireplace(array_keys($tokens), array_values($tokens), $html);
// Backward compatibility with legacy PHP echo tags in templates (e.g. H_Name)
$html = preg_replace_callback('/<\?=(?:[^?]|\?(?!>))*\x3F\x3E/i', function ($m) use ($tokens) {
$tag = $m[0];
if (preg_match('/H_Name/i', $tag) && isset($tokens['{{H_Name}}'])) {
return $tokens['{{H_Name}}'];
}
if (preg_match('/H_address_1/i', $tag) && isset($tokens['{{H_address_1}}'])) {
return $tokens['{{H_address_1}}'];
}
if (preg_match('/H_address_2/i', $tag) && isset($tokens['{{H_address_2}}'])) {
return $tokens['{{H_address_2}}'];
}
if (preg_match('/H_phone_No/i', $tag) && isset($tokens['{{H_phone_No}}'])) {
return $tokens['{{H_phone_No}}'];
}
if (preg_match('/H_logo/i', $tag) && isset($tokens['{{H_logo}}'])) {
return $tokens['{{H_logo}}'];
}
return '';
}, $html);
return $html;
}
private function buildDefaultPatientInfoHtml(array $data): string
@ -2714,6 +2928,13 @@ class Diagnosis extends BaseController
$doctorTokenData = $this->resolveMappedReportDoctorTokenData($labType, $head);
$patientAddress = is_object($patient) ? trim(($patient->add1 ?? '') . ' ' . ($patient->add2 ?? '') . ' ' . ($patient->city ?? '')) : '';
$patientPhone = is_object($patient) ? trim((string) ($patient->mphone1 ?? $patient->mphone2 ?? '')) : '';
$signatureUrl = (string) ($printSetting['signature_image'] ?? '');
if ($signatureUrl === '') {
$signatureUrl = (string) ($doctorTokenData['signature_image'] ?? '');
}
$tokens = $this->buildPdfTokens([
'invoice_code' => $invoice->invoice_code ?? '',
'patient_name' => $patientName,
@ -2722,13 +2943,15 @@ class Diagnosis extends BaseController
'age' => $ageText,
'gender' => $genderText,
'uhid' => $patient->p_code ?? '',
'phone_no' => $patientPhone,
'patient_address'=> $patientAddress,
'collected_time' => $invoiceRequest->collected_time ?? '',
'reported_time' => $invoiceRequest->reported_time ?? '',
'report_title' => $repoTitle,
'doctor_name' => $doctorTokenData['doctor_name'] ?? '',
'doctor_education' => $doctorTokenData['doctor_education'] ?? '',
'technician_name' => $doctorTokenData['technician_name'] ?? '',
'signature_image_url' => (string) ($printSetting['signature_image'] ?? ''),
'signature_image_url' => $signatureUrl,
]);
$rawPatientInfoHtml = trim((string) ($printSetting['patient_info_html'] ?? ''));
@ -2799,6 +3022,7 @@ class Diagnosis extends BaseController
. $autoHeaderHtml
. $autoFooterHtml, $tokens);
$suffixHtml = $this->applyPdfTokens(trim((string) ($printSetting['mpdf_suffix_html'] ?? '')), $tokens);
$pdfHtml = $this->applyPdfTokens($pdfHtml, $tokens);
$pdfHtml = mpdf_normalize_font_weight_css($pdfHtml);
$prefixHtml = mpdf_normalize_font_weight_css($prefixHtml);
$suffixHtml = mpdf_normalize_font_weight_css($suffixHtml);

View File

@ -178,11 +178,74 @@ $compilePlainTemplateName = (string) ($templateNameById[$compilePlainTemplateId]
</div>
</div>
<div class="card border-info mb-3">
<div class="card-header bg-info bg-opacity-10 py-1 d-flex justify-content-between align-items-center">
<span class="fw-semibold small text-info">Available Placeholders — click any to insert into focused textarea</span>
<span class="text-muted small" id="diag_tpl_focus_label">Focus a textarea below first</span>
</div>
<div class="card-body p-2">
<div class="mb-1"><span class="badge bg-secondary me-1">Hospital</span>
<?php foreach ([
'{{H_Name}}' => 'Hospital Name',
'{{H_address_1}}' => 'Address Line 1',
'{{H_address_2}}' => 'Address Line 2',
'{{hospital_address}}' => 'Combined Address',
'{{H_phone_No}}' => 'Phone Number',
'{{H_Email}}' => 'Email Address',
'{{H_logo}}' => 'Logo (image src or data URI)',
'{{hospital_logo_html}}' => 'Logo <img> tag',
] as $ph => $desc): ?>
<button type="button" class="btn btn-outline-info btn-sm py-0 px-1 me-1 mb-1 diag-ph-btn" data-ph="<?= esc($ph) ?>" title="<?= esc($desc) ?>"><code><?= esc($ph) ?></code></button>
<?php endforeach; ?>
</div>
<div class="mb-1"><span class="badge bg-secondary me-1">Patient</span>
<?php foreach ([
'{{patient_name}}' => 'Full Patient Name',
'{{uhid}}' => 'Patient UHID',
'{{invoice_code}}' => 'Invoice Code',
'{{age}}' => 'Patient Age',
'{{gender}}' => 'Gender',
'{{age_sex}}' => 'Age & Gender (e.g. 35 / Male)',
'{{relative}}' => 'Relative Name & Relation',
'{{phoneno}}' => 'Patient Mobile No.',
'{{p_address}}' => 'Patient Address',
'{{collected_time}}' => 'Sample/Study Collected Time',
'{{reported_time}}' => 'Reported Time',
'{{report_title}}' => 'Test / Report Title',
] as $ph => $desc): ?>
<button type="button" class="btn btn-outline-success btn-sm py-0 px-1 me-1 mb-1 diag-ph-btn" data-ph="<?= esc($ph) ?>" title="<?= esc($desc) ?>"><code><?= esc($ph) ?></code></button>
<?php endforeach; ?>
</div>
<div class="mb-1"><span class="badge bg-secondary me-1">Doctor & Sign</span>
<?php foreach ([
'{{doctor_name}}' => 'Treating / Verifying Doctor Name',
'{{doctor_education}}' => 'Doctor Degree / Qualification',
'{{technician_name}}' => 'Technician Name',
'{{doctor_sign_html}}' => 'Doctor Signature <img> Tag',
'{{signature_image_url}}'=> 'Signature Image URL / Data URI',
] as $ph => $desc): ?>
<button type="button" class="btn btn-outline-primary btn-sm py-0 px-1 me-1 mb-1 diag-ph-btn" data-ph="<?= esc($ph) ?>" title="<?= esc($desc) ?>"><code><?= esc($ph) ?></code></button>
<?php endforeach; ?>
</div>
<div class="mb-0"><span class="badge bg-secondary me-1">Date & Page</span>
<?php foreach ([
'{{printed_time}}' => 'Print Date & Time (d-m-Y h:i A)',
'{{print_time}}' => 'Print Timestamp (d-m-Y h:i:s A)',
'{{CURRENT_DATE}}' => 'Today Date (d-m-Y)',
'{PAGENO}' => 'mPDF: Current Page Number',
'{nbpg}' => 'mPDF: Total Pages Count',
] as $ph => $desc): ?>
<button type="button" class="btn btn-outline-warning btn-sm py-0 px-1 me-1 mb-1 diag-ph-btn" data-ph="<?= esc($ph) ?>" title="<?= esc($desc) ?>"><code><?= esc($ph) ?></code></button>
<?php endforeach; ?>
</div>
</div>
</div>
<h6 class="mb-2">2. Header</h6>
<div class="row g-2">
<div class="col-12">
<label class="form-label small">Header HTML / mPDF tags</label>
<textarea class="form-control" name="header_html" rows="6" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['header_html'] ?? '')) ?></textarea>
<textarea class="form-control diag-ph-target" data-label="Header" name="header_html" rows="6" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['header_html'] ?? '')) ?></textarea>
</div>
</div>
@ -190,7 +253,7 @@ $compilePlainTemplateName = (string) ($templateNameById[$compilePlainTemplateId]
<div class="row g-2">
<div class="col-12">
<label class="form-label small">First Page Header HTML / mPDF tags</label>
<textarea class="form-control" name="first_page_header_html" rows="5" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['first_page_header_html'] ?? '')) ?></textarea>
<textarea class="form-control diag-ph-target" data-label="First Page Header" name="first_page_header_html" rows="5" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['first_page_header_html'] ?? '')) ?></textarea>
</div>
</div>
@ -207,15 +270,15 @@ $compilePlainTemplateName = (string) ($templateNameById[$compilePlainTemplateId]
<div class="row g-2">
<div class="col-12">
<label class="form-label small">Patient Info HTML Template (tokens supported)</label>
<textarea class="form-control" name="patient_info_html" rows="8" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['patient_info_html'] ?? '')) ?></textarea>
<textarea class="form-control diag-ph-target" data-label="Patient Info" name="patient_info_html" rows="8" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['patient_info_html'] ?? '')) ?></textarea>
</div>
<div class="col-12">
<label class="form-label small">Content Prefix HTML (before report body)</label>
<textarea class="form-control" name="content_prefix_html" rows="4" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['content_prefix_html'] ?? '')) ?></textarea>
<textarea class="form-control diag-ph-target" data-label="Content Prefix" name="content_prefix_html" rows="4" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['content_prefix_html'] ?? '')) ?></textarea>
</div>
<div class="col-12">
<label class="form-label small">Content Suffix HTML (after report body)</label>
<textarea class="form-control" name="content_suffix_html" rows="4" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['content_suffix_html'] ?? '')) ?></textarea>
<textarea class="form-control diag-ph-target" data-label="Content Suffix" name="content_suffix_html" rows="4" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['content_suffix_html'] ?? '')) ?></textarea>
</div>
</div>
@ -224,7 +287,7 @@ $compilePlainTemplateName = (string) ($templateNameById[$compilePlainTemplateId]
<div class="row g-2">
<div class="col-12">
<label class="form-label small">Footer HTML / mPDF tags</label>
<textarea class="form-control" name="footer_html" rows="5" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['footer_html'] ?? '')) ?></textarea>
<textarea class="form-control diag-ph-target" data-label="Footer" name="footer_html" rows="5" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['footer_html'] ?? '')) ?></textarea>
</div>
</div>
@ -232,7 +295,7 @@ $compilePlainTemplateName = (string) ($templateNameById[$compilePlainTemplateId]
<div class="row g-2">
<div class="col-12">
<label class="form-label small">Last Page Footer HTML / mPDF tags</label>
<textarea class="form-control" name="last_page_footer_html" rows="5" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['last_page_footer_html'] ?? '')) ?></textarea>
<textarea class="form-control diag-ph-target" data-label="Last Page Footer" name="last_page_footer_html" rows="5" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['last_page_footer_html'] ?? '')) ?></textarea>
</div>
</div>
@ -243,14 +306,53 @@ $compilePlainTemplateName = (string) ($templateNameById[$compilePlainTemplateId]
<div class="row g-2">
<div class="col-12">
<label class="form-label small">mPDF Prefix HTML (before content)</label>
<textarea class="form-control" name="mpdf_prefix_html" rows="8" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['mpdf_prefix_html'] ?? '')) ?></textarea>
<textarea class="form-control diag-ph-target" data-label="mPDF Prefix" name="mpdf_prefix_html" rows="8" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['mpdf_prefix_html'] ?? '')) ?></textarea>
</div>
<div class="col-12">
<label class="form-label small">mPDF Suffix HTML (after content)</label>
<textarea class="form-control" name="mpdf_suffix_html" rows="5" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['mpdf_suffix_html'] ?? '')) ?></textarea>
<textarea class="form-control diag-ph-target" data-label="mPDF Suffix" name="mpdf_suffix_html" rows="5" style="font-family:Consolas,Monaco,monospace;"><?= esc((string) ($row['mpdf_suffix_html'] ?? '')) ?></textarea>
</div>
</div>
<script>
(function () {
var activeTarget = null;
document.querySelectorAll('.diag-ph-target').forEach(function (ta) {
ta.addEventListener('focus', function () {
activeTarget = ta;
var label = document.getElementById('diag_tpl_focus_label');
if (label) {
var fieldLabel = ta.getAttribute('data-label') || ta.name || 'textarea';
label.textContent = 'Inserting into: ' + fieldLabel;
label.className = 'small text-success fw-semibold';
}
});
});
document.querySelectorAll('.diag-ph-btn').forEach(function (btn) {
btn.addEventListener('click', function () {
var ph = btn.getAttribute('data-ph') || '';
if (!ph) { return; }
if (!activeTarget) {
var headerTa = document.querySelector('textarea[name="header_html"]');
if (headerTa) {
activeTarget = headerTa;
} else {
alert('Click inside a Header, Content, or Footer textarea first, then click a placeholder.');
return;
}
}
var start = activeTarget.selectionStart || 0;
var end = activeTarget.selectionEnd || 0;
var val = activeTarget.value;
activeTarget.value = val.substring(0, start) + ph + val.substring(end);
activeTarget.selectionStart = activeTarget.selectionEnd = start + ph.length;
activeTarget.focus();
});
});
})();
</script>
<div class="mt-3 d-flex gap-2">
<button type="submit" class="btn btn-primary btn-sm" <?= ! $columnsReady ? 'disabled' : '' ?>>Save Settings</button>
</div>
@ -259,21 +361,41 @@ $compilePlainTemplateName = (string) ($templateNameById[$compilePlainTemplateId]
<div class="modal-dialog modal-lg modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h6 class="modal-title" id="pdfTemplateTipsModalLabel">PDF Template Help Tips</h6>
<h6 class="modal-title" id="pdfTemplateTipsModalLabel">PDF Template Help Tips & Placeholders</h6>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body small">
<p class="mb-2"><b>Patient Tokens</b></p>
<p class="mb-1"><b>Hospital Placeholders</b></p>
<p style="font-family:Consolas,Monaco,monospace; line-height:1.8;">
{{invoice_code}}, {{patient_name}}, {{relative_type}}, {{relative_name}}, {{relative}}, {{age}}, {{gender}}, {{age_sex}}, {{uhid}}, {{collected_time}}, {{reported_time}}, {{printed_time}}, {{report_title}}
</p>
<p style="font-family:Consolas,Monaco,monospace; line-height:1.8;">
{{hospital_logo_url}}, {{signature_image_url}}, {{doctor_name}}, {{doctor_education}}, {{technician_name}}
<code>{{H_Name}}</code> (Hospital name), <code>{{H_address_1}}</code>, <code>{{H_address_2}}</code>, <code>{{hospital_address}}</code>, <code>{{H_phone_No}}</code>, <code>{{H_Email}}</code>, <code>{{H_logo}}</code>, <code>{{hospital_logo_html}}</code>
</p>
<p class="mb-1" style="font-family:Consolas,Monaco,monospace;">Signature image usage: &lt;img src="{{signature_image_url}}" width="100px" /&gt;</p>
<p class="mb-1 mt-3"><b>Patient Placeholders</b></p>
<p style="font-family:Consolas,Monaco,monospace; line-height:1.8;">
<code>{{patient_name}}</code>, <code>{{pName}}</code>, <code>{{uhid}}</code>, <code>{{invoice_code}}</code>, <code>{{age}}</code>, <code>{{gender}}</code>, <code>{{age_sex}}</code>, <code>{{relative}}</code>, <code>{{phoneno}}</code>, <code>{{p_address}}</code>, <code>{{collected_time}}</code>, <code>{{reported_time}}</code>, <code>{{report_title}}</code>
</p>
<p class="mb-2 mt-3"><b>mPDF Constants / Tags (use in Prefix or Suffix)</b></p>
<p class="mb-1 mt-3"><b>Doctor & Staff Placeholders</b></p>
<p style="font-family:Consolas,Monaco,monospace; line-height:1.8;">
<code>{{doctor_name}}</code>, <code>{{doctor_education}}</code>, <code>{{technician_name}}</code>, <code>{{doctor_sign_html}}</code>, <code>{{signature_image_url}}</code>
</p>
<p class="mb-1 mt-3"><b>Date & Time Placeholders</b></p>
<p style="font-family:Consolas,Monaco,monospace; line-height:1.8;">
<code>{{printed_time}}</code> (e.g. 03-10-2026 02:00 PM), <code>{{print_time}}</code>, <code>{{CURRENT_DATE}}</code>
</p>
<p class="mb-1 mt-3"><b>Logo and Signature Image Tag Usage</b></p>
<pre class="bg-light border rounded p-2 mb-0" style="font-family:Consolas,Monaco,monospace; white-space:pre-wrap;">&lt;!-- Option 1: Direct img tag with placeholder src --&gt;
&lt;img style="width:100px; vertical-align:top;" src="assets/images/{{H_logo}}" /&gt;
&lt;!-- Option 2: Pre-formatted logo HTML tag --&gt;
{{hospital_logo_html}}
&lt;!-- Signature image --&gt;
{{doctor_sign_html}} or &lt;img src="{{signature_image_url}}" style="height:40px;" /&gt;</pre>
<p class="mb-2 mt-3"><b>mPDF Constants / Tags (use in Prefix, Header, or Suffix)</b></p>
<p class="mb-1" style="font-family:Consolas,Monaco,monospace;">Page No: {PAGENO}</p>
<p class="mb-1" style="font-family:Consolas,Monaco,monospace;">Total Pages: {nbpg}</p>
<p class="mb-1" style="font-family:Consolas,Monaco,monospace;">Page Break: &lt;pagebreak /&gt;</p>

View File

@ -0,0 +1,119 @@
<?php
namespace Tests\Unit;
use App\Controllers\Diagnosis;
use PHPUnit\Framework\TestCase;
final class DiagnosisTemplateTokenTest extends TestCase
{
private function buildTestController(): Diagnosis
{
return new class extends Diagnosis {
public function __construct()
{
}
public function exposeBuildPdfTokens(array $data): array
{
return $this->buildPdfTokens($data);
}
public function exposeApplyPdfTokens(string $html, array $tokens): string
{
return $this->applyPdfTokens($html, $tokens);
}
};
}
public function testBuildPdfTokensContainsHospitalAndPatientPlaceholders(): void
{
$controller = $this->buildTestController();
$tokens = $controller->exposeBuildPdfTokens([
'patient_name' => 'John Doe',
'uhid' => 'UHID12345',
'invoice_code' => 'INV999',
'age' => '35 Y',
'gender' => 'Male',
'phone_no' => '9876543210',
'patient_address' => '123 Main Street',
'doctor_name' => 'Dr. Smith',
'doctor_education' => 'MBBS, MD',
'technician_name' => 'Alex',
'report_title' => 'X-RAY CHEST',
]);
$this->assertArrayHasKey('{{H_Name}}', $tokens);
$this->assertArrayHasKey('{{H_address_1}}', $tokens);
$this->assertArrayHasKey('{{H_phone_No}}', $tokens);
$this->assertArrayHasKey('{{H_logo}}', $tokens);
$this->assertArrayHasKey('assets/images/{{H_logo}}', $tokens);
$this->assertSame('John Doe', $tokens['{{patient_name}}']);
$this->assertSame('John Doe', $tokens['{{pName}}']);
$this->assertSame('UHID12345', $tokens['{{uhid}}']);
$this->assertSame('INV999', $tokens['{{invoice_code}}']);
$this->assertSame('9876543210', $tokens['{{phoneno}}']);
$this->assertSame('123 Main Street', $tokens['{{p_address}}']);
$this->assertSame('Dr. Smith', $tokens['{{doctor_name}}']);
$this->assertSame('X-RAY CHEST', $tokens['{{report_title}}']);
}
public function testApplyPdfTokensReplacesTemplatePlaceholders(): void
{
$controller = $this->buildTestController();
$templateHtml = '<div class="header">'
. '<img src="assets/images/{{H_logo}}" />'
. '<h1>{{H_Name}}</h1>'
. '<p>{{H_address_1}}, {{H_address_2}} | Phone: {{H_phone_No}}</p>'
. '<div>Patient: {{pName}} ({{uhid}}) | Invoice: {{invoice_code}}</div>'
. '<div>Report: {{report_title}}</div>'
. '</div>';
$tokens = $controller->exposeBuildPdfTokens([
'patient_name' => 'Alice Wonder',
'uhid' => 'UHID-888',
'invoice_code' => 'INV-777',
'report_title' => 'CT SCAN BRAIN',
]);
$output = $controller->exposeApplyPdfTokens($templateHtml, $tokens);
$this->assertStringNotContainsString('{{H_Name}}', $output);
$this->assertStringNotContainsString('assets/images/{{H_logo}}', $output);
$this->assertStringNotContainsString('{{H_address_1}}', $output);
$this->assertStringNotContainsString('{{H_phone_No}}', $output);
$this->assertStringNotContainsString('{{pName}}', $output);
$this->assertStringNotContainsString('{{uhid}}', $output);
$this->assertStringNotContainsString('{{invoice_code}}', $output);
$this->assertStringNotContainsString('{{report_title}}', $output);
$this->assertStringContainsString('Alice Wonder', $output);
$this->assertStringContainsString('UHID-888', $output);
$this->assertStringContainsString('INV-777', $output);
$this->assertStringContainsString('CT SCAN BRAIN', $output);
}
public function testApplyPdfTokensIsCaseInsensitive(): void
{
$controller = $this->buildTestController();
$templateHtml = '<p>{{h_name}} - {{H_NAME}} - {{patient_NAME}} - {{UHID}}</p>';
$tokens = $controller->exposeBuildPdfTokens([
'patient_name' => 'Bob Marley',
'uhid' => 'UHID-999',
]);
$output = $controller->exposeApplyPdfTokens($templateHtml, $tokens);
$this->assertStringNotContainsString('{{h_name}}', $output);
$this->assertStringNotContainsString('{{H_NAME}}', $output);
$this->assertStringNotContainsString('{{patient_NAME}}', $output);
$this->assertStringNotContainsString('{{UHID}}', $output);
$this->assertStringContainsString('Bob Marley', $output);
$this->assertStringContainsString('UHID-999', $output);
}
}