db = db_connect(); helper(['form']); } private function ensureAccess() { $user = auth()->user(); if (! $user) { return $this->response->setStatusCode(403)->setBody('Forbidden'); } foreach ($this->doctorWorkspacePerms as $perm) { if ($user->can($perm)) { return null; } } return $this->response->setStatusCode(403)->setBody('Forbidden'); } private function ensureDocumentTables(): bool { return $this->db->tableExists('doc_format_master') && $this->db->tableExists('doc_format_sub') && $this->db->tableExists('patient_doc') && $this->db->tableExists('patient_doc_raw'); } private function ensureDocumentPrintTemplateTable(): bool { if (! $this->db->tableExists('doc_print_templates')) { return false; } $columnSql = [ 'print_on_type' => 'ALTER TABLE `doc_print_templates` ADD COLUMN `print_on_type` TINYINT(1) NOT NULL DEFAULT 1 AFTER `page_size`', 'page_margin_top_cm' => 'ALTER TABLE `doc_print_templates` ADD COLUMN `page_margin_top_cm` DECIMAL(5,2) NOT NULL DEFAULT 6.10 AFTER `print_on_type`', 'page_margin_bottom_cm' => 'ALTER TABLE `doc_print_templates` ADD COLUMN `page_margin_bottom_cm` DECIMAL(5,2) NOT NULL DEFAULT 2.50 AFTER `page_margin_top_cm`', 'page_margin_left_cm' => 'ALTER TABLE `doc_print_templates` ADD COLUMN `page_margin_left_cm` DECIMAL(5,2) NOT NULL DEFAULT 0.70 AFTER `page_margin_bottom_cm`', 'page_margin_right_cm' => 'ALTER TABLE `doc_print_templates` ADD COLUMN `page_margin_right_cm` DECIMAL(5,2) NOT NULL DEFAULT 0.70 AFTER `page_margin_left_cm`', 'margin_header_cm' => 'ALTER TABLE `doc_print_templates` ADD COLUMN `margin_header_cm` DECIMAL(5,2) NOT NULL DEFAULT 0.50 AFTER `page_margin_right_cm`', 'margin_footer_cm' => 'ALTER TABLE `doc_print_templates` ADD COLUMN `margin_footer_cm` DECIMAL(5,2) NOT NULL DEFAULT 1.50 AFTER `margin_header_cm`', 'header_html' => 'ALTER TABLE `doc_print_templates` ADD COLUMN `header_html` LONGTEXT NULL AFTER `margin_footer_cm`', 'footer_html' => 'ALTER TABLE `doc_print_templates` ADD COLUMN `footer_html` LONGTEXT NULL AFTER `header_html`', ]; foreach ($columnSql as $column => $sql) { if ($this->db->fieldExists($column, 'doc_print_templates')) { continue; } try { $this->db->query($sql); } catch (\Throwable $e) { log_message('error', 'Unable to add {column} column in doc_print_templates: {message}', [ 'column' => $column, 'message' => $e->getMessage(), ]); } } return true; } private function getDocumentPrintTemplates(): array { if (! $this->ensureDocumentPrintTemplateTable()) { return []; } return $this->db->table('doc_print_templates') ->select('id, template_name, is_default, print_on_type') ->where('status', 1) ->orderBy('is_default', 'DESC') ->orderBy('template_name', 'ASC') ->get() ->getResultArray(); } private function ensurePrintTemplateColumns(): void { if (! $this->db->tableExists('doc_format_master')) { return; } $columnSql = [ 'default_print_type' => 'ALTER TABLE `doc_format_master` ADD COLUMN `default_print_type` TINYINT(1) NOT NULL DEFAULT 0 AFTER `doc_raw_format`', 'print_top_margin' => 'ALTER TABLE `doc_format_master` ADD COLUMN `print_top_margin` DECIMAL(5,2) NOT NULL DEFAULT 6.10 AFTER `default_print_type`', 'print_bottom_margin' => 'ALTER TABLE `doc_format_master` ADD COLUMN `print_bottom_margin` DECIMAL(5,2) NOT NULL DEFAULT 2.50 AFTER `print_top_margin`', 'print_left_margin' => 'ALTER TABLE `doc_format_master` ADD COLUMN `print_left_margin` DECIMAL(5,2) NOT NULL DEFAULT 0.70 AFTER `print_bottom_margin`', 'print_right_margin' => 'ALTER TABLE `doc_format_master` ADD COLUMN `print_right_margin` DECIMAL(5,2) NOT NULL DEFAULT 0.70 AFTER `print_left_margin`', 'print_header_margin' => 'ALTER TABLE `doc_format_master` ADD COLUMN `print_header_margin` DECIMAL(5,2) NOT NULL DEFAULT 0.50 AFTER `print_right_margin`', 'print_footer_margin' => 'ALTER TABLE `doc_format_master` ADD COLUMN `print_footer_margin` DECIMAL(5,2) NOT NULL DEFAULT 1.50 AFTER `print_header_margin`', ]; foreach ($columnSql as $column => $sql) { if ($this->db->fieldExists($column, 'doc_format_master')) { continue; } try { $this->db->query($sql); } catch (\Throwable $e) { log_message('error', 'Unable to add {column} column: {message}', [ 'column' => $column, 'message' => $e->getMessage(), ]); } } } private function normalizeMarginValue($value, float $default): float { $number = (float) $value; if ($number <= 0 || $number > 20) { return $default; } return round($number, 2); } /** * Replace {{PLACEHOLDER}} tokens in a custom header/footer HTML string * with live hospital constants and patient data before passing to mPDF. * * @param array $doc Row from patient_doc join query */ private function resolveDocPrintTemplatePlaceholders(string $html, array $doc): string { if ($html === '') { return ''; } $logoFile = defined('H_logo') ? (string) constant('H_logo') : 'logo.png'; $logoAbsPath = FCPATH . 'assets/images/' . $logoFile; $logoSrc = $this->buildImageDataUriFromPath($logoAbsPath); if ($logoSrc === '' && is_file($logoAbsPath)) { $logoSrc = str_replace('\\', '/', $logoAbsPath); } if ($logoSrc === '') { $logoSrc = base_url('assets/images/' . $logoFile); } $logoHtml = ''; $age = (string) ($doc['age'] ?? ''); $gender = (string) ($doc['gender'] ?? ''); $ageSex = trim($age . ($gender !== '' ? '/' . $gender : '')); $drSignImg = ''; $drSignFile = (string) ($doc['dr_sign'] ?? $doc['sign_image'] ?? $doc['doctor_sign'] ?? ''); if ($drSignFile !== '') { $drSignAbsPath = FCPATH . 'assets/images/' . ltrim($drSignFile, '/\\'); $drSignSrc = $this->buildImageDataUriFromPath($drSignAbsPath); if ($drSignSrc === '' && is_file($drSignAbsPath)) { $drSignSrc = str_replace('\\', '/', $drSignAbsPath); } if ($drSignSrc === '') { $drSignSrc = base_url('assets/images/' . $drSignFile); } $drSignImg = ''; } $map = [ '{{H_Name}}' => defined('H_Name') ? (string) constant('H_Name') : '', '{{H_address_1}}' => defined('H_address_1') ? (string) constant('H_address_1') : '', '{{H_address_2}}' => defined('H_address_2') ? (string) constant('H_address_2') : '', '{{H_phone_No}}' => defined('H_phone_No') ? (string) constant('H_phone_No') : '', '{{H_Email}}' => defined('H_Email') ? (string) constant('H_Email') : '', '{{H_logo}}' => $logoFile, '{{H_logo_abs}}' => $logoAbsPath, '{{hospital_logo_html}}' => $logoHtml, '{{PATIENT_NAME}}' => (string) ($doc['p_fname'] ?? ''), '{{pName}}' => (string) ($doc['p_fname'] ?? ''), '{{UHID}}' => (string) ($doc['p_code'] ?? ''), '{{AGE_GENDER}}' => $ageSex, '{{age_sex}}' => $ageSex, '{{phoneno}}' => (string) ($doc['phoneno'] ?? $doc['phone_no'] ?? $doc['contact_no'] ?? ''), '{{doctor_name}}' => (string) ($doc['dr_name'] ?? ''), '{{doctor_sign_html}}' => $drSignImg, '{{CURRENT_DATE}}' => date('d-m-Y'), '{{CURRENT_DATETIME}}' => date('d-m-Y H:i'), '{{print_time}}' => date('d-m-Y H:i:s'), '{{qr_content}}' => (string) ($doc['id'] ?? ''), ]; return str_replace(array_keys($map), array_values($map), $html); } 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); } private function hasColumn(string $table, string $column): bool { if (! $this->db->tableExists($table)) { return false; } return $this->db->fieldExists($column, $table); } /** * @param array $payload * @return array */ private function filterPayloadByExistingColumns(string $table, array $payload): array { if (! $this->db->tableExists($table)) { return []; } $fields = $this->db->getFieldNames($table) ?? []; $out = []; foreach ($payload as $key => $value) { if (in_array($key, $fields, true)) { $out[$key] = $value; } } return $out; } public function workspace() { if ($resp = $this->ensureAccess()) { return $resp; } return view('doctor_document/workspace'); } public function patient_search() { if ($resp = $this->ensureAccess()) { return $resp; } $search = trim((string) $this->request->getGet('q')); $patients = $this->searchDoctorWorkPatients($search); $countsByPatient = $this->getDoctorWorkPatientCounts(array_map(static fn(array $row): int => (int) ($row['id'] ?? 0), $patients)); foreach ($patients as &$patientRow) { $patientId = (int) ($patientRow['id'] ?? 0); $patientCounts = $countsByPatient[$patientId] ?? []; $patientRow['opd_count'] = (int) ($patientCounts['opd_count'] ?? 0); $patientRow['lab_count'] = (int) ($patientCounts['lab_count'] ?? 0); $patientRow['ipd_count'] = (int) ($patientCounts['ipd_count'] ?? 0); } unset($patientRow); return view('doctor_document/patient_search', [ 'search' => $search, 'patients' => $patients, ]); } /** * @return list> */ private function searchDoctorWorkPatients(string $search): array { $search = preg_replace('/[^A-Za-z0-9 _.@\-]/', '', trim($search)) ?? ''; if ($search === '') { return []; } $searchTokens = preg_split('/\s+/', $search) ?: []; $whereParts = ['1=1']; foreach ($searchTokens as $token) { $token = trim((string) $token); if ($token === '') { continue; } $escaped = $this->db->escape($token); $likeToken = $this->db->escapeLikeString($token); if (is_numeric($token)) { $whereParts[] = "(p.p_code LIKE '%{$likeToken}' ESCAPE '!' OR p.mphone1 = {$escaped} OR p.udai = {$escaped})"; continue; } if (ctype_alpha($token)) { $whereParts[] = "(p.p_fname LIKE '%{$likeToken}%' ESCAPE '!' OR p.email1 = {$escaped})"; continue; } $whereParts[] = "(p.p_code LIKE '{$likeToken}' ESCAPE '!' OR p.email1 = {$escaped})"; } $sql = "SELECT p.*, DATE_FORMAT(p.last_visit,'%d-%m-%Y') AS Last_Visit FROM patient_master p WHERE " . implode(' AND ', $whereParts) . " GROUP BY p.id ORDER BY p.last_visit DESC LIMIT 100"; return $this->db->query($sql)->getResultArray(); } /** * @param list $patientIds * @return array> */ private function getDoctorWorkPatientCounts(array $patientIds): array { $patientIds = array_values(array_unique(array_filter(array_map('intval', $patientIds), static fn(int $id): bool => $id > 0))); if ($patientIds === []) { return []; } $countsByPatient = []; foreach ($patientIds as $patientId) { $countsByPatient[$patientId] = [ 'opd_count' => 0, 'lab_count' => 0, 'ipd_count' => 0, ]; } $countSources = [ ['table' => 'opd_master', 'column' => 'p_id', 'alias' => 'opd_count'], ['table' => 'lab_request', 'column' => 'patient_id', 'alias' => 'lab_count'], ['table' => 'ipd_master', 'column' => 'p_id', 'alias' => 'ipd_count'], ]; foreach ($countSources as $source) { if (! $this->hasColumn($source['table'], $source['column'])) { continue; } $rows = $this->db->table($source['table']) ->select($source['column'] . ' AS patient_id, COUNT(*) AS total_count', false) ->whereIn($source['column'], $patientIds) ->groupBy($source['column']) ->get() ->getResultArray(); foreach ($rows as $row) { $patientId = (int) ($row['patient_id'] ?? 0); if ($patientId <= 0 || ! array_key_exists($patientId, $countsByPatient)) { continue; } $countsByPatient[$patientId][$source['alias']] = (int) ($row['total_count'] ?? 0); } } return $countsByPatient; } public function open_by_key() { if ($resp = $this->ensureAccess()) { return $resp; } $rawKey = trim((string) $this->request->getGet('patient_key')); if ($rawKey === '') { return $this->response->setJSON([ 'status' => 0, 'message' => 'Enter patient id', ]); } $key = strtoupper(preg_replace('/\s+/', '', $rawKey)); $digits = preg_replace('/\D+/', '', $key); $patient = null; // Priority 1: exact record by numeric patient_master.id if (ctype_digit($key)) { $patient = $this->db->table('patient_master') ->select('id,p_code,old_uhid,p_fname') ->where('id', (int) $key) ->get(1) ->getRowArray(); } // Priority 2: exact record by UHID / old UHID if (! is_array($patient)) { $patient = $this->db->table('patient_master') ->select('id,p_code,old_uhid,p_fname') ->groupStart() ->where('UPPER(TRIM(p_code))', $key) ->orWhere('UPPER(TRIM(old_uhid))', $key) ->groupEnd() ->orderBy('id', 'DESC') ->get(1) ->getRowArray(); } if (is_array($patient)) { return $this->response->setJSON([ 'status' => 1, 'patient_id' => (int) ($patient['id'] ?? 0), ]); } // Priority 3: allow last digits search; open only when a single record is found. if ($digits !== '') { $builder = $this->db->table('patient_master') ->select('id,p_code,old_uhid,p_fname') ->groupStart() ->like('p_code', $digits, 'before') ->orLike('old_uhid', $digits, 'before') ->groupEnd(); $matches = $builder ->orderBy('id', 'DESC') ->limit(6) ->get() ->getResultArray(); $count = count($matches); if ($count === 1) { return $this->response->setJSON([ 'status' => 1, 'patient_id' => (int) ($matches[0]['id'] ?? 0), ]); } if ($count > 1) { return $this->response->setJSON([ 'status' => 0, 'message' => 'Multiple patients matched. Please enter complete UHID/Patient ID.', ]); } } return $this->response->setJSON([ 'status' => 0, 'message' => 'Patient not found', ]); } public function doc_list() { if ($resp = $this->ensureAccess()) { return $resp; } if (! $this->ensureDocumentTables()) { return $this->response->setStatusCode(500)->setBody('Document tables missing. Run migration: php spark migrate'); } $this->ensurePrintTemplateColumns(); $builder = $this->db->table('doc_format_master'); if ($this->hasColumn('doc_format_master', 'active')) { $builder->where('active', 1); } $rows = $builder->orderBy('doc_name', 'ASC')->get()->getResultArray(); return view('doctor_document/doc_list', ['doc_master' => $rows]); } public function docedit_load(int $docId = 0) { if ($resp = $this->ensureAccess()) { return $resp; } $this->ensurePrintTemplateColumns(); $master = []; if ($docId > 0) { $master = $this->db->table('doc_format_master')->where('df_id', $docId)->get(1)->getRowArray() ?? []; } $items = []; if ($docId > 0) { $itemBuilder = $this->db->table('doc_format_sub') ->select('id as item_id,input_name,input_code,input_type,input_default_value,short_order') ->where('doc_format_id', $docId); if ($this->hasColumn('doc_format_sub', 'active')) { $itemBuilder->where('active', 1); } $items = $itemBuilder->orderBy('short_order', 'ASC')->get()->getResultArray(); } return view('doctor_document/doc_master_edit', [ 'doc_master' => $master, 'doc_Item_List' => $items, 'doc_id' => $docId, ]); } public function report_insert() { if ($resp = $this->ensureAccess()) { return $resp; } if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['insertid' => 0, 'showcontent' => 'Invalid request']); } $name = trim((string) $this->request->getPost('input_docname')); $desc = trim((string) $this->request->getPost('input_doc_desc')); $html = (string) $this->request->getPost('HTMLData'); $defaultPrintType = (int) $this->request->getPost('default_print_type'); $printTopMargin = $this->normalizeMarginValue($this->request->getPost('print_top_margin'), 6.10); $printBottomMargin = $this->normalizeMarginValue($this->request->getPost('print_bottom_margin'), 2.50); $printLeftMargin = $this->normalizeMarginValue($this->request->getPost('print_left_margin'), 0.70); $printRightMargin = $this->normalizeMarginValue($this->request->getPost('print_right_margin'), 0.70); $printHeaderMargin = $this->normalizeMarginValue($this->request->getPost('print_header_margin'), 0.50); $printFooterMargin = $this->normalizeMarginValue($this->request->getPost('print_footer_margin'), 1.50); if ($name === '') { return $this->response->setJSON(['insertid' => 0, 'showcontent' => 'Document name is required']); } $this->ensurePrintTemplateColumns(); $payload = [ 'doc_name' => $name, 'doc_desc' => $desc, 'doc_raw_format' => $html, 'default_print_type' => ($defaultPrintType === 1 ? 1 : 0), 'print_top_margin' => $printTopMargin, 'print_bottom_margin' => $printBottomMargin, 'print_left_margin' => $printLeftMargin, 'print_right_margin' => $printRightMargin, 'print_header_margin' => $printHeaderMargin, 'print_footer_margin' => $printFooterMargin, 'active' => 1, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]; $payload = $this->filterPayloadByExistingColumns('doc_format_master', $payload); $this->db->table('doc_format_master')->insert($payload); return $this->response->setJSON([ 'insertid' => (int) $this->db->insertID(), 'showcontent' => 'Data saved successfully', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function report_update() { if ($resp = $this->ensureAccess()) { return $resp; } if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update_record' => 0, 'showcontent' => 'Invalid request']); } $id = (int) $this->request->getPost('df_id'); $name = trim((string) $this->request->getPost('input_docname')); $desc = trim((string) $this->request->getPost('input_doc_desc')); $html = (string) $this->request->getPost('HTMLData'); $defaultPrintType = (int) $this->request->getPost('default_print_type'); $printTopMargin = $this->normalizeMarginValue($this->request->getPost('print_top_margin'), 6.10); $printBottomMargin = $this->normalizeMarginValue($this->request->getPost('print_bottom_margin'), 2.50); $printLeftMargin = $this->normalizeMarginValue($this->request->getPost('print_left_margin'), 0.70); $printRightMargin = $this->normalizeMarginValue($this->request->getPost('print_right_margin'), 0.70); $printHeaderMargin = $this->normalizeMarginValue($this->request->getPost('print_header_margin'), 0.50); $printFooterMargin = $this->normalizeMarginValue($this->request->getPost('print_footer_margin'), 1.50); if ($id <= 0 || $name === '') { return $this->response->setJSON(['update_record' => 0, 'showcontent' => 'Document id/name required']); } $this->ensurePrintTemplateColumns(); $payload = [ 'doc_name' => $name, 'doc_desc' => $desc, 'doc_raw_format' => $html, 'default_print_type' => ($defaultPrintType === 1 ? 1 : 0), 'print_top_margin' => $printTopMargin, 'print_bottom_margin' => $printBottomMargin, 'print_left_margin' => $printLeftMargin, 'print_right_margin' => $printRightMargin, 'print_header_margin' => $printHeaderMargin, 'print_footer_margin' => $printFooterMargin, 'updated_at' => date('Y-m-d H:i:s'), ]; $payload = $this->filterPayloadByExistingColumns('doc_format_master', $payload); $this->db->table('doc_format_master') ->where('df_id', $id) ->update($payload); return $this->response->setJSON([ 'update_record' => 1, 'showcontent' => 'Data saved successfully', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function doc_input_list(int $docId) { if ($resp = $this->ensureAccess()) { return $resp; } $builder = $this->db->table('doc_format_sub') ->select('id as item_id,input_name,input_code,input_type,input_default_value,short_order') ->where('doc_format_id', $docId); if ($this->hasColumn('doc_format_sub', 'active')) { $builder->where('active', 1); } $items = $builder->orderBy('short_order', 'ASC')->get()->getResultArray(); return view('doctor_document/doc_input_list', [ 'doc_Item_List' => $items, 'doc_id' => $docId, ]); } public function input_parameter_load(int $itemId, int $docId) { if ($resp = $this->ensureAccess()) { return $resp; } $row = $this->db->table('doc_format_sub') ->where('id', $itemId) ->where('doc_format_id', $docId) ->get(1) ->getRowArray() ?? []; return $this->response->setJSON([ 'update' => ! empty($row) ? 1 : 0, 'row' => $row, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function input_parameter_add() { if ($resp = $this->ensureAccess()) { return $resp; } if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['insert_id' => 0, 'showcontent' => 'Invalid request']); } $docId = (int) $this->request->getPost('doc_id'); $inputName = trim((string) $this->request->getPost('input_input_name')); $inputCode = trim((string) $this->request->getPost('input_input_code')); $inputType = trim((string) $this->request->getPost('input_type')); $defaultValue = (string) $this->request->getPost('input_default_value'); if ($docId <= 0 || $inputName === '' || $inputCode === '') { return $this->response->setJSON(['insert_id' => 0, 'showcontent' => 'Required fields missing']); } $existsBuilder = $this->db->table('doc_format_sub') ->where('doc_format_id', $docId) ->where('input_code', $inputCode); if ($this->hasColumn('doc_format_sub', 'active')) { $existsBuilder->where('active', 1); } $exists = $existsBuilder->countAllResults(); if ($exists > 0) { return $this->response->setJSON(['insert_id' => 0, 'showcontent' => 'Input code already exists']); } $maxOrderRow = $this->db->table('doc_format_sub')->selectMax('short_order')->where('doc_format_id', $docId)->get()->getRowArray(); $nextOrder = (int) ($maxOrderRow['short_order'] ?? 0) + 1; $payload = [ 'doc_format_id' => $docId, 'input_name' => $inputName, 'input_code' => $inputCode, 'input_type' => $inputType !== '' ? $inputType : 'text', 'input_default_value' => $defaultValue, 'short_order' => max(1, $nextOrder), 'active' => 1, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]; $payload = $this->filterPayloadByExistingColumns('doc_format_sub', $payload); $this->db->table('doc_format_sub')->insert($payload); return $this->response->setJSON([ 'insert_id' => (int) $this->db->insertID(), 'showcontent' => 'Data saved successfully', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function input_parameter_edit() { if ($resp = $this->ensureAccess()) { return $resp; } if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update_value' => 0, 'showcontent' => 'Invalid request']); } $docId = (int) $this->request->getPost('doc_id'); $subId = (int) $this->request->getPost('doc_sub_id'); $inputName = trim((string) $this->request->getPost('input_input_name')); $inputCode = trim((string) $this->request->getPost('input_input_code')); $inputType = trim((string) $this->request->getPost('input_type')); $defaultValue = (string) $this->request->getPost('input_default_value'); if ($docId <= 0 || $subId <= 0 || $inputName === '' || $inputCode === '') { return $this->response->setJSON(['update_value' => 0, 'showcontent' => 'Required fields missing']); } $existsBuilder = $this->db->table('doc_format_sub') ->where('doc_format_id', $docId) ->where('input_code', $inputCode) ->where('id !=', $subId); if ($this->hasColumn('doc_format_sub', 'active')) { $existsBuilder->where('active', 1); } $exists = $existsBuilder->countAllResults(); if ($exists > 0) { return $this->response->setJSON(['update_value' => 0, 'showcontent' => 'Input code already exists']); } $payload = [ 'input_name' => $inputName, 'input_code' => $inputCode, 'input_type' => $inputType !== '' ? $inputType : 'text', 'input_default_value' => $defaultValue, 'updated_at' => date('Y-m-d H:i:s'), ]; $payload = $this->filterPayloadByExistingColumns('doc_format_sub', $payload); $this->db->table('doc_format_sub') ->where('id', $subId) ->where('doc_format_id', $docId) ->update($payload); return $this->response->setJSON([ 'update_value' => 1, 'showcontent' => 'Data saved successfully', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function p_doc_record(int $patientId) { if ($resp = $this->ensureAccess()) { return $resp; } $patient = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray(); if (! is_array($patient)) { return $this->response->setStatusCode(404)->setBody('Patient not found'); } $docFormatsBuilder = $this->db->table('doc_format_master'); if ($this->hasColumn('doc_format_master', 'active')) { $docFormatsBuilder->where('active', 1); } $docFormats = $docFormatsBuilder->orderBy('doc_name', 'ASC')->get()->getResultArray(); $doctorList = $this->db->table('doctor_master') ->select('id,p_fname') ->where('active', 1) ->orderBy('p_fname', 'ASC') ->get() ->getResultArray(); $patientDocs = $this->db->table('patient_doc pd') ->select('pd.id,pd.date_issue,dm.doc_name') ->join('doc_format_master dm', 'pd.doc_format_id=dm.df_id', 'left') ->where('pd.p_id', $patientId) ->orderBy('pd.date_issue', 'DESC') ->orderBy('pd.id', 'DESC') ->get() ->getResultArray(); return view('doctor_document/patient_doc_master', [ 'person_info' => $patient, 'doc_format' => $docFormats, 'doclist' => $doctorList, 'patient_doc' => $patientDocs, 'pno' => $patientId, 'age_text' => $this->buildAgeText($patient), ]); } public function create_doc() { if ($resp = $this->ensureAccess()) { return $resp; } $patientId = (int) $this->request->getPost('patient_id'); $docFormatId = (int) $this->request->getPost('document_format_id'); $doctorId = (int) $this->request->getPost('doc_id'); $issueDate = trim((string) $this->request->getPost('doc_issue_date')); $template = $this->db->table('doc_format_master')->where('df_id', $docFormatId)->get(1)->getRowArray(); $patient = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray(); $doctor = $this->db->table('doctor_master')->where('id', $doctorId)->get(1)->getRowArray(); if (! is_array($template) || ! is_array($patient)) { return $this->response->setBody('0'); } $reportString = (string) ($template['doc_raw_format'] ?? ''); $reportString = $this->applyDocumentTokens($reportString, $patient, $doctor, $issueDate); $issueMysql = $this->normalizeIssueDate($issueDate); $patientDocPayload = [ 'raw_data' => $reportString, 'doc_format_id' => $docFormatId, 'p_id' => $patientId, 'dr_id' => $doctorId, 'date_issue' => $issueMysql, 'update_pre_value' => 0, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]; $patientDocPayload = $this->filterPayloadByExistingColumns('patient_doc', $patientDocPayload); $this->db->table('patient_doc')->insert($patientDocPayload); $insertId = (int) $this->db->insertID(); $inputBuilder = $this->db->table('doc_format_sub') ->where('doc_format_id', $docFormatId); if ($this->hasColumn('doc_format_sub', 'active')) { $inputBuilder->where('active', 1); } $inputs = $inputBuilder->orderBy('short_order', 'ASC')->get()->getResultArray(); foreach ($inputs as $row) { $patientDocRawPayload = [ 'p_id' => $patientId, 'p_doc_id' => $insertId, 'p_doc_sub_id' => (int) ($row['id'] ?? 0), 'p_doc_raw_value' => (string) ($row['input_default_value'] ?? ''), 'update_data' => 0, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]; $patientDocRawPayload = $this->filterPayloadByExistingColumns('patient_doc_raw', $patientDocRawPayload); $this->db->table('patient_doc_raw')->insert($patientDocRawPayload); } return $this->response->setBody((string) $insertId); } public function re_create_doc(int $docId) { if ($resp = $this->ensureAccess()) { return $resp; } $patientDoc = $this->db->table('patient_doc')->where('id', $docId)->get(1)->getRowArray(); if (! is_array($patientDoc)) { return $this->response->setStatusCode(404)->setBody('Document not found'); } $template = $this->db->table('doc_format_master')->where('df_id', (int) ($patientDoc['doc_format_id'] ?? 0))->get(1)->getRowArray(); $patient = $this->db->table('patient_master')->where('id', (int) ($patientDoc['p_id'] ?? 0))->get(1)->getRowArray(); $doctor = $this->db->table('doctor_master')->where('id', (int) ($patientDoc['dr_id'] ?? 0))->get(1)->getRowArray(); if (! is_array($template) || ! is_array($patient)) { if ($this->request->isAJAX()) { return $this->load_doc($docId); } return redirect()->to(base_url('Document_Patient/load_doc/' . $docId)); } $issueDate = ''; if (! empty($patientDoc['date_issue'])) { $issueDate = date('d/m/Y', strtotime((string) $patientDoc['date_issue'])); } $reportString = $this->applyDocumentTokens((string) ($template['doc_raw_format'] ?? ''), $patient, $doctor, $issueDate); $patientDocUpdatePayload = [ 'raw_data' => $reportString, 'update_pre_value' => 0, 'updated_at' => date('Y-m-d H:i:s'), ]; $patientDocUpdatePayload = $this->filterPayloadByExistingColumns('patient_doc', $patientDocUpdatePayload); $this->db->table('patient_doc')->where('id', $docId)->update($patientDocUpdatePayload); if ($this->request->isAJAX()) { return $this->Pre_Data($docId); } return redirect()->to(base_url('Document_Patient/Pre_Data/' . $docId)); } public function Pre_Data(int $patientDocId) { if ($resp = $this->ensureAccess()) { return $resp; } $patientDoc = $this->db->table('patient_doc')->where('id', $patientDocId)->get(1)->getRowArray(); if (! is_array($patientDoc)) { return $this->response->setStatusCode(404)->setBody('Document not found'); } $patient = $this->db->table('patient_master')->where('id', (int) ($patientDoc['p_id'] ?? 0))->get(1)->getRowArray(); $inputs = $this->db->table('patient_doc_raw r') ->select('r.id,r.p_doc_raw_value,r.update_data,s.input_name,s.input_type') ->join('doc_format_sub s', 'r.p_doc_sub_id=s.id', 'left') ->where('r.p_doc_id', $patientDocId) ->orderBy('s.short_order', 'ASC') ->get() ->getResultArray(); return view('doctor_document/doc_pre_data', [ 'patient_doc' => $patientDoc, 'person_info' => $patient, 'doc_format_sub' => $inputs, 'patient_doc_id' => $patientDocId, ]); } public function Entry_Update() { if ($resp = $this->ensureAccess()) { return $resp; } $testId = (int) $this->request->getPost('test_id'); $testValue = (string) $this->request->getPost('test_value'); $entryUpdatePayload = [ 'p_doc_raw_value' => $testValue, 'update_data' => 1, 'updated_at' => date('Y-m-d H:i:s'), ]; $entryUpdatePayload = $this->filterPayloadByExistingColumns('patient_doc_raw', $entryUpdatePayload); $this->db->table('patient_doc_raw')->where('id', $testId)->update($entryUpdatePayload); return $this->response->setBody($testValue); } public function update_doc_field(int $patientDocId) { if ($resp = $this->ensureAccess()) { return $resp; } try { if (! $this->db->tableExists('patient_doc') || ! $this->db->tableExists('patient_doc_raw')) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Document tables not available', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $pending = 0; if ($this->hasColumn('patient_doc_raw', 'update_data')) { $pending = $this->db->table('patient_doc_raw') ->where('p_doc_id', $patientDocId) ->where('update_data', 0) ->countAllResults(); } if ($pending > 0) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Some field pending', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $patientDoc = $this->db->table('patient_doc')->where('id', $patientDocId)->get(1)->getRowArray(); if (! is_array($patientDoc)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Document not found', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $report = (string) ($patientDoc['raw_data'] ?? ''); $values = $this->db->table('patient_doc_raw r') ->select('r.p_doc_raw_value,s.input_code') ->join('doc_format_sub s', 'r.p_doc_sub_id=s.id', 'left') ->where('r.p_doc_id', $patientDocId) ->get() ->getResultArray(); foreach ($values as $row) { $code = trim((string) ($row['input_code'] ?? '')); if ($code === '') { continue; } $val = nl2br((string) ($row['p_doc_raw_value'] ?? '')); $report = str_replace('{' . $code . '}', $val, $report); } $updatePayload = [ 'raw_data' => $report, 'update_pre_value' => 1, 'updated_at' => date('Y-m-d H:i:s'), ]; $updatePayload = $this->filterPayloadByExistingColumns('patient_doc', $updatePayload); if (empty($updatePayload)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Unable to compile due to schema mismatch', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $this->db->table('patient_doc')->where('id', $patientDocId)->update($updatePayload); return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Compile done', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } catch (\Throwable $e) { log_message('error', 'DoctorDocument update_doc_field failed for doc_id {doc_id}: {message}', [ 'doc_id' => $patientDocId, 'message' => $e->getMessage(), ]); return $this->response->setStatusCode(500)->setJSON([ 'update' => 0, 'error_text' => 'Compile failed due to server error', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } } public function load_doc(int $patientDocId) { if ($resp = $this->ensureAccess()) { return $resp; } $patientDoc = $this->db->table('patient_doc')->where('id', $patientDocId)->get(1)->getRowArray(); if (! is_array($patientDoc)) { return $this->response->setStatusCode(404)->setBody('Document not found'); } $patient = $this->db->table('patient_master')->where('id', (int) ($patientDoc['p_id'] ?? 0))->get(1)->getRowArray(); $printTemplates = $this->getDocumentPrintTemplates(); $defaultTemplateId = 0; foreach ($printTemplates as $templateRow) { if ((int) ($templateRow['is_default'] ?? 0) === 1) { $defaultTemplateId = (int) ($templateRow['id'] ?? 0); break; } } if ($defaultTemplateId <= 0 && ! empty($printTemplates)) { $defaultTemplateId = (int) ($printTemplates[0]['id'] ?? 0); } return view('doctor_document/patient_doc_edit', [ 'patient_doc' => $patientDoc, 'person_info' => $patient, 'print_templates' => $printTemplates, 'default_print_template_id' => $defaultTemplateId, ]); } public function update_doc() { if ($resp = $this->ensureAccess()) { return $resp; } $documentId = (int) $this->request->getPost('document_id'); $htmlData = (string) $this->request->getPost('HTMLData'); $updated = $this->db->table('patient_doc')->where('id', $documentId)->update([ 'raw_data' => $htmlData, 'updated_at' => date('Y-m-d H:i:s'), ]); return $this->response->setJSON([ 'update_value' => $updated ? 1 : 0, 'showcontent' => $updated ? 'Data saved successfully' : 'Unable to save', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function create_final(int $patientDocId, ?int $printOnType = null) { if ($resp = $this->ensureAccess()) { return $resp; } // Release session lock IMMEDIATELY so other requests from the same browser // are not blocked while mPDF generates the PDF (which can take 5-30 seconds). if (session_status() === PHP_SESSION_ACTIVE) { session_write_close(); } // mPDF is memory-heavy; ensure enough headroom and execution time. $currentMemory = ini_get('memory_limit'); if ((int) $currentMemory > 0 && (int) $currentMemory < 256) { ini_set('memory_limit', '256M'); } set_time_limit(120); $this->ensurePrintTemplateColumns(); $this->ensureDocumentPrintTemplateTable(); $requestedTemplateId = (int) ($this->request->getGet('ptid') ?? 0); $patientDoc = $this->db->table('patient_doc pd') ->select('pd.*,dm.doc_name,dm.doc_desc,dm.default_print_type,dm.print_top_margin,dm.print_bottom_margin,dm.print_left_margin,dm.print_right_margin,dm.print_header_margin,dm.print_footer_margin,p.p_fname,p.p_relative,p.p_rname,p.p_code,p.gender,p.age,p.age_in_month,p.estimate_dob,p.dob,dr.p_fname as dr_name') ->join('doc_format_master dm', 'pd.doc_format_id=dm.df_id', 'left') ->join('patient_master p', 'pd.p_id=p.id', 'left') ->join('doctor_master dr', 'pd.dr_id=dr.id', 'left') ->where('pd.id', $patientDocId) ->get(1) ->getRowArray(); if (! is_array($patientDoc)) { return $this->response->setStatusCode(404)->setBody('Document not found'); } $selectedPrintTemplate = null; if ($requestedTemplateId > 0 && $this->db->tableExists('doc_print_templates')) { $selectedPrintTemplate = $this->db->table('doc_print_templates') ->where('id', $requestedTemplateId) ->where('status', 1) ->get(1) ->getRowArray(); } if (! is_array($selectedPrintTemplate) && $this->db->tableExists('doc_print_templates')) { $selectedPrintTemplate = $this->db->table('doc_print_templates') ->where('status', 1) ->where('is_default', 1) ->orderBy('id', 'DESC') ->get(1) ->getRowArray(); } $resolvedPrintType = $printOnType; if ($resolvedPrintType === null) { if (is_array($selectedPrintTemplate)) { $resolvedPrintType = (int) ($selectedPrintTemplate['print_on_type'] ?? 1); } else { $resolvedPrintType = (int) ($patientDoc['default_print_type'] ?? 0); } } $resolvedPrintType = ((int) $resolvedPrintType === 1) ? 1 : 0; if (is_array($selectedPrintTemplate)) { $printTopMargin = $this->normalizeMarginValue($selectedPrintTemplate['page_margin_top_cm'] ?? null, 6.10); $printBottomMargin = $this->normalizeMarginValue($selectedPrintTemplate['page_margin_bottom_cm'] ?? null, 2.50); $printLeftMargin = $this->normalizeMarginValue($selectedPrintTemplate['page_margin_left_cm'] ?? null, 0.70); $printRightMargin = $this->normalizeMarginValue($selectedPrintTemplate['page_margin_right_cm'] ?? null, 0.70); $printHeaderMargin = $this->normalizeMarginValue($selectedPrintTemplate['margin_header_cm'] ?? null, 0.50); $printFooterMargin = $this->normalizeMarginValue($selectedPrintTemplate['margin_footer_cm'] ?? null, 1.50); $pageSize = trim((string) ($selectedPrintTemplate['page_size'] ?? 'A4')); if ($pageSize === '') { $pageSize = 'A4'; } } else { $printTopMargin = $this->normalizeMarginValue($patientDoc['print_top_margin'] ?? null, 6.10); $printBottomMargin = $this->normalizeMarginValue($patientDoc['print_bottom_margin'] ?? null, 2.50); $printLeftMargin = $this->normalizeMarginValue($patientDoc['print_left_margin'] ?? null, 0.70); $printRightMargin = $this->normalizeMarginValue($patientDoc['print_right_margin'] ?? null, 0.70); $printHeaderMargin = $this->normalizeMarginValue($patientDoc['print_header_margin'] ?? null, 0.50); $printFooterMargin = $this->normalizeMarginValue($patientDoc['print_footer_margin'] ?? null, 1.50); $pageSize = 'A4'; } $issueDate = ! empty($patientDoc['date_issue']) ? date('d-m-Y', strtotime((string) $patientDoc['date_issue'])) : date('d-m-Y'); $printNo = 1; if ($this->db->tableExists('file_upload_data')) { $printNo = (int) $this->db->table('file_upload_data')->where('doc_id', $patientDocId)->countAllResults() + 1; } $headerRef = 'Document Ref. No.' . date('Y') . '/' . $printNo . '/' . $patientDocId; $content = '' . $headerRef . 'Date : ' . $issueDate . ''; $rawData = (string) ($patientDoc['raw_data'] ?? ''); $rawData = str_replace(["\\r\\n", "\\n", "\\r"], "\n", $rawData); $rawData = $this->resolveDocPrintTemplatePlaceholders($rawData, $patientDoc); $content .= $rawData; $customHeaderHtml = $this->resolveDocPrintTemplatePlaceholders( (string) ($selectedPrintTemplate['header_html'] ?? ''), $patientDoc ); $customFooterHtml = $this->resolveDocPrintTemplatePlaceholders( (string) ($selectedPrintTemplate['footer_html'] ?? ''), $patientDoc ); $logoFile = defined('H_logo') ? (string) constant('H_logo') : 'logo.png'; $logoAbsPath = FCPATH . 'assets/images/' . $logoFile; $hospitalLogoSrc = $this->buildImageDataUriFromPath($logoAbsPath); if ($hospitalLogoSrc === '' && is_file($logoAbsPath)) { $hospitalLogoSrc = str_replace('\\', '/', $logoAbsPath); } if ($hospitalLogoSrc === '') { $hospitalLogoSrc = base_url('assets/images/' . $logoFile); } $data = [ 'content' => $content, 'print_on_type' => $resolvedPrintType, 'bar_content' => $headerRef . '/' . $issueDate, 'doctor_name' => (string) ($patientDoc['dr_name'] ?? ''), 'report_title' => (string) ($patientDoc['doc_name'] ?? 'Patient Document'), 'print_top_margin' => $printTopMargin, 'print_bottom_margin' => $printBottomMargin, 'print_left_margin' => $printLeftMargin, 'print_right_margin' => $printRightMargin, 'print_header_margin' => $printHeaderMargin, 'print_footer_margin' => $printFooterMargin, 'custom_header_html' => $customHeaderHtml, 'custom_footer_html' => $customFooterHtml, 'has_selected_print_template' => is_array($selectedPrintTemplate), 'hospital_logo_src' => $hospitalLogoSrc, ]; $mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf'; if (! is_dir($mpdfTempDir)) { mkdir($mpdfTempDir, 0755, true); } // FreeSans (GNU FreeFont) ships with mPDF and covers Devanagari (Hindi) Unicode glyphs. $mpdfFontDir = realpath(__DIR__ . '/../../vendor/mpdf/mpdf/ttfonts'); $mpdfFontDirs = $mpdfFontDir !== false ? [$mpdfFontDir] : []; try { $mpdf = new Mpdf([ 'format' => $pageSize, 'margin_top' => $printTopMargin * 10, 'margin_bottom' => $printBottomMargin * 10, 'margin_left' => $printLeftMargin * 10, 'margin_right' => $printRightMargin * 10, 'margin_header' => $printHeaderMargin * 10, 'margin_footer' => $printFooterMargin * 10, 'tempDir' => $mpdfTempDir, 'default_font' => 'freesans', 'fontDir' => $mpdfFontDirs, 'fontdata' => [ 'freesans' => [ 'R' => 'FreeSans.ttf', 'B' => 'FreeSansBold.ttf', 'I' => 'FreeSansOblique.ttf', 'BI' => 'FreeSansBoldOblique.ttf', 'useOTL' => 0xFF, 'useKashida' => 75, ], 'freeserif' => [ 'R' => 'FreeSerif.ttf', 'B' => 'FreeSerifBold.ttf', 'I' => 'FreeSerifItalic.ttf', 'BI' => 'FreeSerifBoldItalic.ttf', 'useOTL' => 0xFF, 'useKashida' => 75, ], ], 'autoScriptToLang' => true, 'autoLanguageDetection' => true, 'autoArabic' => false, 'autoVietnamese' => false, ]); } catch (\Throwable $e) { log_message('error', '[DoctorDocument::create_final] mPDF init failed: {msg}', ['msg' => $e->getMessage()]); return $this->response ->setStatusCode(500) ->setHeader('Content-Type', 'text/plain') ->setBody('PDF init failed: ' . $e->getMessage()); } if ($resolvedPrintType === 1) { $hospitalName = defined('H_Name') ? (string) constant('H_Name') : 'Hospital'; $mpdf->SetWatermarkText($hospitalName, 0.1); $mpdf->showWatermarkText = true; } $html = view('doctor_document/doc_letterhead_print', $data); try { $mpdf->WriteHTML($html, HTMLParserMode::DEFAULT_MODE); } catch (\Throwable $e) { log_message('error', '[DoctorDocument::create_final] mPDF WriteHTML failed: {msg}', ['msg' => $e->getMessage()]); return $this->response ->setStatusCode(500) ->setHeader('Content-Type', 'text/plain') ->setBody('PDF generation failed: ' . $e->getMessage()); } $fileName = 'Document-' . $patientDocId . '-' . date('YmdHis') . '.pdf'; try { $pdfBytes = $mpdf->Output($fileName, 'S'); } catch (\Throwable $e) { log_message('error', '[DoctorDocument::create_final] mPDF Output failed: {msg}', ['msg' => $e->getMessage()]); return $this->response ->setStatusCode(500) ->setHeader('Content-Type', 'text/plain') ->setBody('PDF output failed: ' . $e->getMessage()); } return $this->response ->setHeader('Content-Type', 'application/pdf') ->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"') ->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') ->setHeader('Pragma', 'no-cache') ->setHeader('Expires', '0') ->setBody($pdfBytes); } private function normalizeIssueDate(string $date): ?string { $date = trim($date); if ($date === '') { return date('Y-m-d'); } $formats = ['d/m/Y', 'd-m-Y', 'Y-m-d']; foreach ($formats as $fmt) { $obj = \DateTime::createFromFormat($fmt, $date); if ($obj instanceof \DateTime) { return $obj->format('Y-m-d'); } } $ts = strtotime($date); if ($ts === false) { return date('Y-m-d'); } return date('Y-m-d', $ts); } private function buildAgeText(array $patient): string { if (function_exists('get_age_1')) { return (string) get_age_1( $patient['dob'] ?? null, $patient['age'] ?? '', $patient['age_in_month'] ?? '', $patient['estimate_dob'] ?? '', date('Y-m-d H:i:s') ); } return (string) ($patient['age'] ?? '-'); } private function applyDocumentTokens(string $template, array $patient, ?array $doctor, string $issueDate): string { $gender = (int) ($patient['gender'] ?? 0); $genderText = $gender === 1 ? 'Male' : ($gender === 2 ? 'Female' : 'Other'); $heShe = $gender === 2 ? 'she' : 'he'; $hisHer = $gender === 2 ? 'her' : 'his'; $address = trim(implode(', ', array_filter([ (string) ($patient['add1'] ?? ''), (string) ($patient['add2'] ?? ''), (string) ($patient['city'] ?? ''), (string) ($patient['state'] ?? ''), ]))); $replace = [ '{p_code}' => (string) ($patient['p_code'] ?? ''), '{p_fname}' => (string) ($patient['p_fname'] ?? ''), '{p_rname}' => (string) ($patient['p_rname'] ?? ''), '{str_age}' => $this->buildAgeText($patient), '{p_relative}' => (string) ($patient['p_relative'] ?? ''), '{gender}' => $genderText, '{p_title}' => (string) ($patient['title'] ?? ''), '{p_address}' => $address, '{p_he_she}' => $heShe, '{p_his_her}' => $hisHer, '{dr_name}' => (string) ($doctor['p_fname'] ?? ''), '{dr_sign}' => nl2br((string) ($doctor['doc_sign'] ?? '')), '{current_date}' => date('d-m-Y'), '{issue_date}' => $issueDate !== '' ? str_replace('/', '-', $issueDate) : date('d-m-Y'), ]; return strtr($template, $replace); } }