diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 48425d1..dd5d3da 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -597,6 +597,8 @@ $routes->get('Opd/invoice/(:num)', 'Opd::invoice/$1');
$routes->get('Opd/invoice_print/(:num)', 'Opd::invoice_print/$1');
$routes->get('Opd/opd_lettre_print/(:num)', 'Opd::opd_lettre_print/$1');
$routes->get('Opd/opd_lettre_pdf/(:num)', 'Opd::opd_lettre_pdf/$1');
+$routes->get('Opd/opd_lettre_html', 'Opd::opd_lettre_html');
+$routes->get('Opd/opd_lettre_html/(:num)', 'Opd::opd_lettre_html/$1');
$routes->get('Opd/print_template_builder', 'Opd::print_template_builder');
$routes->post('Opd/print_template_save', 'Opd::print_template_save');
$routes->post('Opd/print_template_rename', 'Opd::print_template_rename');
diff --git a/app/Controllers/Opd.php b/app/Controllers/Opd.php
index 1d19c09..f25cf53 100644
--- a/app/Controllers/Opd.php
+++ b/app/Controllers/Opd.php
@@ -24,6 +24,41 @@ class Opd extends BaseController
*/
private array $scanAiDebug = [];
+ /**
+ * @var array{heading_style:string,line_gap:float,block_gap_mm:float}
+ */
+ private array $opdPlaceholderRenderOptions = [
+ 'heading_style' => 'bold',
+ 'line_gap' => 1.6,
+ 'block_gap_mm' => 2.8,
+ ];
+
+ private function configureOpdPlaceholderRenderOptionsFromRequest(): void
+ {
+ $headingStyle = strtolower(trim((string) $this->request->getGet('heading_style')));
+ if (!in_array($headingStyle, ['bold', 'highlight', 'plain'], true)) {
+ $headingStyle = 'bold';
+ }
+
+ $lineGap = (float) $this->request->getGet('line_gap');
+ if (!is_finite($lineGap) || $lineGap <= 0) {
+ $lineGap = 1.6;
+ }
+ $lineGap = max(1.0, min(3.0, $lineGap));
+
+ $blockGapMm = (float) $this->request->getGet('block_gap_mm');
+ if (!is_finite($blockGapMm) || $blockGapMm < 0) {
+ $blockGapMm = 2.8;
+ }
+ $blockGapMm = max(0.0, min(10.0, $blockGapMm));
+
+ $this->opdPlaceholderRenderOptions = [
+ 'heading_style' => $headingStyle,
+ 'line_gap' => round($lineGap, 2),
+ 'block_gap_mm' => round($blockGapMm, 2),
+ ];
+ }
+
private function canAccessDoctorWorkPanel(): bool
{
$user = auth()->user();
@@ -2111,6 +2146,8 @@ class Opd extends BaseController
public function opd_lettre_pdf(int $opdId)
{
+ $this->configureOpdPlaceholderRenderOptionsFromRequest();
+
$sessionId = (int) $this->request->getGet('session_id');
$debugHtml = ((int) $this->request->getGet('debug_html')) === 1;
$layoutMode = strtolower(trim((string) $this->request->getGet('layout')));
@@ -2423,6 +2460,48 @@ class Opd extends BaseController
->setBody($mpdf->Output($fileName, 'S'));
}
+ public function opd_lettre_html(int $opdId = 0)
+ {
+ if ($opdId <= 0) {
+ $defaultHeading = esc((string) ($this->request->getGet('heading_style') ?: 'bold'));
+ $defaultLineGap = esc((string) ($this->request->getGet('line_gap') ?: '1.6'));
+ $defaultBlockGap = esc((string) ($this->request->getGet('block_gap_mm') ?: '2.8'));
+
+ $html = '
OPD HTML Preview'
+ . ''
+ . ''
+ . 'OPD Letter HTML Preview
'
+ . 'Enter OPD ID and style options. This opens rendered HTML debug output for placeholder and CSS verification.
'
+ . ''
+ . 'Direct URL example: ' . esc(base_url('Opd/opd_lettre_html/2047354?heading_style=highlight&line_gap=1.8&block_gap_mm=4')) . '
'
+ . '';
+
+ return $this->response
+ ->setHeader('Content-Type', 'text/html; charset=UTF-8')
+ ->setBody($html);
+ }
+
+ $query = $this->request->getGet();
+ $query['debug_html'] = 1;
+ $target = base_url('Opd/opd_lettre_pdf/' . (int) $opdId) . '?' . http_build_query($query);
+
+ return redirect()->to($target);
+ }
+
public function opd_pdf_print(int $opdId)
{
$printConfig = $this->resolveDoctorPrintConfigByField($opdId, 'opd_blank_print', 'meta_only');
@@ -3627,6 +3706,14 @@ class Opd extends BaseController
}
}
+ // Keep OPD fee tokens explicit for PDF templates.
+ $opdRow = $data['opd_master'][0] ?? null;
+ $opdFeeAmount = trim((string) ($opdRow->opd_fee_amount ?? ($tokens['opd_fee_amount'] ?? '')));
+ $opdFeeDescription = trim((string) ($opdRow->opd_fee_desc ?? ($tokens['opd_fee_desc'] ?? '')));
+ $tokens['opd_fee_amount'] = $opdFeeAmount;
+ $tokens['opd_fee_desc'] = $opdFeeDescription;
+ $tokens['opd_fee_combined'] = trim($opdFeeAmount . ' ' . $opdFeeDescription);
+
// Add case-insensitive token aliases for legacy template compatibility.
foreach ($tokens as $key => $value) {
$lower = strtolower((string) $key);
@@ -3640,11 +3727,24 @@ class Opd extends BaseController
}
}
+ $headingStyleMode = (string) ($this->opdPlaceholderRenderOptions['heading_style'] ?? 'bold');
+ $lineGap = (float) ($this->opdPlaceholderRenderOptions['line_gap'] ?? 1.6);
+ $blockGapMm = (float) ($this->opdPlaceholderRenderOptions['block_gap_mm'] ?? 2.8);
+ $lineGapCss = number_format($lineGap, 2, '.', '');
+ $blockGapCss = number_format($blockGapMm, 2, '.', '');
+
+ $headingStyleCss = 'font-weight:700;font-size:inherit;letter-spacing:0.04em;text-transform:uppercase;color:#1a1a1a;';
+ if ($headingStyleMode === 'plain') {
+ $headingStyleCss = 'font-weight:400;font-size:inherit;letter-spacing:0.04em;text-transform:uppercase;color:#1a1a1a;';
+ } elseif ($headingStyleMode === 'highlight') {
+ $headingStyleCss = 'font-weight:700;font-size:inherit;letter-spacing:0.04em;text-transform:uppercase;color:#1a1a1a;background:#fff3cd;border:1px solid #f0d98a;border-radius:2px;padding:1px 4px;display:inline-block;';
+ }
+
// mPDF-safe vertical spacer. Using a fixed-height block in mm is more reliable
// than margins inside absolute-positioned containers.
- $blockGapHtml = '
';
+ $blockGapHtml = '
';
- $formatBlock = static function (string $label, string $value, bool $lineBreakAfterLabel = false) use ($blockGapHtml): string {
+ $formatBlock = function (string $label, string $value, bool $lineBreakAfterLabel = false) use ($blockGapHtml, $headingStyleCss, $lineGapCss): string {
$value = trim($value);
if ($value === '') {
return '';
@@ -3652,15 +3752,15 @@ class Opd extends BaseController
// Heading: bold + uppercase, same font-size as surrounding content so
// heading and value stay visually matched (no mismatch from hardcoded px).
- $headingHtml = '' . $label . ' :';
+ $headingHtml = '' . $label . ' :';
if ($lineBreakAfterLabel) {
// List-style sections (Complaint, Diagnosis …):
// heading on its own line, content block below with a small left indent.
// explicit spacer block is appended for consistent separation in mPDF.
- return ''
+ return '
'
. $headingHtml
- . '
'
. $blockGapHtml;
@@ -3668,7 +3768,7 @@ class Opd extends BaseController
// Inline sections (Vitals, Investigation, Next Visit):
// heading + value on the same line; explicit spacer appended after block.
- return '
'
+ return '
'
. $headingHtml
. ' '
. $value
@@ -4258,6 +4358,10 @@ class Opd extends BaseController
'/<\?=\s*\$opd_master\[0\]->doc_name\s*\?>/i' => '{{doctor_name}}',
'/<\?=\s*nl2br\(\s*\$opd_master\[0\]->doc_sign\s*\)\s*\?>/i' => '{{doctor_sign_html}}',
'/<\?=\s*date\(\s*[\"\']d-m-Y\s+H:i:s[\"\']\s*\)\s*\?>/i' => '{{print_time}}',
+ '/<\?=\s*\$opd_master\[0\]->opd_fee_amount\s*\?>/i' => '{{opd_fee_amount}}',
+ '/<\?=\s*\$opd_master\[0\]->opd_fee_desc\s*\?>/i' => '{{opd_fee_desc}}',
+ '/<\?=\s*esc\(\s*\$opd_master\[0\]->opd_fee_amount\s*(?:\?\?\s*[\"\']\s*[\"\'])?\s*\)\s*\?>/i' => '{{opd_fee_amount}}',
+ '/<\?=\s*esc\(\s*\$opd_master\[0\]->opd_fee_desc\s*(?:\?\?\s*[\"\']\s*[\"\'])?\s*\)\s*\?>/i' => '{{opd_fee_desc}}',
'/<\?=\s*\$content\s*\?>/i' => '{{content}}',
'/<\?=\s*\$content\s*;?\s*\?>/i' => '{{content}}',
'/<\?=\s*\$\w+\s*\?>/i' => '',