*/ private array $scanAiDebug = []; private function canAccessDoctorWorkPanel(): bool { $user = auth()->user(); if (! $user) { return false; } if (method_exists($user, 'can') && $user->can('opd.doctor-panel.access')) { return true; } return false; } private function canManageOpdPrintTemplates(): bool { $user = auth()->user(); if (! $user) { return false; } if (method_exists($user, 'can') && $user->can('billing.opd.edit')) { return true; } if (method_exists($user, 'inGroup') && $user->inGroup('OPDEdit')) { return true; } return false; } public function print_template_builder() { if (! $this->canManageOpdPrintTemplates()) { return $this->response->setStatusCode(403)->setBody('Access denied: OPD print template builder is restricted.'); } $mode = strtolower(trim((string) $this->request->getGet('mode'))); if (!in_array($mode, ['list', 'edit'], true)) { $mode = 'list'; } $templateNames = $this->collectOpdTemplateNames(); $name = strtolower(trim((string) $this->request->getGet('name'))); $name = preg_replace('/[^a-z0-9_\-]/', '', $name) ?? ''; if ($mode === 'list') { return view('billing/opd_template_builder_list', [ 'template_names' => $templateNames, ]); } if ($name === '') { $name = !empty($templateNames) ? (string) $templateNames[0] : 'default'; } if (!in_array($name, $templateNames, true)) { $templateNames[] = $name; $templateNames = array_values(array_unique($templateNames)); sort($templateNames); } $content = $this->readOpdTemplateContentByName($name); $placeholders = [ 'hospital_name', 'hospital_address', 'hospital_phone', 'hospital_email', 'doctor_name', 'print_time', 'pName', 'pRelative', 'age_sex', 'phoneno', 'p_address', 'uhid_no', 'opd_sr_no', 'opd_no', 'opd_date', 'exp_date', 'SpecName', 'opd_fee_desc', 'total_no_visit', 'last_opdvisit_date', 'str_opd_book_date', 'Complaint', 'diagnosis', 'Provisional_diagnosis', 'Finding_Examinations', 'medical', 'investigation', 'Prescriber_Remarks', 'advice', 'next_visit', 'refer_to', 'vital_content', 'painscale_img', 'hospital_section', 'patient_section', 'content_section', ]; return view('billing/opd_template_builder', [ 'selected_name' => $name, 'template_content' => $content, 'template_names' => $templateNames, 'placeholders' => $placeholders, ]); } /** * @return array */ private function collectOpdTemplateNames(): array { $names = []; $dir = APPPATH . 'Views/billing/opd_templates'; if (is_dir($dir)) { $htmlFiles = glob($dir . '/*.html') ?: []; foreach ($htmlFiles as $file) { $key = basename((string) $file, '.html'); if ($key !== '') { $names[] = strtolower($key); } } $phpFiles = glob($dir . '/*.php') ?: []; foreach ($phpFiles as $file) { $key = basename((string) $file, '.php'); if ($key !== '') { $names[] = strtolower($key); } } } if ($this->db->tableExists('doctor_master')) { $doctorFields = $this->db->getFieldNames('doctor_master'); $templateFields = array_values(array_intersect( ['opd_print_format', 'opd_blank_print', 'rx_pre_print_letter_head_format', 'rx_blank_letter_head', 'rx_plain_paper'], $doctorFields )); foreach ($templateFields as $field) { $rows = $this->db->table('doctor_master') ->select($field . ' as template_name') ->where($field . ' IS NOT NULL', null, false) ->where($field . ' !=', '') ->distinct() ->get() ->getResultArray(); foreach ($rows as $row) { $templateName = strtolower(trim((string) ($row['template_name'] ?? ''))); $templateName = preg_replace('/[^a-z0-9_\-]/', '', $templateName) ?? ''; if ($templateName !== '') { $names[] = $templateName; } } } } $names = array_values(array_unique($names)); sort($names); return $names; } private function readOpdTemplateContentByName(string $name): string { $name = strtolower(trim($name)); $name = preg_replace('/[^a-z0-9_\-]/', '', $name) ?? ''; if ($name === '') { return ''; } $htmlFile = APPPATH . 'Views/billing/opd_templates/' . $name . '.html'; if (is_file($htmlFile)) { return (string) file_get_contents($htmlFile); } $newPhpFile = APPPATH . 'Views/billing/opd_templates/' . $name . '.php'; if (is_file($newPhpFile)) { return $this->convertLegacyTemplateSourceToBuilderHtml((string) file_get_contents($newPhpFile)); } $legacyPhpFile = ROOTPATH . 'old_code_ci3/views/dashboard/' . $name . '.php'; if (is_file($legacyPhpFile)) { return $this->convertLegacyTemplateSourceToBuilderHtml((string) file_get_contents($legacyPhpFile)); } return ''; } public function print_template_save() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } if (! $this->canManageOpdPrintTemplates()) { return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'Access denied']); } $allowedSections = ['full', 'layout', 'hospital', 'patient', 'content']; $section = strtolower(trim((string) $this->request->getPost('section'))); if (!in_array($section, $allowedSections, true)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid section']); } $name = strtolower(trim((string) $this->request->getPost('name'))); $name = preg_replace('/[^a-z0-9_\-]/', '', $name) ?? ''; if ($name === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Template name required']); } $content = (string) $this->request->getPost('content'); if (trim($content) === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Template content required']); } $dir = $this->resolveTemplateSectionDirectory($section); if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create template directory']); } $file = $dir . '/' . $name . '.html'; $bytes = @file_put_contents($file, $content); if ($bytes === false) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to save template']); } return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Template saved', 'section' => $section, 'name' => $name, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } private function resolveTemplateSectionDirectory(string $section): string { $section = strtolower(trim($section)); if ($section === 'full') { return APPPATH . 'Views/billing/opd_templates'; } return APPPATH . 'Views/billing/opd_parts/' . $section; } private function resetScanAiDebug(): void { $this->scanAiDebug = []; } private function addScanAiDebug(string $message): void { $message = trim($message); if ($message === '') { return; } $this->scanAiDebug[] = $message; } private function formatScanAiDebug(): string { if (empty($this->scanAiDebug)) { return ''; } return implode(' | ', array_slice($this->scanAiDebug, 0, 6)); } private function normalizeAzureEndpointBase(string $endpoint): string { $endpoint = trim($endpoint); if ($endpoint === '') { return ''; } if (! str_starts_with(strtolower($endpoint), 'http://') && ! str_starts_with(strtolower($endpoint), 'https://')) { $endpoint = 'https://' . ltrim($endpoint, '/'); } $parts = parse_url($endpoint); if (! is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) { return rtrim($endpoint, '/'); } $base = strtolower((string) $parts['scheme']) . '://' . (string) $parts['host']; if (! empty($parts['port'])) { $base .= ':' . (int) $parts['port']; } return rtrim($base, '/'); } private function shouldAllowInsecureSslForEndpoint(string $endpoint): bool { $toggle = strtolower($this->readScanAiSettingValue( ['AZURE_ALLOW_INSECURE_SSL', 'AI_AZURE_ALLOW_INSECURE_SSL', 'APP_AZURE_ALLOW_INSECURE_SSL', 'H_AZURE_ALLOW_INSECURE_SSL'], ['AZURE_ALLOW_INSECURE_SSL'] )); if (in_array($toggle, ['1', 'true', 'yes', 'on'], true)) { return true; } $parts = parse_url($endpoint); $host = strtolower((string) ($parts['host'] ?? '')); if ($host === '') { return false; } if (in_array($host, ['localhost', '127.0.0.1', '::1'], true)) { return true; } if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { if (preg_match('/^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/', $host) === 1) { return true; } } if (str_ends_with($host, '.local')) { return true; } return false; } private function isSslCertificateError(\Throwable $e): bool { $msg = strtolower($e->getMessage()); return str_contains($msg, 'ssl certificate problem') || str_contains($msg, 'unable to get local issuer certificate') || str_contains($msg, 'curl error 60') || str_contains($msg, 'error 60'); } /** * @return array */ private function buildAzureCurlOptions(string $endpoint, int $timeout, bool $forceInsecure = false): array { $options = [ 'timeout' => $timeout, 'http_errors' => false, ]; if ($forceInsecure || $this->shouldAllowInsecureSslForEndpoint($endpoint)) { $options['verify'] = false; $options['curl'] = [ CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => 0, ]; $this->addScanAiDebug($forceInsecure ? 'ssl verify disabled after SSL error retry' : 'ssl verify disabled for intranet/local endpoint'); } return $options; } public function appointment(string $opdDate = '') { return $this->get_appointment_data($opdDate); } /** * Load OPD doctor dashboard for the selected date. */ public function get_appointment_data(string $opdDate = '') { if (! $this->canAccessDoctorWorkPanel()) { return $this->response->setStatusCode(403)->setBody('Access denied: OPD doctor work panel is restricted.'); } if ($opdDate === '') { $opdDate = date('Y-m-d'); } $opdDateSql = $this->db->escape($opdDate); $sql = "select d.id as doc_id, d.p_fname, group_concat(DISTINCT m.SpecName) as Spec, count(DISTINCT o.opd_id) as No_opd, count(DISTINCT case when o.opd_status=1 and p.id is null then o.opd_id end) as count_booking, count(DISTINCT case when o.opd_status=1 and p.id is not null then o.opd_id end) as count_wait, count(DISTINCT case when o.opd_status=2 then o.opd_id end) as count_visit, count(DISTINCT case when o.opd_status=3 then o.opd_id end) as count_cancel, MOD(d.id,5) as color_code from (doctor_master d join doc_spec s on d.id=s.doc_id join med_spec m on m.id=s.med_spec_id) left join opd_master o on d.id=o.doc_id and date(o.apointment_date)=" . $opdDateSql . " left join opd_prescription p on o.opd_id=p.opd_id where d.active=1 group by d.id having count(DISTINCT o.opd_id)>0 order by d.p_fname"; $query = $this->db->query($sql); $docMaster = $query->getResult(); $colorMap = [ '0' => 'bg-warning', '1' => 'bg-danger', '2' => 'bg-success', '3' => 'bg-secondary', '4' => 'bg-info', '5' => 'bg-primary', ]; return view('billing/opd_appointment_dashboard', [ 'opd_date' => $opdDate, 'doc_master' => $docMaster, 'color1' => $colorMap, ]); } /** * Load doctor-specific OPD queues split into booked, waiting, visited, and cancelled. */ public function get_appointment_list(int $docId, string $opdDate = '') { if (! $this->canAccessDoctorWorkPanel()) { return $this->response->setStatusCode(403)->setBody('Access denied: OPD doctor work panel is restricted.'); } if ($opdDate === '') { $opdDate = date('Y-m-d'); } $docId = (int) $docId; $opdDateSql = $this->db->escape($opdDate); $sql = "select d.id as doc_id, d.p_fname, group_concat(DISTINCT m.SpecName) as Spec, count(DISTINCT o.opd_id) as No_opd, count(DISTINCT case when o.opd_status=1 and p.id is null then o.opd_id end) as count_booking, count(DISTINCT case when o.opd_status=1 and p.id is not null then o.opd_id end) as count_wait, count(DISTINCT case when o.opd_status=2 then o.opd_id end) as count_visit, count(DISTINCT case when o.opd_status=3 then o.opd_id end) as count_cancel from (doctor_master d join doc_spec s on d.id=s.doc_id join med_spec m on m.id=s.med_spec_id) left join opd_master o on d.id=o.doc_id and date(o.apointment_date)=" . $opdDateSql . " left join opd_prescription p on o.opd_id=p.opd_id where d.id=" . $docId . " group by d.id"; $query = $this->db->query($sql); $docMaster = $query->getResult(); if (empty($docMaster)) { return $this->response->setStatusCode(404)->setBody('Doctor not found for selected date.'); } // Booked: OPD records without an initialized prescription row. $sql = "select o.opd_id,o.opd_code,o.opd_no,p.id,p.p_code,p.p_fname as P_name,p.p_rname, o.opd_fee_desc,o.opd_fee_amount,o.payment_status, if(o.payment_mode=4,'Credit to ECHS',if(o.payment_mode=1,'Cash','Pending')) as opd_type, if(exists( select 1 from opd_prescription pv where pv.opd_id=o.opd_id and ( coalesce(trim(pv.pulse),'')<>'' or coalesce(trim(pv.spo2),'')<>'' or coalesce(trim(pv.bp),'')<>'' or coalesce(trim(pv.diastolic),'')<>'' or coalesce(trim(pv.temp),'')<>'' or coalesce(trim(pv.rr_min),'')<>'' or coalesce(trim(pv.height),'')<>'' or coalesce(trim(pv.weight),'')<>'' or coalesce(trim(pv.waist),'')<>'' ) ),1,0) as has_vitals from opd_master o join patient_master p on o.p_id=p.id left join opd_prescription pr on o.opd_id=pr.opd_id where date(o.apointment_date)=" . $opdDateSql . " and o.doc_id=" . $docId . " and o.opd_status=1 and pr.id is null order by o.opd_code desc"; $query = $this->db->query($sql); $opdList0 = $query->getResult(); // Waiting: OPD records with prescription row and active (status=1). $sql = "select o.opd_id,o.opd_code,o.opd_no,p.id,p.p_code,p.p_fname as P_name,p.p_rname, o.opd_fee_desc,o.opd_fee_amount,o.payment_status, if(o.payment_mode=4,'Credit to ECHS',if(o.payment_mode=1,'Cash','Pending')) as opd_type, if(exists( select 1 from opd_prescription pv where pv.opd_id=o.opd_id and ( coalesce(trim(pv.pulse),'')<>'' or coalesce(trim(pv.spo2),'')<>'' or coalesce(trim(pv.bp),'')<>'' or coalesce(trim(pv.diastolic),'')<>'' or coalesce(trim(pv.temp),'')<>'' or coalesce(trim(pv.rr_min),'')<>'' or coalesce(trim(pv.height),'')<>'' or coalesce(trim(pv.weight),'')<>'' or coalesce(trim(pv.waist),'')<>'' ) ),1,0) as has_vitals, coalesce(pr.queue_no,0) as queue_no from opd_master o join patient_master p on o.p_id=p.id join opd_prescription pr on o.opd_id=pr.opd_id where date(o.apointment_date)=" . $opdDateSql . " and o.doc_id=" . $docId . " and o.opd_status=1 order by has_vitals asc, coalesce(nullif(pr.queue_no,0),999999) asc, o.opd_id asc"; $query = $this->db->query($sql); $opdList1 = $query->getResult(); // Visited: consultation completed (status=2). $sql = "select o.opd_id,o.opd_code,o.opd_no,p.id,p.p_code,p.p_fname as P_name,p.p_rname, o.opd_fee_desc,o.opd_fee_amount,o.payment_status, if(o.payment_mode=4,'Credit to ECHS',if(o.payment_mode=1,'Cash','Pending')) as opd_type, if(exists( select 1 from opd_prescription pv where pv.opd_id=o.opd_id and ( coalesce(trim(pv.pulse),'')<>'' or coalesce(trim(pv.spo2),'')<>'' or coalesce(trim(pv.bp),'')<>'' or coalesce(trim(pv.diastolic),'')<>'' or coalesce(trim(pv.temp),'')<>'' or coalesce(trim(pv.rr_min),'')<>'' or coalesce(trim(pv.height),'')<>'' or coalesce(trim(pv.weight),'')<>'' or coalesce(trim(pv.waist),'')<>'' ) ),1,0) as has_vitals, coalesce(pr.queue_no,0) as queue_no from opd_master o join patient_master p on o.p_id=p.id left join opd_prescription pr on o.opd_id=pr.opd_id where date(o.apointment_date)=" . $opdDateSql . " and o.doc_id=" . $docId . " and o.opd_status=2 order by o.opd_id"; $query = $this->db->query($sql); $opdList2 = $query->getResult(); // Cancelled: OPD records cancelled by staff/doctor (status=3). $sql = "select o.opd_id,o.opd_code,o.opd_no,p.id,p.p_code,p.p_fname as P_name,p.p_rname, o.opd_fee_desc,o.opd_fee_amount,o.payment_status, if(o.payment_mode=4,'Credit to ECHS',if(o.payment_mode=1,'Cash','Pending')) as opd_type, if(exists( select 1 from opd_prescription pv where pv.opd_id=o.opd_id and ( coalesce(trim(pv.pulse),'')<>'' or coalesce(trim(pv.spo2),'')<>'' or coalesce(trim(pv.bp),'')<>'' or coalesce(trim(pv.diastolic),'')<>'' or coalesce(trim(pv.temp),'')<>'' or coalesce(trim(pv.rr_min),'')<>'' or coalesce(trim(pv.height),'')<>'' or coalesce(trim(pv.weight),'')<>'' or coalesce(trim(pv.waist),'')<>'' ) ),1,0) as has_vitals, coalesce(pr.queue_no,0) as queue_no from opd_master o join patient_master p on o.p_id=p.id left join opd_prescription pr on o.opd_id=pr.opd_id where date(o.apointment_date)=" . $opdDateSql . " and o.doc_id=" . $docId . " and o.opd_status=3 order by o.opd_id"; $query = $this->db->query($sql); $opdList3 = $query->getResult(); return view('billing/opd_appointment_list', [ 'opd_date' => $opdDate, 'doc_id' => $docId, 'doc_master' => $docMaster, 'opd_list_0' => $opdList0, 'opd_list_1' => $opdList1, 'opd_list_2' => $opdList2, 'opd_list_3' => $opdList3, ]); } /** * Update OPD status from queue actions. */ public function opd_status(int $opdId, int $opdStatus) { if (! $this->canAccessDoctorWorkPanel()) { return $this->response->setStatusCode(403)->setBody('Access denied: OPD doctor work panel is restricted.'); } $allowed = [1, 2, 3]; if (!in_array($opdStatus, $allowed, true)) { return $this->response->setStatusCode(400)->setBody('Invalid OPD status'); } $sql = "select * from opd_master where opd_id=" . (int) $opdId; $query = $this->db->query($sql); $opdMaster = $query->getResult(); if (empty($opdMaster)) { return $this->response->setStatusCode(404)->setBody('OPD not found'); } $user = auth()->user(); $userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User'; $userId = $user->id ?? 0; $statusRemark = 'Update By:' . $userLabel . '[' . $userId . '] T-' . date('d-m-Y H:i:s'); $this->db->table('opd_master') ->where('opd_id', (int) $opdId) ->update([ 'opd_status' => (string) $opdStatus, 'opd_status_remark' => $statusRemark, ]); return $this->response->setJSON([ 'update' => 1, 'opd_id' => (int) $opdId, 'opd_status' => (int) $opdStatus, 'message' => 'Status updated', ]); } public function addopd(int $pno, int $orgCaseId = 0) { $sql = "select *,if(gender=1,'Male','Female') as xgender from patient_master where id='" . (int) $pno . "'"; $query = $this->db->query($sql); $data['person_info'] = $query->getResult(); if (!empty($data['person_info'])) { $row = $data['person_info'][0]; $row->age = get_age_1($row->dob ?? null, $row->age ?? '', $row->age_in_month ?? '', $row->estimate_dob ?? ''); } $sql = "select d.id, d.p_fname, group_concat(m.SpecName) as SpecName from doctor_master d join doc_spec s join med_spec m on d.id = s.doc_id and s.med_spec_id = m.id where d.active = 1 group by d.id"; $query = $this->db->query($sql); $data['doc_spec_l'] = $query->getResult(); $data['org_case_id'] = $orgCaseId; if ($orgCaseId > 0) { $sql = "select * from organization_case_master where id=" . (int) $orgCaseId; $query = $this->db->query($sql); $data['org_case_master'] = $query->getResult(); } return view('billing/opd_appointment_V', $data); } public function showfee() { if (!$this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON([ 'content' => '', 'error_text' => 'Invalid request', ]); } $docId = (int) $this->request->getPost('doc_id'); $pId = (int) $this->request->getPost('pid'); $sql = "select * from doctor_master where id=" . $docId; $query = $this->db->query($sql); $doctorMaster = $query->getResult(); $noOpdDays = 5 - 1; if (count($doctorMaster) > 0 && !empty($doctorMaster[0]->opd_valid_no_days)) { $noOpdDays = (int) $doctorMaster[0]->opd_valid_no_days - 1; } $sql = "select d.id, d.p_fname, group_concat(m.SpecName) as SpecName, if(d.gender=1,'Male','Female') as xGender from doctor_master d join doc_spec s join med_spec m on d.id = s.doc_id and s.med_spec_id = m.id where d.active = 1 and d.id=" . $docId . " group by d.id"; $query = $this->db->query($sql); $docInfo = $query->getResult(); $sql = "select * from opd_master where p_id=" . $pId . " and doc_id=" . $docId . " and apointment_date >= date_add(curdate(),interval -" . $noOpdDays . " day) and opd_fee_type<>3"; $query = $this->db->query($sql); $opdRunning = $query->getResult(); $sql = "select * from opd_master where p_id=" . $pId; $query = $this->db->query($sql); $opdOld = $query->getResult(); $feeTypeList = '4,0'; $feeTypeSelect = 1; if (count($opdOld) > 0) { if (count($opdRunning) > 0) { $feeTypeSelect = 3; $feeTypeList .= ',3'; } else { $feeTypeSelect = 2; $feeTypeList .= ',2'; } } else { $feeTypeSelect = 1; $feeTypeList .= ',1'; } $content = ''; $content .= '
Doctor Name : ' . esc($docInfo[0]->p_fname ?? '') . ' / Specialization: ' . esc($docInfo[0]->SpecName ?? '') . ' / Gender : ' . esc($docInfo[0]->xGender ?? '') . '
'; $content .= ''; $sql = "select d.*, if(d.doc_fee_desc='', t.fee_type, d.doc_fee_desc) as fee_desc from doc_opd_fee d join doc_fee_type t on d.doc_fee_type = t.id where doc_id=" . $docId . " and t.id in (" . $feeTypeList . ")"; $query = $this->db->query($sql); $docFeeA = $query->getResult(); $sql = "select d.*, if(d.doc_fee_desc='', t.fee_type, d.doc_fee_desc) as fee_desc from doc_opd_fee d join doc_fee_type t on d.doc_fee_type = t.id where doc_id=" . $docId . " and t.id not in (" . $feeTypeList . ")"; $query = $this->db->query($sql); $docFeeB = $query->getResult(); foreach ($docFeeA as $row) { $checked = ((int) $row->id === (int) $feeTypeSelect) ? 'checked' : ''; $content .= ''; } $content .= '
'; $otherContent = ''; foreach ($docFeeB as $row) { $checked = ((int) $row->id === (int) $feeTypeSelect) ? 'checked' : ''; $otherContent .= ''; } $content .= '
' . '
' . '

' . '' . '

' . '
' . '
' . $otherContent . '
' . '
' . '
' . '
'; return $this->response->setJSON([ 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), 'content' => $content, ]); } public function confirm_opd() { if (!$this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON([ 'insertid' => 0, 'error_text' => 'Invalid request', ]); } $docId = (int) $this->request->getPost('doc_id'); $feeId = (int) $this->request->getPost('fee_id'); $pid = (int) $this->request->getPost('pid'); $appointmentDate = (string) $this->request->getPost('datepicker_appointment'); $abhaAddress = trim((string) $this->request->getPost('abha_address')); if ($docId <= 0 || $feeId <= 0 || $pid <= 0 || $appointmentDate === '') { return $this->response->setJSON([ 'insertid' => 0, 'error_text' => 'Missing required fields.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } if ($abhaAddress !== '' && ! $this->isValidAbhaAddress($abhaAddress)) { return $this->response->setJSON([ 'insertid' => 0, 'error_text' => 'Invalid ABHA Address format.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $sql = "select * from patient_master where id=" . $pid; $query = $this->db->query($sql); $personInfo = $query->getResult(); $sql = "select d.id, d.p_fname, group_concat(m.SpecName) as SpecName, if(d.gender=1,'Male','Female') as xGender from doctor_master d join doc_spec s join med_spec m on d.id = s.doc_id and s.med_spec_id = m.id where d.active=1 and d.id=" . $docId . " group by d.id"; $query = $this->db->query($sql); $docInfo = $query->getResult(); $sql = "select * from doc_opd_fee where id=" . $feeId; $query = $this->db->query($sql); $docFee = $query->getResult(); if (count($personInfo) === 0 || count($docInfo) === 0 || count($docFee) === 0) { return $this->response->setJSON([ 'insertid' => 0, 'error_text' => 'Invalid patient, doctor, or fee selection.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $sql = "select * from opd_master where p_id=" . $pid . " order by opd_id desc"; $query = $this->db->query($sql); $opdMaster = $query->getResult(); $user = auth()->user(); $userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User'; $userId = $user->id ?? ''; $userNameInfo = $userLabel . ' [' . date('d-m-Y H:i:s') . '] ' . $userId; $insert = [ 'p_id' => $pid, 'P_name' => strtoupper((string) $personInfo[0]->p_fname), 'doc_id' => $docId, 'insurance_id' => '0', 'opd_fee_id' => $feeId, 'opd_fee_amount' => $docFee[0]->amount, 'opd_fee_gross_amount' => $docFee[0]->amount, 'opd_fee_desc' => $docFee[0]->doc_fee_desc, 'doc_name' => $docInfo[0]->p_fname, 'opd_fee_type' => $docFee[0]->doc_fee_type, 'apointment_date' => str_to_MysqlDate($appointmentDate), 'doc_spec' => $docInfo[0]->SpecName, 'prepared_by' => $userNameInfo, ]; if (count($opdMaster) > 0) { $insert['last_opdvisit_date'] = $opdMaster[0]->apointment_date; } $opdModel = new OpdModel(); $insertId = $opdModel->insertOpd($insert); if ($insertId > 0) { $this->db->table('patient_master') ->where('id', $pid) ->update(['last_visit' => str_to_MysqlDate($appointmentDate)]); $this->auditClinicalUpdate('opd_master', 'created', $insertId, null, $insert); $abhaField = $this->resolvePatientAbhaField(); if ($abhaField !== null && $abhaAddress !== '') { $oldAbha = (string) ($personInfo[0]->{$abhaField} ?? ''); if ($oldAbha !== $abhaAddress) { $this->db->table('patient_master') ->where('id', $pid) ->update([$abhaField => $abhaAddress]); $this->auditClinicalUpdate('patient_master', $abhaField, $pid, $oldAbha, $abhaAddress); } } } return $this->response->setJSON([ 'insertid' => $insertId, 'error_text' => $insertId > 0 ? 'OPD Register' : 'OPD already exists for today.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } private function isValidAbhaAddress(string $value): bool { $validation = service('validation'); $validation->reset(); return $validation->setRules([ 'abha_address' => 'valid_abha_address', ])->run([ 'abha_address' => $value, ]); } private function resolvePatientAbhaField(): ?string { if (! $this->db->tableExists('patient_master')) { return null; } $fields = $this->db->getFieldNames('patient_master'); foreach (['abha_address', 'abha', 'abha_id', 'abha_no'] as $field) { if (in_array($field, $fields, true)) { return $field; } } return null; } public function invoice(int $opdId) { $sql = "select *,date_format(apointment_date,'%d-%m-%Y') as str_apointment_date, (case payment_status when 1 then 'Cash' when 2 then 'Bank Card' when 3 then 'Org. Credit' else 'Pending' end) as Payment_type_str from opd_master where opd_id=" . (int) $opdId; $query = $this->db->query($sql); $data['opd_master'] = $query->getResult(); if (empty($data['opd_master'])) { return $this->response->setStatusCode(404)->setBody('OPD not found'); } $opdRow = $data['opd_master'][0]; $sql = "select *,if(gender=1,'Male','Female') as xgender from patient_master where id='" . (int) $opdRow->p_id . "'"; $query = $this->db->query($sql); $data['patient_master'] = $query->getResult(); if (!empty($data['patient_master'])) { $pRow = $data['patient_master'][0]; $pRow->age = get_age_1($pRow->dob ?? null, $pRow->age ?? '', $pRow->age_in_month ?? '', $pRow->estimate_dob ?? '', $opdRow->apointment_date ?? null); } $sql = "select * from organization_case_master where status=0 and case_type=0 and p_id=" . (int) $opdRow->p_id; $query = $this->db->query($sql); $data['case_master'] = $query->getResult(); $sql = "select * from hc_insurance where id=" . (int) $opdRow->insurance_id; $query = $this->db->query($sql); $data['insurance'] = $query->getResult(); $sql = "select * from refund_order where refund_process=0 and refund_type=1 and refund_type_id=" . (int) $opdId; $query = $this->db->query($sql); $data['refund_order'] = $query->getResult(); $data['refund_status'] = count($data['refund_order']) > 0 ? 1 : 0; $sql = "select sum(if(credit_debit=0,amount,amount*-1)) as paid_amount from payment_history where payof_type=1 and payof_id=" . (int) $opdId; $query = $this->db->query($sql); $data['payment_history'] = $query->getResult(); $paidAmount = 0; if (!empty($data['payment_history']) && $data['payment_history'][0]->paid_amount) { $paidAmount = (float) $data['payment_history'][0]->paid_amount; } $data['paid_amount'] = $paidAmount; $grossAmount = (float) $opdRow->opd_fee_amount; $data['pending_amount'] = ((int) $opdRow->payment_mode === 4) ? 0 : ($grossAmount - $paidAmount); $sql = "select d.id, d.p_fname, group_concat(m.SpecName) as SpecName from doctor_master d join doc_spec s join med_spec m on d.id = s.doc_id and s.med_spec_id = m.id where d.active=1 group by d.id"; $query = $this->db->query($sql); $data['doc_spec_l'] = $query->getResult(); $sql = "select s.id, s.pay_type, m.bank_name from hospital_bank m join hospital_bank_payment_source s on m.id=s.bank_id"; $query = $this->db->query($sql); $data['bank_data'] = $query->getResult(); return view('billing/opd_invoice_V', $data); } public function invoice_print(int $opdId) { $data = $this->buildOpdInvoicePrintData($opdId); if ($data === null) { return $this->response->setStatusCode(404)->setBody('OPD not found'); } return view('billing/opd_invoice_print_v', $data); } public function invoice_print_pdf(int $opdId) { $data = $this->buildOpdInvoicePrintData($opdId); if ($data === null) { return $this->response->setStatusCode(404)->setBody('OPD not found'); } $html = view('billing/opd_invoice_pdf_v', $data); $mpdf = new Mpdf([ 'mode' => 'utf-8', 'format' => 'A4', 'orientation' => 'P', 'margin_left' => 10, 'margin_right' => 10, 'margin_top' => 10, 'margin_bottom' => 10, 'default_font' => 'dejavusans', 'tempDir' => WRITEPATH . 'cache', ]); $opdCode = (string) ($data['opd_master'][0]->opd_code ?? ('OPD-' . $opdId)); $mpdf->SetTitle('OPD Invoice ' . $opdCode); $mpdf->WriteHTML($html); $fileName = 'OPD_Invoice_' . str_replace('/', '-', $opdCode) . '.pdf'; return $this->response ->setHeader('Content-Type', 'application/pdf') ->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"') ->setBody($mpdf->Output($fileName, 'S')); } private function buildOpdInvoicePrintData(int $opdId): ?array { $data = []; $sql = "select *,date_format(apointment_date,'%d-%m-%Y') as str_apointment_date, (case payment_status when 1 then 'Cash' when 2 then 'Bank Card' when 3 then 'Org. Credit' else 'Pending' end) as Payment_type_str from opd_master where opd_id=" . (int) $opdId; $query = $this->db->query($sql); $data['opd_master'] = $query->getResult(); if (empty($data['opd_master'])) { return null; } $opdRow = $data['opd_master'][0]; $sql = "select *,if(gender=1,'Male','Female') as xgender from patient_master where id='" . (int) $opdRow->p_id . "'"; $query = $this->db->query($sql); $data['patient_master'] = $query->getResult(); if (!empty($data['patient_master'])) { $pRow = $data['patient_master'][0]; $pRow->age = get_age_1($pRow->dob ?? null, $pRow->age ?? '', $pRow->age_in_month ?? '', $pRow->estimate_dob ?? '', $opdRow->apointment_date ?? null); } $sql = "select * from hc_insurance where id=" . (int) ($opdRow->insurance_id ?? 0); $query = $this->db->query($sql); $data['insurance'] = $query->getResult(); if ((int) ($opdRow->insurance_case_id ?? 0) > 0) { $sql = "select * from organization_case_master where id=" . (int) $opdRow->insurance_case_id; $query = $this->db->query($sql); $data['case_master'] = $query->getResult(); } else { $data['case_master'] = []; } return $data; } public function opd_lettre_print(int $opdId) { $sessionId = (int) $this->request->getGet('session_id'); $layoutMode = strtolower(trim((string) $this->request->getGet('layout'))); if ($layoutMode === '') { $layoutMode = 'full'; } $includeContent = in_array($layoutMode, ['full', 'meta_content', 'content_only'], true); $data = $this->buildOpdLetterPrintData($opdId, $sessionId, $includeContent); if ($data === null) { return $this->response->setStatusCode(404)->setBody('OPD not found'); } $data['print_layout_mode'] = $layoutMode; return view('billing/opd_letter_head', $data); } public function opd_lettre_pdf(int $opdId) { $sessionId = (int) $this->request->getGet('session_id'); $layoutMode = strtolower(trim((string) $this->request->getGet('layout'))); if ($layoutMode === '') { $layoutMode = 'full'; } $allowedModes = ['full', 'meta_content', 'content_only', 'header_meta', 'meta_only']; if (! in_array($layoutMode, $allowedModes, true)) { $layoutMode = 'full'; } $includeContent = in_array($layoutMode, ['full', 'meta_content', 'content_only'], true); $data = $this->buildOpdLetterPrintData($opdId, $sessionId, $includeContent); if ($data === null) { return $this->response->setStatusCode(404)->setBody('OPD not found'); } $data['print_layout_mode'] = $layoutMode; $templateKey = strtolower(trim((string) $this->request->getGet('template'))); $templateKey = preg_replace('/[^a-z0-9_\-]/', '', $templateKey) ?? ''; $isComposedTemplate = $templateKey !== '' && str_starts_with($templateKey, 'compose_'); if ($isComposedTemplate) { $html = $this->renderComposedOpdPrint($data, $templateKey); } else { $viewPath = 'billing/opd_letter_head_pdf'; $customHtml = ''; if ($templateKey !== '') { $customFile = APPPATH . 'Views/billing/opd_templates/' . $templateKey . '.php'; $customHtmlFile = APPPATH . 'Views/billing/opd_templates/' . $templateKey . '.html'; if (is_file($customFile)) { $viewPath = 'billing/opd_templates/' . $templateKey; } elseif (is_file($customHtmlFile)) { $customHtml = (string) file_get_contents($customHtmlFile); } } if ($viewPath !== 'billing/opd_letter_head_pdf') { $html = view($viewPath, $data); } elseif ($customHtml !== '') { $legacyVars = $this->buildLegacyDashboardTemplateVars($data); $html = $this->renderCurlyTemplate($customHtml, $legacyVars); } elseif ($templateKey !== '') { $legacyFile = ROOTPATH . 'old_code_ci3/views/dashboard/' . $templateKey . '.php'; if (is_file($legacyFile)) { $legacyVars = $this->buildLegacyDashboardTemplateVars($data); $html = $this->renderLegacyPhpTemplate($legacyFile, $legacyVars); } else { $html = view('billing/opd_letter_head_pdf', $data); } } else { $html = view('billing/opd_letter_head_pdf', $data); } } $paper = strtolower(trim((string) $this->request->getGet('paper'))); $format = 'A4'; if (in_array($paper, ['a5', 'a6', 'letter'], true)) { $format = strtoupper($paper); } $marginLeft = 8; $marginRight = 8; $marginTop = 8; $marginBottom = 8; if ($format === 'A6') { $marginLeft = 6; $marginRight = 6; $marginTop = 6; $marginBottom = 6; } $mpdf = new Mpdf([ 'mode' => 'utf-8', 'format' => $format, 'orientation' => 'P', 'margin_left' => $marginLeft, 'margin_right' => $marginRight, 'margin_top' => $marginTop, 'margin_bottom' => $marginBottom, 'default_font' => 'freeserif', 'autoScriptToLang' => true, 'autoLangToFont' => true, 'tempDir' => WRITEPATH . 'cache', ]); $opdCode = (string) ($data['opd_master'][0]->opd_code ?? ('OPD-' . $opdId)); $mpdf->SetTitle('OPD Prescription ' . $opdCode); $mpdf->WriteHTML($html); $fileName = 'OPD_Prescription_' . str_replace('/', '-', $opdCode) . '.pdf'; return $this->response ->setHeader('Content-Type', 'application/pdf') ->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"') ->setBody($mpdf->Output($fileName, 'S')); } public function opd_pdf_print(int $opdId) { $printConfig = $this->resolveDoctorPrintConfigByField($opdId, 'opd_print_format', 'meta_only'); $url = base_url('Opd/opd_lettre_pdf/' . (int) $opdId . '?layout=' . urlencode($printConfig['layout'])); if ($printConfig['template'] !== '') { $url .= '&template=' . urlencode($printConfig['template']); } return redirect()->to($url); } public function opd_blank_print(int $opdId) { $printConfig = $this->resolveDoctorPrintConfigByField($opdId, 'opd_blank_print', 'header_meta'); $url = base_url('Opd/opd_lettre_pdf/' . (int) $opdId . '?layout=' . urlencode($printConfig['layout'])); if ($printConfig['template'] !== '') { $url .= '&template=' . urlencode($printConfig['template']); } return redirect()->to($url); } public function opd_cont_print(int $opdId) { $printConfig = $this->resolveDoctorPrintConfigByField($opdId, 'opd_print_format', 'content_only'); $url = base_url('Opd/opd_lettre_pdf/' . (int) $opdId . '?layout=' . urlencode($printConfig['layout'])); if ($printConfig['template'] !== '') { $url .= '&template=' . urlencode($printConfig['template']); } return redirect()->to($url); } /** * @return array{layout:string,template:string} */ private function resolveDoctorPrintConfigByField(int $opdId, string $fieldName, string $fallback): array { $allowed = ['full', 'meta_content', 'content_only', 'header_meta', 'meta_only']; $fallbackLayout = strtolower(trim($fallback)); if (!in_array($fallbackLayout, $allowed, true)) { $fallbackLayout = 'full'; } $query = $this->db->query( 'SELECT d.' . $fieldName . ' AS layout_field FROM opd_master o LEFT JOIN doctor_master d ON d.id = o.doc_id WHERE o.opd_id = ? LIMIT 1', [(int) $opdId] ); $row = $query->getRowArray(); $raw = strtolower(trim((string) ($row['layout_field'] ?? ''))); if ($raw === '') { return ['layout' => $fallbackLayout, 'template' => '']; } if (in_array($raw, $allowed, true)) { return ['layout' => $raw, 'template' => '']; } if ($raw === '0') { return ['layout' => 'content_only', 'template' => '']; } if ($raw === '1') { return ['layout' => 'meta_content', 'template' => '']; } if ($raw === '2') { return ['layout' => 'full', 'template' => '']; } $templateKey = preg_replace('/[^a-z0-9_\-]/', '', $raw) ?? ''; if ($templateKey !== '') { return ['layout' => $fallbackLayout, 'template' => $templateKey]; } return ['layout' => $fallbackLayout, 'template' => '']; } /** * @param array $data * @return array */ private function buildLegacyDashboardTemplateVars(array $data): array { $opd = $data['opd_master'][0] ?? null; $patient = $data['patient_master'][0] ?? null; $doctor = $data['doctor_master'][0] ?? null; $rx = is_array($data['opd_prescription'] ?? null) ? $data['opd_prescription'] : []; $pName = strtoupper(trim((string) (($patient->title ?? '') . ' ' . ($patient->p_fname ?? '')))); $pRelative = trim((string) (($patient->p_relative ?? '') . ' ' . ($patient->p_rname ?? ''))); $ageSex = trim((string) (($patient->xgender ?? '') . ' / ' . ($patient->age ?? ''))); $phone = (string) ($patient->mphone1 ?? ''); $pAddress = trim((string) (($patient->add1 ?? '') . ', ' . ($patient->add2 ?? '') . ', ' . ($patient->city ?? '')), ' ,'); $uhidNo = (string) ($patient->p_code ?? ''); $opdSrNo = (string) ($opd->opd_no ?? ''); $opdNo = (string) ($opd->opd_code ?? ''); $opdDate = (string) ($opd->str_apointment_date ?? ''); $expDate = trim((string) ($opd->opd_Exp_Date ?? '')); $expDateText = $expDate !== '' ? ('Valid Upto : ' . esc($expDate)) : ''; $specName = trim((string) ($doctor->SpecName ?? $opd->doc_spec ?? '')); $opdFeeDesc = trim((string) (($opd->opd_fee_amount ?? '') . ' ' . ($opd->opd_fee_desc ?? ''))); $totalNoVisit = (string) ($opd->no_visit ?? ''); $lastVisitText = ''; if (!empty($data['old_opd'][0]->str_apointment_date ?? '')) { $lastVisitText = 'Last Visit : ' . (string) $data['old_opd'][0]->str_apointment_date; } $bookTime = (string) ($opd->apointment_date ?? ''); $rxMeds = is_array($data['rx_medicines'] ?? null) ? $data['rx_medicines'] : []; $medicalHtml = ''; if (!empty($rxMeds)) { $medicalHtml .= ''; $medicalHtml .= ''; $i = 0; foreach ($rxMeds as $med) { $i++; $name = trim((string) ($med['drug_name'] ?? $med['medicine_name'] ?? $med['item_name'] ?? '')); $dose = trim((string) ($med['drug_dose'] ?? $med['dose'] ?? '')); $freq = trim((string) ($med['drug_freq'] ?? $med['frequency'] ?? '')); $days = trim((string) ($med['drug_day'] ?? $med['days'] ?? '')); $inst = trim((string) ($med['drug_instruction'] ?? $med['instruction'] ?? '')); $medicalHtml .= '' . '' . '' . '' . '' . '' . ''; } $medicalHtml .= '
#MedicineDose/FreqDayInstruction
' . $i . '' . esc($name) . '' . esc(trim($dose . ' ' . $freq)) . '' . esc($days) . '' . esc($inst) . '
'; } $rxInvestigation = trim((string) ($rx['investigation'] ?? '')); if ($rxInvestigation === '' && !empty($data['rx_investigations']) && is_array($data['rx_investigations'])) { $parts = []; foreach ($data['rx_investigations'] as $inv) { $txt = trim((string) ($inv['investigation_name'] ?? $inv['investigation'] ?? '')); if ($txt !== '') { $parts[] = $txt; } } $rxInvestigation = implode(', ', $parts); } $rxAdvice = trim((string) ($rx['advice'] ?? '')); if ($rxAdvice === '' && !empty($data['rx_advices']) && is_array($data['rx_advices'])) { $parts = []; foreach ($data['rx_advices'] as $adv) { $txt = trim((string) ($adv['advice_txt'] ?? $adv['advice'] ?? '')); if ($txt !== '') { $parts[] = $txt; } } $rxAdvice = implode(', ', $parts); } $bp = trim((string) ($rx['bp'] ?? '')); $diastolic = trim((string) ($rx['diastolic'] ?? '')); $pulse = trim((string) ($rx['pulse'] ?? '')); $temp = trim((string) ($rx['temp'] ?? '')); $spo2 = trim((string) ($rx['spo2'] ?? '')); $vitals = []; if ($bp !== '') { $vitals[] = 'BP: ' . $bp . ($diastolic !== '' ? ('/' . $diastolic) : ''); } if ($pulse !== '') { $vitals[] = 'Pulse: ' . $pulse; } if ($temp !== '') { $vitals[] = 'Temp: ' . $temp; } if ($spo2 !== '') { $vitals[] = 'SpO2: ' . $spo2; } $printContent = '' . '' . '' . '' . '' . '' . '
' . 'Name : ' . esc($pName) . '
' . esc($pRelative) . '
' . 'Gender/Age : ' . esc($ageSex) . '
' . 'Mob :' . esc($phone) . '
' . 'Address :' . esc($pAddress) . '
' . 'UHID : ' . esc($uhidNo) . '
' . 'Sr No.: ' . esc($opdSrNo) . '
' . 'OPD No.: ' . esc($opdNo) . '
' . '
Date: ' . esc($opdDate) . '
' . $expDateText . '
' . 'DEPARTMENT :
' . esc($specName) . '
' . esc($opdFeeDesc) . '
' . 'No. of Visit : ' . esc($totalNoVisit) . '
' . esc($lastVisitText) . '
' . 'Book Time : ' . esc($bookTime) . '
'; return [ 'opd_master' => $data['opd_master'] ?? [], 'patient_master' => $data['patient_master'] ?? [], 'doctor_master' => $data['doctor_master'] ?? [], 'old_opd' => $data['old_opd'] ?? [], 'insurance' => $data['insurance'] ?? [], 'case_master' => $data['case_master'] ?? [], 'content' => $printContent, 'content_2' => $printContent, 'content_3' => $printContent, 'content_4' => $printContent, 'pName' => $pName, 'pRelative' => $pRelative, 'age_sex' => $ageSex, 'phoneno' => $phone, 'p_address' => $pAddress, 'uhid_no' => $uhidNo, 'opd_sr_no' => $opdSrNo, 'opd_no' => $opdNo, 'opd_date' => $opdDate, 'exp_date' => $expDateText, 'SpecName' => $specName, 'opd_fee_desc' => $opdFeeDesc, 'total_no_visit' => $totalNoVisit, 'last_opdvisit_date' => $lastVisitText, 'str_opd_book_date' => $bookTime, 'Complaint' => (string) ($rx['complaints'] ?? ''), 'diagnosis' => (string) (($rx['Provisional_diagnosis'] ?? '') !== '' ? ($rx['Provisional_diagnosis'] ?? '') : ($rx['diagnosis'] ?? '')), 'Provisional_diagnosis' => (string) ($rx['Provisional_diagnosis'] ?? ''), 'investigation' => $rxInvestigation, 'medical' => $medicalHtml, 'doctor' => '

Dr. ' . esc((string) ($opd->doc_name ?? '')) . '

', 'top_content' => '', 'vital_content' => implode(' | ', $vitals), 'Finding_Examinations' => (string) ($rx['Finding_Examinations'] ?? ''), 'Prescriber_Remarks' => (string) ($rx['Prescriber_Remarks'] ?? ''), 'advice' => $rxAdvice, 'next_visit' => (string) ($rx['next_visit'] ?? ''), 'refer_to' => (string) ($rx['refer_to'] ?? ''), 'painscale' => '', 'painscale_img' => '', 'morbidities' => implode(', ', is_array($data['selected_morbidities'] ?? null) ? ($data['selected_morbidities'] ?? []) : []), 'Addiction' => '', 'Complication' => '', ]; } /** * @param array $vars */ private function renderLegacyPhpTemplate(string $filePath, array $vars): string { ob_start(); extract($vars, EXTR_SKIP); include $filePath; return (string) ob_get_clean(); } /** * @param array $data */ private function renderComposedOpdPrint(array $data, string $templateKey): string { $vars = $this->buildLegacyDashboardTemplateVars($data); $hospitalName = defined('H_Name') ? (string) constant('H_Name') : 'Hospital'; $hospitalAddress1 = defined('H_address_1') ? (string) constant('H_address_1') : ''; $hospitalAddress2 = defined('H_address_2') ? (string) constant('H_address_2') : ''; $hospitalPhone = defined('H_phone_No') ? (string) constant('H_phone_No') : ''; $hospitalEmail = defined('H_Email') ? (string) constant('H_Email') : ''; $vars['hospital_name'] = esc($hospitalName); $vars['hospital_address'] = esc(trim($hospitalAddress1 . ', ' . $hospitalAddress2, ', ')); $vars['hospital_phone'] = esc($hospitalPhone); $vars['hospital_email'] = esc($hospitalEmail); $vars['print_time'] = esc(date('d-m-Y H:i:s')); $doctorName = trim((string) ($vars['doctor_name'] ?? '')); if ($doctorName === '') { $opd = $data['opd_master'][0] ?? null; $doctorName = (string) ($opd->doc_name ?? ''); } $vars['doctor_name'] = esc($doctorName); $themeKey = trim((string) substr($templateKey, strlen('compose_'))); if ($themeKey === '') { $themeKey = 'default'; } $hospitalPart = strtolower(trim((string) $this->request->getGet('hospital_part'))); if ($hospitalPart === '') { $hospitalPart = $themeKey; } $contentPart = strtolower(trim((string) $this->request->getGet('content_part'))); if ($contentPart === '') { $contentPart = $themeKey; } $patientView = strtolower(trim((string) $this->request->getGet('patient_view'))); if (!in_array($patientView, ['list', 'edit'], true)) { $patientView = 'list'; } $layoutHtml = $this->readComposedPartTemplate('layout', $themeKey); $hospitalHtml = $this->readComposedPartTemplate('hospital', $hospitalPart); $contentHtml = $this->readComposedPartTemplate('content', $contentPart); $patientHtml = $this->readComposedPartTemplate('patient', $patientView); $hospitalHtml = $this->renderCurlyTemplate($hospitalHtml, $vars); $patientHtml = $this->renderCurlyTemplate($patientHtml, $vars); $contentHtml = $this->renderCurlyTemplate($contentHtml, $vars); $vars['hospital_section'] = $hospitalHtml; $vars['patient_section'] = $patientHtml; $vars['content_section'] = $contentHtml; return $this->renderCurlyTemplate($layoutHtml, $vars); } private function readComposedPartTemplate(string $section, string $name): string { $safeSection = preg_replace('/[^a-z0-9_\-]/', '', strtolower(trim($section))) ?? ''; $safeName = preg_replace('/[^a-z0-9_\-]/', '', strtolower(trim($name))) ?? ''; if ($safeSection === '') { $safeSection = 'layout'; } if ($safeName === '') { $safeName = 'default'; } $baseDir = APPPATH . 'Views/billing/opd_parts/' . $safeSection . '/'; $target = $baseDir . $safeName . '.html'; $fallback = $baseDir . 'default.html'; if (is_file($target)) { return (string) file_get_contents($target); } if (is_file($fallback)) { return (string) file_get_contents($fallback); } return '{{content}}'; } /** * @param array $vars */ private function renderCurlyTemplate(string $html, array $vars): string { return (string) preg_replace_callback('/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/', static function (array $m) use ($vars) { $key = (string) ($m[1] ?? ''); $value = $vars[$key] ?? ''; if (is_array($value) || is_object($value)) { return ''; } return (string) $value; }, $html); } private function convertLegacyTemplateSourceToBuilderHtml(string $source): string { if (trim($source) === '') { return ''; } $html = str_replace(["\r\n", "\r"], "\n", $source); $patterns = [ '/<\?php\s+echo\s+esc\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)[^\)]*\)\s*;?\s*\?>/i' => '{{$1}}', '/<\?php\s+echo\s+\$([a-zA-Z_][a-zA-Z0-9_]*)\s*;?\s*\?>/i' => '{{$1}}', '/<\?=\s*\$([a-zA-Z_][a-zA-Z0-9_]*)\s*;?\s*\?>/i' => '{{$1}}', '/<\?php\s+echo\s*\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)\s*\)\s*;?\s*\?>/i' => '{{$1}}', ]; foreach ($patterns as $regex => $replacement) { $html = (string) preg_replace($regex, $replacement, $html); } $html = (string) preg_replace('/<\?(?:php|=)[\s\S]*?\?>/i', '', $html); $html = trim($html); return $html; } private function buildOpdLetterPrintData(int $opdId, int $sessionId = 0, bool $includeContent = true): ?array { $sql = "Select * from opd_master where opd_id=" . (int) $opdId; $query = $this->db->query($sql); $opdMaster = $query->getResult(); if (empty($opdMaster)) { return null; } $data = []; $docId = (int) ($opdMaster[0]->doc_id ?? 0); $noOpdDays = 5 - 1; $data['doctor_master'] = []; if ($docId > 0) { $sql = "select d.*, group_concat(DISTINCT m.SpecName) as SpecName from doctor_master d left join doc_spec s on d.id=s.doc_id left join med_spec m on m.id=s.med_spec_id where d.id=" . $docId . " group by d.id"; $query = $this->db->query($sql); $data['doctor_master'] = $query->getResult(); if (!empty($data['doctor_master']) && !empty($data['doctor_master'][0]->opd_valid_no_days)) { $noOpdDays = (int) $data['doctor_master'][0]->opd_valid_no_days - 1; } } $sql = "select *,date_format(apointment_date,'%d-%m-%Y') as str_apointment_date, date_format(date_add(apointment_date,interval " . $noOpdDays . " day),'%d-%m-%Y') as opd_Exp_Date, (case payment_status when 1 then 'Cash' when 2 then 'Bank Card' when 3 then 'Org. Credit' else 'Pending' end) as Payment_type_str from opd_master where opd_id=" . (int) $opdId; $query = $this->db->query($sql); $data['opd_master'] = $query->getResult(); $opdRow = $data['opd_master'][0]; if (!isset($opdRow->doc_sign) || trim((string) $opdRow->doc_sign) === '') { $opdRow->doc_sign = (string) ($data['doctor_master'][0]->doc_sign ?? ''); } $sql = "select *,if(gender=1,'M','F') as xgender from patient_master where id='" . (int) $opdRow->p_id . "'"; $query = $this->db->query($sql); $data['patient_master'] = $query->getResult(); if (!empty($data['patient_master'])) { $pRow = $data['patient_master'][0]; $pRow->age = get_age_1($pRow->dob ?? null, $pRow->age ?? '', $pRow->age_in_month ?? '', $pRow->estimate_dob ?? '', $opdRow->apointment_date ?? null); } $sql = "select *,date_format(date_add(apointment_date,interval " . $noOpdDays . " day),'%d-%m-%Y') as opd_Exp_Date from opd_master where p_id=" . (int) $opdRow->p_id . " and opd_id < " . (int) $opdId . " order by opd_id desc limit 1"; $query = $this->db->query($sql); $data['old_opd'] = $query->getResult(); $sql = "select * from hc_insurance where id=" . (int) ($opdRow->insurance_id ?? 0); $query = $this->db->query($sql); $data['insurance'] = $query->getResult(); if ((int) ($opdRow->insurance_case_id ?? 0) > 0) { $sql = "select * from organization_case_master where id=" . (int) $opdRow->insurance_case_id; $query = $this->db->query($sql); $data['case_master'] = $query->getResult(); } else { $data['case_master'] = []; } $data['opd_prescription'] = null; if ($this->db->tableExists('opd_prescription')) { $rowBuilder = $this->db->table('opd_prescription') ->where('opd_id', (int) $opdId); if ($sessionId > 0) { $rowBuilder->where('id', $sessionId); } else { $rowBuilder->orderBy('id', 'DESC'); } $row = $rowBuilder->get(1)->getRowArray(); if (!empty($row)) { if (empty($row['women_related_problems'] ?? '')) { $row['women_related_problems'] = $this->extractWomenRelatedProblemsFromRemarks((string) ($row['Prescriber_Remarks'] ?? '')); } $womenStructured = $this->parseWomenStructuredDetails((string) ($row['women_related_problems'] ?? '')); $row['women_related_problems'] = (string) ($womenStructured['women_related_problems'] ?? ''); $row['women_lmp'] = trim((string) ( $row['women_lmp'] ?? $row['lmp'] ?? $row['lmp_date'] ?? ($womenStructured['women_lmp'] ?? '') )); $row['women_last_baby'] = trim((string) ( $row['women_last_baby'] ?? $row['last_baby'] ?? $row['last_baby_details'] ?? ($womenStructured['women_last_baby'] ?? '') )); $row['women_pregnancy_related'] = trim((string) ( $row['women_pregnancy_related'] ?? $row['pregnancy_related'] ?? $row['pregnancy_related_problem'] ?? ($womenStructured['women_pregnancy_related'] ?? '') )); $nabhInfo = $this->hydrateNabhPrintFields( $row, (string) ($row['Prescriber_Remarks'] ?? '') ); $row['drug_allergy_status'] = (string) ($nabhInfo['drug_allergy_status'] ?? ''); $row['drug_allergy_details'] = (string) ($nabhInfo['drug_allergy_details'] ?? ''); $row['adr_history'] = (string) ($nabhInfo['adr_history'] ?? ''); $row['current_medications'] = (string) ($nabhInfo['current_medications'] ?? ''); $data['opd_prescription'] = $row; } } if (! $includeContent) { $data['selected_morbidities'] = []; $data['rx_medicines'] = []; $data['rx_investigations'] = []; $data['rx_advices'] = []; return $data; } $data['selected_morbidities'] = []; if ($this->db->tableExists('patient_morbidities') && $this->db->tableExists('morbidities_master')) { $pmFields = $this->db->getFieldNames('patient_morbidities'); $mmFields = $this->db->getFieldNames('morbidities_master'); $pmPatientField = $this->resolveFirstField($pmFields, ['p_id', 'patient_id']); $pmMorField = $this->resolveFirstField($pmFields, ['morbidities', 'mor_id', 'morbidity_id']); $mmMorField = $this->resolveFirstField($mmFields, ['mor_id', 'id']); $mmNameField = $this->resolveFirstField($mmFields, ['morbidities', 'name', 'title']); if ($pmPatientField !== null && $pmMorField !== null && $mmMorField !== null && $mmNameField !== null) { $rows = $this->db->table('patient_morbidities pm') ->select('m.' . $mmNameField . ' as morbidities') ->join('morbidities_master m', 'm.' . $mmMorField . '=pm.' . $pmMorField, 'inner') ->where('pm.' . $pmPatientField, (int) ($opdRow->p_id ?? 0)) ->orderBy('m.' . $mmNameField, 'ASC') ->get() ->getResultArray(); foreach ($rows as $morRow) { $name = trim((string) ($morRow['morbidities'] ?? '')); if ($name !== '') { $data['selected_morbidities'][] = $name; } } } } $data['rx_medicines'] = []; $data['rx_investigations'] = []; $data['rx_advices'] = []; $sessionId = (int) ($data['opd_prescription']['id'] ?? 0); $medTable = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']); if ($medTable !== null) { $medFields = $this->db->getFieldNames($medTable); $medBuilder = $this->db->table($medTable); $hasFilter = false; if (in_array('opd_pre_id', $medFields, true) && $sessionId > 0) { $medBuilder->where('opd_pre_id', $sessionId); $hasFilter = true; } elseif (in_array('opd_id', $medFields, true)) { $medBuilder->where('opd_id', (int) $opdId); $hasFilter = true; } if ($hasFilter) { if (in_array('id', $medFields, true)) { $medBuilder->orderBy('id', 'ASC'); } $data['rx_medicines'] = $medBuilder->get()->getResultArray(); } else { $data['rx_medicines'] = []; } } $invTable = $this->findExistingTable(['opd_prescription_investigation']); if ($invTable !== null) { $invFields = $this->db->getFieldNames($invTable); $invBuilder = $this->db->table($invTable); $hasFilter = false; if (in_array('opd_pre_id', $invFields, true) && $sessionId > 0) { $invBuilder->where('opd_pre_id', $sessionId); $hasFilter = true; } elseif (in_array('opd_id', $invFields, true)) { $invBuilder->where('opd_id', (int) $opdId); $hasFilter = true; } if ($hasFilter) { if (in_array('investigation_name', $invFields, true)) { $invBuilder->orderBy('investigation_name', 'ASC'); } elseif (in_array('id', $invFields, true)) { $invBuilder->orderBy('id', 'ASC'); } $data['rx_investigations'] = $invBuilder->get()->getResultArray(); } else { $data['rx_investigations'] = []; } } $adviceTable = $this->findExistingTable(['opd_prescription_advice']); if ($adviceTable !== null) { $advFields = $this->db->getFieldNames($adviceTable); $advBuilder = $this->db->table($adviceTable); $hasFilter = false; if (in_array('opd_pre_id', $advFields, true) && $sessionId > 0) { $advBuilder->where('opd_pre_id', $sessionId); $hasFilter = true; } elseif (in_array('opd_id', $advFields, true)) { $advBuilder->where('opd_id', (int) $opdId); $hasFilter = true; } if ($hasFilter) { if (in_array('id', $advFields, true)) { $advBuilder->orderBy('id', 'ASC'); } $data['rx_advices'] = $advBuilder->get()->getResultArray(); } else { $data['rx_advices'] = []; } } if (!empty($data['rx_advices']) && $this->db->tableExists('opd_advice')) { $masterFields = $this->db->getFieldNames('opd_advice'); $masterIdField = $this->resolveFirstField($masterFields, ['id', 'advice_id']); $masterAdviceField = $this->resolveFirstField($masterFields, ['advice', 'advice_txt', 'advice_text']); $masterHindiField = $this->resolveFirstField($masterFields, ['advice_hindi', 'advice_local', 'hindi_advice', 'advice_hin']); if ($masterAdviceField !== null && $masterHindiField !== null) { $rxAdviceIds = []; $rxAdviceTexts = []; foreach ($data['rx_advices'] as $advRow) { $advIdVal = trim((string) ($advRow['advice_id'] ?? '')); if ($advIdVal !== '') { $rxAdviceIds[] = $advIdVal; } $advTextVal = trim((string) ($advRow['advice_txt'] ?? ($advRow['advice'] ?? ''))); if ($advTextVal !== '') { $rxAdviceTexts[] = $advTextVal; } } $rxAdviceIds = array_values(array_unique($rxAdviceIds)); $rxAdviceTexts = array_values(array_unique($rxAdviceTexts)); $selectParts = [ $masterAdviceField . ' as advice_text', $masterHindiField . ' as advice_hindi', ]; if ($masterIdField !== null) { $selectParts[] = $masterIdField . ' as advice_id'; } $masterBuilder = $this->db->table('opd_advice') ->select(implode(',', $selectParts)) ->where($masterAdviceField . ' !=', ''); $hasScopedFilter = false; if ($masterIdField !== null && !empty($rxAdviceIds)) { $masterBuilder->whereIn($masterIdField, $rxAdviceIds); $hasScopedFilter = true; } if (!empty($rxAdviceTexts)) { if ($hasScopedFilter) { $masterBuilder->orWhereIn($masterAdviceField, $rxAdviceTexts); } else { $masterBuilder->whereIn($masterAdviceField, $rxAdviceTexts); } $hasScopedFilter = true; } if (!$hasScopedFilter) { $masterRows = []; } else { $masterRows = $masterBuilder->limit(500)->get()->getResultArray(); } $byId = []; $byText = []; foreach ($masterRows as $mRow) { $rawHindi = (string) ($mRow['advice_hindi'] ?? ''); if ($rawHindi === '') { continue; } $hindi = trim((string) preg_replace('/\r\n|\r|\n/', "\n", str_replace(['
', '
', '
'], "\n", strip_tags($rawHindi)))); if ($hindi === '') { continue; } $adviceText = trim((string) ($mRow['advice_text'] ?? '')); $textKey = mb_strtolower(preg_replace('/\s+/', ' ', $adviceText) ?? $adviceText); if ($textKey !== '') { $byText[$textKey] = $hindi; } $idKey = trim((string) ($mRow['advice_id'] ?? '')); if ($idKey !== '') { $byId[$idKey] = $hindi; } } foreach ($data['rx_advices'] as $idx => $advRow) { $currentHindi = trim((string) ($advRow['advice_hindi'] ?? '')); if ($currentHindi !== '') { continue; } $resolved = ''; $advIdKey = trim((string) ($advRow['advice_id'] ?? '')); if ($advIdKey !== '' && isset($byId[$advIdKey])) { $resolved = (string) $byId[$advIdKey]; } if ($resolved === '') { $advText = trim((string) ($advRow['advice_txt'] ?? ($advRow['advice'] ?? ''))); $advTextKey = mb_strtolower(preg_replace('/\s+/', ' ', $advText) ?? $advText); if ($advTextKey !== '' && isset($byText[$advTextKey])) { $resolved = (string) $byText[$advTextKey]; } } if ($resolved !== '') { $data['rx_advices'][$idx]['advice_hindi'] = $resolved; } } } } return $data; } public function confirm_payment() { if (!$this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON([ 'update' => 0, 'error_text' => 'Invalid request', ]); } $mode = (int) $this->request->getPost('mode'); $oid = (int) $this->request->getPost('oid'); $spid = (string) $this->request->getPost('spid'); if ($oid <= 0) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Invalid OPD ID', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $sql = "select * from opd_master where opd_id='" . $oid . "'"; $query = $this->db->query($sql); $opdMaster = $query->getResult(); if (empty($opdMaster)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'OPD not found', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $sql = "select coalesce(sum(if(credit_debit=0,amount,amount*-1)),0) as opd_pay from payment_history where payof_id='" . $oid . "' and payof_type=1"; $query = $this->db->query($sql); $chkPayment = $query->getResult(); $paidAmount = (float) ($chkPayment[0]->opd_pay ?? 0); $pendingAmt = (float) $opdMaster[0]->opd_fee_amount - $paidAmount; $payRemark = ''; if (!empty($opdMaster[0]->opd_discount) && (float) $opdMaster[0]->opd_discount > 0) { $payRemark = 'Dis.Amt.:' . $opdMaster[0]->opd_disc_remark . ' /Amount: ' . $opdMaster[0]->opd_discount . '/Update:' . $opdMaster[0]->opd_disc_update_by; } $user = auth()->user(); $userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User'; $userId = $user->id ?? 0; $userNameInfo = $userLabel . '[' . date('d-m-Y H:i:s') . ']'; $paymentModel = new PaymentModel(); $opdModel = new OpdModel(); if ($mode === 0) { $paydata = [ 'payment_mode' => '0', 'payof_type' => '1', 'payof_id' => $oid, 'payof_code' => $opdMaster[0]->opd_code, 'credit_debit' => '0', 'amount' => $opdMaster[0]->opd_fee_amount, 'payment_date' => date('Y-m-d H:i:s'), 'remark' => $payRemark, 'update_by' => $userNameInfo . ' [' . $userId . ']', 'update_by_id' => $userId, 'insert_code' => $spid, ]; $insertId = $paymentModel->insertPayment($paydata); $data = [ 'payment_mode' => '0', 'payment_status' => '1', 'payment_mode_desc' => 'No Cost', 'payment_id' => $insertId, 'confirm_pay_opd' => date('Y-m-d H:i:s'), 'prepared_by_id' => $userId, ]; $this->db->table('opd_master')->where('opd_id', $oid)->update($data); return $this->response->setJSON([ 'update' => 1, 'showcontent' => 'Zero Cost', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } if ($mode === 1 && $pendingAmt > 0) { $paydata = [ 'payment_mode' => '1', 'payof_type' => '1', 'payof_id' => $oid, 'payof_code' => $opdMaster[0]->opd_code, 'credit_debit' => '0', 'amount' => $opdMaster[0]->opd_fee_amount, 'payment_date' => date('Y-m-d H:i:s'), 'remark' => $payRemark, 'update_by' => $userNameInfo . ' [' . $userId . ']', 'update_by_id' => $userId, 'insert_code' => $spid, ]; $insertId = $paymentModel->insertPayment($paydata); $data = [ 'payment_mode' => '1', 'payment_status' => '1', 'payment_mode_desc' => 'Cash', 'payment_id' => $insertId, 'confirm_pay_opd' => date('Y-m-d H:i:s'), 'prepared_by_id' => $userId, ]; $this->db->table('opd_master')->where('opd_id', $oid)->update($data); return $this->response->setJSON([ 'update' => 1, 'showcontent' => 'CASH', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } if ($mode === 2 && $pendingAmt > 0) { $cardTran = trim((string) $this->request->getPost('input_card_tran')); if ($cardTran === '' || strlen($cardTran) < 3 || strlen($cardTran) > 15) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Card Transaction ID is required (3-15 chars).', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $paydata = [ 'payment_mode' => '2', 'payof_type' => '1', 'payof_id' => $oid, 'payof_code' => $opdMaster[0]->opd_code, 'credit_debit' => '0', 'amount' => $opdMaster[0]->opd_fee_amount, 'payment_date' => date('Y-m-d H:i:s'), 'remark' => $payRemark, 'update_by' => $userNameInfo . ' [' . $userId . ']', 'pay_bank_id' => (int) $this->request->getPost('cbo_pay_type'), 'card_tran_id' => $cardTran, 'update_by_id' => $userId, ]; $insertId = $paymentModel->insertPayment($paydata); $data = [ 'payment_mode' => '2', 'payment_status' => '1', 'payment_mode_desc' => 'Bank/Online', 'confirm_pay_opd' => date('Y-m-d H:i:s'), 'payment_id' => $insertId, 'prepared_by_id' => $userId, ]; $this->db->table('opd_master')->where('opd_id', $oid)->update($data); return $this->response->setJSON([ 'update' => 1, 'showcontent' => 'Bank Card', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } if ($mode === 4) { $data = [ 'payment_mode' => '4', 'payment_status' => '1', 'payment_mode_desc' => 'Org.Case Credit', 'confirm_pay_opd' => date('Y-m-d H:i:s'), 'insurance_case_id' => (int) $this->request->getPost('case_id'), 'prepared_by_id' => $userId, ]; $this->db->table('opd_master')->where('opd_id', $oid)->update($data); return $this->response->setJSON([ 'update' => 1, 'showcontent' => 'Org. Case Credit', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Invalid payment request', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_discount_update(int $opdId) { if (!$this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON([ 'update' => 0, 'error_text' => 'Invalid request', ]); } $opdId = (int) $this->request->getPost('oid'); if ($opdId <= 0) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Invalid OPD ID', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $discount = (float) $this->request->getPost('input_dis_amt'); $remark = (string) $this->request->getPost('input_dis_desc'); $user = auth()->user(); $userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User'; $userId = $user->id ?? 0; $userName = $userLabel . '[' . $userId . ']'; $sql = "select * from opd_master where opd_id=" . $opdId; $query = $this->db->query($sql); $opdMaster = $query->getResult(); if (empty($opdMaster)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'OPD not found', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $grossAmount = (float) ($opdMaster[0]->opd_fee_gross_amount ?? 0); $netAmount = $grossAmount - $discount; if ($netAmount < 0) { $netAmount = 0; } $dataUpdate = [ 'opd_discount' => $discount, 'opd_disc_remark' => $remark, 'opd_fee_amount' => $netAmount, 'opd_disc_update_by' => $userName, ]; $this->db->table('opd_master')->where('opd_id', $opdId)->update($dataUpdate); return $this->response->setJSON([ 'update' => 1, 'showcontent' => 'Discount updated', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } /** * Load scan/upload modal context for a specific OPD record. */ public function opd_load_doc(int $opdid) { $sql = "select *,date_format(apointment_date,'%d-%m-%Y') as str_apointment_date, (case payment_status when 1 then 'Cash' when 2 then 'Bank Card' when 3 then 'Org. Credit' else 'Pending' end) as Payment_type_str from opd_master where opd_id=" . (int) $opdid; $query = $this->db->query($sql); $opdMaster = $query->getResult(); if (empty($opdMaster)) { return $this->response->setStatusCode(404)->setBody('OPD not found'); } $opdRow = $opdMaster[0]; $sql = "select *,if(gender=1,'Male','Female') as xgender from patient_master where id='" . (int) $opdRow->p_id . "'"; $query = $this->db->query($sql); $personInfo = $query->getResult(); if (!empty($personInfo)) { $personInfo[0]->age = get_age_1($personInfo[0]->dob ?? null, $personInfo[0]->age ?? '', $personInfo[0]->age_in_month ?? '', $personInfo[0]->estimate_dob ?? '', $opdRow->apointment_date ?? null); } return view('billing/opd_scan_modal_body', [ 'opdid' => $opdid, 'opd_master' => $opdMaster, 'person_info' => $personInfo, ]); } /** * Save uploaded or captured scan file and queue it for asynchronous AI analysis. */ public function save_image(int $opdid) { $opdRow = $this->db->table('opd_master')->where('opd_id', $opdid)->get(1)->getRowArray(); if (empty($opdRow)) { return $this->response->setStatusCode(404)->setJSON(['update' => 0, 'error_text' => 'OPD not found']); } $uploadsDir = rtrim(FCPATH, '/\\') . '/uploads/' . date('Ymd'); if (!is_dir($uploadsDir)) { @mkdir($uploadsDir, 0777, true); } $baseFilename = 'OPD-' . $opdid . '-' . time(); $filename = $baseFilename . '.jpg'; $fullPath = $uploadsDir . '/' . $filename; $detectedMimeType = 'image/jpeg'; $isImageFile = 1; $detectedExt = '.jpg'; $saved = false; // Source 1: direct webcam file upload. $uploadedFile = $this->request->getFile('webcam'); if ($uploadedFile && $uploadedFile->isValid() && !$uploadedFile->hasMoved()) { $ext = strtolower((string) ($uploadedFile->getClientExtension() ?: $uploadedFile->guessExtension() ?: 'jpg')); if (!in_array($ext, ['jpg', 'jpeg', 'png', 'webp'], true)) { $ext = 'jpg'; } $filename = $baseFilename . '.' . $ext; $fullPath = $uploadsDir . '/' . $filename; $uploadedFile->move($uploadsDir, $filename); $saved = is_file($fullPath); $detectedMimeType = (string) ($uploadedFile->getClientMimeType() ?: 'image/jpeg'); $isImageFile = 1; $detectedExt = '.' . $ext; } // Source 2: manual file upload (PDF/image). if (!$saved) { $uploadedFile = $this->request->getFile('userfile'); if ($uploadedFile && $uploadedFile->isValid() && !$uploadedFile->hasMoved()) { $ext = strtolower((string) ($uploadedFile->getClientExtension() ?: $uploadedFile->guessExtension() ?: pathinfo((string) $uploadedFile->getName(), PATHINFO_EXTENSION))); if (!in_array($ext, ['pdf', 'jpg', 'jpeg', 'png', 'webp'], true)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Invalid file type. Upload PDF or image only.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $filename = $baseFilename . '.' . $ext; $fullPath = $uploadsDir . '/' . $filename; $uploadedFile->move($uploadsDir, $filename); $saved = is_file($fullPath); $detectedMimeType = (string) ($uploadedFile->getClientMimeType() ?: ($ext === 'pdf' ? 'application/pdf' : 'image/jpeg')); $isImageFile = $ext === 'pdf' ? 0 : 1; $detectedExt = '.' . $ext; } } // Source 3: base64 image data fallback from older capture flows. if (!$saved) { $dataUri = (string) ($this->request->getPost('webcam_data') ?? $this->request->getPost('image') ?? ''); if ($dataUri !== '' && preg_match('#^data:image/\w+;base64,#i', $dataUri) === 1) { preg_match('#^data:image/(\w+);base64,#i', $dataUri, $match); $imgExt = strtolower((string) ($match[1] ?? 'jpg')); if ($imgExt === 'jpeg') { $imgExt = 'jpg'; } if (!in_array($imgExt, ['jpg', 'png', 'webp'], true)) { $imgExt = 'jpg'; } $filename = $baseFilename . '.' . $imgExt; $fullPath = $uploadsDir . '/' . $filename; $raw = base64_decode((string) preg_replace('#^data:image/\w+;base64,#i', '', $dataUri)); if ($raw !== false && $raw !== '') { file_put_contents($fullPath, $raw); $saved = is_file($fullPath); $detectedMimeType = 'image/' . ($imgExt === 'jpg' ? 'jpeg' : $imgExt); $isImageFile = 1; $detectedExt = '.' . $imgExt; } } } if (!$saved) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Unable to capture image. Please allow camera and retry.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $user = auth()->user(); $userId = (int) ($user->id ?? 0); $userName = (string) ($user->username ?? $user->email ?? 'User'); $publicPath = '/uploads/' . date('Ymd') . '/' . $filename; $insertId = $this->insertOpdFileUploadRecord( $fullPath, $publicPath, (int) ($opdRow['p_id'] ?? 0), $opdid, (int) ($opdRow['insurance_case_id'] ?? 0), $userName, $userId, $detectedMimeType, $isImageFile, $detectedExt ); $scanType = $this->detectScanType((string) $filename); if ($insertId > 0 && $this->db->tableExists('file_upload_data')) { $fields = $this->db->getFieldNames('file_upload_data'); $update = []; if (in_array('scan_type', $fields, true)) { $update['scan_type'] = $scanType; } if (in_array('document_type', $fields, true)) { $update['document_type'] = ''; } if (in_array('content_description', $fields, true)) { $update['content_description'] = 'Queued for AI analysis'; } if (in_array('ai_status', $fields, true)) { $update['ai_status'] = 'pending'; } if (in_array('ai_alert_flag', $fields, true)) { $update['ai_alert_flag'] = 0; } if (!empty($update)) { $this->db->table('file_upload_data')->where('id', $insertId)->update($update); } } return $this->response->setJSON([ 'update' => 1, 'filename' => $filename, 'file_path' => $publicPath, 'file_id' => $insertId, 'scan_type' => $scanType, 'extracted_text' => '', 'auto_saved_to_prescription' => 0, 'opd_session_id' => 0, 'target_field' => '', 'ai_status' => 'pending', 'error_text' => 'File uploaded successfully', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_file_last_list(int $opdid = 0) { if (!$this->db->tableExists('file_upload_data')) { return $this->response->setBody('
No scan table found.
'); } $fields = $this->db->getFieldNames('file_upload_data'); $safeSelect = [ 'id', 'full_path', 'file_ext', 'insert_date', 'pid', 'opd_id', 'show_type', ]; foreach (['scan_type', 'document_type', 'content_description', 'ai_status', 'ai_alert_flag', 'ai_alert_text'] as $optional) { if (in_array($optional, $fields, true)) { $safeSelect[] = $optional; } } if (in_array('ai_provider', $fields, true)) { $safeSelect[] = 'ai_provider'; } $builder = $this->db->table('file_upload_data') ->select(implode(',', $safeSelect)) ->where('show_type', 0) ->orderBy('id', 'DESC') ->limit(10); if ($opdid > 0) { $builder->where('opd_id', $opdid); } $rows = $builder->get()->getResultArray(); $list = []; foreach ($rows as $row) { $raw = (string) ($row['full_path'] ?? ''); $publicPath = $raw; $pos = strpos(str_replace('\\', '/', $raw), '/uploads/'); if ($pos !== false) { $publicPath = substr(str_replace('\\', '/', $raw), $pos); } $ext = strtolower((string) ($row['file_ext'] ?? pathinfo($publicPath, PATHINFO_EXTENSION))); if ($ext !== '' && $ext[0] !== '.') { $ext = '.' . $ext; } $list[] = [ 'id' => (int) ($row['id'] ?? 0), 'path' => $publicPath, 'is_pdf' => $ext === '.pdf', 'insert_date' => !empty($row['insert_date']) ? date('d/m/Y-H:i', strtotime((string) $row['insert_date'])) : '', 'scan_type' => (string) ($row['scan_type'] ?? ''), 'document_type' => (string) ($row['document_type'] ?? ''), 'content_description' => (string) ($row['content_description'] ?? ''), 'ai_status' => (string) ($row['ai_status'] ?? ''), 'ai_alert_flag' => (int) ($row['ai_alert_flag'] ?? 0), 'ai_alert_text' => (string) ($row['ai_alert_text'] ?? ''), 'ai_provider' => (string) ($row['ai_provider'] ?? ''), ]; } return view('billing/opd_scan_last_list', ['opd_file_list' => $list]); } public function scan_ai_assist() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON([ 'update' => 0, 'error_text' => 'Invalid request', ]); } $opdid = (int) $this->request->getPost('opd_id'); $scanType = trim((string) $this->request->getPost('scan_type')); if ($scanType === '') { $scanType = 'general'; } $text = trim((string) $this->request->getPost('extracted_text')); if ($text === '' || strlen($text) < 20) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Extracted report text is too short for AI diagnosis assist.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $analysis = $this->analyzeScanForDiagnosisSupport($text, $scanType); if ($analysis === null) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'AI diagnosis assist unavailable right now.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $applyToOpd = (int) $this->request->getPost('apply_to_opd') === 1; $applied = false; $sessionId = 0; $updatedFields = []; if ($applyToOpd && $opdid > 0) { $applyResult = $this->applyAiScanAssistToPrescription($opdid, $analysis); $applied = (bool) ($applyResult['applied'] ?? false); $sessionId = (int) ($applyResult['session_id'] ?? 0); $updatedFields = is_array($applyResult['updated_fields'] ?? null) ? $applyResult['updated_fields'] : []; } return $this->response->setJSON([ 'update' => 1, 'analysis' => $analysis, 'applied' => $applied ? 1 : 0, 'opd_session_id' => $sessionId, 'updated_fields' => $updatedFields, 'error_text' => $applied ? 'AI diagnosis assist prepared and added to consult fields.' : 'AI diagnosis assist prepared.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } /** * Process one uploaded scan file through AI and update per-file metadata. */ public function scan_ai_process_file() { try { $fileId = (int) $this->request->getPost('file_id'); if ($fileId <= 0) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Invalid file ID.', 'csrfHash' => csrf_hash(), ]); } $applyToOpd = (int) $this->request->getPost('apply_to_opd') === 1; $result = $this->processAiForUploadedFile($fileId, $applyToOpd); return $this->response->setJSON([ 'update' => (int) ($result['update'] ?? 0), 'file_id' => $fileId, 'ai_status' => (string) ($result['ai_status'] ?? 'failed'), 'document_type' => (string) ($result['document_type'] ?? ''), 'content_description' => (string) ($result['content_description'] ?? ''), 'ai_alert_flag' => (int) ($result['ai_alert_flag'] ?? 0), 'ai_alert_text' => (string) ($result['ai_alert_text'] ?? ''), 'error_text' => (string) ($result['error_text'] ?? ''), 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } catch (\Throwable $e) { log_message('error', 'scan_ai_process_file failed: {message}', ['message' => $e->getMessage()]); return $this->response->setStatusCode(200)->setJSON([ 'update' => 0, 'ai_status' => 'failed', 'error_text' => 'Background AI processing failed.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } } public function opd_file_hide(int $fileId) { if (!$this->db->tableExists('file_upload_data')) { return $this->response->setStatusCode(404)->setBody('File table not found'); } $row = $this->db->table('file_upload_data')->select('id,opd_id')->where('id', $fileId)->get(1)->getRowArray(); if (empty($row)) { return $this->response->setStatusCode(404)->setBody('File not found'); } $user = auth()->user(); $userId = (int) ($user->id ?? 0); $fields = $this->db->getFieldNames('file_upload_data'); $update = ['show_type' => 1]; if (in_array('upload_by_id', $fields, true)) { $update['upload_by_id'] = $userId; } $this->db->table('file_upload_data')->where('id', $fileId)->update($update); return $this->opd_file_last_list((int) ($row['opd_id'] ?? 0)); } private function insertOpdFileUploadRecord(string $fullPath, string $publicPath, int $pid, int $opdid, int $caseId, string $uploadBy, int $uploadById, string $mimeType = 'image/jpeg', int $isImageFile = 1, string $fileExt = '.jpg'): int { if (!$this->db->tableExists('file_upload_data')) { return 0; } $fields = $this->db->getFieldNames('file_upload_data'); $ext = $fileExt !== '' ? $fileExt : ('.' . strtolower((string) pathinfo($fullPath, PATHINFO_EXTENSION))); $mimeType = trim($mimeType) !== '' ? $mimeType : 'application/octet-stream'; $isImageFile = $isImageFile > 0 ? 1 : 0; $insert = []; $fieldMap = [ 'name' => basename($fullPath), 'file_name' => basename($fullPath), 'orig_name' => basename($fullPath), 'client_name' => basename($fullPath), 'file_ext' => $ext, 'file_type' => $mimeType, 'full_path' => str_replace('\\', '/', $fullPath), 'file_path' => str_replace('\\', '/', dirname($fullPath)) . '/', 'raw_name' => pathinfo($fullPath, PATHINFO_FILENAME), 'file_size' => @filesize($fullPath) ? round((float) filesize($fullPath) / 1024, 2) : 0, 'is_image' => $isImageFile, 'image_width' => 0, 'image_height' => 0, 'image_type' => ltrim($ext, '.'), 'image_size_str' => '', 'insert_date' => Time::now('Asia/Kolkata')->toDateTimeString(), 'upload_by' => $uploadBy, 'upload_by_id' => $uploadById, 'pid' => $pid, 'opd_id' => $opdid, 'ipd_id' => 0, 'case_id' => $caseId, 'show_type' => 0, 'public_path' => $publicPath, ]; foreach ($fieldMap as $key => $value) { if (in_array($key, $fields, true)) { $insert[$key] = $value; } } $this->db->table('file_upload_data')->insert($insert); return (int) $this->db->insertID(); } private function detectScanType(string $filename): string { $low = strtolower($filename); if (str_contains($low, 'ecg')) { return 'ecg'; } if (str_contains($low, 'lab') || str_contains($low, 'report')) { return 'lab'; } return 'general'; } /** * Run the full AI pipeline for one uploaded scan file and persist per-file AI metadata. * * @return array */ private function processAiForUploadedFile(int $fileId, bool $applyToOpd = false): array { $this->resetScanAiDebug(); if (! $this->db->tableExists('file_upload_data')) { return ['update' => 0, 'ai_status' => 'failed', 'error_text' => 'File table not found']; } $fields = $this->db->getFieldNames('file_upload_data'); $select = ['id']; foreach (['opd_id', 'full_path', 'file_ext', 'file_type', 'name', 'file_name', 'scan_type'] as $col) { if (in_array($col, $fields, true)) { $select[] = $col; } } $row = $this->db->table('file_upload_data') ->select(implode(',', $select)) ->where('id', $fileId) ->get(1) ->getRowArray(); if (empty($row)) { return ['update' => 0, 'ai_status' => 'failed', 'error_text' => 'Scan file not found']; } // Mark row as processing so UI can reflect asynchronous progress. if (in_array('ai_status', $fields, true)) { $this->db->table('file_upload_data')->where('id', $fileId)->update(['ai_status' => 'processing']); } $fullPath = $this->resolveScanFilePath($row); if ($fullPath === '' || !is_file($fullPath)) { $this->updateScanAiColumns($fileId, [ 'ai_status' => 'failed', 'content_description' => 'Scan file not accessible for AI analysis', ], $fields); return ['update' => 0, 'ai_status' => 'failed', 'error_text' => 'Scan file not accessible']; } $filename = (string) ($row['name'] ?? $row['file_name'] ?? basename($fullPath)); $scanType = trim((string) ($row['scan_type'] ?? '')); if ($scanType === '') { $scanType = $this->detectScanType($filename); } $mimeType = $this->guessMimeTypeFromPath($fullPath, (string) ($row['file_type'] ?? '')); $readiness = $this->getScanAiReadiness(); $this->addScanAiDebug('readiness: ' . $readiness['summary']); // Fail early when neither Azure OpenAI nor Document Intelligence is configured. if (! $readiness['azure_openai'] && ! $readiness['docintel']) { $msg = 'AI not configured. Set Azure OpenAI deployment (vision-capable). Optional DocIntel improves OCR.'; $this->updateScanAiColumns($fileId, [ 'scan_type' => $scanType, 'document_type' => $this->classifyDocumentType('', $scanType, $filename), 'content_description' => $msg, 'ai_status' => 'failed', 'ai_alert_flag' => 0, 'ai_alert_text' => '', ], $fields); return [ 'update' => 0, 'ai_status' => 'failed', 'document_type' => $this->classifyDocumentType('', $scanType, $filename), 'content_description' => $msg, 'ai_alert_flag' => 0, 'ai_alert_text' => '', 'error_text' => $msg, ]; } // First try visual interpretation and OCR extraction in parallel-friendly sequence. $visual = $this->analyzeScanVisualSupport($fullPath, $scanType, $mimeType); $text = $this->extractTextFromScanFile($fullPath, $scanType, $mimeType); if ($text === null || trim($text) === '') { // If OCR text is weak, keep visual result as a safe fallback summary. if (is_array($visual) && !empty($visual)) { $documentType = trim((string) ($visual['document_type'] ?? '')); if ($documentType === '') { $documentType = $this->classifyDocumentType('', $scanType, $filename); } $contentDescription = trim((string) ($visual['content_description'] ?? '')); $risk = strtolower(trim((string) ($visual['risk_level'] ?? ''))); $aiAlertText = trim((string) ($visual['alert_text'] ?? '')); $detailLines = []; if ($contentDescription !== '') { $detailLines[] = 'Summary: ' . $contentDescription; } if ($risk !== '') { $detailLines[] = 'Risk level: ' . ucfirst($risk); } if ($aiAlertText !== '') { $detailLines[] = 'Alert: ' . $aiAlertText; } if (empty($detailLines)) { $detailLines[] = 'AI visual review completed.'; } $contentDescription = mb_substr(implode("\n", $detailLines), 0, 2000); $aiAlertFlag = in_array($risk, ['high', 'critical'], true) ? 1 : 0; if ($aiAlertFlag === 1 && $aiAlertText === '') { $aiAlertText = 'AI flagged possible abnormal finding'; } $this->updateScanAiColumns($fileId, [ 'scan_type' => $scanType, 'document_type' => $documentType, 'content_description' => $contentDescription, 'ai_status' => 'completed', 'ai_alert_flag' => $aiAlertFlag, 'ai_alert_text' => $aiAlertText, ], $fields); return [ 'update' => 1, 'ai_status' => 'completed', 'document_type' => $documentType, 'content_description' => $contentDescription, 'ai_alert_flag' => $aiAlertFlag, 'ai_alert_text' => $aiAlertText, 'error_text' => 'AI visual analysis completed', ]; } $documentType = $this->classifyDocumentType('', $scanType, $filename); $debugInfo = $this->formatScanAiDebug(); $failMsg = 'Azure providers could not read usable text from this file. ' . $readiness['summary'] . '. Ensure Azure deployment supports image input and DocIntel endpoint/API is valid.'; if ($debugInfo !== '') { $failMsg .= ' Debug: ' . $debugInfo; } log_message('warning', 'scan_ai_process_file extraction failed fileId={id}; {summary}', [ 'id' => $fileId, 'summary' => $readiness['summary'], ]); $this->updateScanAiColumns($fileId, [ 'scan_type' => $scanType, 'document_type' => $documentType, 'content_description' => $failMsg, 'ai_status' => 'failed', 'ai_alert_flag' => 0, 'ai_alert_text' => '', ], $fields); return [ 'update' => 0, 'ai_status' => 'failed', 'document_type' => $documentType, 'content_description' => $failMsg, 'ai_alert_flag' => 0, 'ai_alert_text' => '', 'error_text' => $failMsg, ]; } // Build structured diagnosis-support output from extracted text. $analysis = $this->analyzeScanForDiagnosisSupport($text, $scanType); $documentType = trim((string) ($visual['document_type'] ?? '')); if ($documentType === '') { $documentType = $this->classifyDocumentType($text, $scanType, $filename); } $visualSummary = trim((string) ($visual['content_description'] ?? '')); $contentDescription = $this->buildScanContentDescription($analysis, $text, $documentType); if ($visualSummary !== '') { $contentDescription = $contentDescription !== '' ? mb_substr($visualSummary . "\n" . $contentDescription, 0, 2000) : mb_substr($visualSummary, 0, 2000); } if (strtoupper($documentType) === 'ECG') { $contentDescription = $this->buildEcgDetailedDescription($text, $analysis, $contentDescription); } [$aiAlertFlag, $aiAlertText] = $this->buildScanAlert($analysis, $text); $visualRisk = strtolower(trim((string) ($visual['risk_level'] ?? ''))); if (in_array($visualRisk, ['high', 'critical'], true)) { $aiAlertFlag = 1; $visualAlertText = trim((string) ($visual['alert_text'] ?? '')); if ($visualAlertText !== '') { $aiAlertText = $visualAlertText; } } // Optionally append AI support to OPD prescription notes. if ($applyToOpd && !empty($analysis) && ((string) ($analysis['diagnosis_suitable'] ?? 'no')) === 'yes') { $this->applyAiScanAssistToPrescription((int) ($row['opd_id'] ?? 0), $analysis); } $this->updateScanAiColumns($fileId, [ 'scan_type' => $scanType, 'document_type' => $documentType, 'content_description' => $contentDescription, 'extract_text' => $text, 'extracted_text' => $text, 'ai_status' => 'completed', 'ai_alert_flag' => $aiAlertFlag, 'ai_alert_text' => $aiAlertText, ], $fields); return [ 'update' => 1, 'ai_status' => 'completed', 'document_type' => $documentType, 'content_description' => $contentDescription, 'ai_alert_flag' => $aiAlertFlag, 'ai_alert_text' => $aiAlertText, 'error_text' => 'AI analysis completed', ]; } /** * @param array $row */ private function resolveScanFilePath(array $row): string { // Accept both stored absolute path and recovered path under /public/uploads. $fullPath = str_replace('\\', '/', (string) ($row['full_path'] ?? '')); if ($fullPath !== '' && is_file($fullPath)) { return $fullPath; } if ($fullPath !== '') { $uploadsPos = strpos($fullPath, '/uploads/'); if ($uploadsPos !== false) { $relative = substr($fullPath, $uploadsPos + 1); $candidate = rtrim(FCPATH, '/\\') . '/' . str_replace('\\', '/', (string) $relative); if (is_file($candidate)) { return $candidate; } } } return ''; } /** * Infer MIME type from saved extension when upload metadata is missing. */ private function guessMimeTypeFromPath(string $path, string $existingMime = ''): string { $existingMime = trim($existingMime); if ($existingMime !== '') { return $existingMime; } $ext = strtolower((string) pathinfo($path, PATHINFO_EXTENSION)); if ($ext === 'pdf') { return 'application/pdf'; } if ($ext === 'png') { return 'image/png'; } if ($ext === 'webp') { return 'image/webp'; } return 'image/jpeg'; } /** * @param array|null $analysis */ private function buildScanContentDescription(?array $analysis, string $text, string $documentType): string { if (!empty($analysis)) { $summary = trim((string) ($analysis['clinical_summary'] ?? '')); $dx = trim((string) ($analysis['probable_diagnosis'] ?? '')); $reason = trim((string) ($analysis['suitability_reason'] ?? '')); $keyFindings = $this->sanitizeAiStringList($analysis['key_findings'] ?? []); $suggestedTests = $this->sanitizeAiStringList($analysis['suggested_tests'] ?? []); $redFlags = $this->sanitizeAiStringList($analysis['red_flags'] ?? []); $confidence = trim((string) ($analysis['confidence_note'] ?? '')); $parts = []; if ($summary !== '') { $parts[] = 'Summary: ' . $summary; } if ($dx !== '') { $parts[] = 'Impression: ' . $dx; } if (!empty($keyFindings)) { $parts[] = 'Findings: ' . implode('; ', array_slice($keyFindings, 0, 6)); } if (!empty($redFlags)) { $parts[] = 'Red flags: ' . implode('; ', array_slice($redFlags, 0, 4)); } if (!empty($suggestedTests)) { $parts[] = 'Suggested tests: ' . implode('; ', array_slice($suggestedTests, 0, 5)); } if ($reason !== '') { $parts[] = 'Suitability: ' . $reason; } if ($confidence !== '') { $parts[] = 'Confidence: ' . $confidence; } if (!empty($parts)) { return mb_substr(implode("\n", $parts), 0, 2000); } } $plain = trim((string) preg_replace('/\s+/', ' ', $text)); if ($plain === '') { return $documentType . ' report uploaded.'; } return mb_substr($plain, 0, 500); } /** * @param array|null $analysis */ private function buildEcgDetailedDescription(string $text, ?array $analysis, string $baseSummary): string { $flat = trim((string) preg_replace('/\s+/', ' ', $text)); $metrics = []; $patterns = [ 'Ventricular rate' => '/(?:vent\.?\s*rate|heart\s*rate|hr)\s*[:=]?\s*(\d{2,3})\s*(?:bpm)?/i', 'PR interval' => '/pr\s*(?:int|interval)?\s*[:=]?\s*(\d{2,4})\s*(?:ms)?/i', 'QRS duration' => '/qrs\s*(?:int|duration)?\s*[:=]?\s*(\d{2,4})\s*(?:ms)?/i', 'QT/QTc' => '/qt\/?qtc\s*(?:int)?\s*[:=]?\s*([\d\.\-\s\/]{3,20})/i', 'Axis' => '/axis\s*(?:deg)?\s*[:=]?\s*([\d\-\+\s\/]{1,20})/i', ]; foreach ($patterns as $label => $regex) { if (preg_match($regex, $flat, $match) === 1) { $value = trim((string) ($match[1] ?? '')); if ($value !== '') { $metrics[] = $label . ': ' . $value; } } } $findings = []; if (is_array($analysis)) { $summary = trim((string) ($analysis['clinical_summary'] ?? '')); $dx = trim((string) ($analysis['probable_diagnosis'] ?? '')); $keyFindings = $this->sanitizeAiStringList($analysis['key_findings'] ?? []); if ($summary !== '') { $findings[] = $summary; } if ($dx !== '') { $findings[] = 'Impression: ' . $dx; } if (!empty($keyFindings)) { $findings[] = 'Findings: ' . implode('; ', array_slice($keyFindings, 0, 4)); } } if (empty($findings) && trim($baseSummary) !== '') { $findings[] = trim($baseSummary); } $chunks = []; if (!empty($metrics)) { $chunks[] = implode(' | ', array_slice($metrics, 0, 5)); } if (!empty($findings)) { $chunks[] = implode(' | ', array_slice($findings, 0, 3)); } if (empty($chunks)) { return 'ECG uploaded. AI review completed.'; } return mb_substr(implode("\n", $chunks), 0, 1000); } /** * @param array|null $analysis * @return array{0:int,1:string} */ private function buildScanAlert(?array $analysis, string $text): array { if (!empty($analysis)) { $redFlags = $this->sanitizeAiStringList($analysis['red_flags'] ?? []); if (!empty($redFlags)) { return [1, mb_substr(implode('; ', $redFlags), 0, 250)]; } $probableDx = strtolower(trim((string) ($analysis['probable_diagnosis'] ?? ''))); foreach (['abnormal', 'acute', 'critical', 'infarct', 'ischemia', 'stroke', 'sepsis', 'hemorrhage', 'emergency'] as $keyword) { if ($probableDx !== '' && str_contains($probableDx, $keyword)) { return [1, 'AI flagged possible ' . $keyword . ' risk']; } } } $txt = strtolower($text); foreach (['critical', 'urgent', 'abnormal', 'positive for', 'high risk'] as $keyword) { if (str_contains($txt, $keyword)) { return [1, 'Report text suggests ' . $keyword]; } } return [0, '']; } /** * Classify broad document type using scan hints, filename and text keywords. */ private function classifyDocumentType(string $text, string $scanType, string $filename): string { $pool = strtolower($scanType . ' ' . $filename . ' ' . $text); $map = [ 'ECG' => ['ecg', 'electrocardio', 'st segment', 'qrs'], 'Pathology' => ['pathology', 'haemoglobin', 'hemoglobin', 'cbc', 'blood', 'urine', 'biochemistry', 'platelet'], 'XRay' => ['xray', 'x-ray', 'radiograph', 'chest pa'], 'CT' => ['ct scan', 'computed tomography'], 'MRI' => ['mri', 'magnetic resonance'], 'Ultrasound' => ['usg', 'ultrasound', 'sonography'], ]; foreach ($map as $type => $keywords) { foreach ($keywords as $keyword) { if (str_contains($pool, $keyword)) { return $type; } } } return strtoupper($scanType) !== 'GENERAL' ? strtoupper($scanType) : 'General'; } /** * @return array|null */ private function analyzeScanVisualSupport(string $filePath, string $scanType, string $mimeType): ?array { if (!is_file($filePath)) { return null; } $raw = @file_get_contents($filePath); if ($raw === false || $raw === '') { return null; } $maxBytes = $mimeType === 'application/pdf' ? (12 * 1024 * 1024) : (8 * 1024 * 1024); if (strlen($raw) > $maxBytes) { return null; } $prompt = "You are a clinical assistant. Analyze this medical report image/document visually (even if OCR text is unclear).\n" . "Return ONLY valid JSON object with keys: document_type, content_description, risk_level, alert_text.\n" . "Rules:\n" . "- document_type must be one of: ECG, Pathology, XRay, CT, MRI, Ultrasound, General.\n" . "- risk_level must be one of: low, moderate, high, critical.\n" . "- content_description should be 1 short cautious summary line.\n" . "- alert_text should be short and only when risk is high/critical; else empty string.\n" . "- If uncertain, set document_type based on visible format pattern and keep conservative language.\n" . "Scan type hint: " . strtoupper($scanType); // Ask vision-capable model for a conservative JSON summary. $azureRaw = $this->callAzureOpenAiVision($filePath, $mimeType, $prompt, 500, true); $azureParsed = $this->parseJsonFromAiRaw($azureRaw); if (is_array($azureParsed)) { $documentType = trim((string) ($azureParsed['document_type'] ?? 'General')); if (!in_array($documentType, ['ECG', 'Pathology', 'XRay', 'CT', 'MRI', 'Ultrasound', 'General'], true)) { $documentType = 'General'; } $riskLevel = strtolower(trim((string) ($azureParsed['risk_level'] ?? 'low'))); if (!in_array($riskLevel, ['low', 'moderate', 'high', 'critical'], true)) { $riskLevel = 'low'; } return [ 'document_type' => $documentType, 'content_description' => trim((string) ($azureParsed['content_description'] ?? '')), 'risk_level' => $riskLevel, 'alert_text' => trim((string) ($azureParsed['alert_text'] ?? '')), ]; } return null; } /** * @param array $payload * @param array $fields */ private function updateScanAiColumns(int $fileId, array $payload, array $fields): void { $update = []; foreach ($payload as $key => $value) { if (in_array($key, $fields, true)) { $update[$key] = $value; } } if (!empty($update)) { $this->db->table('file_upload_data')->where('id', $fileId)->update($update); } } /** * @return array|null */ private function analyzeScanForDiagnosisSupport(string $extractedText, string $scanType): ?array { // No text means there is nothing reliable to assess. if (trim($extractedText) === '') { return null; } $prompt = "You are a clinical decision-support assistant for OPD doctors.\n" . "Input is OCR text from scanned/uploaded patient reports (lab/xray/ecg/general).\n" . "Determine if this report text is suitable for diagnosis support.\n" . "Return ONLY valid JSON object with keys:\n" . "diagnosis_suitable (yes/no), suitability_reason, probable_diagnosis, clinical_summary, key_findings, suggested_tests, red_flags, confidence_note.\n" . "Rules:\n" . "- Keep content concise, medically cautious, non-final.\n" . "- key_findings/suggested_tests/red_flags must be JSON arrays of short strings.\n" . "- Do not invent values not present in text.\n" . "Scan Type: " . strtoupper($scanType) . "\n" . "Report Text:\n" . $extractedText; $azureRaw = $this->callAzureOpenAiText($prompt, 900, true); $azureParsed = $this->parseJsonFromAiRaw($azureRaw); if (is_array($azureParsed)) { return [ 'diagnosis_suitable' => strtolower(trim((string) ($azureParsed['diagnosis_suitable'] ?? 'no'))) === 'yes' ? 'yes' : 'no', 'suitability_reason' => trim((string) ($azureParsed['suitability_reason'] ?? '')), 'probable_diagnosis' => trim((string) ($azureParsed['probable_diagnosis'] ?? '')), 'clinical_summary' => trim((string) ($azureParsed['clinical_summary'] ?? '')), 'key_findings' => $this->sanitizeAiStringList($azureParsed['key_findings'] ?? []), 'suggested_tests' => $this->sanitizeAiStringList($azureParsed['suggested_tests'] ?? []), 'red_flags' => $this->sanitizeAiStringList($azureParsed['red_flags'] ?? []), 'confidence_note' => trim((string) ($azureParsed['confidence_note'] ?? '')), ]; } return null; } /** * @return array{applied:bool,session_id:int,updated_fields:array} */ private function applyAiScanAssistToPrescription(int $opdid, array $analysis): array { if (! $this->db->tableExists('opd_prescription')) { return ['applied' => false, 'session_id' => 0, 'updated_fields' => []]; } $fields = $this->db->getFieldNames('opd_prescription'); $diagnosisField = $this->resolveFirstField($fields, ['diagnosis', 'provisional_diagnosis']); $findingField = $this->resolveFirstField($fields, ['Finding_Examinations', 'finding_examinations']); $investigationField = $this->resolveFirstField($fields, ['investigation']); $row = $this->db->table('opd_prescription') ->where('opd_id', $opdid) ->orderBy('id', 'DESC') ->get(1) ->getRowArray(); if (empty($row)) { $opdMaster = $this->db->table('opd_master')->where('opd_id', $opdid)->get(1)->getRowArray(); if (empty($opdMaster)) { return ['applied' => false, 'session_id' => 0, 'updated_fields' => []]; } $insert = []; foreach ([ 'opd_id' => $opdid, 'p_id' => (int) ($opdMaster['p_id'] ?? 0), 'doc_id' => (int) ($opdMaster['doc_id'] ?? 0), 'date_opd_visit' => date('Y-m-d'), 'visit_status' => 0, 'session_id' => 0, ] as $k => $v) { if (in_array($k, $fields, true)) { $insert[$k] = $v; } } $this->db->table('opd_prescription')->insert($insert); $newId = (int) $this->db->insertID(); $row = $this->db->table('opd_prescription')->where('id', $newId)->get(1)->getRowArray(); } if (empty($row)) { return ['applied' => false, 'session_id' => 0, 'updated_fields' => []]; } $sessionId = (int) ($row['id'] ?? 0); $prefix = '[AI-SCAN ' . date('d-m-Y H:i') . ']'; $summary = trim((string) ($analysis['clinical_summary'] ?? '')); $probableDx = trim((string) ($analysis['probable_diagnosis'] ?? '')); $confidence = trim((string) ($analysis['confidence_note'] ?? '')); $findings = $this->sanitizeAiStringList($analysis['key_findings'] ?? []); $tests = $this->sanitizeAiStringList($analysis['suggested_tests'] ?? []); $redFlags = $this->sanitizeAiStringList($analysis['red_flags'] ?? []); $updateData = []; $updatedFields = []; if ($diagnosisField !== null && $probableDx !== '') { $chunk = $prefix . ' Probable diagnosis support: ' . $probableDx; if ($confidence !== '') { $chunk .= ' (' . $confidence . ')'; } $old = trim((string) ($row[$diagnosisField] ?? '')); if (! str_contains($old, $chunk)) { $updateData[$diagnosisField] = $old === '' ? $chunk : ($old . "\n" . $chunk); $updatedFields[] = $diagnosisField; } } if ($findingField !== null) { $parts = []; if ($summary !== '') { $parts[] = 'Summary: ' . $summary; } if (! empty($findings)) { $parts[] = 'Findings: ' . implode('; ', $findings); } if (! empty($redFlags)) { $parts[] = 'Red flags: ' . implode('; ', $redFlags); } if (! empty($parts)) { $chunk = $prefix . ' ' . implode(' | ', $parts); $old = trim((string) ($row[$findingField] ?? '')); if (! str_contains($old, $chunk)) { $updateData[$findingField] = $old === '' ? $chunk : ($old . "\n" . $chunk); $updatedFields[] = $findingField; } } } if ($investigationField !== null && ! empty($tests)) { $chunk = $prefix . ' Suggested tests: ' . implode('; ', $tests); $old = trim((string) ($row[$investigationField] ?? '')); if (! str_contains($old, $chunk)) { $updateData[$investigationField] = $old === '' ? $chunk : ($old . "\n" . $chunk); $updatedFields[] = $investigationField; } } if (! empty($updateData)) { $this->db->table('opd_prescription')->where('id', $sessionId)->update($updateData); } return [ 'applied' => ! empty($updateData), 'session_id' => $sessionId, 'updated_fields' => $updatedFields, ]; } /** * @param mixed $raw * @return array */ private function sanitizeAiStringList($raw): array { // Normalize AI arrays into unique, non-empty strings. if (! is_array($raw)) { return []; } $out = []; foreach ($raw as $item) { $txt = trim((string) $item); if ($txt !== '') { $out[] = $txt; } } return array_values(array_unique($out)); } /** * @return array|null */ private function parseJsonFromAiRaw(string $raw): ?array { $text = trim($raw); if ($text === '') { return null; } // First attempt: raw response is already valid JSON. $decoded = json_decode($text, true); if (is_array($decoded)) { return $decoded; } // Second attempt: extract JSON object embedded in model prose. if (preg_match('/\{(?:[^{}]|(?R))*\}/s', $text, $match) === 1) { $decoded = json_decode((string) ($match[0] ?? ''), true); if (is_array($decoded)) { return $decoded; } } return null; } /** * @param array $constCandidates * @param array $settingCandidates */ private function readScanAiSettingValue(array $constCandidates, array $settingCandidates): string { foreach ($constCandidates as $constName) { if (! defined($constName)) { continue; } $value = trim((string) constant($constName)); if ($value !== '') { return $value; } } if ($this->db->tableExists('hospital_setting') && ! empty($settingCandidates)) { $row = $this->db->table('hospital_setting') ->select('s_value') ->whereIn('s_name', $settingCandidates) ->where('s_value !=', '') ->orderBy('s_name', 'ASC') ->get(1) ->getRowArray(); if (! empty($row['s_value'])) { return trim((string) $row['s_value']); } } return ''; } /** * @return array{endpoint:string,api_key:string,deployment:string,api_version:string} */ private function getAzureOpenAiConfigForScan(): array { $endpoint = $this->readScanAiSettingValue( ['AZURE_OPENAI_ENDPOINT', 'AI_AZURE_OPENAI_ENDPOINT', 'APP_AZURE_OPENAI_ENDPOINT', 'H_AZURE_OPENAI_ENDPOINT'], ['AZURE_OPENAI_ENDPOINT'] ); $apiKey = $this->readScanAiSettingValue( ['AZURE_OPENAI_API_KEY', 'AI_AZURE_OPENAI_API_KEY', 'APP_AZURE_OPENAI_API_KEY', 'H_AZURE_OPENAI_API_KEY'], ['AZURE_OPENAI_API_KEY'] ); $deployment = $this->readScanAiSettingValue( ['AZURE_OPENAI_DEPLOYMENT', 'AI_AZURE_OPENAI_DEPLOYMENT', 'APP_AZURE_OPENAI_DEPLOYMENT', 'H_AZURE_OPENAI_DEPLOYMENT'], ['AZURE_OPENAI_DEPLOYMENT'] ); $apiVersion = $this->readScanAiSettingValue( ['AZURE_OPENAI_API_VERSION', 'AI_AZURE_OPENAI_API_VERSION', 'APP_AZURE_OPENAI_API_VERSION', 'H_AZURE_OPENAI_API_VERSION'], ['AZURE_OPENAI_API_VERSION'] ); if ($apiVersion === '') { $apiVersion = '2024-10-21'; } $endpoint = $this->normalizeAzureEndpointBase($endpoint); return [ 'endpoint' => $endpoint, 'api_key' => $apiKey, 'deployment' => $deployment, 'api_version' => $apiVersion, ]; } /** * @return array{azure_openai:bool,docintel:bool,summary:string} */ private function getScanAiReadiness(): array { $azure = $this->getAzureOpenAiConfigForScan(); $azureOpenAi = ($azure['endpoint'] !== '' && $azure['api_key'] !== '' && $azure['deployment'] !== ''); $docIntelEndpoint = rtrim($this->readScanAiSettingValue( ['AZURE_DOCINTEL_ENDPOINT', 'AI_AZURE_DOCINTEL_ENDPOINT', 'APP_AZURE_DOCINTEL_ENDPOINT', 'H_AZURE_DOCINTEL_ENDPOINT'], ['AZURE_DOCINTEL_ENDPOINT'] ), '/'); $docIntelKey = $this->readScanAiSettingValue( ['AZURE_DOCINTEL_KEY', 'AI_AZURE_DOCINTEL_KEY', 'APP_AZURE_DOCINTEL_KEY', 'H_AZURE_DOCINTEL_KEY'], ['AZURE_DOCINTEL_KEY'] ); $docIntel = ($docIntelEndpoint !== '' && $docIntelKey !== ''); $parts = []; $parts[] = 'AzureOpenAI=' . ($azureOpenAi ? 'set' : 'missing endpoint/key/deployment'); $parts[] = 'DocIntel=' . ($docIntel ? 'set' : 'missing endpoint/key'); return [ 'azure_openai' => $azureOpenAi, 'docintel' => $docIntel, 'summary' => implode(', ', $parts), ]; } private function callAzureOpenAiVision(string $filePath, string $mimeType, string $prompt, int $maxTokens = 1200, bool $jsonObject = false): string { $cfg = $this->getAzureOpenAiConfigForScan(); if ($cfg['endpoint'] === '' || $cfg['api_key'] === '' || $cfg['deployment'] === '') { $this->addScanAiDebug('azure vision skipped: endpoint/key/deployment missing'); return ''; } if (! str_starts_with($mimeType, 'image/')) { $this->addScanAiDebug('azure vision skipped: non-image mime ' . $mimeType); return ''; } $bytes = @file_get_contents($filePath); if ($bytes === false || $bytes === '') { $this->addScanAiDebug('azure vision skipped: file read failed'); return ''; } $dataUrl = 'data:' . $mimeType . ';base64,' . base64_encode($bytes); $url = $cfg['endpoint'] . '/openai/deployments/' . rawurlencode($cfg['deployment']) . '/chat/completions?api-version=' . rawurlencode($cfg['api_version']); for ($attempt = 0; $attempt < 2; $attempt++) { $forceInsecure = $attempt === 1; try { $client = service('curlrequest', $this->buildAzureCurlOptions($cfg['endpoint'], 30, $forceInsecure)); $payload = [ 'messages' => [[ 'role' => 'user', 'content' => [ ['type' => 'text', 'text' => $prompt], ['type' => 'image_url', 'image_url' => ['url' => $dataUrl]], ], ]], 'temperature' => 0.1, 'max_tokens' => $maxTokens, ]; if ($jsonObject) { $payload['response_format'] = ['type' => 'json_object']; } $response = $client->post($url, [ 'headers' => [ 'api-key' => $cfg['api_key'], 'Content-Type' => 'application/json', ], 'json' => $payload, ]); if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { $this->addScanAiDebug('azure vision HTTP ' . $response->getStatusCode()); return ''; } $decoded = json_decode((string) $response->getBody(), true); $content = trim((string) ($decoded['choices'][0]['message']['content'] ?? '')); if ($content === '') { $this->addScanAiDebug('azure vision returned empty content'); } return $content; } catch (\Throwable $e) { if (! $forceInsecure && $this->isSslCertificateError($e)) { $this->addScanAiDebug('azure vision SSL error; retry insecure'); continue; } $this->addScanAiDebug('azure vision exception: ' . $e->getMessage()); return ''; } } return ''; } private function callAzureOpenAiText(string $prompt, int $maxTokens = 1200, bool $jsonObject = false): string { $cfg = $this->getAzureOpenAiConfigForScan(); if ($cfg['endpoint'] === '' || $cfg['api_key'] === '' || $cfg['deployment'] === '') { $this->addScanAiDebug('azure text skipped: endpoint/key/deployment missing'); return ''; } $url = $cfg['endpoint'] . '/openai/deployments/' . rawurlencode($cfg['deployment']) . '/chat/completions?api-version=' . rawurlencode($cfg['api_version']); for ($attempt = 0; $attempt < 2; $attempt++) { $forceInsecure = $attempt === 1; try { $client = service('curlrequest', $this->buildAzureCurlOptions($cfg['endpoint'], 30, $forceInsecure)); $payload = [ 'messages' => [[ 'role' => 'user', 'content' => $prompt, ]], 'temperature' => 0.1, 'max_tokens' => $maxTokens, ]; if ($jsonObject) { $payload['response_format'] = ['type' => 'json_object']; } $response = $client->post($url, [ 'headers' => [ 'api-key' => $cfg['api_key'], 'Content-Type' => 'application/json', ], 'json' => $payload, ]); if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { $this->addScanAiDebug('azure text HTTP ' . $response->getStatusCode()); return ''; } $decoded = json_decode((string) $response->getBody(), true); $content = trim((string) ($decoded['choices'][0]['message']['content'] ?? '')); if ($content === '') { $this->addScanAiDebug('azure text returned empty content'); } return $content; } catch (\Throwable $e) { if (! $forceInsecure && $this->isSslCertificateError($e)) { $this->addScanAiDebug('azure text SSL error; retry insecure'); continue; } $this->addScanAiDebug('azure text exception: ' . $e->getMessage()); return ''; } } return ''; } private function extractTextWithAzureDocumentIntelligence(string $filePath): string { $endpoint = rtrim($this->readScanAiSettingValue( ['AZURE_DOCINTEL_ENDPOINT', 'AI_AZURE_DOCINTEL_ENDPOINT', 'APP_AZURE_DOCINTEL_ENDPOINT', 'H_AZURE_DOCINTEL_ENDPOINT'], ['AZURE_DOCINTEL_ENDPOINT'] ), '/'); $endpoint = $this->normalizeAzureEndpointBase($endpoint); $key = $this->readScanAiSettingValue( ['AZURE_DOCINTEL_KEY', 'AI_AZURE_DOCINTEL_KEY', 'APP_AZURE_DOCINTEL_KEY', 'H_AZURE_DOCINTEL_KEY'], ['AZURE_DOCINTEL_KEY'] ); if ($endpoint === '' || $key === '' || !is_file($filePath)) { $this->addScanAiDebug('docintel skipped: endpoint/key/file missing'); return ''; } for ($attempt = 0; $attempt < 2; $attempt++) { $forceInsecure = $attempt === 1; try { $client = service('curlrequest', $this->buildAzureCurlOptions($endpoint, 40, $forceInsecure)); $bytes = @file_get_contents($filePath); if ($bytes === false || $bytes === '') { return ''; } $mimeType = $this->guessMimeTypeFromPath($filePath); $analyzeUrls = [ $endpoint . '/documentintelligence/documentModels/prebuilt-read:analyze?api-version=2024-02-29-preview', $endpoint . '/documentintelligence/documentModels/prebuilt-read:analyze?api-version=2023-07-31', $endpoint . '/formrecognizer/documentModels/prebuilt-read:analyze?api-version=2023-07-31', ]; // Try multiple API routes for compatibility with different Azure environments. foreach ($analyzeUrls as $analyzeUrl) { $submit = $client->post($analyzeUrl, [ 'headers' => [ 'Content-Type' => $mimeType !== '' ? $mimeType : 'application/octet-stream', 'Ocp-Apim-Subscription-Key' => $key, ], 'body' => $bytes, ]); if ($submit->getStatusCode() < 200 || $submit->getStatusCode() >= 300) { $this->addScanAiDebug('docintel submit HTTP ' . $submit->getStatusCode() . ' @ ' . basename($analyzeUrl)); continue; } $submitBody = json_decode((string) $submit->getBody(), true); if (is_array($submitBody)) { $inlineText = $this->extractTextFromDocIntelPayload($submitBody); if ($inlineText !== '') { return $inlineText; } } $operationLocation = trim((string) $submit->getHeaderLine('Operation-Location')); if ($operationLocation === '') { $this->addScanAiDebug('docintel missing operation-location @ ' . basename($analyzeUrl)); continue; } for ($i = 0; $i < 14; $i++) { usleep(900000); $poll = $client->get($operationLocation, [ 'headers' => [ 'Ocp-Apim-Subscription-Key' => $key, ], ]); if ($poll->getStatusCode() < 200 || $poll->getStatusCode() >= 300) { $this->addScanAiDebug('docintel poll HTTP ' . $poll->getStatusCode()); continue; } $decoded = json_decode((string) $poll->getBody(), true); if (!is_array($decoded)) { continue; } $status = strtolower((string) ($decoded['status'] ?? '')); if ($status === 'succeeded') { $text = $this->extractTextFromDocIntelPayload($decoded); if ($text !== '') { return $text; } } if ($status === 'failed') { $this->addScanAiDebug('docintel status failed'); break; } } } return ''; } catch (\Throwable $e) { if (! $forceInsecure && $this->isSslCertificateError($e)) { $this->addScanAiDebug('docintel SSL error; retry insecure'); continue; } $this->addScanAiDebug('docintel exception: ' . $e->getMessage()); return ''; } } return ''; } private function extractTextFromDocIntelPayload(array $payload): string { // Prefer native concatenated content when present. $content = trim((string) ($payload['analyzeResult']['content'] ?? '')); if ($content !== '') { return $content; } // Fallback: rebuild text from line entries. $lines = []; $pages = $payload['analyzeResult']['pages'] ?? []; foreach ($pages as $page) { $pageLines = $page['lines'] ?? []; foreach ($pageLines as $line) { $txt = trim((string) ($line['content'] ?? '')); if ($txt !== '') { $lines[] = $txt; } } } return trim(implode("\n", $lines)); } private function extractTextFromScanFile(string $filePath, string $scanType, ?string $mimeType = null): ?string { if (!is_file($filePath)) { return null; } $raw = @file_get_contents($filePath); if ($raw === false || $raw === '') { return null; } $mimeType = trim((string) ($mimeType ?? '')); if ($mimeType === '') { $mimeType = $this->guessMimeTypeFromPath($filePath); } $maxBytes = $mimeType === 'application/pdf' ? (12 * 1024 * 1024) : (8 * 1024 * 1024); if (strlen($raw) > $maxBytes) { return null; } // OCR priority: Document Intelligence first, then vision model fallback. $prompt = 'Extract readable clinical text from this ' . strtoupper($scanType) . ' medical document. Return plain text with sections: Findings, Impression, Values. Do not invent data.'; $docIntelText = $this->extractTextWithAzureDocumentIntelligence($filePath); if ($docIntelText !== '') { return preg_replace('/\s+/', ' ', trim($docIntelText)) ?: trim($docIntelText); } $azurePrompt = 'Read and extract all visible clinical text from this ' . strtoupper($scanType) . ' medical image. Return plain text only. If unclear, return best readable lines.'; $azureText = $this->callAzureOpenAiVision($filePath, $mimeType, $azurePrompt, 1800, false); if ($azureText !== '') { return preg_replace('/\s+/', ' ', trim($azureText)) ?: trim($azureText); } return null; } /** * @return array{updated:bool,session_id:int,target_field:string} */ private function appendExtractedTextToOpdPrescription(int $opdid, string $scanType, string $text): array { if (!$this->db->tableExists('opd_prescription')) { return ['updated' => false, 'session_id' => 0, 'target_field' => '']; } $fields = $this->db->getFieldNames('opd_prescription'); $targetField = in_array('Finding_Examinations', $fields, true) ? 'Finding_Examinations' : ''; if (in_array($scanType, ['ecg', 'lab'], true) && in_array('investigation', $fields, true)) { $targetField = 'investigation'; } elseif ($targetField === '' && in_array('diagnosis', $fields, true)) { $targetField = 'diagnosis'; } if ($targetField === '') { return ['updated' => false, 'session_id' => 0, 'target_field' => '']; } $row = $this->db->table('opd_prescription') ->where('opd_id', $opdid) ->orderBy('id', 'DESC') ->get(1) ->getRowArray(); if (empty($row)) { $opdMaster = $this->db->table('opd_master')->where('opd_id', $opdid)->get(1)->getRowArray(); if (empty($opdMaster)) { return ['updated' => false, 'session_id' => 0, 'target_field' => '']; } $insert = []; foreach ([ 'opd_id' => $opdid, 'p_id' => (int) ($opdMaster['p_id'] ?? 0), 'doc_id' => (int) ($opdMaster['doc_id'] ?? 0), 'date_opd_visit' => date('Y-m-d'), 'visit_status' => 0, 'session_id' => 0, ] as $k => $v) { if (in_array($k, $fields, true)) { $insert[$k] = $v; } } $this->db->table('opd_prescription')->insert($insert); $newId = (int) $this->db->insertID(); $row = $this->db->table('opd_prescription')->where('id', $newId)->get(1)->getRowArray(); } if (empty($row)) { return ['updated' => false, 'session_id' => 0, 'target_field' => '']; } $sessionId = (int) ($row['id'] ?? 0); $oldValue = trim((string) ($row[$targetField] ?? '')); $prefix = '[SCAN-' . strtoupper($scanType) . ' ' . date('d-m-Y H:i') . ']'; $newChunk = $prefix . ' ' . trim($text); if ($oldValue !== '' && str_contains($oldValue, $newChunk)) { return ['updated' => true, 'session_id' => $sessionId, 'target_field' => $targetField]; } $newValue = $oldValue === '' ? $newChunk : ($oldValue . "\n" . $newChunk); $this->db->table('opd_prescription')->where('id', $sessionId)->update([$targetField => $newValue]); return ['updated' => true, 'session_id' => $sessionId, 'target_field' => $targetField]; } private function resolveFirstField(array $fields, array $candidates): ?string { foreach ($candidates as $candidate) { if (in_array($candidate, $fields, true)) { return $candidate; } } return null; } private function findExistingTable(array $candidates): ?string { foreach ($candidates as $table) { if ($this->db->tableExists($table)) { return $table; } } return null; } private function hydrateNabhPrintFields(array $row, string $remarks): array { $pick = function (array $candidates) use ($row): string { foreach ($candidates as $field) { $value = trim((string) ($row[$field] ?? '')); if ($value !== '') { return $value; } } return ''; }; $data = [ 'drug_allergy_status' => $pick(['drug_allergy_status', 'allergy_status', 'drug_allergy']), 'drug_allergy_details' => $pick(['drug_allergy_details', 'allergy_details', 'drug_allergy_note', 'allergy_note']), 'adr_history' => $pick(['adr_history', 'adverse_drug_reaction', 'adr_details', 'adverse_reaction_history']), 'current_medications' => $pick(['current_medications', 'current_medication', 'current_medication_history', 'ongoing_medications']), ]; $parsed = $this->extractNabhFieldsFromRemarks($remarks); foreach ($data as $key => $value) { if ($value === '' && !empty($parsed[$key])) { $data[$key] = (string) $parsed[$key]; } } return $data; } private function extractNabhFieldsFromRemarks(string $remarks): array { $extract = static function (string $pattern, string $source): string { if (preg_match($pattern, $source, $m) !== 1) { return ''; } return trim((string) ($m[1] ?? '')); }; return [ 'drug_allergy_status' => $extract('/^\s*Drug\s*Allergy\s*Status\s*:\s*(.+)$/im', $remarks), 'drug_allergy_details' => $extract('/^\s*Drug\s*Allergy\s*Details\s*:\s*(.+)$/im', $remarks), 'adr_history' => $extract('/^\s*ADR\s*History\s*:\s*(.+)$/im', $remarks), 'current_medications' => $extract('/^\s*Current\s*Medications\s*:\s*(.+)$/im', $remarks), ]; } private function extractWomenRelatedProblemsFromRemarks(string $remarks): string { if ($remarks === '') { return ''; } $lines = preg_split('/\r\n|\r|\n/', $remarks) ?: []; $capture = []; $started = false; foreach ($lines as $line) { $trimmed = trim((string) $line); if ($trimmed === '') { if ($started) { break; } continue; } if (preg_match('/^Women Related Problems\s*:/i', $trimmed) === 1) { $started = true; $capture[] = $trimmed; continue; } if ($started && preg_match('/^(LMP(?:\s*Days\s*Before)?|Last Baby|Pregnancy Related)\s*:/i', $trimmed) === 1) { $capture[] = $trimmed; continue; } if ($started) { break; } } if (!empty($capture)) { return trim(implode("\n", $capture)); } return ''; } /** * @return array{women_related_problems:string,women_lmp:string,women_last_baby:string,women_pregnancy_related:string} */ private function parseWomenStructuredDetails(string $text): array { $remaining = trim($text); $extract = static function (string $pattern, string &$haystack): string { $value = ''; if (preg_match($pattern, $haystack, $match) === 1) { $value = trim((string) ($match[1] ?? '')); } $haystack = trim((string) (preg_replace($pattern, '', $haystack) ?? $haystack)); return $value; }; $womenProblems = $extract('/^\s*Women Related Problems\s*:\s*(.+)$/im', $remaining); $lmp = $extract('/^\s*LMP(?:\s*Days\s*Before)?\s*:\s*(.+)$/im', $remaining); $lastBaby = $extract('/^\s*Last Baby\s*:\s*(.+)$/im', $remaining); $pregnancyRelated = $extract('/^\s*Pregnancy Related\s*:\s*(.+)$/im', $remaining); if ($womenProblems === '') { $womenProblems = trim($remaining); } return [ 'women_related_problems' => $womenProblems, 'women_lmp' => $lmp, 'women_last_baby' => $lastBaby, 'women_pregnancy_related' => $pregnancyRelated, ]; } }