db = \Config\Database::connect(); } public function index() { // Check if it's an AJAX request $isAjax = $this->request->isAJAX() || $this->request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest'; if (!$isAjax) { return view('welcome_message'); } $user = service('auth')->user(); return view('diagnosis/index', [ 'user' => $user ]); } public function pathology() { return $this->labPath(5); } public function biopsy() { return $this->labPath(30); } public function mri() { return $this->labPath(2); } public function xray() { return $this->labPath(3); } public function ultrasound() { return $this->labPath(1); } public function ctscan() { return $this->labPath(4); } public function labMaster() { return $this->index(); } public function labPathLegacy($labType) { return $this->labPath((int) $labType); } private function labPath(int $labType) { // Check if it's an AJAX request $isAjax = $this->request->isAJAX() || $this->request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest'; if (!$isAjax) { return view('welcome_message'); } $user = service('auth')->user(); $labTypeName = $this->getLabTypeName($labType); return view('diagnosis/request_list', [ 'user' => $user, 'lab_type' => $labType, 'lab_type_name' => $labTypeName ]); } public function searchLab($labType = null) { $labType = $labType !== null ? (int) $labType : (int) $this->request->getPost('lab_type'); $searchText = $this->request->getPost('txtsearch'); // Sanitize input $searchText = preg_replace('/[^A-Za-z0-9_ \-]/', '', $searchText); if (trim($searchText) == '') { // Get recent 200 records $sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date, CONCAT(p.p_fname, '/', IF(p.gender=1,'M','F'), '/', p.p_rname) as inv_name, p.dob, p.age, p.age_in_month, p.estimate_dob, GROUP_CONCAT(CONCAT_WS(';', i.item_name, i.item_name, i.id, check_item_request(m.id, i.id)) SEPARATOR '#') as data_array, r.daily_sr_no, r.lab_test_no FROM invoice_item i INNER JOIN invoice_master m ON m.id = i.inv_master_id INNER JOIN hc_item_type y ON i.item_type = y.itype_id INNER JOIN patient_master p ON p.id = m.attach_id LEFT JOIN lab_invoice_request r ON m.id = r.invoice_id WHERE m.payment_status = 1 AND y.itype_id = ? GROUP BY m.id ORDER BY m.id DESC LIMIT 200"; $query = $this->db->query($sql, [$labType]); } else { // Search with criteria $sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date, CONCAT(p.p_fname, '/', IF(p.gender=1,'M','F'), '/', p.p_rname) as inv_name, p.dob, p.age, p.age_in_month, p.estimate_dob, GROUP_CONCAT(CONCAT_WS(';', i.item_name, i.item_name, i.id, check_item_request(m.id, i.id)) SEPARATOR '#') as data_array, r.daily_sr_no, r.lab_test_no FROM invoice_item i INNER JOIN invoice_master m ON m.id = i.inv_master_id INNER JOIN hc_item_type y ON i.item_type = y.itype_id INNER JOIN patient_master p ON p.id = m.attach_id LEFT JOIN lab_invoice_request r ON m.id = r.invoice_id WHERE m.payment_status = 1 AND y.itype_id = ? AND (m.invoice_code LIKE ? OR p.p_fname LIKE ? OR p.p_code LIKE ?) GROUP BY m.id ORDER BY m.id DESC LIMIT 50"; $searchPattern = '%' . $searchText . '%'; $query = $this->db->query($sql, [$labType, $searchPattern, $searchPattern, $searchPattern]); } $results = $query->getResult(); // Calculate age using PHP helper foreach ($results as $row) { $row->age = get_age_1($row->dob ?? null, $row->age ?? '', $row->age_in_month ?? '', $row->estimate_dob ?? ''); } $data = [ 'labreport_preprocess' => $results, 'lab_type' => $labType ]; // Return different view based on lab type if ($labType == 5 || $labType == 30) { // Pathology or Biopsy return view('diagnosis/lab_report_tab_path', $data); } else { return view('diagnosis/lab_report_tab', $data); } } public function searchLabBySrno($labType = null) { $labType = $labType !== null ? (int) $labType : (int) $this->request->getPost('lab_type'); $srNo = $this->request->getPost('txtsearch_srno'); // Sanitize input $srNo = preg_replace('/[^A-Za-z0-9_ \-]/', '', $srNo); if (trim($srNo) == '') { // Get recent 3 days records with serial number $sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date, CONCAT(p.p_fname, '/', IF(p.gender=1,'M','F'), '/', p.p_rname) as inv_name, p.dob, p.age, p.age_in_month, p.estimate_dob, GROUP_CONCAT(CONCAT_WS(';', i.item_name, i.item_name, i.id, check_item_request(m.id, i.id)) SEPARATOR '#') as data_array, r.daily_sr_no, r.lab_test_no FROM invoice_item i INNER JOIN invoice_master m ON m.id = i.inv_master_id INNER JOIN hc_item_type y ON i.item_type = y.itype_id INNER JOIN patient_master p ON p.id = m.attach_id INNER JOIN lab_invoice_request r ON m.id = r.invoice_id AND r.daily_sr_no > 0 WHERE m.payment_status = 1 AND y.itype_id = ? AND m.inv_date >= DATE_ADD(CURDATE(), INTERVAL -3 DAY) GROUP BY m.id ORDER BY m.id DESC LIMIT 50"; $query = $this->db->query($sql, [$labType]); } else { // Search by specific serial number $sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date, CONCAT(p.p_fname, '/', IF(p.gender=1,'M','F'), '/', p.p_rname) as inv_name, p.dob, p.age, p.age_in_month, p.estimate_dob, GROUP_CONCAT(CONCAT_WS(';', i.item_name, i.item_name, i.id, check_item_request(m.id, i.id)) SEPARATOR '#') as data_array, r.daily_sr_no, r.lab_test_no FROM invoice_item i INNER JOIN invoice_master m ON m.id = i.inv_master_id INNER JOIN hc_item_type y ON i.item_type = y.itype_id INNER JOIN patient_master p ON p.id = m.attach_id INNER JOIN lab_invoice_request r ON m.id = r.invoice_id AND r.daily_sr_no > 0 WHERE m.payment_status = 1 AND y.itype_id = ? AND r.daily_sr_no = ? GROUP BY m.id ORDER BY m.id DESC LIMIT 50"; $query = $this->db->query($sql, [$labType, $srNo]); } $results = $query->getResult(); // Calculate age using PHP helper foreach ($results as $row) { $row->age = get_age_1($row->dob ?? null, $row->age ?? '', $row->age_in_month ?? '', $row->estimate_dob ?? ''); } $data = [ 'labreport_preprocess' => $results, 'lab_type' => $labType ]; return view('diagnosis/lab_report_tab', $data); } public function searchLabByLabno($labType = null) { $labType = $labType !== null ? (int) $labType : (int) $this->request->getPost('lab_type'); $labNo = $this->request->getPost('txtsearch_labno'); // Sanitize input $labNo = preg_replace('/[^A-Za-z0-9_ \-]/', '', $labNo); if (trim($labNo) == '') { // Get recent 3 days records with lab number $sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date, CONCAT(p.p_fname, '/', IF(p.gender=1,'M','F'), '/', p.p_rname) as inv_name, p.dob, p.age, p.age_in_month, p.estimate_dob, GROUP_CONCAT(CONCAT_WS(';', i.item_name, i.item_name, i.id, check_item_request(m.id, i.id)) SEPARATOR '#') as data_array, r.daily_sr_no, r.lab_test_no FROM invoice_item i INNER JOIN invoice_master m ON m.id = i.inv_master_id INNER JOIN hc_item_type y ON i.item_type = y.itype_id INNER JOIN patient_master p ON p.id = m.attach_id INNER JOIN lab_invoice_request r ON m.id = r.invoice_id AND r.lab_test_no > 0 WHERE m.payment_status = 1 AND y.itype_id = ? AND m.inv_date >= DATE_ADD(CURDATE(), INTERVAL -3 DAY) GROUP BY m.id ORDER BY m.id DESC LIMIT 50"; $query = $this->db->query($sql, [$labType]); } else { // Search by specific lab number $sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date, CONCAT(p.p_fname, '/', IF(p.gender=1,'M','F'), '/', p.p_rname) as inv_name, p.dob, p.age, p.age_in_month, p.estimate_dob, GROUP_CONCAT(CONCAT_WS(';', i.item_name, i.item_name, i.id, check_item_request(m.id, i.id)) SEPARATOR '#') as data_array, r.daily_sr_no, r.lab_test_no FROM invoice_item i INNER JOIN invoice_master m ON m.id = i.inv_master_id INNER JOIN hc_item_type y ON i.item_type = y.itype_id INNER JOIN patient_master p ON p.id = m.attach_id INNER JOIN lab_invoice_request r ON m.id = r.invoice_id AND r.lab_test_no > 0 WHERE m.payment_status = 1 AND y.itype_id = ? AND r.lab_test_no = ? GROUP BY m.id ORDER BY m.id DESC LIMIT 50"; $query = $this->db->query($sql, [$labType, $labNo]); } $results = $query->getResult(); // Calculate age using PHP helper foreach ($results as $row) { $row->age = get_age_1($row->dob ?? null, $row->age ?? '', $row->age_in_month ?? '', $row->estimate_dob ?? ''); } $data = [ 'labreport_preprocess' => $results, 'lab_type' => $labType ]; return view('diagnosis/lab_report_tab', $data); } public function selectLabInvoicePath($invoiceId, $labType) { // Allow both AJAX and regular GET requests $isAjax = $this->request->isAJAX() || $this->request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest'; // Get invoice and patient information $sql = "SELECT m.id as inv_id, m.invoice_code, m.inv_date, m.attach_id, p.id, p.p_fname, p.p_relative, p.p_rname, p.gender, p.dob, p.age, p.age_in_month, p.estimate_dob, p.p_code, p.udai, p.relation_patient_cardholder, p.p_relative as relative_name, p.mphone1 as phone_number, p.email1 as email, p.add1 as address_line1, p.city, p.state FROM invoice_master m INNER JOIN patient_master p ON p.id = m.attach_id WHERE m.id = ? AND m.payment_status = 1"; $query = $this->db->query($sql, [$invoiceId]); $invoice = $query->getRow(); if (!$invoice) { return view('welcome_message'); } // Calculate age $invoice->age = get_age_1($invoice->dob ?? null, $invoice->age ?? '', $invoice->age_in_month ?? '', $invoice->estimate_dob ?? ''); // Get invoice items (tests) for this invoice $testSql = "SELECT i.id, i.item_name, i.item_id, m.inv_date FROM invoice_item i INNER JOIN invoice_master m ON m.id = i.inv_master_id WHERE i.inv_master_id = ? AND i.item_type = ? ORDER BY i.item_name"; $testQuery = $this->db->query($testSql, [$invoiceId, $labType]); $tests = $testQuery->getResult(); // Get invoice items to correlate with tests $itemSql = "SELECT i.id, i.item_name, i.item_id FROM invoice_item i WHERE i.inv_master_id = ? AND i.item_type = ?"; $itemQuery = $this->db->query($itemSql, [$invoiceId, $labType]); $items = $itemQuery->getResult(); // Get lab invoice request for timing information $labInvoiceSql = "SELECT * FROM lab_invoice_request WHERE invoice_id = ? AND lab_type = ?"; $labInvoiceQuery = $this->db->query($labInvoiceSql, [$invoiceId, $labType]); $labInvoice = $labInvoiceQuery->getRow(); $data = [ 'invoice' => $invoice, 'tests' => $tests ?? [], 'items' => $items ?? [], 'lab_invoice' => $labInvoice, 'lab_type' => $labType, 'lab_type_name' => $this->getLabTypeName($labType), 'lab_type_route' => $this->getLabTypeRoute($labType), 'print_templates' => $this->getDiagnosisPrintTemplates($labType), ]; return view('diagnosis/pathology_detail', $data); } public function labDateShow($invoiceId, $labType) { // AJAX endpoint to return lab date/time information try { log_message('debug', "labDateShow called: invoiceId={$invoiceId}, labType={$labType}"); $isAjax = $this->request->isAJAX() || $this->request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest'; $sql = "SELECT * FROM lab_invoice_request WHERE invoice_id = ? AND lab_type = ?"; $query = $this->db->query($sql, [$invoiceId, $labType]); $labInvoice = $query->getRow(); log_message('debug', "Lab invoice request: " . ($labInvoice ? "found" : "not found")); $data = [ 'lab_invoice_request' => $labInvoice ? [$labInvoice] : [], 'invoiceId' => $invoiceId, 'labType' => $labType ]; $view = view('diagnosis/lab_date_show', $data); log_message('debug', "labDateShow view rendered, length: " . strlen($view)); return $view; } catch (\Throwable $e) { log_message('error', "labDateShow exception: " . $e->getMessage()); return '
' . htmlspecialchars((string) $reportTitle, ENT_QUOTES, 'UTF-8') . ' |
' . htmlspecialchars($reportTitle, ENT_QUOTES, 'UTF-8') . ' |
No report data available for this request. Please use Edit and save report content.
'; } $genderValue = (int) ($row->gender ?? 0); $genderText = $genderValue === 1 ? 'Male' : ($genderValue === 2 ? 'Female' : '-'); $ageText = '-'; if (function_exists('get_age_1')) { $ageText = (string) get_age_1( $row->dob ?? null, $row->age ?? '', $row->age_in_month ?? '', $row->estimate_dob ?? '' ); } elseif (! empty($row->age)) { $ageText = (string) $row->age; } $printSetting = $this->getDiagnosisTemplateSetting((int) ($row->lab_type ?? 0), $templateId); $isPlainPaper = false; // Compute display values here so they are available for token substitution. $patientName = trim((string) ($row->p_fname ?? '')); $relativeName = trim((string) ($row->p_relative ?? '')); $relativeText = trim((string) ($row->p_rname ?? '')); $repoTitle = trim((string) ($row->report_name ?? $row->repo_title ?? $row->RepoGrp ?? 'Report')); $headRow = $this->db->table('diagnosis_head_name') ->where('d_type', (int) ($row->lab_type ?? 0)) ->get(1) ->getRow(); $tokens = $this->buildPdfTokens([ 'invoice_code' => $row->invoice_code ?? '', 'patient_name' => $patientName, 'relative_type' => $relativeName, 'relative_name' => $relativeText, 'age' => $ageText, 'gender' => $genderText, 'uhid' => $row->p_code ?? '', 'collected_time' => $row->collected_time ?? '', 'reported_time' => $row->reported_time ?? '', 'report_title' => $repoTitle, 'doctor_name' => $headRow->doc_name ?? '', 'doctor_education' => $headRow->doc_edu ?? '', 'technician_name' => $headRow->tech_name ?? '', 'signature_image_url' => (string) ($printSetting['signature_image'] ?? ''), ]); // Render the optional patient info template (tokens resolved). $rawPatientInfoHtml = trim((string) ($printSetting['patient_info_html'] ?? '')); if ($rawPatientInfoHtml !== '') { $patientInfoHtml = $this->applyPdfTokens($rawPatientInfoHtml, $tokens); } elseif (($printSetting['id'] ?? 0) <= 0) { $patientInfoHtml = $this->buildDefaultPatientInfoHtml([ 'invoice_code' => $row->invoice_code ?? '', 'patient_name' => $patientName, 'relative_name' => $relativeName, 'relative_text' => $relativeText, 'age_text' => $ageText, 'gender_text' => $genderText, 'uhid' => $row->p_code ?? '', 'collected_time' => $row->collected_time ?? '', 'reported_time' => $row->reported_time ?? '', ]); } else { $patientInfoHtml = ''; } $reportHtml = trim((string) ($printSetting['content_prefix_html'] ?? '')) . $reportHtml . trim((string) ($printSetting['content_suffix_html'] ?? '')); $pdfHtml = view('diagnosis/pdf_single_report', [ 'report' => $row, 'reportHtml' => $reportHtml, 'isPlainPaper' => $isPlainPaper, 'ageText' => $ageText, 'genderText' => $genderText, 'printSetting' => $printSetting, 'patientName' => $patientName, 'relativeName' => $relativeName, 'relativeText' => $relativeText, 'repoTitle' => $repoTitle, 'patientInfoHtml' => $patientInfoHtml, ]); $rawHeaderHtml = trim((string) ($printSetting['header_html'] ?? '')); $rawFirstPageHeaderHtml = trim((string) ($printSetting['first_page_header_html'] ?? '')); $rawFooterHtml = trim((string) ($printSetting['footer_html'] ?? '')); $rawLastPageFooterHtml = trim((string) ($printSetting['last_page_footer_html'] ?? '')); $autoHeaderHtml = $this->buildAutoMpdfHeaderBlock( $rawHeaderHtml, $rawFirstPageHeaderHtml, 'diag_auto_header_single' ); $autoFooterHtml = $this->buildAutoMpdfFooterBlock( $rawFooterHtml, $rawLastPageFooterHtml, 'diag_auto_footer_single' ); $prefixHtml = $this->applyPdfTokens(trim((string) ($printSetting['mpdf_prefix_html'] ?? '')) . $autoHeaderHtml . $autoFooterHtml, $tokens); $suffixHtml = $this->applyPdfTokens(trim((string) ($printSetting['mpdf_suffix_html'] ?? '')), $tokens); $marginTop = $this->cmToMm($printSetting['page_margin_top_cm'] ?? 1.2, 1.2); $marginBottom = $this->cmToMm($printSetting['page_margin_bottom_cm'] ?? 1.2, 1.2); $marginLeft = $this->cmToMm($printSetting['page_margin_left_cm'] ?? 1.0, 1.0); $marginRight = $this->cmToMm($printSetting['page_margin_right_cm'] ?? 1.0, 1.0); $marginHeader = $this->cmToMm($printSetting['margin_header_cm'] ?? 0.5, 0.5); $marginFooter = $this->cmToMm($printSetting['margin_footer_cm'] ?? 1.5, 1.5); $mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf'; if (! is_dir($mpdfTempDir)) { mkdir($mpdfTempDir, 0755, true); } $mpdf = new Mpdf([ 'format' => (string) ($printSetting['page_size'] ?? 'A4'), 'margin_top' => $marginTop, 'margin_bottom' => $marginBottom, 'margin_left' => $marginLeft, 'margin_right' => $marginRight, 'margin_header' => $marginHeader, 'margin_footer' => $marginFooter, 'tempDir' => $mpdfTempDir, 'autoScriptToLang' => false, 'autoLanguageDetection' => false, 'autoArabic' => false, 'autoVietnamese' => false, ]); // For plain HTML (without explicit mPDF tags), set header/footer via API. $headerHasTags = preg_match('/<\s*(htmlpageheader|sethtmlpageheader)\b/i', $rawHeaderHtml . $rawFirstPageHeaderHtml) === 1; $footerHasTags = preg_match('/<\s*(htmlpagefooter|sethtmlpagefooter)\b/i', $rawFooterHtml . $rawLastPageFooterHtml) === 1; if (! $headerHasTags) { $plainHeader = $this->applyPdfTokens($rawHeaderHtml . $rawFirstPageHeaderHtml, $tokens); if ($plainHeader !== '') { $mpdf->SetHTMLHeader($plainHeader, 'O'); $mpdf->SetHTMLHeader($plainHeader, 'E'); } } if (! $footerHasTags) { $plainFooter = $this->applyPdfTokens($rawFooterHtml, $tokens); if ($plainFooter === '') { $plainFooter = $this->applyPdfTokens($rawLastPageFooterHtml, $tokens); } if ($plainFooter !== '') { $mpdf->SetHTMLFooter($plainFooter, 'O'); $mpdf->SetHTMLFooter($plainFooter, 'E'); } } $backgroundImage = trim((string) ($printSetting['page_background_image'] ?? '')); if ($backgroundImage !== '') { $absolutePath = FCPATH . ltrim(str_replace('\\', '/', $backgroundImage), '/'); if (is_file($absolutePath)) { $mpdf->SetWatermarkImage($absolutePath, 1, [210, 297], 'F'); $mpdf->showWatermarkImage = true; } } $watermarkType = (string) ($printSetting['watermark_type'] ?? 'none'); $watermarkAlpha = (float) ($printSetting['watermark_alpha'] ?? 0.12); if ($watermarkType === 'text') { $text = trim((string) ($printSetting['watermark_text'] ?? '')); if ($text !== '') { $mpdf->SetWatermarkText($text, $watermarkAlpha); $mpdf->showWatermarkText = true; } } elseif ($watermarkType === 'image') { $wmImage = trim((string) ($printSetting['watermark_image'] ?? '')); if ($wmImage !== '') { $absolutePath = FCPATH . ltrim(str_replace('\\', '/', $wmImage), '/'); if (is_file($absolutePath)) { $mpdf->SetWatermarkImage($absolutePath, $watermarkAlpha); $mpdf->showWatermarkImage = true; } } } if ($prefixHtml !== '') { $mpdf->WriteHTML($prefixHtml, HTMLParserMode::HTML_HEADER_BUFFER); } $mpdf->WriteHTML($pdfHtml); if ($suffixHtml !== '') { $mpdf->WriteHTML($suffixHtml, HTMLParserMode::HTML_HEADER_BUFFER, false, false); } $invoiceCode = preg_replace('/[^A-Za-z0-9\-_]/', '_', (string) ($row->invoice_code ?? 'invoice')); $fileName = 'Report_' . $invoiceCode . '_' . $labReqId . '.pdf'; return $this->response ->setHeader('Content-Type', 'application/pdf') ->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') ->setHeader('Pragma', 'no-cache') ->setHeader('Expires', '0') ->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"') ->setBody($mpdf->Output($fileName, 'S')); } private function getDiagnosisPrintTemplates(int $labType): array { if ($labType <= 0 || ! $this->db->tableExists('diagnosis_print_templates')) { return []; } return $this->db->table('diagnosis_print_templates') ->select('id, template_name, is_default') ->where('modality', $labType) ->where('status', 1) ->orderBy('is_default', 'DESC') ->orderBy('template_name', 'ASC') ->get() ->getResultArray(); } private function getDiagnosisTemplateSetting(int $labType, int $templateId = 0): array { $legacy = $this->getDiagnosisPrintSetting($labType); $defaults = array_merge($legacy, [ 'page_size' => 'A4', 'page_margin_top_cm' => 6.1, 'id' => 0, 'page_margin_bottom_cm' => 2.5, 'page_margin_left_cm' => 0.7, 'page_margin_right_cm' => 0.7, 'margin_header_cm' => 0.5, 'margin_footer_cm' => 1.5, 'page_background_image' => '', 'watermark_type' => 'none', 'watermark_text' => '', 'watermark_image' => '', 'signature_image' => '', 'watermark_alpha' => 0.12, 'header_html' => '', 'first_page_header_html' => '', 'content_prefix_html' => '', 'content_suffix_html' => '', 'footer_html' => '', 'last_page_footer_html' => '', ]); if ($labType <= 0 || ! $this->db->tableExists('diagnosis_print_templates')) { return $defaults; } $builder = $this->db->table('diagnosis_print_templates') ->where('modality', $labType) ->where('status', 1); if ($templateId > 0) { $builder->where('id', $templateId); } else { $builder->orderBy('is_default', 'DESC')->orderBy('id', 'ASC'); } $row = $builder->get(1)->getRowArray(); if (! is_array($row)) { return $defaults; } foreach ($defaults as $key => $value) { if (array_key_exists($key, $row) && $row[$key] !== null && $row[$key] !== '') { if (is_float($value)) { $defaults[$key] = (float) $row[$key]; } else { $defaults[$key] = (string) $row[$key]; } } } $pageSize = strtoupper(trim((string) ($defaults['page_size'] ?? 'A4'))); $defaults['id'] = (int) ($row['id'] ?? 0); if (! in_array($pageSize, ['A4', 'A4-L', 'LETTER', 'LEGAL'], true)) { $pageSize = 'A4'; } $defaults['page_size'] = $pageSize; return $defaults; } private function getDiagnosisPrintSetting(int $labType): array { $defaults = [ 'letter_margin_top' => 12.0, 'letter_margin_left' => 10.0, 'letter_margin_right' => 10.0, 'letter_margin_bottom' => 12.0, 'plain_header_html' => '', 'plain_background_image' => '', 'mpdf_prefix_html' => '', 'mpdf_suffix_html' => '', 'patient_info_html' => '', ]; if ($labType <= 0 || ! $this->db->tableExists('diagnosis_head_name')) { return $defaults; } $fields = $this->db->getFieldNames('diagnosis_head_name'); $select = ['d_type']; foreach (array_keys($defaults) as $field) { if (in_array($field, $fields, true)) { $select[] = $field; } } $row = $this->db->table('diagnosis_head_name') ->select(implode(',', $select)) ->where('d_type', $labType) ->get(1) ->getRowArray(); if (! is_array($row)) { return $defaults; } foreach ($defaults as $key => $defaultValue) { if (array_key_exists($key, $row) && $row[$key] !== null && $row[$key] !== '') { $defaults[$key] = is_float($defaultValue) ? (float) $row[$key] : (string) $row[$key]; } } return $defaults; } /** * 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 { $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([ $hospitalLogoName !== '' ? 'assets/images/' . ltrim($hospitalLogoName, '/\\') : '', 'assets/img/logo.png', 'assets/images/no_image.svg', ]); $signatureImageUrl = $this->resolvePdfAssetPath([ (string) ($data['signature_image_url'] ?? ''), 'assets/images/drPreetiSingh.jpg', ]); 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), '{{hospital_address_1}}' => $e($hospitalAddress1), '{{hospital_phone}}' => $e($hospitalPhone), '{{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), ]; } private function resolvePdfAssetPath(array $candidates): string { foreach ($candidates as $candidate) { $candidate = trim((string) $candidate); if ($candidate === '') { continue; } if (preg_match('/^https?:\/\//i', $candidate) === 1 || str_starts_with($candidate, 'data:')) { continue; } if (preg_match('/^[A-Za-z]:[\\\\\/]/', $candidate) === 1) { $absolutePath = $candidate; } else { $relativePath = ltrim(str_replace('\\', '/', $candidate), '/'); $absolutePath = str_starts_with($relativePath, 'public/') ? ROOTPATH . $relativePath : FCPATH . $relativePath; } $absolutePath = str_replace('\\', '/', $absolutePath); if (is_file($absolutePath)) { return $absolutePath; } } return ''; } /** * Replace all {{token}} placeholders in $html using the token map returned by buildPdfTokens(). */ private function applyPdfTokens(string $html, array $tokens): string { return str_replace(array_keys($tokens), array_values($tokens), $html); } private function buildDefaultPatientInfoHtml(array $data): string { $e = static function (string $v): string { return htmlspecialchars($v, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }; $ageSex = $e((string) ($data['age_text'] ?? '-')) . ' / ' . $e((string) ($data['gender_text'] ?? '-')); $relative = $e(trim((string) ($data['relative_name'] ?? '') . ' ' . (string) ($data['relative_text'] ?? ''))); return '| '
. 'Invoice ID: ' . $e((string) ($data['invoice_code'] ?? '')) . ' ' . 'Patient: ' . $e((string) ($data['patient_name'] ?? '')) . ' ' . 'Relative: ' . $relative . ' ' . 'Age/Sex: ' . $ageSex . ' | '
. ''
. 'UHID: ' . $e((string) ($data['uhid'] ?? '')) . ' ' . 'Collected: ' . $e((string) ($data['collected_time'] ?? '')) . ' ' . 'Reported: ' . $e((string) ($data['reported_time'] ?? '')) . ' ' . 'Printed: ' . $e(date('d-m-Y h:i A')) . ' | '
. '
| ';
$header .= 'Invoice ID : ' . htmlspecialchars((string) ($invoice->invoice_code ?? ''), ENT_QUOTES, 'UTF-8') . ' '; $header .= 'Patient Name : ' . htmlspecialchars((string) ($patient->p_fname ?? ''), ENT_QUOTES, 'UTF-8') . ' '; $header .= 'UHID : ' . htmlspecialchars((string) ($patient->p_code ?? ''), ENT_QUOTES, 'UTF-8') . ' '; $header .= ' | ';
$header .= '';
$header .= 'Print Date : ' . date('d-m-Y h:i:s A') . ' '; $header .= ' | ';
$header .= '
No compiled report data available for this request.
'; } $printSetting = $this->getDiagnosisTemplateSetting($labType, $templateId); $patientName = trim((string) ($patient->p_fname ?? '')); $relativeName = trim((string) ($patient->p_relative ?? '')); $relativeText = trim((string) ($patient->p_rname ?? '')); $repoTitle = trim((string) ($itemType->group_desc ?? 'Lab Report')); $tokens = $this->buildPdfTokens([ 'invoice_code' => $invoice->invoice_code ?? '', 'patient_name' => $patientName, 'relative_type' => $relativeName, 'relative_name' => $relativeText, 'age' => $ageText, 'gender' => $genderText, 'uhid' => $patient->p_code ?? '', 'collected_time' => $invoiceRequest->collected_time ?? '', 'reported_time' => $invoiceRequest->reported_time ?? '', 'report_title' => $repoTitle, 'doctor_name' => $head->doc_name ?? '', 'doctor_education' => $head->doc_edu ?? '', 'technician_name' => $head->tech_name ?? '', 'signature_image_url' => (string) ($printSetting['signature_image'] ?? ''), ]); $rawPatientInfoHtml = trim((string) ($printSetting['patient_info_html'] ?? '')); if ($rawPatientInfoHtml !== '') { $patientInfoHtml = $this->applyPdfTokens($rawPatientInfoHtml, $tokens); } elseif (($printSetting['id'] ?? 0) <= 0) { $patientInfoHtml = $this->buildDefaultPatientInfoHtml([ 'invoice_code' => $invoice->invoice_code ?? '', 'patient_name' => $patientName, 'relative_name' => $relativeName, 'relative_text' => $relativeText, 'age_text' => $ageText, 'gender_text' => $genderText, 'uhid' => $patient->p_code ?? '', 'collected_time' => $invoiceRequest->collected_time ?? '', 'reported_time' => $invoiceRequest->reported_time ?? '', ]); } else { $patientInfoHtml = ''; } $reportHtml = trim((string) ($printSetting['content_prefix_html'] ?? '')) . $reportHtml . trim((string) ($printSetting['content_suffix_html'] ?? '')); $marginTop = $this->cmToMm($printSetting['page_margin_top_cm'] ?? 1.2, 1.2); $marginBottom = $this->cmToMm($printSetting['page_margin_bottom_cm'] ?? 1.2, 1.2); $marginLeft = $this->cmToMm($printSetting['page_margin_left_cm'] ?? 1.0, 1.0); $marginRight = $this->cmToMm($printSetting['page_margin_right_cm'] ?? 1.0, 1.0); $marginHeader = $this->cmToMm($printSetting['margin_header_cm'] ?? 0.5, 0.5); $marginFooter = $this->cmToMm($printSetting['margin_footer_cm'] ?? 1.5, 1.5); $pdfHtml = view('diagnosis/pdf_compiled_report', [ 'invoiceRequest' => $invoiceRequest, 'invoice' => $invoice, 'patient' => $patient, 'reportHead' => $reportHead, 'reportHtml' => $reportHtml, 'isPlainPaper' => $isPlainPaper, 'ageText' => $ageText, 'genderText' => $genderText, 'printSetting' => $printSetting, 'labType' => $labType, 'invoiceId' => $invoiceId, 'itemType' => $itemType, 'patientName' => $patientName, 'relativeName' => $relativeName, 'relativeText' => $relativeText, 'repoTitle' => $repoTitle, 'patientInfoHtml' => $patientInfoHtml, ]); $rawHeaderHtml = trim((string) ($printSetting['header_html'] ?? '')); $rawFirstPageHeaderHtml = trim((string) ($printSetting['first_page_header_html'] ?? '')); $rawFooterHtml = trim((string) ($printSetting['footer_html'] ?? '')); $rawLastPageFooterHtml = trim((string) ($printSetting['last_page_footer_html'] ?? '')); $autoHeaderHtml = $this->buildAutoMpdfHeaderBlock( $rawHeaderHtml, $rawFirstPageHeaderHtml, 'diag_auto_header_compiled' ); $autoFooterHtml = $this->buildAutoMpdfFooterBlock( $rawFooterHtml, $rawLastPageFooterHtml, 'diag_auto_footer_compiled' ); $prefixHtml = $this->applyPdfTokens(trim((string) ($printSetting['mpdf_prefix_html'] ?? '')) . $autoHeaderHtml . $autoFooterHtml, $tokens); $suffixHtml = $this->applyPdfTokens(trim((string) ($printSetting['mpdf_suffix_html'] ?? '')), $tokens); $mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf'; if (! is_dir($mpdfTempDir)) { mkdir($mpdfTempDir, 0755, true); } $t_start = microtime(true); $mpdf = new Mpdf([ 'format' => (string) ($printSetting['page_size'] ?? 'A4'), 'margin_top' => $marginTop, 'margin_bottom' => $marginBottom, 'margin_left' => $marginLeft, 'margin_right' => $marginRight, 'margin_header' => $marginHeader, 'margin_footer' => $marginFooter, 'tempDir' => $mpdfTempDir, 'autoScriptToLang' => false, 'autoLanguageDetection' => false, 'autoArabic' => false, 'autoVietnamese' => false, ]); $t_mpdf_init = microtime(true); // For plain HTML (without explicit mPDF tags), set header/footer via API. $headerHasTags = preg_match('/<\s*(htmlpageheader|sethtmlpageheader)\b/i', $rawHeaderHtml . $rawFirstPageHeaderHtml) === 1; $footerHasTags = preg_match('/<\s*(htmlpagefooter|sethtmlpagefooter)\b/i', $rawFooterHtml . $rawLastPageFooterHtml) === 1; if (! $headerHasTags) { $plainHeader = $this->applyPdfTokens($rawHeaderHtml . $rawFirstPageHeaderHtml, $tokens); if ($plainHeader !== '') { $mpdf->SetHTMLHeader($plainHeader, 'O'); $mpdf->SetHTMLHeader($plainHeader, 'E'); } } if (! $footerHasTags) { $plainFooter = $this->applyPdfTokens($rawFooterHtml, $tokens); if ($plainFooter === '') { $plainFooter = $this->applyPdfTokens($rawLastPageFooterHtml, $tokens); } if ($plainFooter !== '') { $mpdf->SetHTMLFooter($plainFooter, 'O'); $mpdf->SetHTMLFooter($plainFooter, 'E'); } } $backgroundImage = trim((string) ($printSetting['page_background_image'] ?? '')); if ($backgroundImage !== '') { $absolutePath = FCPATH . ltrim(str_replace('\\', '/', $backgroundImage), '/'); if (is_file($absolutePath)) { $mpdf->SetWatermarkImage($absolutePath, 1, [210, 297], 'F'); $mpdf->showWatermarkImage = true; } } $watermarkType = (string) ($printSetting['watermark_type'] ?? 'none'); $watermarkAlpha = (float) ($printSetting['watermark_alpha'] ?? 0.12); if ($watermarkType === 'text') { $text = trim((string) ($printSetting['watermark_text'] ?? '')); if ($text !== '') { $mpdf->SetWatermarkText($text, $watermarkAlpha); $mpdf->showWatermarkText = true; } } elseif ($watermarkType === 'image') { $wmImage = trim((string) ($printSetting['watermark_image'] ?? '')); if ($wmImage !== '') { $absolutePath = FCPATH . ltrim(str_replace('\\', '/', $wmImage), '/'); if (is_file($absolutePath)) { $mpdf->SetWatermarkImage($absolutePath, $watermarkAlpha); $mpdf->showWatermarkImage = true; } } } if ($prefixHtml !== '') { $mpdf->WriteHTML($prefixHtml, HTMLParserMode::HTML_HEADER_BUFFER); } $t_prefix = microtime(true); $mpdf->WriteHTML($pdfHtml); $t_body = microtime(true); if ($suffixHtml !== '') { $mpdf->WriteHTML($suffixHtml, HTMLParserMode::HTML_HEADER_BUFFER, false, false); } $t_suffix = microtime(true); $invoiceCode = preg_replace('/[^A-Za-z0-9\-_]/', '_', (string) ($invoice->invoice_code ?? 'invoice')); $fileName = 'Compiled_Report_' . $invoiceCode . '_' . $labType . '.pdf'; $pdfOutput = $mpdf->Output($fileName, 'S'); $t_output = microtime(true); log_message('debug', sprintf( '[PDF_TIMING printPdfCreate] invoice=%s lab=%s | new_Mpdf()=%.2fs | WriteHTML(prefix)=%.2fs | WriteHTML(body)=%.2fs | WriteHTML(suffix)=%.2fs | Output()=%.2fs | TOTAL=%.2fs | body_bytes=%d', $invoiceId, $labType, $t_mpdf_init - $t_start, $t_prefix - $t_mpdf_init, $t_body - $t_prefix, $t_suffix - $t_body, $t_output - $t_suffix, $t_output - $t_start, strlen($pdfHtml) )); return $this->response ->setHeader('Content-Type', 'application/pdf') ->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') ->setHeader('Pragma', 'no-cache') ->setHeader('Expires', '0') ->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"') ->setBody($pdfOutput); } public function reportFileList($invoiceId, $labType, $delete = 0) { $invoiceId = (int) $invoiceId; $labType = (int) $labType; $delete = (int) $delete; if ($invoiceId <= 0 || $labType <= 0) { return view('diagnosis/lab_report_file_list', ['lab_file_list' => []]); } $files = []; if ($this->db->tableExists('file_upload_data')) { $files = $this->db->table('file_upload_data') ->where('isdelete', $delete) ->where('charge_id', $invoiceId) ->where('charge_type', $labType) ->orderBy('id', 'DESC') ->get() ->getResult(); } return view('diagnosis/lab_report_file_list', [ 'lab_file_list' => $files, 'invoice_id' => $invoiceId, 'lab_type' => $labType, ]); } public function imagingUploadGallery($invoiceId, $labType, $labReqId = 0) { $invoiceId = (int) $invoiceId; $labType = (int) $labType; $labReqId = (int) $labReqId; if ($invoiceId <= 0 || $labType <= 0 || ! $this->db->tableExists('file_upload_data')) { return view('diagnosis/imaging_upload_gallery', [ 'files' => [], 'study_name' => '', ]); } $studyName = ''; if ($labReqId > 0) { $requestRow = $this->db->table('lab_request') ->select('id, report_name') ->where('id', $labReqId) ->get(1) ->getRowArray(); $studyName = trim((string) ($requestRow['report_name'] ?? '')); } $builder = $this->db->table('file_upload_data') ->where('isdelete', 0) ->where('charge_id', $invoiceId) ->where('charge_type', $labType); $fields = $this->db->getFieldNames('file_upload_data'); if ($labReqId > 0 && in_array('repo_id', $fields, true)) { $builder->where('repo_id', $labReqId); } $rows = $builder->orderBy('id', 'DESC')->get()->getResultArray(); $files = []; foreach ($rows as $row) { $fullPath = trim((string) ($row['full_path'] ?? '')); $url = $this->buildUploadPublicUrl($fullPath); $mime = trim((string) ($row['file_type'] ?? '')); $ext = strtolower(pathinfo((string) ($row['file_name'] ?? ''), PATHINFO_EXTENSION)); $isImage = (int) ($row['is_image'] ?? 0) === 1 || str_starts_with($mime, 'image/'); $isPdf = $ext === 'pdf' || $mime === 'application/pdf'; $isDicom = $this->isDicomMimeOrExtension($mime, $ext); $files[] = [ 'id' => (int) ($row['id'] ?? 0), 'name' => (string) ($row['orig_name'] ?? $row['file_name'] ?? 'Uploaded file'), 'desc' => (string) ($row['file_desc'] ?? ''), 'url' => $url, 'is_image' => $isImage, 'is_pdf' => $isPdf, 'is_dicom' => $isDicom, 'dicom_preview_url' => $isDicom ? base_url('diagnosis/imaging-dicom-preview/' . (int) ($row['id'] ?? 0)) : '', 'insert_time' => (string) ($row['insert_time'] ?? $row['insert_date'] ?? ''), 'ai_status' => (string) ($row['ai_status'] ?? ''), 'ai_alert_text' => (string) ($row['ai_alert_text'] ?? ''), 'scan_type' => (string) ($row['scan_type'] ?? ''), ]; } return view('diagnosis/imaging_upload_gallery', [ 'files' => $files, 'study_name' => $studyName, ]); } public function imagingDicomPreview($fileId) { $fileId = (int) $fileId; if ($fileId <= 0 || ! $this->db->tableExists('file_upload_data')) { return $this->response->setStatusCode(404)->setBody('DICOM file not found'); } $fileRow = $this->db->table('file_upload_data') ->where('id', $fileId) ->where('isdelete', 0) ->get(1) ->getRowArray(); if (empty($fileRow)) { return $this->response->setStatusCode(404)->setBody('DICOM file not found'); } $absolutePath = $this->resolveUploadAbsolutePath((string) ($fileRow['full_path'] ?? '')); if ($absolutePath === '' || ! is_file($absolutePath)) { return $this->response->setStatusCode(404)->setBody('DICOM file missing on server'); } $mime = $this->detectMimeType($absolutePath); $ext = strtolower(pathinfo($absolutePath, PATHINFO_EXTENSION)); if (! $this->isDicomMimeOrExtension($mime, $ext)) { return $this->response->setStatusCode(415)->setBody('File is not a DICOM'); } $preview = $this->callAiServerDicomPreview($absolutePath, $mime); if (($preview['error'] ?? '') !== '') { return $this->response->setStatusCode(422)->setBody((string) $preview['error']); } $bytes = (string) ($preview['bytes'] ?? ''); if ($bytes === '') { return $this->response->setStatusCode(422)->setBody('Unable to render DICOM preview'); } return $this->response ->setHeader('Content-Type', (string) ($preview['mime'] ?? 'image/jpeg')) ->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate') ->setBody($bytes); } public function uploadReportFile() { if (! $this->request->isAJAX()) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } if (! $this->db->tableExists('file_upload_data')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'file_upload_data table not found']); } $invoiceId = (int) $this->request->getPost('invoice_id'); $labType = (int) $this->request->getPost('lab_type'); $reqId = (int) $this->request->getPost('req_id'); $fileDesc = trim((string) $this->request->getPost('file_desc')); $scanType = trim((string) $this->request->getPost('scan_type')); if ($invoiceId <= 0 || $labType <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Missing invoice/lab context']); } $upload = $this->request->getFile('report_file'); if (! $upload || ! $upload->isValid()) { return $this->response->setJSON(['update' => 0, 'error_text' => 'No valid file uploaded']); } $ext = strtolower((string) $upload->getExtension()); $allowed = ['jpg', 'jpeg', 'png', 'webp', 'bmp', 'gif', 'pdf', 'dcm', 'dicom']; if (! in_array($ext, $allowed, true)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unsupported file type. Allowed: image, PDF, DICOM (.dcm)']); } $targetDir = ROOTPATH . 'public' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . 'diagnosis' . DIRECTORY_SEPARATOR . date('Y') . DIRECTORY_SEPARATOR . date('m'); if (! is_dir($targetDir) && ! @mkdir($targetDir, 0775, true) && ! is_dir($targetDir)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create upload directory']); } $storedName = 'diag_' . date('Ymd_His') . '_' . substr(md5((string) mt_rand()), 0, 8) . '.' . $ext; try { $upload->move($targetDir, $storedName, true); } catch (\Throwable $e) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Upload failed: ' . $e->getMessage()]); } $absolutePath = $targetDir . DIRECTORY_SEPARATOR . $storedName; $relativePath = 'uploads/diagnosis/' . date('Y') . '/' . date('m') . '/' . $storedName; $publicUrl = base_url($relativePath); $user = function_exists('auth') ? auth()->user() : null; $uploadBy = trim((string) ($user->username ?? $user->email ?? 'system')); $uploadById = (int) ($user->id ?? 0); $fields = $this->db->getFieldNames('file_upload_data'); $insert = []; $fieldMap = [ 'name' => $storedName, 'file_name' => $storedName, 'orig_name' => (string) $upload->getClientName(), 'client_name' => (string) $upload->getClientName(), 'file_ext' => '.' . $ext, 'file_type' => (string) $upload->getClientMimeType(), 'full_path' => str_replace('\\', '/', $absolutePath), 'file_path' => str_replace('\\', '/', $targetDir) . '/', 'raw_name' => pathinfo($storedName, PATHINFO_FILENAME), 'store_file_name' => $storedName, 'file_size' => round(((float) $upload->getSize()) / 1024, 2), 'is_image' => in_array($ext, ['jpg', 'jpeg', 'png', 'webp', 'bmp', 'gif'], true) ? 1 : 0, 'image_type' => $ext, 'insert_date' => Time::now('Asia/Kolkata')->toDateTimeString(), 'insert_time' => Time::now('Asia/Kolkata')->toDateTimeString(), 'file_desc' => $fileDesc !== '' ? $fileDesc : 'Diagnosis Report Upload', 'repo_id' => $reqId, 'charge_id' => $invoiceId, 'charge_type' => $labType, 'upload_by' => $uploadBy, 'upload_by_id' => $uploadById, 'scan_type' => $scanType !== '' ? $scanType : 'upload', 'show_type' => 0, 'isdelete' => 0, 'document_type' => 'diagnosis-report', 'content_description' => 'Uploaded from diagnosis editor', 'ai_status' => 'pending', 'ai_alert_flag' => 0, 'ai_alert_text' => null, ]; foreach ($fieldMap as $key => $value) { if (in_array($key, $fields, true)) { $insert[$key] = $value; } } $ok = $this->db->table('file_upload_data')->insert($insert); if (! $ok) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to save upload metadata']); } return $this->response->setJSON([ 'update' => 1, 'error_text' => 'File uploaded successfully', 'file_id' => (int) $this->db->insertID(), 'file_name' => $storedName, 'file_url' => $publicUrl, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function aiExtractLabReportValues() { if (! $this->request->isAJAX()) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } if (! $this->db->tableExists('file_upload_data')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'file_upload_data table not found']); } if (! $this->db->tableExists('lab_ai_extraction_batches') || ! $this->db->tableExists('lab_ai_extraction_values')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'AI extraction tables not found. Run migrations first.']); } $fileId = (int) $this->request->getPost('file_upload_id'); $invoiceId = (int) $this->request->getPost('invoice_id'); $labType = (int) $this->request->getPost('lab_type'); $panelName = trim((string) $this->request->getPost('panel_name')); if ($fileId <= 0 || $invoiceId <= 0 || $labType <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Required fields missing']); } $fileRow = $this->db->table('file_upload_data') ->where('id', $fileId) ->where('charge_id', $invoiceId) ->where('charge_type', $labType) ->get(1) ->getRowArray(); if (empty($fileRow)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Report file not found']); } $fullPath = trim((string) ($fileRow['full_path'] ?? '')); $absolutePath = $this->resolveUploadAbsolutePath($fullPath); if ($absolutePath === '' || ! is_file($absolutePath)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'File not accessible on server']); } $ocrText = $this->extractTextFromLabFile($absolutePath); if ($ocrText === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'OCR failed. Configure DIAGNOSIS_AI_OCR_ENDPOINT on your AI server.']); } $parsed = $this->parseLabValuesWithAi($ocrText, $panelName); if (empty($parsed['values']) || ! is_array($parsed['values'])) { $errorText = trim((string) ($parsed['error'] ?? '')); if ($errorText === '') { $errorText = 'AI parsing failed. Check AI endpoint/settings and try retry.'; } return $this->response->setJSON(['update' => 0, 'error_text' => $errorText]); } $modelName = trim((string) ($parsed['model'] ?? '')); $rawResponse = (string) json_encode($parsed['raw'] ?? [], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $batchId = $this->insertLabAiBatch( $invoiceId, $labType, $fileId, $panelName, $modelName, $ocrText, $rawResponse, (string) ($parsed['provider'] ?? 'ai-server') ); if ($batchId <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to store AI extraction batch']); } $storedCount = 0; foreach ($parsed['values'] as $item) { if (! is_array($item)) { continue; } $testName = trim((string) ($item['test_name'] ?? '')); if ($testName === '') { continue; } $ok = $this->db->table('lab_ai_extraction_values')->insert([ 'batch_id' => $batchId, 'test_name' => $testName, 'test_value' => trim((string) ($item['value'] ?? '')), 'unit' => trim((string) ($item['unit'] ?? '')), 'reference_range' => trim((string) ($item['reference_range'] ?? '')), 'abnormal_flag' => trim((string) ($item['abnormal_flag'] ?? '')), 'raw_line' => trim((string) ($item['raw_line'] ?? '')), 'created_at' => Time::now('Asia/Kolkata')->toDateTimeString(), 'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(), ]); if ($ok) { $storedCount++; } } $this->auditClinicalUpdate('lab_ai_extraction_batches', 'created', $batchId, null, [ 'invoice_id' => $invoiceId, 'lab_type' => $labType, 'file_upload_id' => $fileId, 'stored_values' => $storedCount, ]); return $this->response->setJSON([ 'update' => 1, 'error_text' => 'AI extracted ' . $storedCount . ' values. Doctor verification pending.', 'ai_provider' => (string) ($parsed['provider'] ?? 'ai-server'), 'ai_model' => $modelName, 'batch_id' => $batchId, 'stored_values' => $storedCount, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function aiExtractedValues($invoiceId, $labType) { $invoiceId = (int) $invoiceId; $labType = (int) $labType; if ($invoiceId <= 0 || $labType <= 0 || ! $this->db->tableExists('lab_ai_extraction_batches')) { return view('diagnosis/lab_ai_extracted_values', [ 'batch' => null, 'values' => [], ]); } $batch = $this->db->table('lab_ai_extraction_batches') ->where('invoice_id', $invoiceId) ->where('lab_type', $labType) ->orderBy('id', 'DESC') ->get(1) ->getRowArray(); $values = []; if (! empty($batch['id']) && $this->db->tableExists('lab_ai_extraction_values')) { $values = $this->db->table('lab_ai_extraction_values') ->where('batch_id', (int) $batch['id']) ->orderBy('id', 'ASC') ->get() ->getResultArray(); } return view('diagnosis/lab_ai_extracted_values', [ 'batch' => $batch, 'values' => $values, ]); } public function aiVerifyExtractedValues($batchId) { if (! $this->request->isAJAX()) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $batchId = (int) $batchId; if ($batchId <= 0 || ! $this->db->tableExists('lab_ai_extraction_batches')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid batch id']); } $user = function_exists('auth') ? auth()->user() : null; $verifiedBy = (string) ($user->id ?? 'system'); $updated = $this->db->table('lab_ai_extraction_batches') ->where('id', $batchId) ->update([ 'doctor_verified' => 1, 'verified_by' => $verifiedBy, 'verified_at' => Time::now('Asia/Kolkata')->toDateTimeString(), 'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(), ]); if (! $updated) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to verify batch']); } $this->auditClinicalUpdate('lab_ai_extraction_batches', 'doctor_verified', $batchId, 0, 1, $verifiedBy); return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Marked as doctor-verified', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function imagingAiDiagnosis($labReqId) { if (! $this->request->isAJAX()) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $labReqId = (int) $labReqId; if ($labReqId <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid request id']); } $labRequest = $this->db->table('lab_request') ->where('id', $labReqId) ->get(1) ->getRowArray(); if (empty($labRequest)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Imaging request not found']); } $labType = (int) ($labRequest['lab_type'] ?? 0); if (! $this->isRadiologyType($labType)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'AI diagnosis is available for imaging only']); } if (! $this->db->tableExists('file_upload_data')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'file_upload_data table not found']); } if (! $this->db->tableExists('lab_ai_extraction_batches')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'AI batch table not found. Run migrations first.']); } $invoiceId = (int) ($labRequest['charge_id'] ?? 0); $studyName = trim((string) ($labRequest['report_name'] ?? 'Imaging Study')); $files = $this->findImagingFilesForRequest($invoiceId, $labType, $labReqId); if (empty($files)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'No uploaded image/PDF found for this study']); } $fileIds = array_values(array_filter(array_map(static fn($row) => (int) ($row['id'] ?? 0), $files))); $this->updateImagingFileAiState($fileIds, 'processing', 'AI diagnosis is running'); $ocrChunks = []; $visionImages = []; $sourceFiles = []; foreach ($files as $fileRow) { $absolutePath = $this->resolveUploadAbsolutePath((string) ($fileRow['full_path'] ?? '')); if ($absolutePath === '' || ! is_file($absolutePath)) { continue; } $fileName = (string) ($fileRow['orig_name'] ?? $fileRow['file_name'] ?? basename($absolutePath)); $mime = $this->detectMimeType($absolutePath); $sourceFiles[] = [ 'id' => (int) ($fileRow['id'] ?? 0), 'name' => $fileName, 'scan_type' => (string) ($fileRow['scan_type'] ?? ''), ]; $ocrText = trim($this->extractTextFromLabFile($absolutePath)); if ($ocrText !== '') { $ocrChunks[] = 'FILE: ' . $fileName . "\n" . $ocrText; } $ext = strtolower(pathinfo($absolutePath, PATHINFO_EXTENSION)); if ((str_starts_with($mime, 'image/') || $this->isDicomMimeOrExtension($mime, $ext)) && count($visionImages) < 4) { $visionImages[] = [ 'abs_path' => $absolutePath, 'mime' => $mime, ]; } } $patientName = trim((string) ($labRequest['patient_name'] ?? 'Unknown')); $patientAge = 0; $patientGender = 'Unknown'; $patientId = (int) ($labRequest['patient_id'] ?? 0); if ($patientId > 0 && $this->db->tableExists('patient_master')) { $patient = $this->db->table('patient_master') ->select('gender, age, age_in_month, dob') ->where('id', $patientId) ->get(1) ->getRowArray(); $genderRaw = (int) ($patient['gender'] ?? 0); $patientGender = $genderRaw === 1 ? 'Male' : ($genderRaw === 2 ? 'Female' : 'Unknown'); $patientAge = (int) ($patient['age'] ?? 0); if ($patientAge <= 0) { $months = (int) ($patient['age_in_month'] ?? 0); if ($months > 0) { $patientAge = max(1, (int) floor($months / 12)); } } if ($patientAge <= 0) { $dob = trim((string) ($patient['dob'] ?? '')); if ($dob !== '' && $dob !== '0000-00-00') { try { $patientAge = (int) ((new \DateTime($dob))->diff(new \DateTime('today'))->y ?? 0); } catch (\Throwable $e) { $patientAge = 0; } } } } $analysis = $this->generateImagingDiagnosisDraft($studyName, $ocrChunks, $visionImages, $patientName, $patientAge, $patientGender); if (($analysis['error'] ?? '') !== '') { $this->updateImagingFileAiState($fileIds, 'failed', (string) $analysis['error'], 1); return $this->response->setJSON(['update' => 0, 'error_text' => (string) $analysis['error']]); } $promptUsed = $this->buildImagingDiagnosisPrompt($studyName); $rawPayload = [ 'type' => 'imaging-ai-diagnosis', 'request_id' => $labReqId, 'invoice_id' => $invoiceId, 'lab_type' => $labType, 'study_name' => $studyName, 'title' => (string) ($analysis['title'] ?? $studyName), 'findings_html' => (string) ($analysis['findings_html'] ?? ''), 'impression_html' => (string) ($analysis['impression_html'] ?? ''), 'summary_text' => (string) ($analysis['summary_text'] ?? ''), 'provider' => (string) ($analysis['provider'] ?? 'ai-server'), 'model' => (string) ($analysis['model'] ?? ''), 'prompt_used' => $promptUsed, 'source_files' => $sourceFiles, 'raw' => $analysis['raw'] ?? [], ]; $ocrText = trim(implode("\n\n", $ocrChunks)); $batchId = $this->insertLabAiBatch( $invoiceId, $labType, (int) ($fileIds[0] ?? 0), $studyName, (string) ($analysis['model'] ?? ''), $ocrText, (string) json_encode($rawPayload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), (string) ($analysis['provider'] ?? 'ai-server') ); if ($batchId <= 0) { $this->updateImagingFileAiState($fileIds, 'failed', 'Unable to store AI diagnosis result', 1); return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to store AI diagnosis result']); } $this->updateImagingFileAiState($fileIds, 'completed', (string) ($analysis['summary_text'] ?? 'AI draft ready')); $this->auditClinicalUpdate('lab_ai_extraction_batches', 'created', $batchId, null, [ 'invoice_id' => $invoiceId, 'lab_type' => $labType, 'request_id' => $labReqId, 'source_file_ids' => $fileIds, 'mode' => 'imaging-ai-diagnosis', ]); $html = view('diagnosis/imaging_ai_result_modal', [ 'batch_id' => $batchId, 'title' => (string) ($analysis['title'] ?? $studyName), 'study_name' => $studyName, 'findings_html' => (string) ($analysis['findings_html'] ?? ''), 'impression_html' => (string) ($analysis['impression_html'] ?? ''), 'summary_text' => (string) ($analysis['summary_text'] ?? ''), 'provider' => (string) ($analysis['provider'] ?? 'ai-server'), 'model' => (string) ($analysis['model'] ?? ''), ]); return $this->response->setJSON([ 'update' => 1, 'error_text' => 'AI diagnosis draft is ready and stored.', 'batch_id' => $batchId, 'html' => $html, 'findings_html' => (string) ($analysis['findings_html'] ?? ''), 'impression_html' => (string) ($analysis['impression_html'] ?? ''), 'summary_text' => (string) ($analysis['summary_text'] ?? ''), 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } private function insertLabAiBatch( int $invoiceId, int $labType, int $fileId, string $panelName, string $modelName, string $ocrText, string $rawResponse, string $provider = 'ai-server' ): int { $ok = $this->db->table('lab_ai_extraction_batches')->insert([ 'invoice_id' => $invoiceId, 'lab_type' => $labType, 'file_upload_id' => $fileId, 'panel_name' => $panelName, 'ai_provider' => $provider, 'model_name' => $modelName, 'ocr_text' => $ocrText, 'raw_response_json' => $rawResponse, 'status' => 'completed', 'doctor_verified' => 0, 'created_at' => Time::now('Asia/Kolkata')->toDateTimeString(), 'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(), ]); if (! $ok) { return 0; } return (int) $this->db->insertID(); } private function resolveUploadAbsolutePath(string $fullPath): string { $fullPath = trim($fullPath); if ($fullPath === '') { return ''; } if (is_file($fullPath)) { return $fullPath; } $normalized = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $fullPath); if (is_file($normalized)) { return $normalized; } $fileName = basename($normalized); $candidates = [ ROOTPATH . 'public' . DIRECTORY_SEPARATOR . 'uploads' . DIRECTORY_SEPARATOR . $fileName, WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . $fileName, ]; foreach ($candidates as $candidate) { if (is_file($candidate)) { return $candidate; } } return ''; } /** * @return arrayDetected labels: ' . esc(implode(', ', $safeLabels)) . '
'; } } $ocrHtml = ''; if ($ocrText !== []) { $ocrFirst = trim((string) ($ocrText[0] ?? '')); if ($ocrFirst !== '') { $ocrHtml = 'OCR notes: ' . nl2br(esc(mb_substr($ocrFirst, 0, 1500))) . '
'; } } $findingsHtml = 'Technique: ' . esc($technique) . '
'; if ($narrative !== '') { $paragraphs = preg_split('/\R{2,}|\n/u', $narrative) ?: []; $paragraphs = array_values(array_filter(array_map(static fn($item) => trim((string) $item), $paragraphs), static fn($item) => $item !== '')); if ($paragraphs === []) { $findingsHtml .= '' . nl2br(esc($narrative)) . '
'; } else { foreach ($paragraphs as $paragraph) { $findingsHtml .= '' . esc($paragraph) . '
'; } } } else { $findingsHtml .= $labelHtml . $ocrHtml; } return [ 'provider' => (string) ($response['provider'] ?? 'ai-server'), 'model' => (string) ($response['model'] ?? 'ai-server-diagnosis'), 'title' => trim((string) ($studyName !== '' ? $studyName : ($technique !== '' ? $technique : 'Imaging Study'))), 'findings_html' => $findingsHtml, 'impression_html' => '' . esc($impression) . '
', 'summary_text' => mb_substr($impression !== '' ? $impression : 'AI diagnosis draft prepared.', 0, 220), 'raw' => $response['raw'] ?? [], 'error' => '', ]; } /** * @param mixed $value */ private function normalizeAiTextValue($value, string $fallback = ''): string { if (is_string($value)) { $text = trim($value); return $text !== '' ? $text : trim($fallback); } if (is_scalar($value)) { $text = trim((string) $value); return $text !== '' ? $text : trim($fallback); } if (is_array($value)) { $chunks = []; array_walk_recursive($value, static function ($item) use (&$chunks): void { if (is_scalar($item)) { $text = trim((string) $item); if ($text !== '') { $chunks[] = $text; } } }); if ($chunks !== []) { return trim(implode(' ', array_slice($chunks, 0, 8))); } } return trim($fallback); } /** * @param mixed $value * @return array