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 - . '
' + . '
' . $value . '
' . $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' => '',