user(); if (! $user) { return false; } if (method_exists($user, 'can') && $user->can('opd.doctor-panel.access')) { return true; } return false; } public function Prescription(int $opdId) { if (! $this->canAccessPrescription()) { return $this->response->setStatusCode(403)->setBody('Access denied: OPD Prescription is restricted to doctor-authorized users.'); } $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'); } $opdRow = $opdMaster[0]; $patientId = (int) ($opdRow->p_id ?? 0); $sql = "select *,if(gender=1,'Male','Female') as xgender from patient_master where id='" . (int) $opdRow->p_id . "'"; $query = $this->db->query($sql); $patient = $query->getResult(); if (!empty($patient)) { $patient[0]->str_age = get_age_1($patient[0]->dob ?? null, $patient[0]->age ?? '', $patient[0]->age_in_month ?? '', $patient[0]->estimate_dob ?? '', $opdRow->apointment_date ?? null); } $prescription = []; if ($this->db->tableExists('opd_prescription')) { $prescription = $this->db->table('opd_prescription') ->where('opd_id', $opdId) ->orderBy('id', 'DESC') ->get(1) ->getResult(); if (!empty($prescription)) { $first = $prescription[0]; $firstRow = (array) $first; $womenText = ''; if (isset($first->women_related_problems)) { $womenText = trim((string) $first->women_related_problems); } if ($womenText === '') { $womenText = $this->extractWomenRelatedProblemsFromRemarks((string) ($first->Prescriber_Remarks ?? '')); } $womenStructured = $this->parseWomenStructuredDetails($womenText); $first->women_related_problems = (string) ($womenStructured['women_related_problems'] ?? ''); $first->women_lmp = trim((string) ( $firstRow['women_lmp'] ?? $firstRow['lmp'] ?? $firstRow['lmp_date'] ?? ($womenStructured['women_lmp'] ?? '') )); $first->women_last_baby = trim((string) ( $firstRow['women_last_baby'] ?? $firstRow['last_baby'] ?? $firstRow['last_baby_details'] ?? ($womenStructured['women_last_baby'] ?? '') )); $first->women_pregnancy_related = trim((string) ( $firstRow['women_pregnancy_related'] ?? $firstRow['pregnancy_related'] ?? $firstRow['pregnancy_related_problem'] ?? ($womenStructured['women_pregnancy_related'] ?? '') )); $nabhInfo = $this->hydrateNabhPrescriptionFields( $firstRow, (string) ($first->Prescriber_Remarks ?? '') ); $first->drug_allergy_status = (string) ($nabhInfo['drug_allergy_status'] ?? ''); $first->drug_allergy_details = (string) ($nabhInfo['drug_allergy_details'] ?? ''); $first->adr_history = (string) ($nabhInfo['adr_history'] ?? ''); $first->current_medications = (string) ($nabhInfo['current_medications'] ?? ''); $prescription[0] = $first; } } return view('billing/opd_prescription_basic', [ 'opd_id' => $opdId, 'opd_master' => $opdMaster, 'patient_master' => $patient, 'opd_prescription' => $prescription, 'addiction_flags' => $this->getPatientAddictionFlags($patientId), 'co_morbidities' => $this->getCoMorbiditiesWithSelection($patientId), 'co_morbidities_prefilled_from_history' => $this->hasHistoricalCoMorbiditySelection($patientId), 'history_alerts' => $this->buildPatientHistoryAlerts((int) ($opdRow->p_id ?? 0), $opdId), 'left_remarks' => $this->fetchPatientRemarks((int) ($opdRow->p_id ?? 0), 8), ]); } public function vitals_get(int $opdId) { if (! $this->canAccessPrescription()) { return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'Access denied']); } if ($opdId <= 0 || ! $this->db->tableExists('opd_prescription')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'OPD prescription table not found']); } $row = $this->db->table('opd_prescription') ->where('opd_id', (int) $opdId) ->orderBy('id', 'DESC') ->get(1) ->getRowArray() ?? []; $vitals = [ 'pulse' => (string) ($row['pulse'] ?? ''), 'spo2' => (string) ($row['spo2'] ?? ''), 'bp' => (string) ($row['bp'] ?? ''), 'diastolic' => (string) ($row['diastolic'] ?? ''), 'temp' => (string) ($row['temp'] ?? ''), 'rr_min' => (string) ($row['rr_min'] ?? ''), 'height' => (string) ($row['height'] ?? ''), 'weight' => (string) ($row['weight'] ?? ''), 'waist' => (string) ($row['waist'] ?? ''), ]; return $this->response->setJSON([ 'update' => 1, 'opd_session_id' => (int) ($row['id'] ?? 0), 'vitals' => $vitals, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function vitals_save() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } if (! $this->canAccessPrescription()) { return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'Access denied']); } $opdId = (int) $this->request->getPost('opd_id'); $sessionId = (int) $this->request->getPost('opd_session_id'); if ($opdId <= 0 || ! $this->db->tableExists('opd_prescription')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'OPD prescription table not found']); } $sessionId = $this->ensurePrescriptionSession($opdId, $sessionId); if ($sessionId <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create prescription session']); } $fields = $this->db->getFieldNames('opd_prescription'); $input = [ 'pulse' => trim((string) $this->request->getPost('pulse')), 'spo2' => trim((string) $this->request->getPost('spo2')), 'bp' => trim((string) $this->request->getPost('bp')), 'diastolic' => trim((string) $this->request->getPost('diastolic')), 'temp' => trim((string) $this->request->getPost('temp')), 'rr_min' => trim((string) $this->request->getPost('rr_min')), 'height' => trim((string) $this->request->getPost('height')), 'weight' => trim((string) $this->request->getPost('weight')), 'waist' => trim((string) $this->request->getPost('waist')), ]; $update = []; foreach ($input as $field => $value) { if (in_array($field, $fields, true)) { $update[$field] = $value; } } if (empty($update)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'No vital fields available']); } $this->db->table('opd_prescription')->where('id', $sessionId)->update($update); return $this->response->setJSON([ 'update' => 1, 'opd_session_id' => $sessionId, 'error_text' => 'Vitals saved successfully', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_prescription_save() { if (!$this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON([ 'update' => 0, 'error_text' => 'Invalid request', ]); } if (!$this->db->tableExists('opd_prescription')) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Table opd_prescription not found.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $opdId = (int) $this->request->getPost('opd_id'); $sessionId = (int) $this->request->getPost('opd_session_id'); $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(), ]); } $opdRow = $opdMaster[0]; $fields = $this->db->getFieldNames('opd_prescription'); $patientRow = $this->db->table('patient_master')->where('id', (int) ($opdRow->p_id ?? 0))->get(1)->getRowArray() ?? []; $abhaAddress = trim((string) $this->request->getPost('abha_address')); if ($abhaAddress !== '' && ! $this->isValidAbhaAddress($abhaAddress)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Invalid ABHA Address format.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $abhaField = $this->resolvePatientAbhaField(); if ($abhaField !== null && $abhaAddress !== '') { $currentAbha = trim((string) ($patientRow[$abhaField] ?? '')); if ($currentAbha !== $abhaAddress) { $this->db->table('patient_master')->where('id', (int) ($opdRow->p_id ?? 0))->update([$abhaField => $abhaAddress]); $this->auditClinicalUpdate('patient_master', $abhaField, (int) ($opdRow->p_id ?? 0), $currentAbha, $abhaAddress); $patientRow[$abhaField] = $abhaAddress; } } $payload = [ 'complaints' => trim((string) $this->request->getPost('complaints')), 'diagnosis' => trim((string) $this->request->getPost('diagnosis')), 'Provisional_diagnosis' => trim((string) $this->request->getPost('provisional_diagnosis')), 'Finding_Examinations' => trim((string) $this->request->getPost('finding_examinations')), 'Prescriber_Remarks' => trim((string) $this->request->getPost('prescriber_remarks')), 'women_related_problems' => trim((string) $this->request->getPost('women_related_problems')), 'women_lmp' => trim((string) $this->request->getPost('women_lmp')), 'women_last_baby' => trim((string) $this->request->getPost('women_last_baby')), 'women_pregnancy_related' => trim((string) $this->request->getPost('women_pregnancy_related')), 'drug_allergy_status' => trim((string) $this->request->getPost('drug_allergy_status')), 'drug_allergy_details' => trim((string) $this->request->getPost('drug_allergy_details')), 'adr_history' => trim((string) $this->request->getPost('adr_history')), 'current_medications' => trim((string) $this->request->getPost('current_medications')), 'investigation' => trim((string) $this->request->getPost('investigation')), 'advice' => trim((string) $this->request->getPost('advice')), 'next_visit' => trim((string) $this->request->getPost('next_visit')), 'refer_to' => trim((string) $this->request->getPost('refer_to')), 'bp' => trim((string) $this->request->getPost('bp')), 'diastolic' => trim((string) $this->request->getPost('diastolic')), 'pulse' => trim((string) $this->request->getPost('pulse')), 'temp' => trim((string) $this->request->getPost('temp')), 'spo2' => trim((string) $this->request->getPost('spo2')), 'rr_min' => trim((string) $this->request->getPost('rr_min')), 'height' => trim((string) $this->request->getPost('height')), 'weight' => trim((string) $this->request->getPost('weight')), 'waist' => trim((string) $this->request->getPost('waist')), 'pallor' => trim((string) $this->request->getPost('pallor')), 'Icterus' => trim((string) $this->request->getPost('icterus')), 'cyanosis' => trim((string) $this->request->getPost('cyanosis')), 'clubbing' => trim((string) $this->request->getPost('clubbing')), 'edema' => trim((string) $this->request->getPost('edema')), 'pain_value' => trim((string) $this->request->getPost('pain_value')), 'pregnancy' => (int) $this->request->getPost('pregnancy'), 'lactation' => (int) $this->request->getPost('lactation'), 'liver_insufficiency' => (int) $this->request->getPost('liver_insufficiency'), 'renal_insufficiency' => (int) $this->request->getPost('renal_insufficiency'), 'pulmonary_insufficiency' => (int) $this->request->getPost('pulmonary_insufficiency'), 'corona_suspected' => (int) $this->request->getPost('corona_suspected'), 'dengue' => (int) $this->request->getPost('dengue'), ]; $allergyStatusRaw = trim((string) ($payload['drug_allergy_status'] ?? '')); $allergyStatusNorm = mb_strtolower(preg_replace('/\s+/', ' ', $allergyStatusRaw) ?? $allergyStatusRaw); $allergyDetails = trim((string) ($payload['drug_allergy_details'] ?? '')); if ($allergyStatusRaw === '') { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Drug Allergy Status is required as per NABH documentation.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } if (in_array($allergyStatusNorm, ['known', 'yes', 'present'], true) && $allergyDetails === '') { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Drug Allergy Details are required when Drug Allergy Status is Known.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $womenStructuredInput = [ 'women_lmp' => (string) ($payload['women_lmp'] ?? ''), 'women_last_baby' => (string) ($payload['women_last_baby'] ?? ''), 'women_pregnancy_related' => (string) ($payload['women_pregnancy_related'] ?? ''), ]; $payload['women_related_problems'] = $this->buildWomenCombinedText( (string) ($payload['women_related_problems'] ?? ''), $womenStructuredInput ); $this->mapWomenStructuredToExistingColumns($payload, $fields, $womenStructuredInput); $nabhInput = [ 'drug_allergy_status' => (string) ($payload['drug_allergy_status'] ?? ''), 'drug_allergy_details' => (string) ($payload['drug_allergy_details'] ?? ''), 'adr_history' => (string) ($payload['adr_history'] ?? ''), 'current_medications' => (string) ($payload['current_medications'] ?? ''), ]; $unmappedNabh = $this->mapNabhFieldsToExistingColumns($payload, $fields, $nabhInput); if (!in_array('women_related_problems', $fields, true)) { $womenText = trim((string) ($payload['women_related_problems'] ?? '')); if ($womenText !== '') { $payload['Prescriber_Remarks'] = $this->upsertWomenRelatedProblemsIntoRemarks( (string) ($payload['Prescriber_Remarks'] ?? ''), $womenText ); } } if (!empty(array_filter($unmappedNabh, static fn ($v) => trim((string) $v) !== ''))) { $payload['Prescriber_Remarks'] = $this->upsertNabhFieldsIntoRemarks( (string) ($payload['Prescriber_Remarks'] ?? ''), $unmappedNabh ); } foreach ($payload as $value) { if (mb_strlen($value) > 4000) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Each section allows up to 4000 characters.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } } $insertBase = [ 'opd_id' => $opdId, 'p_id' => (int) ($opdRow->p_id ?? 0), 'doc_id' => (int) ($opdRow->doc_id ?? 0), 'date_opd_visit' => date('Y-m-d'), 'visit_status' => 0, 'session_id' => 0, ]; $allowedData = []; foreach (array_merge($insertBase, $payload) as $key => $value) { if (in_array($key, $fields, true)) { $allowedData[$key] = $value; } } if (in_array('queue_no', $fields, true) && !isset($allowedData['queue_no'])) { $maxQueue = $this->db->table('opd_prescription') ->selectMax('queue_no', 'max_queue') ->where('date_opd_visit', date('Y-m-d')) ->where('doc_id', (int) ($opdRow->doc_id ?? 0)) ->get() ->getRow(); $allowedData['queue_no'] = (int) ($maxQueue->max_queue ?? 0) + 1; } $table = $this->db->table('opd_prescription'); $beforeRow = []; if ($sessionId > 0) { $beforeRow = $table->where('id', $sessionId)->where('opd_id', $opdId)->get(1)->getRowArray() ?? []; $table->where('id', $sessionId)->where('opd_id', $opdId)->update($allowedData); $recordId = $sessionId; } else { $table->insert($allowedData); $recordId = (int) $this->db->insertID(); } $afterRow = $this->db->table('opd_prescription')->where('id', $recordId)->where('opd_id', $opdId)->get(1)->getRowArray() ?? []; if (!empty($afterRow)) { $this->clinicalAuditTrail->logChangedFields('opd_prescription', $recordId, $beforeRow, $afterRow, $this->getCurrentUserId()); } $fhirStored = $this->storePrescriptionFhirBundle((int) $opdId, (int) $recordId, $patientRow, (array) $opdRow); $this->syncPatientAddictionFlags((int) ($opdRow->p_id ?? 0), [ 'is_smoking' => (int) $this->request->getPost('is_smoking'), 'is_alcohol' => (int) $this->request->getPost('is_alcohol'), 'is_drug_abuse' => (int) $this->request->getPost('is_drug_abuse'), ]); $morbidityIds = $this->parseNumericList((string) $this->request->getPost('morbidities_list')); $this->syncPatientMorbidities((int) ($opdRow->p_id ?? 0), $morbidityIds); if (in_array('Prescriber_Remarks', $fields, true)) { $coMorbiditiesText = trim((string) $this->request->getPost('morbidities_text')); $remarksNow = (string) ($allowedData['Prescriber_Remarks'] ?? ($afterRow['Prescriber_Remarks'] ?? '')); $updatedRemarks = $this->upsertCoMorbiditiesIntoRemarks($remarksNow, $coMorbiditiesText); if ($updatedRemarks !== $remarksNow) { $this->db->table('opd_prescription') ->where('id', $recordId) ->where('opd_id', $opdId) ->update(['Prescriber_Remarks' => $updatedRemarks]); } } return $this->response->setJSON([ 'update' => 1, 'opd_session_id' => $recordId, 'error_text' => 'Prescription saved', 'fhir_stored' => $fhirStored ? 1 : 0, 'saved_at' => date('d-m-Y H:i:s'), 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function clinical_autotype() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $text = trim((string) $this->request->getPost('text')); $mode = trim((string) $this->request->getPost('mode')); if ($mode === '') { $mode = 'autotype'; } if ($text === '') { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Please enter text first.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $prompt = $this->buildClinicalPrompt($mode, $text); $this->lastAiProvider = 'none'; $draft = $this->generateClinicalTextWithAi($prompt); if ($draft !== null && trim($draft) !== '') { $provider = $this->lastAiProvider === 'ai-server' ? 'ai-server' : 'ai'; $providerLabel = $provider === 'ai-server' ? 'AI Server' : 'AI'; return $this->response->setJSON([ 'update' => 1, 'draft_text' => trim($draft), 'error_text' => 'AI text prepared using ' . $providerLabel, 'ai_mode' => $provider, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $fallback = $text; $fallback = $this->buildClinicalAutotypeLocalFallback($mode, $text); $fallbackMessage = 'External AI unavailable. Local rewrite applied.'; if (trim($fallback) === trim($text)) { $fallbackMessage = 'External AI unavailable. Original text kept.'; } return $this->response->setJSON([ 'update' => 1, 'draft_text' => $fallback, 'error_text' => $fallbackMessage, 'ai_mode' => 'fallback', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function ai_full_draft() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $input = [ 'complaints' => trim((string) $this->request->getPost('complaints')), 'finding_examinations' => trim((string) $this->request->getPost('finding_examinations')), 'diagnosis' => trim((string) $this->request->getPost('diagnosis')), 'provisional_diagnosis' => trim((string) $this->request->getPost('provisional_diagnosis')), 'prescriber_remarks' => trim((string) $this->request->getPost('prescriber_remarks')), 'investigation' => trim((string) $this->request->getPost('investigation')), 'advice' => trim((string) $this->request->getPost('advice')), 'next_visit' => trim((string) $this->request->getPost('next_visit')), 'refer_to' => trim((string) $this->request->getPost('refer_to')), 'bp' => trim((string) $this->request->getPost('bp')), 'diastolic' => trim((string) $this->request->getPost('diastolic')), 'pulse' => trim((string) $this->request->getPost('pulse')), 'temp' => trim((string) $this->request->getPost('temp')), 'spo2' => trim((string) $this->request->getPost('spo2')), 'rr_min' => trim((string) $this->request->getPost('rr_min')), 'height' => trim((string) $this->request->getPost('height')), 'weight' => trim((string) $this->request->getPost('weight')), 'waist' => trim((string) $this->request->getPost('waist')), 'pallor' => trim((string) $this->request->getPost('pallor')), 'icterus' => trim((string) $this->request->getPost('icterus')), 'cyanosis' => trim((string) $this->request->getPost('cyanosis')), 'clubbing' => trim((string) $this->request->getPost('clubbing')), 'edema' => trim((string) $this->request->getPost('edema')), 'pain_value' => trim((string) $this->request->getPost('pain_value')), 'pregnancy' => (int) $this->request->getPost('pregnancy'), 'lactation' => (int) $this->request->getPost('lactation'), 'liver_insufficiency' => (int) $this->request->getPost('liver_insufficiency'), 'renal_insufficiency' => (int) $this->request->getPost('renal_insufficiency'), 'pulmonary_insufficiency' => (int) $this->request->getPost('pulmonary_insufficiency'), 'corona_suspected' => (int) $this->request->getPost('corona_suspected'), 'dengue' => (int) $this->request->getPost('dengue'), 'is_smoking' => (int) $this->request->getPost('is_smoking'), 'is_alcohol' => (int) $this->request->getPost('is_alcohol'), 'is_drug_abuse' => (int) $this->request->getPost('is_drug_abuse'), 'women_related_problems' => trim((string) $this->request->getPost('women_related_problems')), 'women_lmp' => trim((string) $this->request->getPost('women_lmp')), 'women_last_baby' => trim((string) $this->request->getPost('women_last_baby')), 'women_pregnancy_related' => trim((string) $this->request->getPost('women_pregnancy_related')), ]; if ($input['complaints'] === '' && $input['finding_examinations'] === '' && $input['diagnosis'] === '') { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Add complaints/finding/diagnosis first, then use AI Draft.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $prompt = $this->buildFullClinicalDraftPrompt($input); $this->lastAiProvider = 'none'; $draftText = $this->generateClinicalTextWithAi($prompt); $drafts = $this->extractFullDraftJson($draftText); if ($drafts === null) { $drafts = $this->buildFullDraftFallback($input); $aiMode = 'fallback'; $message = 'AI unavailable. Template draft prepared from entered data.'; } else { $aiMode = $this->lastAiProvider === 'ai-server' ? 'ai-server' : 'ai'; $message = 'Full clinical draft prepared using ' . ($aiMode === 'ai-server' ? 'AI Server' : 'AI') . '.'; } return $this->response->setJSON([ 'update' => 1, 'drafts' => $drafts, 'ai_mode' => $aiMode, 'error_text' => $message, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function local_clinical_assist() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $input = [ 'complaints' => trim((string) $this->request->getPost('complaints')), 'finding_examinations' => trim((string) $this->request->getPost('finding_examinations')), 'diagnosis' => trim((string) $this->request->getPost('diagnosis')), 'investigation' => trim((string) $this->request->getPost('investigation')), 'advice' => trim((string) $this->request->getPost('advice')), 'bp' => trim((string) $this->request->getPost('bp')), 'diastolic' => trim((string) $this->request->getPost('diastolic')), 'pulse' => trim((string) $this->request->getPost('pulse')), 'temp' => trim((string) $this->request->getPost('temp')), 'spo2' => trim((string) $this->request->getPost('spo2')), 'rr_min' => trim((string) $this->request->getPost('rr_min')), 'pain_value' => trim((string) $this->request->getPost('pain_value')), 'pregnancy' => (int) $this->request->getPost('pregnancy'), 'lactation' => (int) $this->request->getPost('lactation'), 'dengue' => (int) $this->request->getPost('dengue'), 'corona_suspected' => (int) $this->request->getPost('corona_suspected'), ]; if ($input['complaints'] === '' && $input['finding_examinations'] === '') { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Enter complaints or findings first for clinical assist.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $assist = $this->buildLocalClinicalAssist($input); return $this->response->setJSON([ 'update' => 1, 'assist' => $assist, 'error_text' => 'Local clinical assist prepared. Final decision remains with doctor.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function section_template_save() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $section = trim((string) $this->request->getPost('section')); $templateName = trim((string) $this->request->getPost('template_name')); $templateText = trim((string) $this->request->getPost('template_text')); $templateScope = strtolower(trim((string) $this->request->getPost('template_scope'))); $templateDocId = (int) $this->request->getPost('template_doc_id'); if ($templateName === '' || $templateText === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Template name and text are required']); } if (! $this->isTemplateSectionAllowed($section)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unsupported section']); } if (! $this->ensureClinicalTemplateTable()) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to access template storage']); } $docId = $this->getCurrentUserId(); if ($templateScope === 'master') { $docId = 0; } elseif ($templateDocId > 0) { $docId = $templateDocId; } elseif ($docId < 0) { $docId = 0; } $table = $this->db->table('opd_clinical_templates'); $existing = $table ->where('doc_id', $docId) ->where('section_key', $section) ->where('template_name', $templateName) ->where('is_active', 1) ->get(1) ->getRowArray(); $now = date('Y-m-d H:i:s'); if (! empty($existing)) { $table->where('id', (int) ($existing['id'] ?? 0))->update([ 'template_text' => $templateText, 'updated_at' => $now, ]); } else { $table->insert([ 'doc_id' => $docId, 'section_key' => $section, 'template_name' => $templateName, 'template_text' => $templateText, 'is_active' => 1, 'created_at' => $now, 'updated_at' => $now, ]); } return $this->response->setJSON([ 'update' => 1, 'error_text' => ($docId === 0 ? 'Master template saved' : 'My template saved'), 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function section_template_list() { $section = trim((string) $this->request->getGet('section')); if (! $this->isTemplateSectionAllowed($section)) { return $this->response->setJSON(['rows' => []]); } if (! $this->ensureClinicalTemplateTable()) { return $this->response->setJSON(['rows' => []]); } $docId = $this->getCurrentUserId(); $rows = $this->db->table('opd_clinical_templates') ->select('id,template_name,template_text,doc_id,section_key') ->where('section_key', $section) ->where('is_active', 1) ->groupStart() ->where('doc_id', $docId) ->orWhere('doc_id', 0) ->groupEnd() ->orderBy('doc_id', 'DESC') ->orderBy('template_name', 'ASC') ->limit(100) ->get() ->getResultArray(); $usageByTemplateId = []; if (! empty($rows) && $this->ensureClinicalTemplateUsageTable() && $docId !== null) { $templateIds = array_map(static fn($row) => (int) ($row['id'] ?? 0), $rows); $templateIds = array_values(array_filter($templateIds, static fn($id) => $id > 0)); if (! empty($templateIds)) { $usageRows = $this->db->table('opd_clinical_template_usage') ->select('template_id,use_count,last_used_at') ->where('doc_id', (int) $docId) ->where('section_key', $section) ->whereIn('template_id', $templateIds) ->get() ->getResultArray(); foreach ($usageRows as $usageRow) { $templateId = (int) ($usageRow['template_id'] ?? 0); if ($templateId <= 0) { continue; } $usageByTemplateId[$templateId] = [ 'use_count' => (int) ($usageRow['use_count'] ?? 0), 'last_used_at' => (string) ($usageRow['last_used_at'] ?? ''), ]; } } } foreach ($rows as &$row) { $templateId = (int) ($row['id'] ?? 0); $usage = $usageByTemplateId[$templateId] ?? ['use_count' => 0, 'last_used_at' => '']; $row['use_count'] = (int) ($usage['use_count'] ?? 0); $row['last_used_at'] = (string) ($usage['last_used_at'] ?? ''); } unset($row); usort($rows, static function (array $a, array $b): int { $aUse = (int) ($a['use_count'] ?? 0); $bUse = (int) ($b['use_count'] ?? 0); if ($aUse !== $bUse) { return $bUse <=> $aUse; } $aDoc = (int) ($a['doc_id'] ?? 0); $bDoc = (int) ($b['doc_id'] ?? 0); if ($aDoc !== $bDoc) { return $bDoc <=> $aDoc; } $aLast = trim((string) ($a['last_used_at'] ?? '')); $bLast = trim((string) ($b['last_used_at'] ?? '')); if ($aLast !== $bLast) { return strcmp($bLast, $aLast); } return strcasecmp((string) ($a['template_name'] ?? ''), (string) ($b['template_name'] ?? '')); }); return $this->response->setJSON(['rows' => $rows]); } public function section_template_track_usage() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $section = trim((string) $this->request->getPost('section')); $templateId = (int) $this->request->getPost('template_id'); if ($templateId <= 0 || ! $this->isTemplateSectionAllowed($section)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Invalid template usage payload', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } if (! $this->ensureClinicalTemplateTable() || ! $this->ensureClinicalTemplateUsageTable()) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Usage storage unavailable', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $docId = $this->getCurrentUserId(); if ($docId === null) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'User not found', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $templateRow = $this->db->table('opd_clinical_templates') ->select('id,section_key,is_active') ->where('id', $templateId) ->where('is_active', 1) ->get(1) ->getRowArray(); if (empty($templateRow)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Template not found', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } if (trim((string) ($templateRow['section_key'] ?? '')) !== $section) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Section mismatch for template', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $usageTable = $this->db->table('opd_clinical_template_usage'); $existing = $usageTable ->where('doc_id', (int) $docId) ->where('template_id', $templateId) ->where('section_key', $section) ->get(1) ->getRowArray(); $now = date('Y-m-d H:i:s'); if (! empty($existing)) { $usageTable->where('id', (int) ($existing['id'] ?? 0))->update([ 'use_count' => ((int) ($existing['use_count'] ?? 0)) + 1, 'last_used_at' => $now, 'updated_at' => $now, ]); } else { $usageTable->insert([ 'doc_id' => (int) $docId, 'template_id' => $templateId, 'section_key' => $section, 'use_count' => 1, 'last_used_at' => $now, 'created_at' => $now, 'updated_at' => $now, ]); } return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Template usage tracked', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function section_past_data() { $section = trim((string) $this->request->getGet('section')); $opdId = (int) $this->request->getGet('opd_id'); if ($opdId <= 0 || ! $this->isTemplateSectionAllowed($section) || ! $this->db->tableExists('opd_master') || ! $this->db->tableExists('opd_prescription')) { return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Past data not found']); } $field = $this->resolveSectionColumn($section); if ($field === null) { return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Unsupported section']); } $fields = $this->db->getFieldNames('opd_prescription'); if (! in_array($field, $fields, true)) { return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Past data column missing']); } $opdRow = $this->db->table('opd_master')->select('p_id')->where('opd_id', $opdId)->get(1)->getRowArray(); $patientId = (int) ($opdRow['p_id'] ?? 0); if ($patientId <= 0) { return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Patient not found']); } $row = $this->db->table('opd_prescription') ->select($field . ' as section_text,opd_id,id') ->where('p_id', $patientId) ->where('opd_id !=', $opdId) ->where($field . ' IS NOT NULL', null, false) ->where($field . ' !=', '') ->orderBy('id', 'DESC') ->get(1) ->getRowArray(); if (empty($row)) { return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'No past data found']); } return $this->response->setJSON([ 'update' => 1, 'past_text' => (string) ($row['section_text'] ?? ''), 'past_opd_id' => (int) ($row['opd_id'] ?? 0), 'error_text' => 'Past data loaded', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function template_workspace() { return view('billing/opd_template_workspace'); } public function clinical_template_monitor() { if (! $this->ensureClinicalTemplateTable()) { return $this->response->setJSON(['rows' => []]); } $section = trim((string) $this->request->getGet('section')); $scope = strtolower(trim((string) $this->request->getGet('scope'))); $doctorId = (int) $this->request->getGet('doctor_id'); $docId = $this->getCurrentUserId(); $builder = $this->db->table('opd_clinical_templates') ->select('id,doc_id,section_key,template_name,template_text,is_active,created_at,updated_at') ->where('is_active', 1); if ($this->isTemplateSectionAllowed($section)) { $builder->where('section_key', $section); } if ($scope === 'master') { $builder->where('doc_id', 0); } elseif ($scope === 'doctor') { if ($doctorId > 0) { $builder->where('doc_id', $doctorId); } else { $builder->where('doc_id', $docId); } } else { $builder->groupStart() ->where('doc_id', 0) ->orWhere('doc_id', $docId) ->groupEnd(); } $rows = $builder ->orderBy('updated_at', 'DESC') ->orderBy('id', 'DESC') ->limit(300) ->get() ->getResultArray(); $doctorNameById = []; if ($this->db->tableExists('doctor_master')) { $doctorRows = $this->db->table('doctor_master')->select('id,p_fname')->get()->getResultArray(); foreach ($doctorRows as $doctorRow) { $doctorNameById[(int) ($doctorRow['id'] ?? 0)] = (string) ($doctorRow['p_fname'] ?? ''); } } foreach ($rows as &$row) { $row['scope_label'] = ((int) ($row['doc_id'] ?? -1) === 0) ? 'Master' : 'Doctor'; $row['doctor_name'] = ((int) ($row['doc_id'] ?? 0) > 0) ? ($doctorNameById[(int) ($row['doc_id'] ?? 0)] ?? '') : ''; } unset($row); return $this->response->setJSON(['rows' => $rows]); } public function clinical_template_update() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } if (! $this->ensureClinicalTemplateTable()) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Template storage unavailable']); } $id = (int) $this->request->getPost('id'); $section = trim((string) $this->request->getPost('section')); $templateName = trim((string) $this->request->getPost('template_name')); $templateText = trim((string) $this->request->getPost('template_text')); $templateScope = strtolower(trim((string) $this->request->getPost('template_scope'))); $templateDocId = (int) $this->request->getPost('template_doc_id'); if ($id <= 0 || ! $this->isTemplateSectionAllowed($section) || $templateName === '' || $templateText === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Template id, section, name and text are required']); } $targetDocId = $templateScope === 'master' ? 0 : ($templateDocId > 0 ? $templateDocId : $this->getCurrentUserId()); if ($targetDocId < 0) { $targetDocId = 0; } $current = $this->db->table('opd_clinical_templates')->where('id', $id)->where('is_active', 1)->get(1)->getRowArray(); if (empty($current)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Template not found']); } $currentDocId = (int) ($current['doc_id'] ?? -1); $myDocId = $this->getCurrentUserId(); if ($currentDocId !== 0 && $currentDocId !== $myDocId) { return $this->response->setJSON(['update' => 0, 'error_text' => 'You can edit only your own or master templates']); } $existing = $this->db->table('opd_clinical_templates') ->where('id !=', $id) ->where('doc_id', $targetDocId) ->where('section_key', $section) ->where('template_name', $templateName) ->where('is_active', 1) ->get(1) ->getRowArray(); if (!empty($existing)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Another template with same name already exists in selected scope']); } $this->db->table('opd_clinical_templates')->where('id', $id)->update([ 'doc_id' => $targetDocId, 'section_key' => $section, 'template_name' => $templateName, 'template_text' => $templateText, 'updated_at' => date('Y-m-d H:i:s'), ]); return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Template updated', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function clinical_template_doctors() { if (! $this->db->tableExists('doctor_master')) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames('doctor_master'); $idField = $this->resolveFirstField($fields, ['id', 'doc_id']); $nameField = $this->resolveFirstField($fields, ['p_fname', 'name', 'doc_name']); if ($idField === null || $nameField === null) { return $this->response->setJSON(['rows' => []]); } $builder = $this->db->table('doctor_master') ->select($idField . ' as id,' . $nameField . ' as name'); if (in_array('active', $fields, true)) { $builder->where('active', 1); } $rows = $builder->orderBy($nameField, 'ASC')->get()->getResultArray(); return $this->response->setJSON(['rows' => $rows]); } public function clinical_template_usage_analytics() { if (! $this->ensureClinicalTemplateTable() || ! $this->ensureClinicalTemplateUsageTable()) { return $this->response->setJSON([ 'top_rows' => [], 'section_rows' => [], 'summary' => ['total_uses' => 0, 'used_templates' => 0], ]); } $section = trim((string) $this->request->getGet('section')); $scope = strtolower(trim((string) $this->request->getGet('scope'))); $doctorId = (int) $this->request->getGet('doctor_id'); $currentDocId = (int) ($this->getCurrentUserId() ?? 0); $targetDocId = $scope === 'doctor' && $doctorId > 0 ? $doctorId : $currentDocId; $builder = $this->db->table('opd_clinical_template_usage u') ->select('u.template_id,u.section_key,u.use_count,u.last_used_at,t.template_name,t.doc_id') ->join('opd_clinical_templates t', 't.id = u.template_id', 'inner') ->where('u.doc_id', $targetDocId) ->where('t.is_active', 1); if ($this->isTemplateSectionAllowed($section)) { $builder->where('u.section_key', $section); } if ($scope === 'master') { $builder->where('t.doc_id', 0); } elseif ($scope === 'doctor') { $builder->where('t.doc_id', $targetDocId); } else { $builder->groupStart() ->where('t.doc_id', 0) ->orWhere('t.doc_id', $targetDocId) ->groupEnd(); } $usageRows = $builder->get()->getResultArray(); if (empty($usageRows)) { return $this->response->setJSON([ 'top_rows' => [], 'section_rows' => [], 'summary' => ['total_uses' => 0, 'used_templates' => 0], ]); } usort($usageRows, static function (array $a, array $b): int { $aUse = (int) ($a['use_count'] ?? 0); $bUse = (int) ($b['use_count'] ?? 0); if ($aUse !== $bUse) { return $bUse <=> $aUse; } return strcmp((string) ($b['last_used_at'] ?? ''), (string) ($a['last_used_at'] ?? '')); }); $totalUses = 0; $usedTemplates = []; $sectionAgg = []; foreach ($usageRows as $row) { $use = (int) ($row['use_count'] ?? 0); $sec = trim((string) ($row['section_key'] ?? '')); $totalUses += $use; $templateId = (int) ($row['template_id'] ?? 0); if ($templateId > 0) { $usedTemplates[$templateId] = true; } if ($sec === '') { continue; } if (! isset($sectionAgg[$sec])) { $sectionAgg[$sec] = ['section_key' => $sec, 'total_uses' => 0, 'template_count' => 0, 'template_ids' => []]; } $sectionAgg[$sec]['total_uses'] += $use; if ($templateId > 0 && ! isset($sectionAgg[$sec]['template_ids'][$templateId])) { $sectionAgg[$sec]['template_ids'][$templateId] = true; $sectionAgg[$sec]['template_count']++; } } $sectionRows = array_values(array_map(static function (array $row): array { unset($row['template_ids']); return $row; }, $sectionAgg)); usort($sectionRows, static function (array $a, array $b): int { $aUse = (int) ($a['total_uses'] ?? 0); $bUse = (int) ($b['total_uses'] ?? 0); if ($aUse !== $bUse) { return $bUse <=> $aUse; } return strcasecmp((string) ($a['section_key'] ?? ''), (string) ($b['section_key'] ?? '')); }); return $this->response->setJSON([ 'top_rows' => array_slice($usageRows, 0, 15), 'section_rows' => $sectionRows, 'summary' => [ 'total_uses' => $totalUses, 'used_templates' => count($usedTemplates), ], ]); } public function rx_group_panel() { return view('billing/opd_rx_group_workspace', ['initial_rx_id' => 0]); } public function save_rx_group_edit(int $rxId = 0) { return view('billing/opd_rx_group_workspace', ['initial_rx_id' => max(0, $rxId)]); } public function rx_group_data() { if (! $this->db->tableExists('opd_prescription_template')) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames('opd_prescription_template'); $idField = $this->resolveFirstField($fields, ['id']); $nameField = $this->resolveFirstField($fields, ['rx_group_name']); if ($idField === null || $nameField === null) { return $this->response->setJSON(['rows' => []]); } $userId = max(0, $this->getCurrentUserId()); $hasDocId = in_array('doc_id', $fields, true); $builder = $this->db->table('opd_prescription_template') ->select('MAX(' . $idField . ') as id,' . $nameField . ' as rx_group_name,COUNT(*) as row_count') ->where($nameField . ' <>', ''); if ($hasDocId) { $builder->select('SUM(CASE WHEN doc_id = 0 THEN 1 ELSE 0 END) as global_count', false); $builder->select('SUM(CASE WHEN doc_id = ' . (int) $userId . ' THEN 1 ELSE 0 END) as doctor_count', false); } if ($hasDocId) { $builder->groupStart() ->where('doc_id', $userId) ->orWhere('doc_id', 0) ->groupEnd(); } $rows = $builder ->groupBy($nameField) ->orderBy($nameField, 'ASC') ->limit(500) ->get() ->getResultArray(); foreach ($rows as &$row) { if (! $hasDocId) { $row['access_label'] = 'Global'; continue; } $globalCount = (int) ($row['global_count'] ?? 0); $doctorCount = (int) ($row['doctor_count'] ?? 0); if ($globalCount > 0 && $doctorCount > 0) { $row['access_label'] = 'Global + Doctor'; } elseif ($globalCount > 0) { $row['access_label'] = 'Global'; } elseif ($doctorCount > 0) { $row['access_label'] = 'Doctor'; } else { $row['access_label'] = 'Specific User'; } } unset($row); return $this->response->setJSON(['rows' => $rows]); } public function rx_group_apply_to_session() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $opdId = (int) $this->request->getPost('opd_id'); $sessionId = (int) $this->request->getPost('opd_session_id'); $rxGroupId = (int) $this->request->getPost('rx_group_id'); if ($opdId <= 0 || $rxGroupId <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid OPD or Rx-Group']); } $templateTable = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']); if ($templateTable === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx-Group medicine template not found']); } $sessionTable = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']); if ($sessionTable === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']); } $templateFields = $this->db->getFieldNames($templateTable); if (! in_array('rx_group_id', $templateFields, true)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid Rx-Group template structure']); } $templateRows = $this->db->table($templateTable) ->where('rx_group_id', $rxGroupId) ->orderBy('id', 'ASC') ->get() ->getResultArray(); if (empty($templateRows)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'No medicines found in selected Rx-Group']); } $sessionId = $this->ensurePrescriptionSession($opdId, $sessionId); if ($sessionId <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create prescription session']); } $sessionFields = $this->db->getFieldNames($sessionTable); $user = auth()->user(); $userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User'; $updateByText = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s'); $inserted = 0; $skipped = 0; foreach ($templateRows as $row) { $insert = []; if (in_array('opd_pre_id', $sessionFields, true)) { $insert['opd_pre_id'] = $sessionId; } if (in_array('opd_id', $sessionFields, true)) { $insert['opd_id'] = $opdId; } foreach (['med_id', 'med_name', 'med_type', 'dosage', 'dosage_when', 'dosage_freq', 'dosage_where', 'no_of_days', 'qty', 'remark', 'genericname'] as $field) { if (! in_array($field, $sessionFields, true) || ! array_key_exists($field, $row)) { continue; } $value = $row[$field]; if (in_array($field, ['med_name', 'remark'], true)) { $value = strtoupper((string) $value); } $insert[$field] = $value; } if (in_array('update_by', $sessionFields, true)) { $insert['update_by'] = $updateByText; } if (! empty($insert)) { if ($this->isDuplicateMedicineRow($sessionTable, $sessionFields, $sessionId, $insert)) { $skipped++; continue; } $this->db->table($sessionTable)->insert($insert); $inserted++; $medId = (int) ($insert['med_id'] ?? 0); if ($medId > 0) { $this->trackMedicineUsage($medId); } } } return $this->response->setJSON([ 'update' => 1, 'opd_session_id' => $sessionId, 'inserted_count' => $inserted, 'skipped_count' => $skipped, 'error_text' => $inserted > 0 ? ($inserted . ' medicine(s) added from Rx-Group.' . ($skipped > 0 ? (' ' . $skipped . ' duplicate(s) skipped.') : '')) : ($skipped > 0 ? ($skipped . ' duplicate medicine(s) already present.') : 'No medicine inserted from Rx-Group.'), 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function rx_group_get(int $rxId) { if (! $this->db->tableExists('opd_prescription_template')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group table not found']); } $fields = $this->db->getFieldNames('opd_prescription_template'); $builder = $this->db->table('opd_prescription_template')->where('id', (int) $rxId); if (in_array('doc_id', $fields, true)) { $userId = max(0, $this->getCurrentUserId()); $builder->groupStart() ->where('doc_id', $userId) ->orWhere('doc_id', 0) ->groupEnd(); } $row = $builder->get(1)->getRowArray() ?? []; if (empty($row)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group not found']); } return $this->response->setJSON(['update' => 1, 'row' => $row]); } public function rx_group_save() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } if (! $this->db->tableExists('opd_prescription_template')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group table not found']); } $id = (int) $this->request->getPost('id'); $name = trim((string) $this->request->getPost('rx_group_name')); if ($name === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx-Group Name is required']); } $fields = $this->db->getFieldNames('opd_prescription_template'); $data = []; if (in_array('rx_group_name', $fields, true)) { $data['rx_group_name'] = $name; } if (in_array('complaints', $fields, true)) { $data['complaints'] = trim((string) $this->request->getPost('complaints')); } if (in_array('diagnosis', $fields, true)) { $data['diagnosis'] = trim((string) $this->request->getPost('diagnosis')); } if (in_array('Finding_Examinations', $fields, true)) { $data['Finding_Examinations'] = trim((string) $this->request->getPost('finding_examinations')); } if (in_array('investigation', $fields, true)) { $data['investigation'] = trim((string) $this->request->getPost('investigation')); } $userId = max(0, $this->getCurrentUserId()); if (in_array('rx_group_name', $fields, true)) { $dupBuilder = $this->db->table('opd_prescription_template') ->select('id') ->where('rx_group_name', $name); if (in_array('doc_id', $fields, true)) { $dupBuilder->groupStart() ->where('doc_id', $userId) ->orWhere('doc_id', 0) ->groupEnd(); } if ($id > 0) { $dupBuilder->where('id <>', $id); } $exists = $dupBuilder->orderBy('id', 'DESC')->get(1)->getRowArray(); if (! empty($exists)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Rx-Group name already exists. Please use Edit for existing group.', 'existing_id' => (int) ($exists['id'] ?? 0), 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } } if (in_array('doc_id', $fields, true) && $id <= 0) { $data['doc_id'] = $userId; } $user = auth()->user(); if (in_array('update_by', $fields, true)) { $userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User'; $data['update_by'] = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s'); } if ($id > 0) { $this->db->table('opd_prescription_template')->where('id', $id)->update($data); $savedId = $id; $message = 'Rx-Group updated'; } else { $this->db->table('opd_prescription_template')->insert($data); $savedId = (int) $this->db->insertID(); $message = 'Rx-Group saved'; } return $this->response->setJSON([ 'update' => 1, 'insertid' => $savedId, 'error_text' => $message, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function rx_group_medicine(int $rxId) { $rxGroupId = max(0, $rxId); $rxGroupName = ''; if ($rxGroupId > 0 && $this->db->tableExists('opd_prescription_template')) { $templateFields = $this->db->getFieldNames('opd_prescription_template'); $nameField = $this->resolveFirstField($templateFields, ['rx_group_name']); if ($nameField !== null) { $row = $this->db->table('opd_prescription_template') ->select($nameField . ' as rx_group_name') ->where('id', $rxGroupId) ->get(1) ->getRowArray(); $rxGroupName = trim((string) ($row['rx_group_name'] ?? '')); } } return view('billing/rx_group_medicine_workspace', [ 'rx_group_id' => $rxGroupId, 'rx_group_name' => $rxGroupName, ]); } public function rx_group_medicine_list(int $rxId) { $table = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']); if ($table === null) { return $this->response->setJSON(['rows' => [], 'rx_group_name' => '']); } $fields = $this->db->getFieldNames($table); if (! in_array('rx_group_id', $fields, true)) { return $this->response->setJSON(['rows' => [], 'rx_group_name' => '']); } $rxGroupName = ''; if ($this->db->tableExists('opd_prescription_template')) { $templateFields = $this->db->getFieldNames('opd_prescription_template'); $nameField = $this->resolveFirstField($templateFields, ['rx_group_name']); if ($nameField !== null) { $groupRow = $this->db->table('opd_prescription_template') ->select($nameField . ' as rx_group_name') ->where('id', (int) $rxId) ->get(1) ->getRowArray(); $rxGroupName = trim((string) ($groupRow['rx_group_name'] ?? '')); } } $builder = $this->db->table($table)->where('rx_group_id', (int) $rxId); if (in_array('id', $fields, true)) { $builder->orderBy('id', 'DESC'); } $rows = $builder->get()->getResultArray(); if (empty($rows)) { return $this->response->setJSON(['rows' => [], 'rx_group_name' => $rxGroupName]); } $medGenericMap = []; $missingGenericMedIds = []; foreach ($rows as $row) { $existingGeneric = trim((string) ($row['genericname'] ?? '')); $medId = (int) ($row['med_id'] ?? 0); if ($existingGeneric === '' && $medId > 0) { $missingGenericMedIds[$medId] = true; } } if (! empty($missingGenericMedIds)) { $masterTable = $this->findExistingTable(['opd_med_master']); if ($masterTable !== null) { $masterFields = $this->db->getFieldNames($masterTable); $masterIdField = $this->resolveFirstField($masterFields, ['id']); $masterGenericField = $this->resolveFirstField($masterFields, ['genericname', 'generic_name']); $masterSaltField = $this->resolveFirstField($masterFields, ['salt_name', 'sal_name', 'salt', 'saltname']); $labelField = $masterSaltField ?? $masterGenericField; if ($masterIdField !== null && $labelField !== null) { $medRows = $this->db->table($masterTable) ->select($masterIdField . ' as med_id,' . $labelField . ' as generic_fallback') ->whereIn($masterIdField, array_map('intval', array_keys($missingGenericMedIds))) ->get() ->getResultArray(); foreach ($medRows as $medRow) { $id = (int) ($medRow['med_id'] ?? 0); $label = trim((string) ($medRow['generic_fallback'] ?? '')); if ($id > 0 && $label !== '') { $medGenericMap[$id] = $label; } } } } } $doseMap = $this->getDoseMasterLabelMap('opd_dose_shed', ['dose_shed_id', 'id'], ['dose_show_sign', 'dose_sign', 'dose_sign_desc', 'name']); $whenMap = $this->getDoseMasterLabelMap('opd_dose_when', ['dose_when_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']); $freqMap = $this->getDoseMasterLabelMap('opd_dose_frequency', ['dose_freq_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']); $whereMap = $this->getDoseMasterLabelMap('opd_dose_where', ['dose_where_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']); foreach ($rows as &$row) { if (trim((string) ($row['genericname'] ?? '')) === '') { $inlineSalt = trim((string) ($row['salt_name'] ?? '')); if ($inlineSalt !== '') { $row['genericname'] = $inlineSalt; } $fallback = $medGenericMap[(int) ($row['med_id'] ?? 0)] ?? ''; if (($row['genericname'] ?? '') === '' && $fallback !== '') { $row['genericname'] = $fallback; } } $row['dosage_label'] = $this->resolveDoseMasterLabel($row['dosage'] ?? '', $doseMap); $row['dosage_when_label'] = $this->resolveDoseMasterLabel($row['dosage_when'] ?? '', $whenMap); $row['dosage_freq_label'] = $this->resolveDoseMasterLabel($row['dosage_freq'] ?? '', $freqMap); $row['dosage_where_label'] = $this->resolveDoseMasterLabel($row['dosage_where'] ?? '', $whereMap); } unset($row); return $this->response->setJSON(['rows' => $rows, 'rx_group_name' => $rxGroupName]); } public function rx_group_medicine_get(int $itemId) { $table = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx medicine table not found']); } $row = $this->db->table($table)->where('id', (int) $itemId)->get(1)->getRowArray() ?? []; if (empty($row)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx medicine not found']); } return $this->response->setJSON(['update' => 1, 'row' => $row]); } public function rx_group_medicine_save(int $rxId) { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } if (! $this->db->tableExists('opd_prescription_template')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group table not found']); } $rxExists = $this->db->table('opd_prescription_template')->where('id', (int) $rxId)->get(1)->getRowArray(); if (empty($rxExists)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group not found']); } $table = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx medicine table not found']); } $itemId = (int) $this->request->getPost('item_id'); $medId = (int) $this->request->getPost('med_id'); $medName = trim((string) $this->request->getPost('med_name')); if ($medName === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']); } $genericName = trim((string) $this->request->getPost('genericname')); $medType = trim((string) $this->request->getPost('med_type')); $masterTable = $this->findExistingTable(['opd_med_master']); if ($masterTable !== null) { $masterFields = $this->db->getFieldNames($masterTable); $masterNameField = $this->resolveFirstField($masterFields, ['item_name', 'med_name']); $masterTypeField = $this->resolveFirstField($masterFields, ['formulation']); $masterGenericField = $this->resolveFirstField($masterFields, ['genericname', 'generic_name']); $masterSaltField = $this->resolveFirstField($masterFields, ['salt_name', 'sal_name', 'salt', 'saltname']); if ($medId <= 0 && $masterNameField !== null) { $existingMaster = $this->db->table($masterTable)->where($masterNameField, $medName)->get(1)->getRowArray(); if (! empty($existingMaster)) { $medId = (int) ($existingMaster['id'] ?? 0); } } if ($medId > 0) { $updateMaster = []; if ($masterTypeField !== null && $medType !== '') { $updateMaster[$masterTypeField] = $medType; } if ($masterGenericField !== null && $genericName !== '') { $updateMaster[$masterGenericField] = $genericName; } if ($masterSaltField !== null && $genericName !== '') { $updateMaster[$masterSaltField] = $genericName; } if (! empty($updateMaster)) { $this->db->table($masterTable)->where('id', $medId)->update($updateMaster); } } elseif ($masterNameField !== null) { $insertMaster = [$masterNameField => $medName]; if ($masterTypeField !== null) { $insertMaster[$masterTypeField] = $medType; } if ($masterGenericField !== null) { $insertMaster[$masterGenericField] = $genericName; } if ($masterSaltField !== null) { $insertMaster[$masterSaltField] = $genericName; } $this->db->table($masterTable)->insert($insertMaster); $medId = (int) $this->db->insertID(); } } $fields = $this->db->getFieldNames($table); $data = []; if (in_array('rx_group_id', $fields, true)) { $data['rx_group_id'] = (int) $rxId; } if (in_array('med_id', $fields, true)) { $data['med_id'] = $medId; } if (in_array('med_name', $fields, true)) { $data['med_name'] = strtoupper($medName); } if (in_array('med_type', $fields, true)) { $data['med_type'] = $medType; } if (in_array('dosage', $fields, true)) { $data['dosage'] = trim((string) $this->request->getPost('dosage')); } if (in_array('dosage_when', $fields, true)) { $data['dosage_when'] = trim((string) $this->request->getPost('dosage_when')); } if (in_array('dosage_freq', $fields, true)) { $data['dosage_freq'] = trim((string) $this->request->getPost('dosage_freq')); } if (in_array('dosage_where', $fields, true)) { $data['dosage_where'] = trim((string) $this->request->getPost('dosage_where')); } if (in_array('no_of_days', $fields, true)) { $data['no_of_days'] = trim((string) $this->request->getPost('no_of_days')); } if (in_array('qty', $fields, true)) { $data['qty'] = trim((string) $this->request->getPost('qty')); } if (in_array('remark', $fields, true)) { $data['remark'] = strtoupper(trim((string) $this->request->getPost('remark'))); } if (in_array('genericname', $fields, true)) { $data['genericname'] = $genericName; } $user = auth()->user(); if (in_array('update_by', $fields, true)) { $userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User'; $data['update_by'] = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s'); } if ($itemId > 0) { $this->db->table($table)->where('id', $itemId)->update($data); $savedId = $itemId; $message = 'Rx group medicine updated'; } else { $this->db->table($table)->insert($data); $savedId = (int) $this->db->insertID(); $message = 'Rx group medicine added'; } return $this->response->setJSON([ 'update' => 1, 'insertid' => $savedId, 'error_text' => $message, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function rx_group_medicine_remove(int $itemId) { $table = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx medicine table not found']); } $this->db->table($table)->where('id', (int) $itemId)->delete(); return $this->response->setJSON(['update' => 1, 'error_text' => 'Rx group medicine removed']); } public function rx_group_medicine_suggest() { $q = trim((string) $this->request->getGet('q')); $table = $this->findExistingTable(['opd_med_master']); if ($table === null || $q === '') { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames($table); $idField = $this->resolveFirstField($fields, ['id']); $nameField = $this->resolveFirstField($fields, ['item_name', 'med_name']); $typeField = $this->resolveFirstField($fields, ['formulation']); $genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']); $saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']); $companyField = $this->resolveFirstField($fields, ['company_name', 'company']); if ($idField === null || $nameField === null) { return $this->response->setJSON(['rows' => []]); } $select = $idField . ' as med_id,' . $nameField . ' as med_name'; if ($typeField !== null) { $select .= ',' . $typeField . ' as med_type'; } if ($genericField !== null) { $select .= ',' . $genericField . ' as genericname'; } if ($saltField !== null) { $select .= ',' . $saltField . ' as salt_name'; } if ($companyField !== null) { $select .= ',' . $companyField . ' as company_name'; } $builder = $this->db->table($table)->select($select)->groupStart()->like($nameField, $q); if ($genericField !== null) { $builder->orLike($genericField, $q); } if ($saltField !== null) { $builder->orLike($saltField, $q); } $rows = $builder->groupEnd()->orderBy($nameField, 'ASC')->limit(30)->get()->getResultArray(); return $this->response->setJSON(['rows' => $rows]); } public function rx_group_generic_suggest() { $q = trim((string) $this->request->getGet('q')); $table = $this->findExistingTable(['opd_med_master']); if ($table === null) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames($table); $genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']); $saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']); if ($genericField === null && $saltField === null) { return $this->response->setJSON(['rows' => []]); } $values = []; $builder = $this->db->table($table); if ($genericField !== null) { $gBuilder = clone $builder; $gBuilder->select($genericField . ' as value')->where($genericField . ' !=', ''); if ($q !== '') { $gBuilder->like($genericField, $q); } foreach ($gBuilder->orderBy($genericField, 'ASC')->limit(40)->get()->getResultArray() as $row) { $value = trim((string) ($row['value'] ?? '')); if ($value !== '') { $values[$value] = ['value' => $value]; } } } if ($saltField !== null) { $sBuilder = clone $builder; $sBuilder->select($saltField . ' as value')->where($saltField . ' !=', ''); if ($q !== '') { $sBuilder->like($saltField, $q); } foreach ($sBuilder->orderBy($saltField, 'ASC')->limit(40)->get()->getResultArray() as $row) { $value = trim((string) ($row['value'] ?? '')); if ($value !== '') { $values[$value] = ['value' => $value]; } } } return $this->response->setJSON(['rows' => array_values($values)]); } public function rx_group_dose_masters() { return $this->response->setJSON([ 'dose' => $this->getDoseMasterRows('opd_dose_shed', ['dose_shed_id', 'id'], ['dose_show_sign', 'dose_sign', 'dose_sign_desc', 'name']), 'when' => $this->getDoseMasterRows('opd_dose_when', ['dose_when_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']), 'freq' => $this->getDoseMasterRows('opd_dose_frequency', ['dose_freq_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']), 'where' => $this->getDoseMasterRows('opd_dose_where', ['dose_where_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']), ]); } public function opd_medicince() { return view('billing/opd_medicince_workspace', ['initial_med_id' => 0]); } public function opd_medicince_edit(int $medId) { return view('billing/opd_medicince_workspace', ['initial_med_id' => max(0, $medId)]); } public function opd_medicince_data() { $filter = trim((string) $this->request->getGet('filter')); $scope = strtolower(trim((string) $this->request->getGet('scope'))); $showAll = (int) $this->request->getGet('show_all') === 1; if (! in_array($scope, ['all', 'favorite', 'active'], true)) { $scope = 'all'; } return $this->response->setJSON([ 'rows' => $this->getOpdMedicineRows($filter, $scope, $showAll), 'active_filter' => $filter, 'active_scope' => $scope, 'show_all' => $showAll ? 1 : 0, ]); } public function opd_medicince_export() { $filter = trim((string) $this->request->getGet('filter')); $rows = $this->getOpdMedicineRows($filter); $output = fopen('php://temp', 'r+'); if ($output === false) { return $this->response->setStatusCode(500)->setBody('Unable to generate export file.'); } fputcsv($output, ['ID', 'Medicine Name', 'Formulation', 'Generic Name', 'Salt Name', 'Company Name']); foreach ($rows as $row) { fputcsv($output, [ (int) ($row['id'] ?? 0), (string) ($row['item_name'] ?? ''), (string) ($row['formulation'] ?? ''), (string) ($row['genericname'] ?? ''), (string) ($row['salt_name'] ?? ''), (string) ($row['company_name'] ?? ''), ]); } rewind($output); $csv = stream_get_contents($output); fclose($output); if ($csv === false) { return $this->response->setStatusCode(500)->setBody('Unable to generate export file.'); } $suffix = $filter !== '' ? preg_replace('/[^a-z0-9_\-]/i', '_', strtolower($filter)) : 'all'; $filename = 'opd_medicine_' . $suffix . '_' . date('Ymd_His') . '.csv'; return $this->response ->setHeader('Content-Type', 'text/csv; charset=UTF-8') ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"') ->setBody($csv); } private function getOpdMedicineRows(string $filter = '', string $scope = 'all', bool $showAll = false): array { $table = $this->findExistingTable(['opd_med_master']); if ($table === null) { return []; } $fields = $this->db->getFieldNames($table); $idField = $this->resolveFirstField($fields, ['id']); $nameField = $this->resolveFirstField($fields, ['item_name', 'med_name']); $genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']); $saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']); $companyField = $this->resolveFirstField($fields, ['company_name', 'company']); $restrictionField = $this->resolveFirstField($fields, ['dosage_restriction', 'dose_restriction', 'restriction_note', 'restriction']); if ($idField === null || $nameField === null) { return []; } $select = $idField . ' as id,' . $nameField . ' as item_name'; if (in_array('formulation', $fields, true)) { $select .= ',formulation'; } if ($genericField !== null) { $select .= ',' . $genericField . ' as genericname'; } if ($saltField !== null) { $select .= ',' . $saltField . ' as salt_name'; } if ($companyField !== null) { $select .= ',' . $companyField . ' as company_name'; } if ($restrictionField !== null) { $select .= ',' . $restrictionField . ' as dosage_restriction'; } $rows = $this->db->table($table) ->select($select) ->orderBy($nameField, 'ASC') ->limit(5000) ->get() ->getResultArray(); $unclearTokens = ['na', 'n/a', 'same', '-', '--', 'unknown', '?']; $normalize = static function ($value): string { $text = trim((string) $value); if ($text === '') { return ''; } $text = preg_replace('/\s+/', ' ', $text) ?? $text; return mb_strtolower($text); }; if ($filter === 'generic_issue') { $rows = array_values(array_filter($rows, static function (array $row) use ($normalize, $unclearTokens): bool { $name = $normalize($row['item_name'] ?? ''); $generic = $normalize($row['genericname'] ?? ''); $salt = $normalize($row['salt_name'] ?? ''); $genericUnclear = ($generic === '' || in_array($generic, $unclearTokens, true) || ($name !== '' && $generic === $name)); $saltUnclear = ($salt === '' || in_array($salt, $unclearTokens, true) || ($name !== '' && $salt === $name)); return $genericUnclear && $saltUnclear; })); } elseif ($filter === 'generic_same_name') { $rows = array_values(array_filter($rows, static function (array $row) use ($normalize): bool { $name = $normalize($row['item_name'] ?? ''); $generic = $normalize($row['genericname'] ?? ''); $salt = $normalize($row['salt_name'] ?? ''); if ($name === '') { return false; } return ($generic !== '' && $generic === $name) || ($salt !== '' && $salt === $name); })); } elseif ($filter === 'company_blank') { $rows = array_values(array_filter($rows, static function (array $row) use ($normalize, $unclearTokens): bool { $company = $normalize($row['company_name'] ?? ''); return $company === '' || in_array($company, $unclearTokens, true); })); } foreach ($rows as &$row) { $generic = trim((string) ($row['genericname'] ?? '')); if ($generic === '') { $salt = trim((string) ($row['salt_name'] ?? '')); if ($salt !== '') { $row['genericname'] = $salt; } } } unset($row); $userId = (int) ($this->getCurrentUserId() ?? 0); $favoriteMap = $userId > 0 ? $this->getUserFavoriteMedicineIds($userId) : []; $usageMap = $userId > 0 ? $this->getUserMedicineUsageMap($userId) : []; foreach ($rows as &$row) { $medId = (int) ($row['id'] ?? 0); $row['is_favorite'] = isset($favoriteMap[$medId]) ? 1 : 0; $row['use_count'] = (int) ($usageMap[$medId]['use_count'] ?? 0); $row['last_used_at'] = (string) ($usageMap[$medId]['last_used_at'] ?? ''); } unset($row); if (! $showAll) { if ($scope === 'favorite') { $rows = array_values(array_filter($rows, static fn(array $row): bool => (int) ($row['is_favorite'] ?? 0) === 1)); } elseif ($scope === 'active') { $rows = array_values(array_filter($rows, static fn(array $row): bool => ((int) ($row['is_favorite'] ?? 0) === 1) || ((int) ($row['use_count'] ?? 0) > 0))); } } return $rows; } public function opd_medicince_company_suggest() { if (! $this->db->tableExists('med_company')) { return $this->response->setJSON(['rows' => []]); } $term = trim((string) $this->request->getGet('term')); $fields = $this->db->getFieldNames('med_company'); $nameField = $this->resolveFirstField($fields, ['company_name', 'name', 'short_name']); if ($nameField === null) { return $this->response->setJSON(['rows' => []]); } $builder = $this->db->table('med_company')->select($nameField . ' as company_name')->orderBy($nameField, 'ASC'); if ($term !== '') { $builder->like($nameField, $term); } $rows = $builder->limit(200)->get()->getResultArray(); $names = []; foreach ($rows as $row) { $name = trim((string) ($row['company_name'] ?? '')); if ($name === '') { continue; } $names[$name] = ['company_name' => $name]; } return $this->response->setJSON(['rows' => array_values($names)]); } public function opd_medicince_get(int $medId) { $table = $this->findExistingTable(['opd_med_master']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']); } $row = $this->db->table($table)->where('id', (int) $medId)->get(1)->getRowArray() ?? []; if (empty($row)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine not found']); } return $this->response->setJSON(['update' => 1, 'row' => $row]); } public function opd_medicince_save() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $table = $this->findExistingTable(['opd_med_master']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']); } $id = (int) $this->request->getPost('id'); $itemName = trim((string) $this->request->getPost('item_name')); if ($itemName === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']); } $medicineModel = new OpdMedicineModel($this->db); $duplicate = $medicineModel->findDuplicateByName($itemName, $id); if (! empty($duplicate)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Duplicate medicine name found (ID: ' . (int) ($duplicate['id'] ?? 0) . '). Please use existing entry or merge duplicates.', 'duplicate_id' => (int) ($duplicate['id'] ?? 0), 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $fields = $this->db->getFieldNames($table); $genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']); $saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']); $companyField = $this->resolveFirstField($fields, ['company_name', 'company']); $restrictionField = $this->resolveFirstField($fields, ['dosage_restriction', 'dose_restriction', 'restriction_note', 'restriction']); $data = []; if (in_array('item_name', $fields, true)) { $data['item_name'] = $itemName; } if (in_array('formulation', $fields, true)) { $data['formulation'] = trim((string) $this->request->getPost('formulation')); } if ($genericField !== null) { $data[$genericField] = trim((string) $this->request->getPost('genericname')); } if ($saltField !== null) { $data[$saltField] = trim((string) $this->request->getPost('salt_name')); } if ($companyField !== null) { $data[$companyField] = trim((string) $this->request->getPost('company_name')); } if ($restrictionField !== null) { $data[$restrictionField] = trim((string) $this->request->getPost('dosage_restriction')); } if ($id > 0) { $this->db->table($table)->where('id', $id)->update($data); $savedId = $id; $message = 'Medicine updated'; } else { $this->db->table($table)->insert($data); $savedId = (int) $this->db->insertID(); $message = 'Medicine added'; } return $this->response->setJSON([ 'update' => 1, 'insertid' => $savedId, 'error_text' => $message, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_medicince_ai_details() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $name = trim((string) $this->request->getPost('item_name')); $formulation = trim((string) $this->request->getPost('formulation')); if ($name === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']); } $local = $this->lookupMedicineDetailsFromMaster($name, $formulation); if (($local['genericname'] ?? '') !== '' || ($local['salt_name'] ?? '') !== '') { return $this->response->setJSON([ 'update' => 1, 'provider' => 'local-master', 'match_confidence' => (string) ($local['match_confidence'] ?? 'partial'), 'matched_item_name' => (string) ($local['matched_item_name'] ?? ''), 'details' => [ 'genericname' => trim((string) ($local['genericname'] ?? '')), 'salt_name' => trim((string) ($local['salt_name'] ?? '')), 'dosage_restriction' => trim((string) ($local['dosage_restriction'] ?? '')), 'caution_note' => trim((string) ($local['caution_note'] ?? '')), ], 'error_text' => 'Medicine details found from local master data', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $serverDetails = $this->fetchMedicineDetailsFromAiServer($name, $formulation); if (($serverDetails['genericname'] ?? '') !== '' || ($serverDetails['salt_name'] ?? '') !== '') { return $this->response->setJSON([ 'update' => 1, 'provider' => 'ai-server', 'match_confidence' => 'ai-server', 'matched_item_name' => '', 'details' => [ 'genericname' => trim((string) ($serverDetails['genericname'] ?? '')), 'salt_name' => trim((string) ($serverDetails['salt_name'] ?? '')), 'dosage_restriction' => trim((string) ($serverDetails['dosage_restriction'] ?? '')), 'caution_note' => trim((string) ($serverDetails['caution_note'] ?? '')), ], 'error_text' => 'Medicine details prepared from AI server', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $prompt = "You are a medicine data enrichment assistant for OPD master data in India.\n" . "For this medicine, provide structured details based on public medical references and standard labeling guidance.\n" . "Medicine Name: {$name}\n" . "Formulation: {$formulation}\n" . "Return ONLY valid JSON object with keys: genericname, salt_name, dosage_restriction, caution_note.\n" . "Use short plain text values. If unsure for a field, keep empty string."; $this->lastAiProvider = 'none'; $aiText = $this->generateClinicalTextWithAi($prompt, 280); if ($aiText === null || trim($aiText) === '') { $fallback = $this->inferMedicineDetailsFromBrandName($name, $formulation); if (($fallback['genericname'] ?? '') !== '' || ($fallback['salt_name'] ?? '') !== '') { return $this->response->setJSON([ 'update' => 1, 'provider' => 'local-brand-fallback', 'match_confidence' => 'brand-fallback', 'matched_item_name' => '', 'details' => [ 'genericname' => trim((string) ($fallback['genericname'] ?? '')), 'salt_name' => trim((string) ($fallback['salt_name'] ?? '')), 'dosage_restriction' => trim((string) ($fallback['dosage_restriction'] ?? '')), 'caution_note' => trim((string) ($fallback['caution_note'] ?? '')), ], 'error_text' => 'AI unavailable. Fallback generic/salt suggestion prepared.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $aiServerHint = $this->lastAiServerError !== '' ? (' AI-server attempts: ' . $this->lastAiServerError) : ''; return $this->response->setJSON([ 'update' => 0, 'error_text' => 'AI unavailable and no local match found. Check AI settings (DIAGNOSIS_AI_SERVER_URL or DIAGNOSIS_AI_MEDICINE_ENDPOINT).' . $aiServerHint, ]); } $parsed = $this->parseJsonFromAiResponse($aiText); if ($parsed === null) { $fallback = $this->inferMedicineDetailsFromBrandName($name, $formulation); if (($fallback['genericname'] ?? '') !== '' || ($fallback['salt_name'] ?? '') !== '') { return $this->response->setJSON([ 'update' => 1, 'provider' => 'local-brand-fallback', 'match_confidence' => 'brand-fallback', 'matched_item_name' => '', 'details' => [ 'genericname' => trim((string) ($fallback['genericname'] ?? '')), 'salt_name' => trim((string) ($fallback['salt_name'] ?? '')), 'dosage_restriction' => trim((string) ($fallback['dosage_restriction'] ?? '')), 'caution_note' => trim((string) ($fallback['caution_note'] ?? '')), ], 'error_text' => 'AI parse failed. Fallback generic/salt suggestion prepared.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } return $this->response->setJSON(['update' => 0, 'error_text' => 'AI response parse failed. Try again.']); } return $this->response->setJSON([ 'update' => 1, 'provider' => $this->lastAiProvider, 'match_confidence' => 'ai', 'matched_item_name' => '', 'details' => [ 'genericname' => trim((string) ($parsed['genericname'] ?? '')), 'salt_name' => trim((string) ($parsed['salt_name'] ?? '')), 'dosage_restriction' => trim((string) ($parsed['dosage_restriction'] ?? '')), 'caution_note' => trim((string) ($parsed['caution_note'] ?? '')), ], 'error_text' => 'AI details prepared', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } /** * @return array */ private function fetchMedicineDetailsFromAiServer(string $itemName, string $formulation): array { $this->lastAiServerError = ''; $itemName = trim($itemName); if ($itemName === '') { return []; } $configuredEndpointsRaw = trim($this->readAiSettingValue([ 'DIAGNOSIS_AI_MEDICINE_ENDPOINT', 'DIAGNOSIS_AI_MEDICINE_DETAILS_ENDPOINT', 'AI_SERVER_MEDICINE_ENDPOINT', ])); $token = trim($this->readAiSettingValue(['DIAGNOSIS_AI_PARSE_TOKEN', 'AI_SERVER_TOKEN'])); if ($token === '') { $token = trim((string) getenv('DIAGNOSIS_AI_PARSE_TOKEN')); } if ($token === '') { $token = trim((string) getenv('AI_SERVER_TOKEN')); } $payload = [ 'item_name' => $itemName, 'medicine_name' => $itemName, 'name' => $itemName, 'formulation' => trim($formulation), 'mode' => 'medicine-details', ]; $headers = [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ]; if ($token !== '') { $headers['Authorization'] = 'Bearer ' . $token; } $baseCandidates = $this->resolveAiServerBaseCandidates(); $defaultPaths = [ '/medicine-details', '/medicine/details', '/medicine/enrich', '/medicine/lookup', '/api/medicine/details', '/api/medicine/enrich', '/api/medicine/lookup', '/api/v1/medicine/details', '/api/v1/medicine/enrich', '/api/v1/medicine/lookup', '/v1/medicine/details', '/v1/medicine/enrich', '/v1/medicine/lookup', ]; $endpointCandidates = []; if ($configuredEndpointsRaw !== '') { $parts = preg_split('/[\r\n,]+/', $configuredEndpointsRaw) ?: []; foreach ($parts as $part) { $candidate = trim((string) $part); if ($candidate === '') { continue; } // Normalize accidental Windows-style slashes from settings values. $candidate = str_replace('\\', '/', $candidate); if (preg_match('#^https?://#i', $candidate) === 1) { $endpointCandidates[] = rtrim($candidate, '/'); continue; } $path = '/' . ltrim($candidate, '/'); foreach ($baseCandidates as $base) { $endpointCandidates[] = rtrim($base, '/') . $path; } } } foreach ($baseCandidates as $base) { foreach ($defaultPaths as $path) { $endpointCandidates[] = rtrim($base, '/') . $path; } } $endpointCandidates = array_values(array_unique($endpointCandidates)); $endpointCandidates = array_values(array_filter($endpointCandidates, static function ($url): bool { $text = (string) $url; if ($text === '') { return false; } $lower = strtolower($text); if (strpos($text, '\\') !== false || strpos($lower, '%5c') !== false) { return false; } return true; })); $statusTrail = []; foreach ($endpointCandidates as $endpointUrl) { if (strpos($endpointUrl, '\\') !== false || strpos(strtolower($endpointUrl), '%5c') !== false) { $statusTrail[] = 'skipped-malformed @ ' . $endpointUrl; continue; } try { $options = [ 'timeout' => 20, 'http_errors' => false, ]; if (defined('ENVIRONMENT') && in_array((string) ENVIRONMENT, ['development', 'testing'], true)) { $options['verify'] = false; } $client = service('curlrequest', $options); $response = $client->post($endpointUrl, [ 'headers' => $headers, 'json' => $payload, ]); $status = (int) $response->getStatusCode(); $statusTrail[] = $status . ' @ ' . $endpointUrl; if ($status < 200 || $status >= 300) { continue; } $decoded = json_decode((string) $response->getBody(), true); if (!is_array($decoded)) { continue; } $details = is_array($decoded['details'] ?? null) ? $decoded['details'] : $decoded; $data = is_array($decoded['data'] ?? null) ? $decoded['data'] : []; $generic = trim((string) ( $details['genericname'] ?? $details['generic_name'] ?? $data['genericname'] ?? $data['generic_name'] ?? $data['medicine_name'] ?? '' )); $salt = trim((string) ( $details['salt_name'] ?? $details['salt'] ?? $data['salt_name'] ?? $data['salt'] ?? '' )); if ($salt === '' && is_array($data['brand_names'] ?? null)) { $salt = trim(implode(', ', array_slice(array_map('strval', $data['brand_names']), 0, 3))); } $dosage = trim((string) ( $details['dosage_restriction'] ?? $details['restriction'] ?? $data['dosage_restriction'] ?? $data['restriction'] ?? ((is_array($data['dosage'] ?? null) ? ($data['dosage']['adult_dose'] ?? '') : '')) ?? '' )); $caution = trim((string) ( $details['caution_note'] ?? $details['caution'] ?? $data['caution_note'] ?? $data['caution'] ?? '' )); if ($caution === '') { $pieces = []; if (is_array($data['contraindications'] ?? null) && !empty($data['contraindications'])) { $pieces[] = 'Contra: ' . implode(', ', array_slice(array_map('strval', $data['contraindications']), 0, 2)); } if (is_array($data['precautions'] ?? null) && !empty($data['precautions'])) { $pieces[] = 'Prec: ' . implode(', ', array_slice(array_map('strval', $data['precautions']), 0, 2)); } if (is_array($data['drug_interactions'] ?? null) && !empty($data['drug_interactions'])) { $pieces[] = 'Interacts: ' . implode(', ', array_slice(array_map('strval', $data['drug_interactions']), 0, 2)); } $caution = trim(implode(' | ', $pieces)); } $out = [ 'genericname' => $generic, 'salt_name' => $salt, 'dosage_restriction' => $this->formatMedicineNote($dosage, 420), 'caution_note' => $this->formatMedicineNote($caution, 520), ]; if ($out['genericname'] !== '' || $out['salt_name'] !== '') { $this->lastAiProvider = 'ai-server'; $this->lastAiServerError = ''; return $out; } $statusTrail[] = 'empty-payload @ ' . $endpointUrl; } catch (\Throwable $e) { $statusTrail[] = 'error @ ' . $endpointUrl . ' :: ' . $e->getMessage(); } } if (! empty($statusTrail)) { $this->lastAiServerError = implode(' | ', array_slice($statusTrail, 0, 5)); } return []; } private function formatMedicineNote(string $value, int $maxLen = 500): string { $text = trim($value); if ($text === '') { return ''; } // Normalize separators for UI readability while preserving meaning. $text = str_replace(["\r\n", "\n", "\r"], ' | ', $text); $text = preg_replace('/\s*\|\s*/', ' | ', $text) ?? $text; $text = preg_replace('/\s+/', ' ', $text) ?? $text; $text = trim($text, " \t\n\r\0\x0B|"); if ($maxLen < 80) { $maxLen = 80; } if (mb_strlen($text) <= $maxLen) { return $text; } $short = mb_substr($text, 0, $maxLen); $lastSep = mb_strrpos($short, ' | '); if ($lastSep !== false && $lastSep > (int) ($maxLen * 0.6)) { $short = mb_substr($short, 0, $lastSep); } return rtrim($short, " ,;|.") . '...'; } /** * @return array */ private function lookupMedicineDetailsFromMaster(string $name, string $formulation = ''): array { $table = $this->findExistingTable(['opd_med_master']); if ($table === null) { return []; } $fields = $this->db->getFieldNames($table); $nameField = $this->resolveFirstField($fields, ['item_name', 'med_name']); $formField = $this->resolveFirstField($fields, ['formulation', 'med_type']); $genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']); $saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']); $restrictionField = $this->resolveFirstField($fields, ['dosage_restriction', 'dose_restriction', 'restriction_note', 'restriction']); if ($nameField === null || ($genericField === null && $saltField === null)) { return []; } $name = trim($name); if ($name === '') { return []; } $exact = $this->db->table($table) ->where('UPPER(' . $nameField . ')', strtoupper($name)); if ($formField !== null && trim($formulation) !== '') { $exact->where('UPPER(' . $formField . ')', strtoupper(trim($formulation))); } $row = $exact->get(1)->getRowArray() ?? []; $matchConfidence = 'partial'; if (empty($row)) { $builder = $this->db->table($table); $builder->groupStart() ->like($nameField, $name) ->orLike($nameField, preg_replace('/\s+/', ' ', $name) ?? $name) ->groupEnd(); if ($formField !== null && trim($formulation) !== '') { $builder->like($formField, trim($formulation)); } $candidates = $builder->limit(15)->get()->getResultArray(); if (! empty($candidates)) { $target = strtoupper($name); usort($candidates, static function (array $a, array $b) use ($nameField, $target): int { $aName = strtoupper(trim((string) ($a[$nameField] ?? ''))); $bName = strtoupper(trim((string) ($b[$nameField] ?? ''))); $aScore = 0; $bScore = 0; if ($aName === $target) { $aScore += 1000; } elseif (str_starts_with($aName, $target)) { $aScore += 600; } elseif (str_contains($aName, $target)) { $aScore += 400; } if ($bName === $target) { $bScore += 1000; } elseif (str_starts_with($bName, $target)) { $bScore += 600; } elseif (str_contains($bName, $target)) { $bScore += 400; } if ($aScore !== $bScore) { return $bScore <=> $aScore; } return strlen($aName) <=> strlen($bName); }); $row = $candidates[0] ?? []; $picked = strtoupper(trim((string) ($row[$nameField] ?? ''))); $target = strtoupper($name); if ($picked === $target) { $matchConfidence = 'exact'; } elseif (str_starts_with($picked, $target) || str_contains($picked, $target)) { $matchConfidence = 'close'; } } } else { $matchConfidence = 'exact'; } if (empty($row)) { return []; } return [ 'genericname' => $genericField !== null ? trim((string) ($row[$genericField] ?? '')) : '', 'salt_name' => $saltField !== null ? trim((string) ($row[$saltField] ?? '')) : '', 'dosage_restriction' => $restrictionField !== null ? trim((string) ($row[$restrictionField] ?? '')) : '', 'caution_note' => '', 'match_confidence' => $matchConfidence, 'matched_item_name' => trim((string) ($row[$nameField] ?? '')), ]; } /** * @return array */ private function inferMedicineDetailsFromBrandName(string $name, string $formulation = ''): array { $text = strtoupper(trim($name)); if ($text === '') { return []; } preg_match('/(\d{2,4})\s*MG/i', $name, $m); $strength = isset($m[1]) ? trim((string) $m[1]) : ''; $map = [ 'DOLO' => 'Paracetamol', 'CROCIN' => 'Paracetamol', 'CALPOL' => 'Paracetamol', 'PARACIP' => 'Paracetamol', 'PANTOCID' => 'Pantoprazole', 'PAN' => 'Pantoprazole', 'AZEE' => 'Azithromycin', 'AZTOR' => 'Atorvastatin', 'MONTAIR' => 'Montelukast', 'LEVOCET' => 'Levocetirizine', 'AUGMENTIN' => 'Amoxicillin + Clavulanic Acid', ]; $generic = ''; foreach ($map as $brand => $gen) { if (str_contains($text, $brand)) { $generic = $gen; break; } } if ($generic === '') { return []; } $salt = $generic; if ($strength !== '') { if ($generic === 'Amoxicillin + Clavulanic Acid' && $strength === '625') { $salt = 'Amoxicillin 500 mg + Clavulanic Acid 125 mg'; } else { $salt = $generic . ' ' . $strength . ' mg'; } } $doseRestriction = ''; if ($generic === 'Paracetamol') { $doseRestriction = 'Adults: usually max 4 g/day total from all paracetamol-containing products.'; } elseif ($generic === 'Pantoprazole') { $doseRestriction = 'Usually once daily before food unless specifically advised otherwise.'; } elseif ($generic === 'Azithromycin') { $doseRestriction = 'Follow fixed daily schedule and complete full course.'; } return [ 'genericname' => $generic, 'salt_name' => $salt, 'dosage_restriction' => $doseRestriction, 'caution_note' => '', ]; } public function opd_medicince_remove(int $medId) { $table = $this->findExistingTable(['opd_med_master']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']); } $this->db->table($table)->where('id', (int) $medId)->delete(); return $this->response->setJSON(['update' => 1, 'error_text' => 'Medicine removed']); } public function opd_advice() { return view('billing/opd_advice_workspace', ['initial_advice_id' => 0]); } public function opd_advice_edit(int $adviceId) { return view('billing/opd_advice_workspace', ['initial_advice_id' => max(0, $adviceId)]); } public function opd_advice_data() { if (! $this->db->tableExists('opd_advice')) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames('opd_advice'); $idField = $this->resolveFirstField($fields, ['id', 'Code']); $adviceField = $this->resolveFirstField($fields, ['advice', 'Name', 'advice_txt']); $hindiField = $this->resolveFirstField($fields, ['advice_hindi', 'advice_local', 'hindi_advice', 'advice_hin']); if ($idField === null || $adviceField === null) { return $this->response->setJSON(['rows' => []]); } $select = $idField . ' as id,' . $adviceField . ' as advice'; if ($hindiField !== null) { $select .= ',' . $hindiField . ' as advice_hindi'; } $rows = $this->db->table('opd_advice') ->select($select) ->orderBy($adviceField, 'ASC') ->limit(5000) ->get() ->getResultArray(); return $this->response->setJSON(['rows' => $rows]); } public function opd_advice_get(int $adviceId) { if (! $this->db->tableExists('opd_advice')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice table not found']); } $fields = $this->db->getFieldNames('opd_advice'); $idField = $this->resolveFirstField($fields, ['id', 'Code']); if ($idField === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice ID field not found']); } $row = $this->db->table('opd_advice')->where($idField, (int) $adviceId)->get(1)->getRowArray() ?? []; if (empty($row)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice not found']); } return $this->response->setJSON(['update' => 1, 'row' => $row]); } public function opd_advice_save() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } if (! $this->db->tableExists('opd_advice')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice table not found']); } $fields = $this->db->getFieldNames('opd_advice'); $idField = $this->resolveFirstField($fields, ['id', 'Code']); $adviceField = $this->resolveFirstField($fields, ['advice', 'Name', 'advice_txt']); $hindiField = $this->resolveFirstField($fields, ['advice_hindi', 'advice_local', 'hindi_advice', 'advice_hin']); if ($idField === null || $adviceField === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice fields not found']); } $id = (int) $this->request->getPost('id'); $adviceText = trim((string) $this->request->getPost('advice')); $adviceHindi = trim((string) $this->request->getPost('advice_hindi')); if ($adviceText === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice text is required']); } $data = []; $data[$adviceField] = $adviceText; if ($hindiField !== null) { $data[$hindiField] = $adviceHindi; } if ($id > 0) { $this->db->table('opd_advice')->where($idField, $id)->update($data); $savedId = $id; $message = 'Advice updated'; } else { $this->db->table('opd_advice')->insert($data); $savedId = (int) $this->db->insertID(); $message = 'Advice added'; } return $this->response->setJSON([ 'update' => 1, 'insertid' => $savedId, 'error_text' => $message, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_advice_translate_ai() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $sourceText = trim((string) $this->request->getPost('advice')); $targetLang = strtolower(trim((string) $this->request->getPost('target_lang'))); if ($sourceText === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'English advice text is required']); } $supportedLanguages = [ 'hi' => 'Hindi', 'bn' => 'Bengali', 'mr' => 'Marathi', 'ta' => 'Tamil', 'te' => 'Telugu', 'gu' => 'Gujarati', 'kn' => 'Kannada', 'ml' => 'Malayalam', 'pa' => 'Punjabi', 'or' => 'Odia', 'as' => 'Assamese', 'ur' => 'Urdu', ]; if (! isset($supportedLanguages[$targetLang])) { $targetLang = 'hi'; } $targetLanguageName = $supportedLanguages[$targetLang]; $this->lastAiProvider = 'none'; $translatedText = $this->translateAdviceWithAiServer($sourceText, $targetLang); if ($translatedText === '') { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'AI server unavailable. Check DIAGNOSIS_AI_SERVER_URL and token settings.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } return $this->response->setJSON([ 'update' => 1, 'provider' => 'ai-server', 'target_lang' => $targetLang, 'target_language_name' => $targetLanguageName, 'translated_text' => $translatedText, 'error_text' => 'Advice translated', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_advice_remove(int $adviceId) { if (! $this->db->tableExists('opd_advice')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice table not found']); } $fields = $this->db->getFieldNames('opd_advice'); $idField = $this->resolveFirstField($fields, ['id', 'Code']); if ($idField === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice ID field not found']); } $this->db->table('opd_advice')->where($idField, (int) $adviceId)->delete(); return $this->response->setJSON(['update' => 1, 'error_text' => 'Advice removed']); } public function opd_medicince_duplicate_report() { $medicineModel = new OpdMedicineModel($this->db); if (! $medicineModel->tableExists()) { return $this->response->setJSON(['rows' => []]); } return $this->response->setJSON([ 'rows' => $medicineModel->getDuplicateGroups(), 'update' => 1, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_medicince_merge_duplicates() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $keepId = (int) $this->request->getPost('keep_id'); $mergeRaw = trim((string) $this->request->getPost('merge_ids')); $mergeIds = []; if ($mergeRaw !== '') { foreach (explode(',', $mergeRaw) as $token) { $value = (int) trim($token); if ($value > 0) { $mergeIds[] = $value; } } } $medicineModel = new OpdMedicineModel($this->db); $merged = $medicineModel->mergeDuplicates($keepId, $mergeIds); if (! ($merged['ok'] ?? false)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => (string) ($merged['error'] ?? 'Unable to merge duplicates'), 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Merged ' . (int) ($merged['merged_count'] ?? 0) . ' duplicate medicine(s).', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_medicince_autofix_duplicates() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $medicineModel = new OpdMedicineModel($this->db); if (! $medicineModel->tableExists()) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Medicine table not found', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $groups = $medicineModel->getDuplicateGroups(); if (empty($groups)) { return $this->response->setJSON([ 'update' => 1, 'error_text' => 'No duplicate medicine names found.', 'merged_groups' => 0, 'merged_rows' => 0, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $mergedGroups = 0; $mergedRows = 0; foreach ($groups as $group) { $keepId = (int) ($group['keep_id'] ?? 0); $mergeIds = is_array($group['merge_ids'] ?? null) ? $group['merge_ids'] : []; if ($keepId <= 0 || empty($mergeIds)) { continue; } $result = $medicineModel->mergeDuplicates($keepId, $mergeIds); if (! ($result['ok'] ?? false)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => (string) ($result['error'] ?? 'Auto-fix stopped due to merge failure'), 'merged_groups' => $mergedGroups, 'merged_rows' => $mergedRows, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $mergedGroups++; $mergedRows += (int) ($result['merged_count'] ?? 0); } return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Auto-fix completed. Merged ' . $mergedRows . ' duplicate medicine row(s) in ' . $mergedGroups . ' group(s).', 'merged_groups' => $mergedGroups, 'merged_rows' => $mergedRows, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function scan_text_extract() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $filePath = trim((string) $this->request->getPost('file_path')); $scanType = trim((string) $this->request->getPost('scan_type')); if ($filePath === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Scan file path is required']); } $localPath = $this->resolveLocalFilePathFromStoredPath($filePath); if ($localPath === null || !is_file($localPath)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Scan file not found']); } $extracted = $this->extractClinicalTextFromScan($localPath, $scanType); if ($extracted === null || trim($extracted) === '') { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Unable to extract report text from this scan currently.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } return $this->response->setJSON([ 'update' => 1, 'extracted_text' => trim($extracted), 'error_text' => 'Scan text extracted', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function patient_scan_history(int $opdId) { if (! $this->canAccessPrescription()) { return $this->response->setStatusCode(403)->setBody('
Access denied.
'); } if ($opdId <= 0 || ! $this->db->tableExists('opd_master')) { return $this->response->setBody('
No scan history found.
'); } $opdRow = $this->db->table('opd_master')->select('p_id')->where('opd_id', $opdId)->get(1)->getRowArray(); $patientId = (int) ($opdRow['p_id'] ?? 0); if ($patientId <= 0) { return $this->response->setBody('
No scan history found.
'); } return view('billing/opd_scan_history_panel', [ 'scan_history_items' => $this->getPatientScanHistoryForConsult($patientId), ]); } public function fhir_bundle(int $opdId, int $sessionId = 0) { if (! $this->db->tableExists('opd_fhir_documents')) { return $this->response->setStatusCode(404)->setBody('FHIR bundle storage not available.'); } $builder = $this->db->table('opd_fhir_documents') ->where('opd_id', (int) $opdId) ->where('bundle_type', 'MedicationRequestBundle'); if ($sessionId > 0) { $builder->where('opd_session_id', (int) $sessionId); } $row = $builder->orderBy('id', 'DESC')->get(1)->getRowArray(); if (empty($row)) { return $this->response->setStatusCode(404)->setBody('FHIR bundle not found.'); } $bundleJson = (string) ($row['bundle_json'] ?? '{}'); $fileSession = (int) ($row['opd_session_id'] ?? 0); $filename = 'opd_' . (int) $opdId . '_session_' . $fileSession . '_fhir.json'; return $this->response ->setHeader('Content-Type', 'application/fhir+json; charset=utf-8') ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"') ->setBody($bundleJson); } public function fhir_bundle_history(int $opdId, int $sessionId = 0) { if (! $this->db->tableExists('opd_fhir_documents')) { return $this->response->setJSON(['rows' => []]); } $builder = $this->db->table('opd_fhir_documents') ->where('opd_id', (int) $opdId) ->where('bundle_type', 'MedicationRequestBundle'); if ($sessionId > 0) { $builder->where('opd_session_id', (int) $sessionId); } $rows = $builder->orderBy('id', 'DESC')->limit(5)->get()->getResultArray(); $result = []; foreach ($rows as $row) { $result[] = [ 'id' => (int) ($row['id'] ?? 0), 'opd_session_id' => (int) ($row['opd_session_id'] ?? 0), 'generated_at' => (string) ($row['generated_at'] ?? ''), 'generated_by' => (string) ($row['generated_by'] ?? ''), 'download_url' => base_url('Opd_prescription/fhir_bundle/' . (int) $opdId . '/' . (int) ($row['opd_session_id'] ?? 0)), ]; } return $this->response->setJSON(['rows' => $result]); } public function advice_search() { $q = trim((string) $this->request->getGet('q')); if (! $this->db->tableExists('opd_advice')) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames('opd_advice'); $idField = in_array('id', $fields, true) ? 'id' : (in_array('Code', $fields, true) ? 'Code' : null); $textField = in_array('advice', $fields, true) ? 'advice' : (in_array('Name', $fields, true) ? 'Name' : null); if ($idField === null || $textField === null) { return $this->response->setJSON(['rows' => []]); } $builder = $this->db->table('opd_advice') ->select($idField . ' as id,' . $textField . ' as label'); if ($q !== '') { $builder->like($textField, $q); } $rows = $builder ->orderBy($idField, 'DESC') ->limit(20) ->get() ->getResultArray(); return $this->response->setJSON(['rows' => $rows]); } public function complaints_search() { $q = trim((string) $this->request->getGet('q')); if ($q === '' || ! $this->db->tableExists('complaints_master')) { return $this->response->setJSON(['rows' => []]); } $rows = $this->db->table('complaints_master') ->select('Code as code, Name as name, name_hinglish, show_in_short, ai_hint') ->where('is_active', 1) ->groupStart() ->like('Name', $q) ->orLike('name_hinglish', $q) ->orLike('keywords', $q) ->groupEnd() ->orderBy('show_in_short', 'DESC') ->orderBy('Name', 'ASC') ->limit(20) ->get() ->getResultArray(); return $this->response->setJSON(['rows' => $rows]); } public function complaints_parse() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $text = trim((string) $this->request->getPost('text')); if ($text === '') { return $this->response->setJSON(['update' => 0, 'rows' => [], 'error_text' => 'Enter complaint text']); } $matched = $this->getComplaintMatchesFromText($text); return $this->response->setJSON([ 'update' => 1, 'rows' => $matched, 'error_text' => empty($matched) ? 'No standard complaints matched.' : 'Complaint terms matched', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function complaints_ai_draft() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $currentText = trim((string) $this->request->getPost('current_text')); $complaints = $this->request->getPost('complaints'); $complaints = is_array($complaints) ? $complaints : []; $normalized = []; foreach ($complaints as $item) { $item = trim((string) $item); if ($item === '') { continue; } $normalized[strtoupper($item)] = $item; } if ($currentText !== '') { foreach ($this->getComplaintMatchesFromText($currentText) as $item) { $normalized[strtoupper($item)] = $item; } } if (! empty($normalized) && $this->db->tableExists('complaints_master')) { $names = array_values($normalized); $hintRows = $this->db->table('complaints_master') ->select('Name, ai_hint') ->whereIn('Name', $names) ->get() ->getResultArray(); foreach ($hintRows as $hintRow) { $name = trim((string) ($hintRow['Name'] ?? '')); $hint = trim((string) ($hintRow['ai_hint'] ?? '')); if ($name !== '' && $hint !== '') { $normalized[strtoupper($name)] = $name . ' (' . $hint . ')'; } } } $finalComplaints = array_values($normalized); $this->lastAiProvider = 'none'; $aiDraft = $this->generateComplaintDraftWithAi($finalComplaints, $currentText); if ($aiDraft !== null && trim($aiDraft) !== '') { $provider = $this->lastAiProvider === 'ai-server' ? 'ai-server' : 'ai'; $providerLabel = $provider === 'ai-server' ? 'AI Server' : 'AI'; return $this->response->setJSON([ 'update' => 1, 'draft_text' => trim($aiDraft), 'error_text' => 'AI draft prepared using ' . $providerLabel, 'ai_mode' => $provider, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $line = ''; if (! empty($finalComplaints)) { $line = 'Chief complaints: ' . implode(', ', $finalComplaints) . '.'; } $draft = trim($line . ' ' . $currentText); if ($draft === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Please add complaint terms first.']); } return $this->response->setJSON([ 'update' => 1, 'draft_text' => $draft, 'error_text' => 'Complaint draft prepared (template mode)', 'ai_mode' => 'template', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function advice_list($opdId, $sessionId = 0) { $table = $this->findExistingTable(['opd_prescription_advice']); if ($table === null) { return $this->response->setJSON(['rows' => []]); } $builder = $this->db->table($table); $fields = $this->db->getFieldNames($table); if (in_array('opd_pre_id', $fields, true)) { if ((int) $sessionId <= 0) { return $this->response->setJSON(['rows' => []]); } $builder->where('opd_pre_id', (int) $sessionId); } elseif (in_array('opd_id', $fields, true)) { $builder->where('opd_id', (int) $opdId); } else { return $this->response->setJSON(['rows' => []]); } if (in_array('id', $fields, true)) { $builder->orderBy('id', 'DESC'); } $rows = $builder->get()->getResultArray(); return $this->response->setJSON(['rows' => $rows]); } public function advice_add() { if (!$this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $opdId = (int) $this->request->getPost('opd_id'); $sessionId = (int) $this->request->getPost('opd_session_id'); $adviceText = trim((string) $this->request->getPost('advice_text')); $adviceId = (int) $this->request->getPost('advice_id'); if ($opdId <= 0 || $adviceText === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice text is required']); } $table = $this->findExistingTable(['opd_prescription_advice']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice table not found']); } $sessionId = $this->ensurePrescriptionSession($opdId, $sessionId); if ($sessionId <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create prescription session']); } $fields = $this->db->getFieldNames($table); $insert = []; if (in_array('opd_id', $fields, true)) { $insert['opd_id'] = $opdId; } if (in_array('opd_pre_id', $fields, true)) { $insert['opd_pre_id'] = $sessionId; } if (in_array('advice_id', $fields, true)) { $insert['advice_id'] = $adviceId; } if (in_array('advice_txt', $fields, true)) { $insert['advice_txt'] = $adviceText; } elseif (in_array('advice', $fields, true)) { $insert['advice'] = $adviceText; } $this->db->table($table)->insert($insert); $insertId = (int) $this->db->insertID(); $this->auditClinicalUpdate('opd_prescription_advice', 'added', $insertId, null, $insert); return $this->response->setJSON([ 'update' => 1, 'opd_session_id' => $sessionId, 'error_text' => 'Advice added', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function advice_remove($id) { $table = $this->findExistingTable(['opd_prescription_advice']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice table not found']); } $fields = $this->db->getFieldNames($table); if (!in_array('id', $fields, true)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice id field missing']); } $row = $this->db->table($table)->where('id', (int) $id)->get(1)->getRowArray() ?? []; $this->db->table($table)->where('id', (int) $id)->delete(); $this->auditClinicalUpdate('opd_prescription_advice', 'removed', (int) $id, $row, null); return $this->response->setJSON(['update' => 1, 'error_text' => 'Advice removed']); } public function investigation_search() { $q = trim((string) $this->request->getGet('q')); if ($q === '' || !$this->db->tableExists('investigation')) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames('investigation'); $codeField = in_array('Code', $fields, true) ? 'Code' : (in_array('code', $fields, true) ? 'code' : null); $nameField = in_array('Name', $fields, true) ? 'Name' : (in_array('name', $fields, true) ? 'name' : null); $shortField = in_array('short_name', $fields, true) ? 'short_name' : null; if ($codeField === null || $nameField === null) { return $this->response->setJSON(['rows' => []]); } $builder = $this->db->table('investigation')->select($codeField . ' as code,' . $nameField . ' as name'); $builder->groupStart()->like($nameField, $q); if ($shortField !== null) { $builder->orLike($shortField, $q); } $builder->groupEnd(); $builder->orderBy('LOWER(' . $this->db->protectIdentifiers($nameField) . ')', 'ASC', false); $rows = $builder->limit(20)->get()->getResultArray(); return $this->response->setJSON(['rows' => $rows]); } public function investigation_shortcuts() { $profiles = []; $shortTests = []; if ($this->db->tableExists('investigation')) { $invFields = $this->db->getFieldNames('investigation'); $codeField = $this->resolveFirstField($invFields, ['Code', 'code']); $nameField = $this->resolveFirstField($invFields, ['Name', 'name']); $shortField = $this->resolveFirstField($invFields, ['short_name', 'shortName', 'short']); $sortField = $this->resolveFirstField($invFields, ['sort_id', 'sort_order', 'id']); if ($codeField !== null && $nameField !== null && $shortField !== null) { $builder = $this->db->table('investigation') ->select($codeField . ' as code,' . $nameField . ' as name,' . $shortField . ' as short_name') ->where($shortField . ' !=', '') ->where($shortField . ' is not null', null, false) ->limit(200); if ($sortField !== null) { $builder->orderBy($shortField, 'ASC')->orderBy($sortField, 'ASC'); } else { $builder->orderBy($shortField, 'ASC')->orderBy($nameField, 'ASC'); } $shortTests = $builder->get()->getResultArray(); } } if ($this->db->tableExists('invprofiles') && $this->db->tableExists('invtprofiles') && $this->db->tableExists('investigation')) { $pFields = $this->db->getFieldNames('invprofiles'); $jFields = $this->db->getFieldNames('invtprofiles'); $iFields = $this->db->getFieldNames('investigation'); $pCode = $this->resolveFirstField($pFields, ['Code', 'code']); $pName = $this->resolveFirstField($pFields, ['Name', 'name']); $jProfileCode = $this->resolveFirstField($jFields, ['ProfileCode', 'profile_code']); $jInvestCode = $this->resolveFirstField($jFields, ['InvestigationCode', 'investigation_code']); $jPrintOrder = $this->resolveFirstField($jFields, ['printOrder', 'print_order', 'id']); $iCode = $this->resolveFirstField($iFields, ['Code', 'code']); $iName = $this->resolveFirstField($iFields, ['Name', 'name']); if ($pCode !== null && $pName !== null && $jProfileCode !== null && $jInvestCode !== null && $iCode !== null && $iName !== null) { $orderExpr = 'p.`' . $pName . '` ASC'; if ($jPrintOrder !== null) { $orderExpr .= ', j.`' . $jPrintOrder . '` ASC'; } $sql = 'SELECT ' . 'p.`' . $pCode . '` AS profile_code, ' . 'p.`' . $pName . '` AS profile_name, ' . 'i.`' . $iCode . '` AS investigation_code, ' . 'i.`' . $iName . '` AS investigation_name ' . 'FROM `invprofiles` p ' . 'JOIN `invtprofiles` j ON p.`' . $pCode . '` = j.`' . $jProfileCode . '` ' . 'JOIN `investigation` i ON i.`' . $iCode . '` = j.`' . $jInvestCode . '` ' . 'ORDER BY ' . $orderExpr; $rows = $this->db->query($sql)->getResultArray(); $profileMap = []; foreach ($rows as $row) { $profileCode = trim((string) ($row['profile_code'] ?? '')); $profileName = trim((string) ($row['profile_name'] ?? '')); $testCode = trim((string) ($row['investigation_code'] ?? '')); $testName = trim((string) ($row['investigation_name'] ?? '')); if ($profileCode === '' || $profileName === '' || $testName === '') { continue; } if (!isset($profileMap[$profileCode])) { $profileMap[$profileCode] = [ 'profile_code' => $profileCode, 'profile_name' => $profileName, 'tests' => [], '_seen' => [], ]; } $key = strtolower($testCode . '|' . $testName); if (isset($profileMap[$profileCode]['_seen'][$key])) { continue; } $profileMap[$profileCode]['_seen'][$key] = true; $profileMap[$profileCode]['tests'][] = [ 'code' => $testCode, 'name' => $testName, ]; } foreach ($profileMap as $item) { unset($item['_seen']); $profiles[] = $item; } } } return $this->response->setJSON([ 'update' => 1, 'profiles' => $profiles, 'short_tests' => $shortTests, ]); } public function investigation_list($opdId, $sessionId = 0) { $table = $this->findExistingTable(['opd_prescription_investigation']); if ($table === null) { return $this->response->setJSON(['rows' => []]); } $builder = $this->db->table($table); $fields = $this->db->getFieldNames($table); if (in_array('opd_pre_id', $fields, true)) { if ((int) $sessionId <= 0) { return $this->response->setJSON(['rows' => []]); } $builder->where('opd_pre_id', (int) $sessionId); } elseif (in_array('opd_id', $fields, true)) { $builder->where('opd_id', (int) $opdId); } else { return $this->response->setJSON(['rows' => []]); } if (in_array('investigation_name', $fields, true)) { $builder->orderBy('investigation_name', 'ASC'); } elseif (in_array('id', $fields, true)) { $builder->orderBy('id', 'DESC'); } $rows = $builder->get()->getResultArray(); return $this->response->setJSON(['rows' => $rows]); } public function investigation_add() { if (!$this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $opdId = (int) $this->request->getPost('opd_id'); $sessionId = (int) $this->request->getPost('opd_session_id'); $code = trim((string) $this->request->getPost('investigation_code')); $name = trim((string) $this->request->getPost('investigation_name')); if ($opdId <= 0 || ($code === '' && $name === '')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation is required']); } $table = $this->findExistingTable(['opd_prescription_investigation']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation table not found']); } $sessionId = $this->ensurePrescriptionSession($opdId, $sessionId); if ($sessionId <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create prescription session']); } $fields = $this->db->getFieldNames($table); $insert = []; if (in_array('opd_id', $fields, true)) { $insert['opd_id'] = $opdId; } if (in_array('opd_pre_id', $fields, true)) { $insert['opd_pre_id'] = $sessionId; } if (in_array('investigation_code', $fields, true)) { $insert['investigation_code'] = $code; } if (in_array('investigation_name', $fields, true)) { $insert['investigation_name'] = $name; } $this->db->table($table)->insert($insert); $insertId = (int) $this->db->insertID(); $this->auditClinicalUpdate('opd_prescription_investigation', 'added', $insertId, null, $insert); return $this->response->setJSON([ 'update' => 1, 'opd_session_id' => $sessionId, 'error_text' => 'Investigation added', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function investigation_remove($id) { $table = $this->findExistingTable(['opd_prescription_investigation']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation table not found']); } $fields = $this->db->getFieldNames($table); if (!in_array('id', $fields, true)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation id field missing']); } $row = $this->db->table($table)->where('id', (int) $id)->get(1)->getRowArray() ?? []; $this->db->table($table)->where('id', (int) $id)->delete(); $this->auditClinicalUpdate('opd_prescription_investigation', 'removed', (int) $id, $row, null); return $this->response->setJSON(['update' => 1, 'error_text' => 'Investigation removed']); } public function medicine_search() { $q = trim((string) $this->request->getGet('q')); $scope = strtolower(trim((string) $this->request->getGet('scope'))); if (! in_array($scope, ['active', 'favorite', 'all'], true)) { $scope = 'active'; } $table = $this->findExistingTable(['opd_med_master']); if ($table === null) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames($table); $idField = in_array('id', $fields, true) ? 'id' : null; $nameField = in_array('item_name', $fields, true) ? 'item_name' : null; $formField = in_array('formulation', $fields, true) ? 'formulation' : null; if ($idField === null || $nameField === null) { return $this->response->setJSON(['rows' => []]); } $select = $idField . ' as id,' . $nameField . ' as med_name'; if ($formField !== null) { $select .= ',' . $formField . ' as med_type'; } $builder = $this->db->table($table)->select($select); if ($q !== '') { $builder->groupStart() ->like($nameField, $q); if ($formField !== null) { $builder->orLike($formField, $q); } $builder->groupEnd(); } $rows = $builder ->limit(500) ->get() ->getResultArray(); if (empty($rows)) { return $this->response->setJSON(['rows' => []]); } $userId = (int) ($this->getCurrentUserId() ?? 0); $favoriteMap = $userId > 0 ? $this->getUserFavoriteMedicineIds($userId) : []; $usageMap = $userId > 0 ? $this->getUserMedicineUsageMap($userId) : []; $qUpper = strtoupper($q); foreach ($rows as &$row) { $medId = (int) ($row['id'] ?? 0); $name = trim((string) ($row['med_name'] ?? '')); $nameUpper = strtoupper($name); $isFavorite = isset($favoriteMap[$medId]); $useCount = (int) ($usageMap[$medId]['use_count'] ?? 0); $lastUsedAt = (string) ($usageMap[$medId]['last_used_at'] ?? ''); $score = 0; if ($isFavorite) { $score += 1000; } if ($useCount > 0) { $score += min(300, $useCount * 10); } if ($lastUsedAt !== '') { $daysAgo = (int) floor((time() - strtotime($lastUsedAt)) / 86400); if ($daysAgo <= 7) { $score += 120; } elseif ($daysAgo <= 30) { $score += 80; } elseif ($daysAgo <= 90) { $score += 40; } elseif ($daysAgo <= 180) { $score += 20; } } if ($qUpper !== '') { if (str_starts_with($nameUpper, $qUpper)) { $score += 200; } elseif (str_contains($nameUpper, $qUpper)) { $score += 100; } } $row['is_favorite'] = $isFavorite ? 1 : 0; $row['use_count'] = $useCount; $row['last_used_at'] = $lastUsedAt; $row['_score'] = $score; } unset($row); if ($scope === 'favorite') { $rows = array_values(array_filter($rows, static fn(array $row): bool => (int) ($row['is_favorite'] ?? 0) === 1)); } elseif ($scope === 'active') { $rows = array_values(array_filter($rows, static fn(array $row): bool => ((int) ($row['is_favorite'] ?? 0) === 1) || ((int) ($row['use_count'] ?? 0) > 0))); } usort($rows, static function (array $a, array $b): int { $scoreA = (int) ($a['_score'] ?? 0); $scoreB = (int) ($b['_score'] ?? 0); if ($scoreA !== $scoreB) { return $scoreB <=> $scoreA; } $nameA = strtoupper(trim((string) ($a['med_name'] ?? ''))); $nameB = strtoupper(trim((string) ($b['med_name'] ?? ''))); return $nameA <=> $nameB; }); $rows = array_slice($rows, 0, 40); foreach ($rows as &$row) { unset($row['_score']); } unset($row); return $this->response->setJSON(['rows' => $rows]); } public function medicine_substitutes() { $table = $this->findExistingTable(['opd_med_master']); if ($table === null) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames($table); $idField = $this->resolveFirstField($fields, ['id']); $nameField = $this->resolveFirstField($fields, ['item_name', 'med_name']); $formField = $this->resolveFirstField($fields, ['formulation']); $genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']); $saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']); $companyField = $this->resolveFirstField($fields, ['company_name', 'company']); if ($idField === null || $nameField === null || ($genericField === null && $saltField === null)) { return $this->response->setJSON(['rows' => []]); } $medId = (int) $this->request->getGet('med_id'); $medName = trim((string) $this->request->getGet('med_name')); $current = []; if ($medId > 0) { $current = $this->db->table($table)->where($idField, $medId)->get(1)->getRowArray() ?? []; } if (empty($current) && $medName !== '') { $current = $this->db->table($table)->where('UPPER(' . $nameField . ')', strtoupper($medName))->get(1)->getRowArray() ?? []; if (! empty($current)) { $medId = (int) ($current[$idField] ?? 0); } } if (empty($current)) { return $this->response->setJSON(['rows' => []]); } $generic = $genericField !== null ? trim((string) ($current[$genericField] ?? '')) : ''; $salt = $saltField !== null ? trim((string) ($current[$saltField] ?? '')) : ''; if ($generic === '' && $salt === '') { return $this->response->setJSON(['rows' => []]); } $select = $idField . ' as id,' . $nameField . ' as med_name'; if ($formField !== null) { $select .= ',' . $formField . ' as med_type'; } if ($genericField !== null) { $select .= ',' . $genericField . ' as genericname'; } if ($saltField !== null) { $select .= ',' . $saltField . ' as salt_name'; } if ($companyField !== null) { $select .= ',' . $companyField . ' as company_name'; } $builder = $this->db->table($table)->select($select); $builder->groupStart(); if ($genericField !== null && $generic !== '') { $builder->where($genericField, $generic); } if ($saltField !== null && $salt !== '') { if ($genericField !== null && $generic !== '') { $builder->orWhere($saltField, $salt); } else { $builder->where($saltField, $salt); } } $builder->groupEnd(); if ($medId > 0) { $builder->where($idField . ' !=', $medId); } $rows = $builder->orderBy($nameField, 'ASC')->limit(25)->get()->getResultArray(); return $this->response->setJSON(['rows' => $rows]); } public function medicine_favorite_toggle() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $userId = (int) ($this->getCurrentUserId() ?? 0); $medId = (int) $this->request->getPost('med_id'); if ($userId <= 0 || $medId <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid user or medicine']); } if (! $this->db->tableExists('opd_medicine_favorites')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine favorites table not found. Run migration first.']); } $exists = $this->db->table('opd_medicine_favorites') ->where('user_id', $userId) ->where('med_id', $medId) ->get(1) ->getRowArray(); if ($exists) { $this->db->table('opd_medicine_favorites') ->where('user_id', $userId) ->where('med_id', $medId) ->delete(); return $this->response->setJSON([ 'update' => 1, 'is_favorite' => 0, 'error_text' => 'Removed from favorites', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $this->db->table('opd_medicine_favorites')->insert([ 'user_id' => $userId, 'med_id' => $medId, 'created_at' => date('Y-m-d H:i:s'), ]); return $this->response->setJSON([ 'update' => 1, 'is_favorite' => 1, 'error_text' => 'Added to favorites', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function medicine_list($sessionId) { $table = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']); if ($table === null) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames($table); $builder = $this->db->table($table); if (in_array('opd_pre_id', $fields, true)) { if ((int) $sessionId <= 0) { return $this->response->setJSON(['rows' => []]); } $builder->where('opd_pre_id', (int) $sessionId); } else { return $this->response->setJSON(['rows' => []]); } if (in_array('id', $fields, true)) { $builder->orderBy('id', 'DESC'); } $rows = $builder->get()->getResultArray(); $doseMap = $this->getDoseMasterLabelMap('opd_dose_shed', ['dose_shed_id', 'id'], ['dose_show_sign', 'dose_sign', 'dose_sign_desc', 'name']); $whenMap = $this->getDoseMasterLabelMap('opd_dose_when', ['dose_when_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']); $freqMap = $this->getDoseMasterLabelMap('opd_dose_frequency', ['dose_freq_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']); foreach ($rows as &$row) { $row['dosage_label'] = $this->resolveDoseMasterLabel($row['dosage'] ?? '', $doseMap); $row['dosage_when_label'] = $this->resolveDoseMasterLabel($row['dosage_when'] ?? '', $whenMap); $row['dosage_freq_label'] = $this->resolveDoseMasterLabel($row['dosage_freq'] ?? '', $freqMap); } unset($row); return $this->response->setJSON(['rows' => $rows]); } public function medicine_add() { if (!$this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $opdId = (int) $this->request->getPost('opd_id'); $sessionId = (int) $this->request->getPost('opd_session_id'); $medName = trim((string) $this->request->getPost('med_name')); $medType = trim((string) $this->request->getPost('med_type')); $medId = (int) $this->request->getPost('med_id'); if ($opdId <= 0 || $medName === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']); } $table = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']); } $sessionId = $this->ensurePrescriptionSession($opdId, $sessionId); if ($sessionId <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create prescription session']); } if ($medId <= 0) { $medId = $this->resolveMedicineMasterIdByName($medName); } $fields = $this->db->getFieldNames($table); $insert = []; if (in_array('opd_pre_id', $fields, true)) { $insert['opd_pre_id'] = $sessionId; } if (in_array('med_id', $fields, true) && $medId > 0) { $insert['med_id'] = $medId; } if (in_array('med_name', $fields, true)) { $insert['med_name'] = strtoupper($medName); } if (in_array('med_type', $fields, true)) { $insert['med_type'] = $medType; } if (in_array('dosage', $fields, true)) { $insert['dosage'] = trim((string) $this->request->getPost('dosage')); } if (in_array('dosage_when', $fields, true)) { $insert['dosage_when'] = trim((string) $this->request->getPost('dosage_when')); } if (in_array('dosage_freq', $fields, true)) { $insert['dosage_freq'] = trim((string) $this->request->getPost('dosage_freq')); } if (in_array('no_of_days', $fields, true)) { $insert['no_of_days'] = trim((string) $this->request->getPost('no_of_days')); } if (in_array('qty', $fields, true)) { $insert['qty'] = trim((string) $this->request->getPost('qty')); } if (in_array('remark', $fields, true)) { $insert['remark'] = strtoupper(trim((string) $this->request->getPost('remark'))); } $user = auth()->user(); if (in_array('update_by', $fields, true)) { $userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User'; $insert['update_by'] = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s'); } if ($this->isDuplicateMedicineRow($table, $fields, $sessionId, $insert)) { return $this->response->setJSON([ 'update' => 1, 'opd_session_id' => $sessionId, 'error_text' => 'Medicine already exists in this prescription. Duplicate skipped.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $this->db->table($table)->insert($insert); $insertId = (int) $this->db->insertID(); $this->auditClinicalUpdate('opd_prescription_medicine', 'added', $insertId, null, $insert); if ($medId > 0) { $this->trackMedicineUsage($medId); } return $this->response->setJSON([ 'update' => 1, 'opd_session_id' => $sessionId, 'error_text' => 'Medicine added', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } private function resolveMedicineMasterIdByName(string $medName): int { $medName = trim($medName); if ($medName === '') { return 0; } $table = $this->findExistingTable(['opd_med_master']); if ($table === null) { return 0; } $fields = $this->db->getFieldNames($table); if (! in_array('id', $fields, true)) { return 0; } $nameField = in_array('item_name', $fields, true) ? 'item_name' : (in_array('med_name', $fields, true) ? 'med_name' : null); if ($nameField === null) { return 0; } $row = $this->db->table($table) ->select('id') ->where('UPPER(' . $nameField . ')', strtoupper($medName)) ->get(1) ->getRowArray(); return (int) ($row['id'] ?? 0); } private function trackMedicineUsage(int $medId): void { $userId = (int) ($this->getCurrentUserId() ?? 0); if ($userId <= 0 || $medId <= 0 || ! $this->db->tableExists('opd_medicine_usage')) { return; } $existing = $this->db->table('opd_medicine_usage') ->where('user_id', $userId) ->where('med_id', $medId) ->get(1) ->getRowArray(); if ($existing) { $this->db->table('opd_medicine_usage') ->where('user_id', $userId) ->where('med_id', $medId) ->update([ 'use_count' => (int) ($existing['use_count'] ?? 0) + 1, 'last_used_at' => date('Y-m-d H:i:s'), ]); return; } $this->db->table('opd_medicine_usage')->insert([ 'user_id' => $userId, 'med_id' => $medId, 'use_count' => 1, 'last_used_at' => date('Y-m-d H:i:s'), ]); } /** * @return array */ private function getUserFavoriteMedicineIds(int $userId): array { if ($userId <= 0 || ! $this->db->tableExists('opd_medicine_favorites')) { return []; } $rows = $this->db->table('opd_medicine_favorites') ->select('med_id') ->where('user_id', $userId) ->get() ->getResultArray(); $out = []; foreach ($rows as $row) { $medId = (int) ($row['med_id'] ?? 0); if ($medId > 0) { $out[$medId] = true; } } return $out; } /** * @return array */ private function getUserMedicineUsageMap(int $userId): array { if ($userId <= 0 || ! $this->db->tableExists('opd_medicine_usage')) { return []; } $rows = $this->db->table('opd_medicine_usage') ->select('med_id,use_count,last_used_at') ->where('user_id', $userId) ->get() ->getResultArray(); $out = []; foreach ($rows as $row) { $medId = (int) ($row['med_id'] ?? 0); if ($medId <= 0) { continue; } $out[$medId] = [ 'use_count' => (int) ($row['use_count'] ?? 0), 'last_used_at' => (string) ($row['last_used_at'] ?? ''), ]; } return $out; } public function medicine_update($id) { if (!$this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $id = (int) $id; if ($id <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid medicine id']); } $table = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']); } $fields = $this->db->getFieldNames($table); if (!in_array('id', $fields, true)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine id field missing']); } $current = $this->db->table($table)->where('id', $id)->get(1)->getRowArray(); if (!$current) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine row not found']); } $update = []; $map = [ 'med_name' => strtoupper(trim((string) $this->request->getPost('med_name'))), 'med_type' => trim((string) $this->request->getPost('med_type')), 'dosage' => trim((string) $this->request->getPost('dosage')), 'dosage_when' => trim((string) $this->request->getPost('dosage_when')), 'dosage_freq' => trim((string) $this->request->getPost('dosage_freq')), 'no_of_days' => trim((string) $this->request->getPost('no_of_days')), 'qty' => trim((string) $this->request->getPost('qty')), 'remark' => strtoupper(trim((string) $this->request->getPost('remark'))), ]; foreach ($map as $field => $value) { if (in_array($field, $fields, true)) { $update[$field] = $value; } } if (empty($update)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'No editable fields found']); } if (isset($update['med_name']) && $update['med_name'] === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']); } $candidate = array_replace($current, $update); $sessionId = (int) ($candidate['opd_pre_id'] ?? 0); if ($sessionId > 0 && $this->isDuplicateMedicineRow($table, $fields, $sessionId, $candidate, $id)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Same medicine row already exists. Duplicate update blocked.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $user = auth()->user(); if (in_array('update_by', $fields, true)) { $userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User'; $update['update_by'] = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s'); } $this->db->table($table)->where('id', $id)->update($update); $this->clinicalAuditTrail->logChangedFields('opd_prescription_medicine', $id, $current, array_replace($current, $update), $this->getCurrentUserId()); return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Medicine updated', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } private function isDuplicateMedicineRow(string $table, array $fields, int $sessionId, array $row, int $excludeId = 0): bool { if ($sessionId <= 0 || ! in_array('opd_pre_id', $fields, true)) { return false; } $builder = $this->db->table($table)->where('opd_pre_id', $sessionId); if ($excludeId > 0 && in_array('id', $fields, true)) { $builder->where('id !=', $excludeId); } foreach (['med_id', 'med_name', 'med_type', 'dosage', 'dosage_when', 'dosage_freq', 'no_of_days', 'qty', 'remark'] as $field) { if (! in_array($field, $fields, true)) { continue; } $value = isset($row[$field]) ? trim((string) $row[$field]) : ''; if ($value === '') { $builder->groupStart()->where($field, '')->orWhere($field, null)->groupEnd(); } else { $builder->where($field, $value); } } return $builder->get(1)->getRowArray() !== null; } public function medicine_remove($id) { $table = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']); } $fields = $this->db->getFieldNames($table); if (!in_array('id', $fields, true)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine id field missing']); } $row = $this->db->table($table)->where('id', (int) $id)->get(1)->getRowArray() ?? []; $this->db->table($table)->where('id', (int) $id)->delete(); $this->auditClinicalUpdate('opd_prescription_medicine', 'removed', (int) $id, $row, null); return $this->response->setJSON(['update' => 1, 'error_text' => 'Medicine removed']); } public function vital_data(int $opdSessionId) { if (! $this->db->tableExists('opd_prescription')) { return $this->response->setBody(''); } $row = $this->db->table('opd_prescription')->where('id', $opdSessionId)->get(1)->getRowArray(); if (empty($row)) { return $this->response->setBody(''); } $lines = []; if (!empty($row['bp'])) { $lines[] = 'BP : ' . esc($row['bp']) . ' / ' . esc((string) ($row['diastolic'] ?? '')); } if (!empty($row['pulse'])) { $lines[] = 'Pulse : ' . esc($row['pulse']); } if (!empty($row['temp'])) { $lines[] = 'Temp : ' . esc($row['temp']); } if (!empty($row['spo2'])) { $lines[] = 'SpO2 : ' . esc($row['spo2']) . ' %'; } return $this->response->setBody('

' . implode('
', $lines) . '

'); } public function get_remark(int $pId) { if (! $this->db->tableExists('patient_remark')) { return $this->response->setBody(''); } $rows = $this->db->table('patient_remark') ->select('remark,insert_by,insert_datetime') ->where('p_id', $pId) ->orderBy('id', 'DESC') ->limit(10) ->get() ->getResultArray(); if (empty($rows)) { return $this->response->setBody('

No patient remarks found.

'); } $out = ''; foreach ($rows as $row) { $stamp = !empty($row['insert_datetime']) ? date('d-m-Y H:i', strtotime((string) $row['insert_datetime'])) : ''; $out .= '

' . nl2br(esc((string) ($row['remark'] ?? ''))) . '
Update By: ' . esc((string) ($row['insert_by'] ?? '')) . ' ' . esc($stamp) . '

'; } return $this->response->setBody($out); } public function patient_remark(int $pId) { return $this->get_remark($pId); } public function show_profile_opd(int $pId, int $opdSessionId = 0) { return redirect()->to(base_url('patient/show_profile_opd/' . $pId . '/' . $opdSessionId)); } public function show_medical_item(int $pId) { if (! $this->db->tableExists('invoice_med_master')) { return $this->response->setBody('

Medical purchase table not found.

'); } $rows = $this->db->table('invoice_med_master') ->where('patient_id', $pId) ->orderBy('id', 'DESC') ->limit(50) ->get() ->getResultArray(); if (empty($rows)) { return $this->response->setBody('

No medicine purchase found.

'); } $html = ''; foreach ($rows as $row) { $html .= '' . '' . '' . '' . ''; } $html .= '
DateInvoiceAmount
' . esc((string) ($row['date'] ?? $row['invoice_date'] ?? '')) . '' . esc((string) ($row['invoice_code'] ?? $row['invoice_no'] ?? $row['id'] ?? '')) . '' . esc((string) ($row['net_amount'] ?? $row['total_amount'] ?? '')) . '
'; return $this->response->setBody($html); } public function show_old_Prescribed(int $pId, int $currentOpd) { if (! $this->db->tableExists('opd_master') || ! $this->db->tableExists('opd_prescription')) { return $this->response->setBody('

Old prescription data not found.

'); } $rows = $this->db->table('opd_master o') ->select('o.opd_id,o.opd_code,o.apointment_date,p.id as session_id,p.complaints,p.diagnosis') ->join('opd_prescription p', 'p.opd_id=o.opd_id', 'left') ->where('o.p_id', $pId) ->where('o.opd_id !=', $currentOpd) ->orderBy('o.opd_id', 'DESC') ->limit(20) ->get() ->getResultArray(); if (empty($rows)) { return $this->response->setBody('

No old prescriptions found.

'); } $medicineTable = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']); $html = '
'; foreach ($rows as $index => $row) { $rxLines = []; if ($medicineTable !== null && !empty($row['session_id'])) { $medRows = $this->db->table($medicineTable) ->select('med_type,med_name,dosage,dosage_when,dosage_freq,no_of_days,qty,remark') ->where('opd_pre_id', (int) $row['session_id']) ->orderBy('id', 'ASC') ->get() ->getResultArray(); foreach ($medRows as $med) { $rxLines[] = esc(trim((string) ($med['med_type'] ?? ''))) . ' ' . esc(trim((string) ($med['med_name'] ?? ''))) . ' | ' . esc(trim((string) ($med['dosage'] ?? ''))) . ' ' . esc(trim((string) ($med['dosage_when'] ?? ''))) . ' ' . esc(trim((string) ($med['dosage_freq'] ?? ''))) . ' ' . esc(trim((string) ($med['no_of_days'] ?? ''))); } } $dateLabel = !empty($row['apointment_date']) ? date('d-m-Y', strtotime((string) $row['apointment_date'])) : ''; $html .= '
' . '
' . esc((string) ($row['opd_code'] ?? $row['opd_id'])) . ' ' . esc($dateLabel) . '
' . '
' . '
Complaints: ' . esc((string) ($row['complaints'] ?? '')) . '
' . '
Diagnosis: ' . esc((string) ($row['diagnosis'] ?? '')) . '
' . '
Rx:
' . (empty($rxLines) ? 'No medicine rows' : implode('
', $rxLines)) . '
' . '
'; } $html .= '
'; return $this->response->setBody($html); } public function save_rx_group_list(int $docId = 0) { if (! $this->db->tableExists('opd_prescription_template')) { return $this->response->setBody('

RX template table not found.

'); } $fields = $this->db->getFieldNames('opd_prescription_template'); $idField = $this->resolveFirstField($fields, ['id']); $nameField = $this->resolveFirstField($fields, ['rx_group_name']); if ($idField === null || $nameField === null) { return $this->response->setJSON(['rows' => []]); } $userId = $docId > 0 ? (int) $docId : max(0, $this->getCurrentUserId()); $hasDocId = in_array('doc_id', $fields, true); $builder = $this->db->table('opd_prescription_template') ->select('MAX(' . $idField . ') as id,' . $nameField . ' as rx_group_name,COUNT(*) as row_count') ->where($nameField . ' <>', ''); if ($hasDocId) { $builder->groupStart() ->where('doc_id', $userId) ->orWhere('doc_id', 0) ->groupEnd(); } $rows = $builder ->groupBy($nameField) ->orderBy($nameField, 'ASC') ->limit(500) ->get() ->getResultArray(); $medTable = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']); $medCountMap = []; if ($medTable !== null) { $medFields = $this->db->getFieldNames($medTable); if (in_array('rx_group_id', $medFields, true)) { $medRows = $this->db->table($medTable) ->select('rx_group_id,COUNT(*) as med_count') ->groupBy('rx_group_id') ->get() ->getResultArray(); foreach ($medRows as $medRow) { $rxId = (int) ($medRow['rx_group_id'] ?? 0); if ($rxId > 0) { $medCountMap[$rxId] = (int) ($medRow['med_count'] ?? 0); } } } } foreach ($rows as &$row) { $row['med_count'] = (int) ($medCountMap[(int) ($row['id'] ?? 0)] ?? 0); } unset($row); if ($this->request->isAJAX() || strtolower((string) $this->request->getHeaderLine('Accept')) === 'application/json') { return $this->response->setJSON(['rows' => $rows]); } if (empty($rows)) { return $this->response->setBody('

No RX groups found.

'); } $html = '
    '; foreach ($rows as $row) { $html .= '
  • ' . '' . esc((string) ($row['rx_group_name'] ?? 'RX Group')) . '' . '#' . (int) ($row['id'] ?? 0) . ' (' . (int) ($row['med_count'] ?? 0) . ')' . '
  • '; } $html .= '
'; return $this->response->setBody($html); } public function prescribed_dose(int $opdId, int $opdSessionId) { $medicineTable = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']); if ($medicineTable === null) { return $this->response->setBody('

No prescribed medicine table found.

'); } $rows = $this->db->table($medicineTable) ->select('id,med_type,med_name,dosage,dosage_when,dosage_freq,no_of_days,qty,remark') ->where('opd_pre_id', $opdSessionId) ->orderBy('id', 'ASC') ->get() ->getResultArray(); if (empty($rows)) { return $this->response->setBody('

No prescribed medicine found for this OPD session.

'); } $html = ''; foreach ($rows as $row) { $html .= '' . '' . '' . '' . '' . '' . '' . '' . '' . ''; } $html .= '
TypeMedicineDoseWhenFreqDaysQtyRemark
' . esc((string) ($row['med_type'] ?? '')) . '' . esc((string) ($row['med_name'] ?? '')) . '' . esc((string) ($row['dosage'] ?? '')) . '' . esc((string) ($row['dosage_when'] ?? '')) . '' . esc((string) ($row['dosage_freq'] ?? '')) . '' . esc((string) ($row['no_of_days'] ?? '')) . '' . esc((string) ($row['qty'] ?? '')) . '' . esc((string) ($row['remark'] ?? '')) . '
'; return $this->response->setBody($html); } private function findExistingTable(array $candidates): ?string { foreach ($candidates as $table) { if ($this->db->tableExists($table)) { return $table; } } return null; } private function getDoseMasterRows(string $table, array $idCandidates, array $labelCandidates): array { if (! $this->db->tableExists($table)) { return []; } $fields = $this->db->getFieldNames($table); $idField = $this->resolveFirstField($fields, $idCandidates); $labelField = $this->resolveFirstField($fields, $labelCandidates); if ($idField === null || $labelField === null) { return []; } return $this->db->table($table) ->select($idField . ' as id,' . $labelField . ' as label') ->where($labelField . ' !=', '') ->orderBy($labelField, 'ASC') ->get() ->getResultArray(); } private function getDoseMasterLabelMap(string $table, array $idCandidates, array $labelCandidates): array { $rows = $this->getDoseMasterRows($table, $idCandidates, $labelCandidates); if (empty($rows)) { return []; } $map = []; foreach ($rows as $row) { $id = trim((string) ($row['id'] ?? '')); $label = trim((string) ($row['label'] ?? '')); if ($id === '' || $label === '') { continue; } $map[$id] = $label; } return $map; } private function resolveDoseMasterLabel($value, array $map): string { $raw = trim((string) $value); if ($raw === '') { return ''; } return $map[$raw] ?? $raw; } private function hydrateNabhPrescriptionFields(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 mapNabhFieldsToExistingColumns(array &$payload, array $fields, array $input): array { $mapping = [ 'drug_allergy_status' => ['drug_allergy_status', 'allergy_status', 'drug_allergy'], 'drug_allergy_details' => ['drug_allergy_details', 'allergy_details', 'drug_allergy_note', 'allergy_note'], 'adr_history' => ['adr_history', 'adverse_drug_reaction', 'adr_details', 'adverse_reaction_history'], 'current_medications' => ['current_medications', 'current_medication', 'current_medication_history', 'ongoing_medications'], ]; $unmapped = []; foreach ($mapping as $logical => $candidates) { $value = trim((string) ($input[$logical] ?? '')); $target = null; foreach ($candidates as $candidate) { if (in_array($candidate, $fields, true)) { $target = $candidate; break; } } if ($target !== null) { $payload[$target] = $value; } else { $unmapped[$logical] = $value; } } return $unmapped; } 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 upsertNabhFieldsIntoRemarks(string $remarks, array $input): string { $clean = trim($remarks); $clean = preg_replace('/\n?\s*Drug\s*Allergy\s*Status\s*:\s*.+$/im', '', $clean) ?? $clean; $clean = preg_replace('/\n?\s*Drug\s*Allergy\s*Details\s*:\s*.+$/im', '', $clean) ?? $clean; $clean = preg_replace('/\n?\s*ADR\s*History\s*:\s*.+$/im', '', $clean) ?? $clean; $clean = preg_replace('/\n?\s*Current\s*Medications\s*:\s*.+$/im', '', $clean) ?? $clean; $clean = trim($clean); $lines = []; if (trim((string) ($input['drug_allergy_status'] ?? '')) !== '') { $lines[] = 'Drug Allergy Status: ' . trim((string) ($input['drug_allergy_status'] ?? '')); } if (trim((string) ($input['drug_allergy_details'] ?? '')) !== '') { $lines[] = 'Drug Allergy Details: ' . trim((string) ($input['drug_allergy_details'] ?? '')); } if (trim((string) ($input['adr_history'] ?? '')) !== '') { $lines[] = 'ADR History: ' . trim((string) ($input['adr_history'] ?? '')); } if (trim((string) ($input['current_medications'] ?? '')) !== '') { $lines[] = 'Current Medications: ' . trim((string) ($input['current_medications'] ?? '')); } if (empty($lines)) { return $clean; } if ($clean !== '') { return $clean . "\n" . implode("\n", $lines); } return implode("\n", $lines); } private function ensureClinicalTemplateTable(): bool { if ($this->db->tableExists('opd_clinical_templates')) { return true; } try { $sql = "CREATE TABLE IF NOT EXISTS opd_clinical_templates ( id INT AUTO_INCREMENT PRIMARY KEY, doc_id INT NOT NULL DEFAULT 0, section_key VARCHAR(80) NOT NULL, template_name VARCHAR(255) NOT NULL, template_text TEXT NOT NULL, is_active TINYINT NOT NULL DEFAULT 1, created_at DATETIME NULL, updated_at DATETIME NULL, INDEX idx_doc_section (doc_id, section_key), INDEX idx_section_active (section_key, is_active) )"; $this->db->query($sql); } catch (\Throwable $e) { return false; } return $this->db->tableExists('opd_clinical_templates'); } private function ensureClinicalTemplateUsageTable(): bool { if ($this->db->tableExists('opd_clinical_template_usage')) { return true; } try { $sql = "CREATE TABLE IF NOT EXISTS opd_clinical_template_usage ( id INT AUTO_INCREMENT PRIMARY KEY, doc_id INT NOT NULL, template_id INT NOT NULL, section_key VARCHAR(80) NOT NULL, use_count INT NOT NULL DEFAULT 0, last_used_at DATETIME NULL, created_at DATETIME NULL, updated_at DATETIME NULL, UNIQUE KEY uniq_doc_template_section (doc_id, template_id, section_key), INDEX idx_template (template_id), INDEX idx_section_use (section_key, use_count) )"; $this->db->query($sql); } catch (\Throwable $e) { return false; } return $this->db->tableExists('opd_clinical_template_usage'); } private function isTemplateSectionAllowed(string $section): bool { return in_array($section, ['finding_examinations', 'diagnosis', 'provisional_diagnosis', 'prescriber_remarks', 'advice'], true); } private function resolveSectionColumn(string $section): ?string { $map = [ 'finding_examinations' => 'Finding_Examinations', 'diagnosis' => 'diagnosis', 'provisional_diagnosis' => 'Provisional_diagnosis', 'prescriber_remarks' => 'Prescriber_Remarks', 'advice' => 'advice', ]; return $map[$section] ?? null; } private function ensurePrescriptionSession(int $opdId, int $sessionId = 0): int { if (!$this->db->tableExists('opd_prescription')) { return 0; } $fields = $this->db->getFieldNames('opd_prescription'); if ($sessionId > 0) { $row = $this->db->table('opd_prescription')->where('id', $sessionId)->where('opd_id', $opdId)->get(1)->getRow(); if ($row) { return $sessionId; } } $last = $this->db->table('opd_prescription')->where('opd_id', $opdId)->orderBy('id', 'DESC')->get(1)->getRow(); if ($last && !empty($last->id)) { return (int) $last->id; } $opdRow = $this->db->table('opd_master')->where('opd_id', $opdId)->get(1)->getRow(); if (!$opdRow) { return 0; } $insert = []; if (in_array('opd_id', $fields, true)) { $insert['opd_id'] = $opdId; } if (in_array('p_id', $fields, true)) { $insert['p_id'] = (int) ($opdRow->p_id ?? 0); } if (in_array('doc_id', $fields, true)) { $insert['doc_id'] = (int) ($opdRow->doc_id ?? 0); } if (in_array('date_opd_visit', $fields, true)) { $insert['date_opd_visit'] = date('Y-m-d'); } if (in_array('visit_status', $fields, true)) { $insert['visit_status'] = 0; } if (in_array('session_id', $fields, true)) { $insert['session_id'] = 0; } if (in_array('queue_no', $fields, true)) { $maxQueue = $this->db->table('opd_prescription') ->selectMax('queue_no', 'max_queue') ->where('date_opd_visit', date('Y-m-d')) ->where('doc_id', (int) ($opdRow->doc_id ?? 0)) ->get() ->getRow(); $insert['queue_no'] = (int) ($maxQueue->max_queue ?? 0) + 1; } $this->db->table('opd_prescription')->insert($insert); return (int) $this->db->insertID(); } 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; } /** * @return array */ private function getPatientAddictionFlags(int $patientId): array { $flags = [ 'is_smoking' => 0, 'is_alcohol' => 0, 'is_drug_abuse' => 0, ]; if ($patientId <= 0 || ! $this->db->tableExists('patient_master')) { return $flags; } $patientFields = $this->db->getFieldNames('patient_master'); $selectFields = []; foreach (array_keys($flags) as $field) { if (in_array($field, $patientFields, true)) { $selectFields[] = $field; } } if (empty($selectFields)) { return $flags; } $row = $this->db->table('patient_master') ->select(implode(',', $selectFields)) ->where('id', $patientId) ->get(1) ->getRowArray() ?? []; foreach ($flags as $field => $_) { if (array_key_exists($field, $row)) { $flags[$field] = ((int) $row[$field]) > 0 ? 1 : 0; } } return $flags; } /** * @return array */ private function getCoMorbiditiesWithSelection(int $patientId): array { if (! $this->db->tableExists('morbidities_master')) { return $this->getDefaultCoMorbidityRows($patientId); } $masterFields = $this->db->getFieldNames('morbidities_master'); $idField = $this->resolveFirstField($masterFields, ['mor_id', 'id']); $nameField = $this->resolveFirstField($masterFields, ['morbidities', 'name', 'title']); if ($idField === null || $nameField === null) { return []; } $builder = $this->db->table('morbidities_master') ->select($idField . ' as mor_id,' . $nameField . ' as morbidities') ->orderBy($nameField, 'ASC'); if (in_array('active', $masterFields, true)) { $builder->where('active', 1); } $rows = $builder->get()->getResultArray(); if (empty($rows)) { return $this->getDefaultCoMorbidityRows($patientId); } $selectedMap = []; if ($patientId > 0 && $this->db->tableExists('patient_morbidities')) { $patientMorFields = $this->db->getFieldNames('patient_morbidities'); $pIdField = $this->resolveFirstField($patientMorFields, ['p_id', 'patient_id']); $morIdField = $this->resolveFirstField($patientMorFields, ['morbidities', 'mor_id', 'morbidity_id']); if ($pIdField !== null && $morIdField !== null) { $selectedRows = $this->db->table('patient_morbidities') ->select($morIdField . ' as mor_id') ->where($pIdField, $patientId) ->get() ->getResultArray(); foreach ($selectedRows as $selectedRow) { $selectedMap[(int) ($selectedRow['mor_id'] ?? 0)] = true; } } } $result = []; foreach ($rows as $row) { $morId = (int) ($row['mor_id'] ?? 0); if ($morId <= 0) { continue; } $result[] = [ 'id' => $morId, 'name' => (string) ($row['morbidities'] ?? ''), 'selected' => ! empty($selectedMap[$morId]) ? 1 : 0, ]; } return $result; } /** * @return array */ private function getDefaultCoMorbidityRows(int $patientId = 0): array { $historical = $this->getHistoricalCoMorbiditySelection($patientId); return [ ['id' => 'DM', 'name' => 'Diabetes mellitus (DM)', 'selected' => !empty($historical['DM']) ? 1 : 0], ['id' => 'HTN', 'name' => 'high blood pressure (HTn)', 'selected' => !empty($historical['HTN']) ? 1 : 0], ['id' => 'CAD', 'name' => 'Coronary artery disease (CAD)', 'selected' => !empty($historical['CAD']) ? 1 : 0], ['id' => 'COPD', 'name' => 'Chronic Obstructive Pulmonary Disease', 'selected' => !empty($historical['COPD']) ? 1 : 0], ['id' => 'CVA', 'name' => 'cerebral vascular accident (CVA)', 'selected' => !empty($historical['CVA']) ? 1 : 0], ]; } /** * @return array */ private function getHistoricalCoMorbiditySelection(int $patientId): array { if ($patientId <= 0 || ! $this->db->tableExists('opd_prescription')) { return []; } $fields = $this->db->getFieldNames('opd_prescription'); if (! in_array('Prescriber_Remarks', $fields, true)) { return []; } $rows = $this->db->table('opd_prescription') ->select('Prescriber_Remarks') ->where('p_id', $patientId) ->orderBy('id', 'DESC') ->limit(20) ->get() ->getResultArray(); $selected = []; foreach ($rows as $row) { $remarks = trim((string) ($row['Prescriber_Remarks'] ?? '')); if ($remarks === '') { continue; } if (preg_match('/Co-Morbidities\s*:\s*(.+)$/im', $remarks, $match) !== 1) { continue; } $list = (string) ($match[1] ?? ''); $tokens = preg_split('/[,;|]+/', $list) ?: []; foreach ($tokens as $token) { $canonical = $this->mapCoMorbidityTokenToCanonical((string) $token); if ($canonical !== null) { $selected[$canonical] = true; } } } return $selected; } private function hasHistoricalCoMorbiditySelection(int $patientId): bool { return !empty($this->getHistoricalCoMorbiditySelection($patientId)); } private function mapCoMorbidityTokenToCanonical(string $token): ?string { $normalized = strtolower((string) preg_replace('/[^a-z0-9]+/', '', trim($token))); if ($normalized === '') { return null; } $map = [ 'dm' => 'DM', 'diabetes' => 'DM', 'diabetesmellitus' => 'DM', 'htn' => 'HTN', 'highbloodpressure' => 'HTN', 'hypertension' => 'HTN', 'cad' => 'CAD', 'coronaryarterydisease' => 'CAD', 'copd' => 'COPD', 'chronicobstructivepulmonarydisease' => 'COPD', 'cva' => 'CVA', 'cerebralvascularaccident' => 'CVA', 'stroke' => 'CVA', ]; return $map[$normalized] ?? null; } /** * @param array $flags */ private function syncPatientAddictionFlags(int $patientId, array $flags): void { if ($patientId <= 0 || ! $this->db->tableExists('patient_master')) { return; } $patientFields = $this->db->getFieldNames('patient_master'); $update = []; foreach ($flags as $field => $value) { if (in_array($field, $patientFields, true)) { $update[$field] = ((int) $value) > 0 ? 1 : 0; } } if (! empty($update)) { $this->db->table('patient_master')->where('id', $patientId)->update($update); } } /** * @param array $selectedMorbidityIds */ private function syncPatientMorbidities(int $patientId, array $selectedMorbidityIds): void { if ($patientId <= 0 || ! $this->db->tableExists('patient_morbidities')) { return; } $patientMorFields = $this->db->getFieldNames('patient_morbidities'); $pIdField = $this->resolveFirstField($patientMorFields, ['p_id', 'patient_id']); $morIdField = $this->resolveFirstField($patientMorFields, ['morbidities', 'mor_id', 'morbidity_id']); if ($pIdField === null || $morIdField === null) { return; } $selectedMap = []; foreach ($selectedMorbidityIds as $morId) { $morId = (int) $morId; if ($morId > 0) { $selectedMap[$morId] = true; } } $existingRows = $this->db->table('patient_morbidities') ->select('id,' . $morIdField . ' as mor_id') ->where($pIdField, $patientId) ->get() ->getResultArray(); $existingMap = []; foreach ($existingRows as $existingRow) { $morId = (int) ($existingRow['mor_id'] ?? 0); if ($morId > 0) { $existingMap[$morId] = (int) ($existingRow['id'] ?? 0); } } if (! empty($existingMap)) { foreach ($existingMap as $morId => $rowId) { if (empty($selectedMap[$morId])) { $this->db->table('patient_morbidities')->where('id', $rowId)->delete(); } } } $insertFields = $patientMorFields; foreach (array_keys($selectedMap) as $morId) { if (! isset($existingMap[$morId])) { $insert = [ $pIdField => $patientId, $morIdField => $morId, ]; if (in_array('update_date', $insertFields, true)) { $insert['update_date'] = date('Y-m-d H:i:s'); } if (in_array('insert_date', $insertFields, true)) { $insert['insert_date'] = date('Y-m-d H:i:s'); } if (in_array('create_date', $insertFields, true)) { $insert['create_date'] = date('Y-m-d H:i:s'); } $this->db->table('patient_morbidities')->insert($insert); } } } /** * @return array */ private function parseNumericList(string $list): array { if ($list === '') { return []; } $tokens = preg_split('/[^0-9]+/', $list) ?: []; $result = []; foreach ($tokens as $token) { $id = (int) trim((string) $token); if ($id > 0) { $result[$id] = $id; } } return array_values($result); } private function buildLocalClinicalAssist(array $input): array { $text = mb_strtolower(trim((string) (($input['complaints'] ?? '') . ' ' . ($input['finding_examinations'] ?? '')))); $normalizedText = preg_replace('/\s+/', ' ', $text) ?: $text; $pulse = $this->parseNumericFromText((string) ($input['pulse'] ?? '')); $spo2 = $this->parseNumericFromText((string) ($input['spo2'] ?? '')); $temp = $this->parseNumericFromText((string) ($input['temp'] ?? '')); $rr = $this->parseNumericFromText((string) ($input['rr_min'] ?? '')); $bpSys = $this->parseNumericFromText((string) ($input['bp'] ?? '')); $bpDia = $this->parseNumericFromText((string) ($input['diastolic'] ?? '')); $vitalChecks = []; if ($temp !== null && $temp >= 100.0) { $vitalChecks[] = 'Fever range temperature noted (' . rtrim(rtrim(number_format($temp, 1, '.', ''), '0'), '.') . ').'; } if ($pulse !== null && $pulse > 100) { $vitalChecks[] = 'Pulse is elevated (>100/min).'; } if ($spo2 !== null && $spo2 < 94) { $vitalChecks[] = 'SpO2 is below 94%; respiratory risk check advised.'; } if ($rr !== null && $rr > 22) { $vitalChecks[] = 'Respiratory rate is high (>22/min).'; } if ($bpSys !== null && $bpDia !== null && ($bpSys >= 140 || $bpDia >= 90)) { $vitalChecks[] = 'Blood pressure is elevated (' . (int) round($bpSys) . '/' . (int) round($bpDia) . ').'; } $diagnosis = []; $investigations = []; $advice = []; $redFlags = []; $has = static function (string $haystack, array $needles): bool { foreach ($needles as $needle) { if ($needle !== '' && str_contains($haystack, $needle)) { return true; } } return false; }; $hasFever = $has($normalizedText, ['fever', 'pyrexia', 'bukhar']); $hasCough = $has($normalizedText, ['cough', 'khansi']); $hasSoreThroat = $has($normalizedText, ['sore throat', 'throat pain', 'gale', 'pharyngitis']); $hasCold = $has($normalizedText, ['cold', 'coryza', 'running nose', 'dripping nose', 'rhinorrhoea']); $hasBreathless = $has($normalizedText, ['breathlessness', 'breathing difficulty', 'dyspnea', 'shortness of breath']); $hasChestPain = $has($normalizedText, ['chest pain', 'pain chest']); $hasAcidity = $has($normalizedText, ['acidity', 'burning epigastrium', 'heartburn', 'epigastric']); $hasLooseMotions = $has($normalizedText, ['diarrhoea', 'diarrhea', 'loose motion', 'motions']); $hasVomiting = $has($normalizedText, ['vomiting', 'emesis']); $hasDysuria = $has($normalizedText, ['burning micturition', 'dysuria', 'urinary frequency', 'urine burning']); $hasHeadache = $has($normalizedText, ['headache', 'migraine']); if ($hasFever && ($hasCough || $hasSoreThroat || $hasCold)) { $diagnosis[] = 'Acute upper respiratory infection (viral etiology likely).'; $investigations[] = 'CBC'; if ($hasFever) { $investigations[] = 'CRP (if persistent fever)'; } $advice[] = 'Hydration, steam inhalation, and symptomatic care.'; } if ($hasFever && !$hasCough && !$hasSoreThroat && !$hasLooseMotions) { $diagnosis[] = 'Acute febrile illness (source to be localized clinically).'; $investigations[] = 'CBC'; $investigations[] = 'Urine routine (if clinically indicated)'; if ((int) ($input['dengue'] ?? 0) === 1 || ($temp !== null && $temp >= 101.0)) { $investigations[] = 'Dengue NS1 / IgM (as per day of fever)'; $investigations[] = 'Platelet count trend'; } } if ($hasAcidity) { $diagnosis[] = 'Acid peptic disease / gastritis.'; $advice[] = 'Avoid spicy/oily meals, late-night meals, and prolonged fasting.'; } if ($hasLooseMotions || $hasVomiting) { $diagnosis[] = 'Acute gastroenteritis.'; $investigations[] = 'Serum electrolytes (if dehydration suspected)'; $advice[] = 'ORS and oral hydration with warning signs explained.'; } if ($hasDysuria) { $diagnosis[] = 'Probable urinary tract infection.'; $investigations[] = 'Urine routine & microscopy'; $investigations[] = 'Urine culture (if recurrent/complicated)'; } if ($hasHeadache && $bpSys !== null && $bpDia !== null && ($bpSys >= 140 || $bpDia >= 90)) { $diagnosis[] = 'Headache with elevated blood pressure; evaluate hypertension status.'; $investigations[] = 'RFT and urine albumin (hypertension workup baseline)'; } if ($hasChestPain || $hasBreathless) { $redFlags[] = 'Chest pain/breathlessness present: rule out acute cardiopulmonary emergency.'; $investigations[] = 'ECG'; if ($spo2 !== null && $spo2 < 94) { $investigations[] = 'Chest X-ray / ABG as per severity'; } } if ($spo2 !== null && $spo2 < 92) { $redFlags[] = 'SpO2 <92%: urgent escalation and oxygen assessment advised.'; } if ($temp !== null && $temp >= 103.0) { $redFlags[] = 'High-grade fever (>=103): monitor for systemic complications.'; } if ((int) ($input['pregnancy'] ?? 0) === 1) { $redFlags[] = 'Pregnancy noted: verify medicine safety before final prescription.'; } if (empty($diagnosis)) { $diagnosis[] = 'Syndromic provisional diagnosis to be finalized clinically.'; } if (empty($advice)) { $advice[] = 'Review warning signs and follow-up if symptoms persist/worsen.'; } $diagnosis = array_values(array_unique($diagnosis)); $investigations = array_values(array_unique($investigations)); $advice = array_values(array_unique($advice)); $redFlags = array_values(array_unique($redFlags)); $vitalChecks = array_values(array_unique($vitalChecks)); return [ 'disclaimer' => 'Local rule-based clinical assist only. Final diagnosis and treatment decision must be by treating doctor.', 'vital_checks' => $vitalChecks, 'red_flags' => $redFlags, 'suggested_diagnosis' => implode("\n", array_map(static fn(string $v): string => '- ' . $v, $diagnosis)), 'suggested_investigation' => implode("\n", array_map(static fn(string $v): string => '- ' . $v, $investigations)), 'suggested_advice' => implode("\n", array_map(static fn(string $v): string => '- ' . $v, $advice)), ]; } private function parseNumericFromText(string $value): ?float { $value = trim($value); if ($value === '') { return null; } if (preg_match('/-?\d+(?:\.\d+)?/', $value, $m) !== 1) { return null; } return (float) $m[0]; } private function resolveFirstField(array $fields, array $candidates): ?string { foreach ($candidates as $candidate) { if (in_array($candidate, $fields, true)) { return $candidate; } } return null; } 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 ''; } private function upsertWomenRelatedProblemsIntoRemarks(string $remarks, string $womenText): string { $remarks = preg_replace('/\n?\s*Women Related Problems\s*:\s*.+$/im', '', $remarks) ?? $remarks; $remarks = preg_replace('/\n?\s*LMP(?:\s*Days\s*Before)?\s*:\s*.+$/im', '', $remarks) ?? $remarks; $remarks = preg_replace('/\n?\s*Last Baby\s*:\s*.+$/im', '', $remarks) ?? $remarks; $remarks = preg_replace('/\n?\s*Pregnancy Related\s*:\s*.+$/im', '', $remarks) ?? $remarks; $remarks = trim($remarks); $labelled = trim($womenText); if ($remarks === '') { return $labelled; } return $remarks . "\n" . $labelled; } private function upsertCoMorbiditiesIntoRemarks(string $remarks, string $coMorbiditiesText): string { $remarks = preg_replace('/\n?\s*Co-Morbidities\s*:\s*.+$/im', '', $remarks) ?? $remarks; $remarks = trim($remarks); $coMorbiditiesText = trim($coMorbiditiesText); if ($coMorbiditiesText === '') { return $remarks; } $line = 'Co-Morbidities: ' . $coMorbiditiesText; if ($remarks === '') { return $line; } return $remarks . "\n" . $line; } /** * @param array $womenStructured */ private function buildWomenCombinedText(string $womenProblems, array $womenStructured): string { $lines = []; $womenProblems = trim($womenProblems); if ($womenProblems !== '') { $lines[] = 'Women Related Problems: ' . $womenProblems; } $lmp = trim((string) ($womenStructured['women_lmp'] ?? '')); if ($lmp !== '') { $lines[] = 'LMP Days Before: ' . $lmp; } $lastBaby = trim((string) ($womenStructured['women_last_baby'] ?? '')); if ($lastBaby !== '') { $lines[] = 'Last Baby: ' . $lastBaby; } $pregnancyRelated = trim((string) ($womenStructured['women_pregnancy_related'] ?? '')); if ($pregnancyRelated !== '') { $lines[] = 'Pregnancy Related: ' . $pregnancyRelated; } return trim(implode("\n", $lines)); } /** * @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, ]; } /** * @param array $payload * @param array $fields * @param array $womenStructured */ private function mapWomenStructuredToExistingColumns(array &$payload, array $fields, array $womenStructured): void { $columnMap = [ 'women_lmp' => ['women_lmp', 'lmp', 'lmp_date'], 'women_last_baby' => ['women_last_baby', 'last_baby', 'last_baby_details'], 'women_pregnancy_related' => ['women_pregnancy_related', 'pregnancy_related', 'pregnancy_related_problem'], ]; foreach ($columnMap as $key => $candidates) { $resolved = $this->resolveFirstField($fields, $candidates); if ($resolved !== null) { $payload[$resolved] = trim((string) ($womenStructured[$key] ?? '')); } } } /** * @param array $patientRow * @param array $opdRow */ private function storePrescriptionFhirBundle(int $opdId, int $sessionId, array $patientRow, array $opdRow): bool { if (! $this->db->tableExists('opd_fhir_documents')) { return false; } $abhaField = $this->resolvePatientAbhaField(); $abhaAddress = $abhaField !== null ? trim((string) ($patientRow[$abhaField] ?? '')) : ''; $patient = [ 'id' => (string) ($patientRow['id'] ?? ($opdRow['p_id'] ?? 0)), 'name' => (string) ($patientRow['p_fname'] ?? $opdRow['P_name'] ?? ''), 'gender' => $this->normalizeGender((string) ($patientRow['gender'] ?? '')), 'birthDate' => !empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '', 'abhaAddress' => $abhaAddress, ]; $encounter = [ 'id' => (string) ($opdRow['opd_id'] ?? $opdId), 'status' => 'finished', ]; $medications = $this->getPrescriptionMedicines($sessionId); $bundle = $this->fhirR4Builder->buildPrescriptionBundle($patient, $encounter, $medications); $bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); $user = auth()->user(); $generatedBy = (string) ($user->id ?? 'system'); $inserted = (bool) $this->db->table('opd_fhir_documents')->insert([ 'opd_id' => $opdId, 'opd_session_id' => $sessionId, 'bundle_type' => 'MedicationRequestBundle', 'bundle_json' => $bundleJson, 'generated_by' => $generatedBy, 'generated_at' => Time::now('Asia/Kolkata')->toDateTimeString(), ]); if (! $inserted) { return false; } try { $documentId = (int) $this->db->insertID(); $bridgeSync = new BridgeSyncService(); $bridgeSync->enqueue( 'opd.fhir.generated', [ 'opd_fhir_document_id' => $documentId, 'opd_id' => $opdId, 'opd_session_id' => $sessionId, 'bundle_type' => 'MedicationRequestBundle', 'generated_by' => $generatedBy, 'generated_at' => Time::now('Asia/Kolkata')->toDateTimeString(), 'bundle_json' => $bundle, ], 'opd_fhir_document', (string) $documentId ); } catch (\Throwable $e) { } return true; } /** * @return array> */ private function getPrescriptionMedicines(int $sessionId): array { $table = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']); if ($table === null) { return []; } $rows = $this->db->table($table)->where('opd_pre_id', $sessionId)->get()->getResultArray(); $medications = []; foreach ($rows as $row) { $dosageParts = array_filter([ trim((string) ($row['dosage'] ?? '')), trim((string) ($row['dosage_when'] ?? '')), trim((string) ($row['dosage_freq'] ?? '')), trim((string) ($row['no_of_days'] ?? '')) !== '' ? ('for ' . trim((string) $row['no_of_days']) . ' days') : '', ]); $medications[] = [ 'drug_name' => (string) ($row['med_name'] ?? ''), 'status' => 'active', 'dosage' => implode(' | ', $dosageParts), ]; } return $medications; } private function normalizeGender(string $value): string { $value = trim($value); if ($value === '1' || strtolower($value) === 'male') { return 'male'; } if ($value === '2' || strtolower($value) === 'female') { return 'female'; } return 'unknown'; } /** * @return string|int|null */ private function getCurrentUserId() { $user = auth()->user(); if ($user === null) { return null; } return $user->id ?? null; } /** * @return array */ private function getComplaintMatchesFromText(string $text): array { $pieces = preg_split('/[,;\n\r]+/', strtolower($text)) ?: []; $out = []; $directMap = [ 'kamar' => 'PAIN BACK', 'back pain' => 'PAIN BACK', 'pet dard' => 'PAIN IN ABDOMEN', 'body pain' => 'BODYACHE', 'kamzori' => 'WEAKNESS', 'weakness' => 'WEAKNESS', 'bukhar' => 'FEVER', 'khansi' => 'COUGH DRY', 'chakkar' => 'GIDDINESS', 'ulti' => 'VOMITING', 'thakan' => 'TIREDNESS', ]; foreach ($pieces as $piece) { $token = trim((string) $piece); if ($token === '') { continue; } $matchedName = null; foreach ($directMap as $key => $value) { if (str_contains($token, $key)) { $matchedName = $value; break; } } if ($matchedName !== null) { $out[strtoupper($matchedName)] = $matchedName; continue; } if (! $this->db->tableExists('complaints_master')) { $out[strtoupper($token)] = strtoupper($token); continue; } $row = $this->db->table('complaints_master') ->select('Name') ->where('is_active', 1) ->groupStart() ->like('Name', $token) ->orLike('name_hinglish', $token) ->orLike('keywords', $token) ->groupEnd() ->orderBy('show_in_short', 'DESC') ->orderBy('Code', 'ASC') ->get(1) ->getRowArray(); if (! empty($row['Name'])) { $name = trim((string) $row['Name']); $out[strtoupper($name)] = $name; } } return array_values($out); } /** * @param array $settingCandidates */ private function readAiSettingValue(array $settingCandidates): string { 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 ''; } /** * @param array $complaints */ private function generateComplaintDraftWithAi(array $complaints, string $currentText): ?string { if (empty($complaints)) { return null; } $prompt = "You are a clinical documentation assistant for Indian OPD. Rewrite chief complaints in concise professional style. Input complaints: " . implode(', ', $complaints) . " Additional raw doctor notes: " . $currentText . " Output only one plain text sentence starting with 'Chief complaints:' and no markdown."; return $this->generateClinicalTextWithAi($prompt, 180); } private function buildClinicalPrompt(string $mode, string $text): string { if ($mode === 'hinglish_to_english') { return "You are a clinical OPD assistant. Convert Hinglish medical text to clear professional English. Keep all clinical meaning unchanged. Do not add new diagnosis. Return plain text only, no markdown. Input: " . $text; } if ($mode === 'local_to_medical') { return "You are a medical documentation specialist for Indian OPD. Convert the following patient complaints written in Hindi, Hinglish, or a mix of local language and English into precise clinical English with correct standard medical terminology. Rules: - Replace colloquial terms with proper medical terms (e.g., bukhar → pyrexia/fever, khansi → cough, saans phoolna → dyspnoea, sir dard → cephalalgia/headache, body dard → myalgia, thakaan → fatigue, chakkar → vertigo/giddiness, ulti → vomiting/emesis, jalan → burning sensation, thandi lagana → chills/rigors, kamzori → weakness/asthenia). - Preserve all durations, severity, and context exactly as mentioned. - Output concise, professional clinical English suitable for OPD documentation. - Return plain text only, no bullet points, no markdown. Input: " . $text; } if ($mode === 'local_to_patient_voice') { return "You are a translator for Indian hospital OPD. The following text is a patient describing their symptoms in Hindi, Hinglish, or mixed language. Translate it into natural, first-person English exactly as the patient would say it — conversational, not clinical. Rules: - Use first-person (\"I feel\", \"I have\", \"I am\", \"I've had\"). - Preserve the exact meaning, durations, and sequence. - Do NOT use medical jargon or clinical terms — keep it as natural spoken English. - Example: \"Teen din se bukhar hai\" → \"I've had a fever for three days.\" - Example: \"Thand lag rahi hai aur jukham hai\" → \"I'm feeling cold and I have a cold.\" - Return plain text only, no bullet points, no markdown. Input: " . $text; } if ($mode === 'diagnosis') { return "You are a clinical OPD assistant. Rewrite diagnosis text into concise professional medical English. Preserve meaning, do not invent findings. Return plain text only. Input: " . $text; } if ($mode === 'finding') { return "You are a clinical OPD assistant. Rewrite findings/examination notes into concise clinical English. Preserve original meaning and sequence. Return plain text only. Input: " . $text; } return "You are a clinical OPD assistant. Rewrite this OPD note into concise, professional English suitable for prescription documentation. Preserve meaning, do not add new facts. Return plain text only. Input: " . $text; } private function buildClinicalAutotypeLocalFallback(string $mode, string $text): string { $source = trim((string) $text); if ($source === '') { return ''; } if ($mode === 'hinglish_to_english') { $dictionary = [ 'bukhar' => 'fever', 'khansi' => 'cough', 'pet dard' => 'abdominal pain', 'sir dard' => 'headache', 'ulti' => 'vomiting', 'kamzori' => 'weakness', 'saans' => 'breath', 'saans phoolna' => 'breathlessness', 'jukaam' => 'cold', 'chakkar' => 'giddiness', 'dast' => 'loose motions', 'jalan' => 'burning sensation', ]; $converted = mb_strtolower($source); foreach ($dictionary as $from => $to) { $converted = preg_replace('/\b' . preg_quote($from, '/') . '\b/u', $to, $converted) ?? $converted; } return $this->normalizeClinicalSentenceFlow($converted); } if ($mode === 'local_to_medical') { $dictionary = [ 'bukhar' => 'fever', 'khansi' => 'cough', 'pet dard' => 'abdominal pain', 'sir dard' => 'headache', 'sar dard' => 'headache', 'ulti' => 'vomiting', 'kamzori' => 'weakness', 'saans phoolna' => 'dyspnoea', 'saans' => 'breathing', 'jukaam' => 'rhinitis', 'chakkar' => 'vertigo', 'dast' => 'loose motions', 'jalan' => 'burning sensation', 'thakaan' => 'fatigue', 'body dard' => 'myalgia', 'badan dard' => 'myalgia', 'thandi lagna' => 'chills', 'thandi lagana' => 'chills', 'kaanp' => 'rigors', 'neend' => 'sleep', 'bhookh' => 'appetite', 'paani' => 'water intake', 'peshab' => 'urination', 'seena dard' => 'chest pain', 'ghabrahat' => 'palpitations', ]; $converted = mb_strtolower($source); foreach ($dictionary as $from => $to) { $converted = preg_replace('/\b' . preg_quote($from, '/') . '\b/u', $to, $converted) ?? $converted; } return $this->normalizeClinicalSentenceFlow($converted); } if ($mode === 'local_to_patient_voice') { // Basic word-swap into first-person English; AI path is strongly preferred for this mode $dictionary = [ 'teen din se' => 'for three days I have had', 'do din se' => 'for two days I have had', 'ek din se' => 'since yesterday I have had', 'subah se' => 'since morning I have had', 'raat se' => 'since last night I have had', 'bukhar hai' => 'a fever', 'bukhar' => 'fever', 'khansi hai' => 'a cough', 'khansi' => 'a cough', 'jukham hai' => 'a cold', 'jukham' => 'a cold', 'thand lag rahi hai' => 'I am feeling cold', 'thandi lag rahi hai' => 'I am feeling cold', 'thand lag raha hai' => 'I am feeling cold', 'chakkar aa raha hai' => 'I am feeling dizzy', 'chakkar' => 'dizziness', 'ulti ho rahi hai' => 'I am vomiting', 'ulti' => 'vomiting', 'pet dard hai' => 'I have abdominal pain', 'pet dard' => 'abdominal pain', 'sir dard hai' => 'I have a headache', 'sir dard' => 'a headache', 'saans phool rahi hai' => 'I am having difficulty breathing', 'saans' => 'breathing', 'kamzori' => 'weakness', 'thakaan' => 'tiredness', 'dast' => 'loose motions', 'jalan' => 'a burning sensation', 'aur' => 'and', 'hai' => '', 'hain' => '', ]; $converted = mb_strtolower($source); foreach ($dictionary as $from => $to) { $converted = str_replace($from, $to, $converted); } $converted = preg_replace('/\s+/', ' ', trim($converted)) ?? $converted; $converted = ucfirst($converted); return $converted; } return $this->normalizeClinicalSentenceFlow($source); } /** * @return array|null */ private function parseJsonFromAiResponse(string $raw): ?array { $text = trim($raw); if ($text === '') { return null; } $decoded = json_decode($text, true); if (is_array($decoded)) { return $decoded; } if (preg_match('/\{(?:[^{}]|(?R))*\}/s', $text, $match) === 1) { $decoded = json_decode((string) ($match[0] ?? ''), true); if (is_array($decoded)) { return $decoded; } } return null; } private function normalizeClinicalSentenceFlow(string $text): string { $normalized = trim($text); if ($normalized === '') { return ''; } $normalized = preg_replace('/\s+/', ' ', $normalized) ?? $normalized; $normalized = str_replace([' ,', ' .', ' ;', ' :'], [',', '.', ';', ':'], $normalized); $normalized = str_replace(["\r\n", "\r"], "\n", $normalized); $chunks = preg_split('/(?<=[\.!?;])\s+|\n+/', $normalized) ?: []; $sentences = []; foreach ($chunks as $chunk) { $chunk = trim((string) $chunk); if ($chunk === '') { continue; } $chunk = ucfirst($chunk); if (! preg_match('/[\.!?]$/', $chunk)) { $chunk .= '.'; } $sentences[] = $chunk; } if (empty($sentences)) { return ucfirst($normalized); } return implode(' ', $sentences); } private function generateClinicalTextWithAi(string $prompt, int $maxOutputTokens = 300): ?string { $prompt = trim($prompt); if ($prompt === '') { return null; } $apiKey = trim($this->readAiSettingValue([ 'GEMINI_API_KEY', 'DIAGNOSIS_AI_GEMINI_API_KEY', 'DIAGNOSIS_GEMINI_API_KEY', ])); if ($apiKey === '') { $apiKey = trim((string) getenv('GEMINI_API_KEY')); } if ($apiKey === '') { return null; } $modelCandidatesRaw = trim($this->readAiSettingValue([ 'DIAGNOSIS_AI_GEMINI_MODELS', 'DIAGNOSIS_AI_GEMINI_MODEL', 'GEMINI_MODEL_CANDIDATES', 'GEMINI_MODEL', ])); $models = []; if ($modelCandidatesRaw !== '') { $parts = preg_split('/[,\s]+/', $modelCandidatesRaw) ?: []; foreach ($parts as $part) { $model = trim((string) $part); if ($model !== '') { $models[] = $model; } } } if (empty($models)) { $models = ['gemini-2.0-flash', 'gemini-1.5-flash']; } $maxOutputTokens = max(64, min(2048, (int) $maxOutputTokens)); foreach ($models as $model) { try { $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . rawurlencode($model) . ':generateContent?key=' . rawurlencode($apiKey); $payload = [ 'contents' => [[ 'parts' => [[ 'text' => $prompt, ]], ]], 'generationConfig' => [ 'temperature' => 0.2, 'maxOutputTokens' => $maxOutputTokens, ], ]; $options = [ 'timeout' => 30, 'http_errors' => false, ]; if (defined('ENVIRONMENT') && in_array((string) ENVIRONMENT, ['development', 'testing'], true)) { $options['verify'] = false; } $client = service('curlrequest', $options); $response = $client->post($url, [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'body' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ]); $code = (int) $response->getStatusCode(); if ($code < 200 || $code >= 300) { continue; } $decoded = json_decode((string) $response->getBody(), true); if (!is_array($decoded)) { continue; } $out = ''; $candidates = $decoded['candidates'] ?? []; if (is_array($candidates)) { foreach ($candidates as $candidate) { $parts = $candidate['content']['parts'] ?? []; if (!is_array($parts)) { continue; } foreach ($parts as $part) { $piece = trim((string) ($part['text'] ?? '')); if ($piece !== '') { $out .= ($out === '' ? '' : "\n") . $piece; } } } } $out = trim($out); if ($out !== '') { $this->lastAiProvider = 'gemini'; return $out; } } catch (\Throwable $e) { continue; } } return null; } private function resolveAiServerBaseUrl(): string { $base = rtrim($this->readAiSettingValue(['DIAGNOSIS_AI_SERVER_URL']), '/'); if ($base !== '') { return $base; } $candidates = [ trim($this->readAiSettingValue(['DIAGNOSIS_AI_PARSE_ENDPOINT'])), trim($this->readAiSettingValue(['DIAGNOSIS_AI_OCR_ENDPOINT'])), ]; foreach ($candidates as $url) { if ($url === '') { continue; } $parts = @parse_url($url); if (! is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) { continue; } $base = (string) $parts['scheme'] . '://' . (string) $parts['host']; if (! empty($parts['port'])) { $base .= ':' . (int) $parts['port']; } return rtrim($base, '/'); } return ''; } /** * @return array */ private function resolveAiServerBaseCandidates(): array { $bases = []; $direct = trim($this->resolveAiServerBaseUrl()); if ($direct !== '') { $bases[] = rtrim($direct, '/'); } $derivedFromEndpoint = [ trim($this->readAiSettingValue(['DIAGNOSIS_AI_PARSE_ENDPOINT'])), trim($this->readAiSettingValue(['DIAGNOSIS_AI_OCR_ENDPOINT'])), ]; foreach ($derivedFromEndpoint as $endpointUrl) { if ($endpointUrl === '') { continue; } $parts = @parse_url($endpointUrl); if (! is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) { continue; } $origin = (string) $parts['scheme'] . '://' . (string) $parts['host']; if (! empty($parts['port'])) { $origin .= ':' . (int) $parts['port']; } $origin = rtrim($origin, '/'); $bases[] = $origin; $path = trim((string) ($parts['path'] ?? '')); if ($path !== '' && $path !== '/') { $dirPath = trim((string) dirname($path)); if ($dirPath !== '' && $dirPath !== '.') { $dirPath = '/' . trim($dirPath, '/'); $bases[] = $origin . $dirPath; $segments = array_values(array_filter(explode('/', trim($dirPath, '/')), static fn($x) => $x !== '')); if (count($segments) > 1) { $running = ''; foreach ($segments as $index => $segment) { if ($index >= count($segments) - 1) { break; } $running .= '/' . $segment; $bases[] = $origin . $running; } } } } } $bases[] = 'http://139.59.13.39:8000'; return array_values(array_unique(array_filter(array_map(static fn($v) => rtrim((string) $v, '/'), $bases)))); } private function translateAdviceWithAiServer(string $sourceText, string $targetLang): string { $base = $this->resolveAiServerBaseUrl(); if ($base === '') { return ''; } $token = trim($this->readAiSettingValue(['DIAGNOSIS_AI_PARSE_TOKEN'])); try { $options = [ 'timeout' => 20, 'http_errors' => false, ]; if (defined('ENVIRONMENT') && in_array((string) ENVIRONMENT, ['development', 'testing'], true)) { $options['verify'] = false; } $headers = ['Accept' => 'application/json']; if ($token !== '') { $headers['Authorization'] = 'Bearer ' . $token; } $client = service('curlrequest', $options); $url = $base . '/translate?text=' . rawurlencode($sourceText) . '&target_lang=' . rawurlencode($targetLang); $response = $client->get($url, ['headers' => $headers]); if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { return ''; } $decoded = json_decode((string) $response->getBody(), true); $translated = trim((string) ($decoded['translated_text'] ?? '')); if ($translated === '') { return ''; } $this->lastAiProvider = 'ai-server'; return $translated; } catch (\Throwable $e) { return ''; } } /** * @param array $input */ private function buildFullClinicalDraftPrompt(array $input): string { $payload = json_encode($input, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($payload === false) { $payload = '{}'; } return "You are a senior OPD clinical documentation assistant. Using the complete OPD snapshot JSON, produce concise professional drafts. Do not invent facts. Use only the data provided. Return ONLY valid minified JSON object with exactly these keys: finding_examinations, diagnosis, provisional_diagnosis, prescriber_remarks, investigation, advice. Each value must be plain text string (no markdown). OPD SNAPSHOT JSON: " . $payload; } /** * @return array|null */ private function extractFullDraftJson(?string $aiText): ?array { if ($aiText === null || trim($aiText) === '') { return null; } $candidate = trim($aiText); if (!str_starts_with($candidate, '{') || !str_ends_with($candidate, '}')) { if (preg_match('/\{.*\}/s', $candidate, $match) === 1) { $candidate = (string) ($match[0] ?? ''); } } $decoded = json_decode($candidate, true); if (!is_array($decoded)) { return null; } $keys = ['finding_examinations', 'diagnosis', 'provisional_diagnosis', 'prescriber_remarks', 'investigation', 'advice']; $result = []; foreach ($keys as $key) { $result[$key] = trim((string) ($decoded[$key] ?? '')); } return $result; } /** * @param array $input * @return array */ private function buildFullDraftFallback(array $input): array { $vitals = $this->joinParts([ ($input['bp'] ?? '') !== '' || ($input['diastolic'] ?? '') !== '' ? 'BP ' . trim((string) ($input['bp'] ?? '')) . '/' . trim((string) ($input['diastolic'] ?? '')) : '', ($input['pulse'] ?? '') !== '' ? 'Pulse ' . trim((string) $input['pulse']) . '/min' : '', ($input['temp'] ?? '') !== '' ? 'Temp ' . trim((string) $input['temp']) : '', ($input['spo2'] ?? '') !== '' ? 'SpO2 ' . trim((string) $input['spo2']) . '%' : '', ($input['rr_min'] ?? '') !== '' ? 'RR ' . trim((string) $input['rr_min']) . '/min' : '', ], '; '); $exam = $this->joinParts([ ($input['pallor'] ?? '') !== '' ? 'Pallor: ' . trim((string) $input['pallor']) : '', ($input['icterus'] ?? '') !== '' ? 'Icterus: ' . trim((string) $input['icterus']) : '', ($input['cyanosis'] ?? '') !== '' ? 'Cyanosis: ' . trim((string) $input['cyanosis']) : '', ($input['clubbing'] ?? '') !== '' ? 'Clubbing: ' . trim((string) $input['clubbing']) : '', ($input['edema'] ?? '') !== '' ? 'Edema: ' . trim((string) $input['edema']) : '', ($input['pain_value'] ?? '') !== '' ? 'Pain scale: ' . trim((string) $input['pain_value']) . '/4' : '', ], '; '); $complications = $this->joinParts([ !empty($input['pregnancy']) ? 'Pregnancy' : '', !empty($input['lactation']) ? 'Lactation' : '', !empty($input['liver_insufficiency']) ? 'Liver insufficiency' : '', !empty($input['renal_insufficiency']) ? 'Renal insufficiency' : '', !empty($input['pulmonary_insufficiency']) ? 'Pulmonary insufficiency' : '', !empty($input['corona_suspected']) ? 'Corona suspected' : '', !empty($input['dengue']) ? 'Dengue' : '', ], ', '); $addiction = $this->joinParts([ !empty($input['is_smoking']) ? 'Smoking' : '', !empty($input['is_alcohol']) ? 'Alcohol' : '', !empty($input['is_drug_abuse']) ? 'Drug abuse' : '', ], ', '); $women = $this->joinParts([ ($input['women_related_problems'] ?? '') !== '' ? 'Women related: ' . trim((string) $input['women_related_problems']) : '', ($input['women_lmp'] ?? '') !== '' ? 'LMP days before: ' . trim((string) $input['women_lmp']) : '', ($input['women_last_baby'] ?? '') !== '' ? 'Last baby: ' . trim((string) $input['women_last_baby']) : '', ($input['women_pregnancy_related'] ?? '') !== '' ? 'Pregnancy related: ' . trim((string) $input['women_pregnancy_related']) : '', ], '; '); $finding = $this->joinParts([ trim((string) ($input['finding_examinations'] ?? '')), $vitals !== '' ? 'Vitals: ' . $vitals : '', $exam !== '' ? 'General examination: ' . $exam : '', ], "\n"); $remarks = $this->joinParts([ trim((string) ($input['prescriber_remarks'] ?? '')), $complications !== '' ? 'Complication: ' . $complications : '', $addiction !== '' ? 'Addiction: ' . $addiction : '', $women, ], "\n"); $diagnosis = trim((string) ($input['diagnosis'] ?? '')); if ($diagnosis === '' && trim((string) ($input['complaints'] ?? '')) !== '') { $diagnosis = 'Provisional diagnosis to correlate with complaints: ' . trim((string) ($input['complaints'] ?? '')); } $provisional = trim((string) ($input['provisional_diagnosis'] ?? '')); if ($provisional === '') { $provisional = $diagnosis; } $investigation = trim((string) ($input['investigation'] ?? '')); $advice = $this->joinParts([ trim((string) ($input['advice'] ?? '')), trim((string) ($input['next_visit'] ?? '')) !== '' ? 'Next visit: ' . trim((string) ($input['next_visit'] ?? '')) : '', trim((string) ($input['refer_to'] ?? '')) !== '' ? 'Refer to: ' . trim((string) ($input['refer_to'] ?? '')) : '', ], "\n"); return [ 'finding_examinations' => $finding, 'diagnosis' => $diagnosis, 'provisional_diagnosis' => $provisional, 'prescriber_remarks' => $remarks, 'investigation' => $investigation, 'advice' => $advice, ]; } /** * @param array $parts */ private function joinParts(array $parts, string $separator = ', '): string { $filtered = []; foreach ($parts as $part) { $part = trim((string) $part); if ($part !== '') { $filtered[] = $part; } } return implode($separator, $filtered); } /** * @return array> */ private function buildPatientHistoryAlerts(int $patientId, int $currentOpdId): array { if ($patientId <= 0 || ! $this->db->tableExists('opd_prescription') || ! $this->db->tableExists('opd_master')) { return []; } $alerts = []; $rows = $this->db->table('opd_prescription p') ->select('p.opd_id,p.complaints,p.diagnosis,p.bp,p.diastolic,p.pulse,p.temp,p.spo2,p.investigation,p.advice,o.apointment_date') ->join('opd_master o', 'o.opd_id=p.opd_id', 'left') ->where('p.p_id', $patientId) ->where('p.opd_id !=', $currentOpdId) ->orderBy('p.id', 'DESC') ->limit(5) ->get() ->getResultArray(); foreach ($rows as $row) { $dateLabel = ! empty($row['apointment_date']) ? date('d-m-Y', strtotime((string) $row['apointment_date'])) : 'Previous OPD'; if (trim((string) ($row['diagnosis'] ?? '')) !== '') { $alerts[] = [ 'type' => 'Diagnosis History', 'message' => $dateLabel . ': ' . trim((string) $row['diagnosis']), ]; } if (trim((string) ($row['complaints'] ?? '')) !== '') { $alerts[] = [ 'type' => 'Complaint History', 'message' => $dateLabel . ': ' . trim((string) $row['complaints']), ]; } $vitalLine = []; foreach ([ 'BP' => trim((string) ($row['bp'] ?? '')), 'DIA' => trim((string) ($row['diastolic'] ?? '')), 'Pulse' => trim((string) ($row['pulse'] ?? '')), 'Temp' => trim((string) ($row['temp'] ?? '')), 'SpO2' => trim((string) ($row['spo2'] ?? '')), ] as $k => $v) { if ($v !== '') { $vitalLine[] = $k . ':' . $v; } } if (! empty($vitalLine)) { $alerts[] = [ 'type' => 'Vitals Trend', 'message' => $dateLabel . ': ' . implode(' | ', $vitalLine), ]; } } if ($this->db->tableExists('patient_remark')) { $remarks = $this->db->table('patient_remark') ->select('remark,insert_datetime') ->where('p_id', $patientId) ->orderBy('id', 'DESC') ->limit(3) ->get() ->getResultArray(); foreach ($remarks as $remarkRow) { $msg = trim((string) ($remarkRow['remark'] ?? '')); if ($msg === '') { continue; } $time = ! empty($remarkRow['insert_datetime']) ? date('d-m-Y H:i', strtotime((string) $remarkRow['insert_datetime'])) : 'Recent'; $alerts[] = [ 'type' => 'Patient Remark', 'message' => $time . ': ' . $msg, ]; } } return array_slice($alerts, 0, 8); } /** * @return array> */ private function fetchPatientRemarks(int $patientId, int $limit = 8): array { if ($patientId <= 0 || ! $this->db->tableExists('patient_remark')) { return []; } $rows = $this->db->table('patient_remark') ->select('remark,insert_by,insert_datetime') ->where('p_id', $patientId) ->orderBy('id', 'DESC') ->limit(max(1, $limit)) ->get() ->getResultArray(); $out = []; foreach ($rows as $row) { $msg = trim((string) ($row['remark'] ?? '')); if ($msg === '') { continue; } $time = ! empty($row['insert_datetime']) ? date('d-m-Y H:i', strtotime((string) $row['insert_datetime'])) : ''; $out[] = [ 'remark' => $msg, 'insert_by' => trim((string) ($row['insert_by'] ?? '')), 'insert_time' => $time, ]; } return $out; } /** * @return array> */ private function buildOldPrescriptionFastView(int $patientId, int $currentOpdId): array { if ($patientId <= 0 || ! $this->db->tableExists('opd_master')) { return []; } $opdRows = $this->db->table('opd_master o') ->select('o.opd_id,o.opd_code,o.apointment_date,p.complaints,p.diagnosis,p.id as session_id') ->join('opd_prescription p', 'p.opd_id=o.opd_id', 'left') ->where('o.p_id', $patientId) ->where('o.opd_id !=', $currentOpdId) ->orderBy('o.opd_id', 'DESC') ->limit(6) ->get() ->getResultArray(); if (empty($opdRows)) { return []; } $medicineMap = []; $medicineTable = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']); if ($medicineTable !== null) { $sessionIds = []; foreach ($opdRows as $opdRow) { $sessionId = (int) ($opdRow['session_id'] ?? 0); if ($sessionId > 0) { $sessionIds[$sessionId] = true; } } if (! empty($sessionIds)) { $medicineFields = $this->db->getFieldNames($medicineTable); if (in_array('opd_pre_id', $medicineFields, true)) { $doseMap = $this->getDoseMasterLabelMap('opd_dose_shed', ['dose_shed_id', 'id'], ['dose_show_sign', 'dose_sign', 'dose_sign_desc', 'name']); $whenMap = $this->getDoseMasterLabelMap('opd_dose_when', ['dose_when_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']); $freqMap = $this->getDoseMasterLabelMap('opd_dose_frequency', ['dose_freq_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']); $query = $this->db->table($medicineTable) ->whereIn('opd_pre_id', array_map('intval', array_keys($sessionIds))); if (in_array('id', $medicineFields, true)) { $query->orderBy('id', 'ASC'); } $medicineRows = $query->get()->getResultArray(); foreach ($medicineRows as $medicineRow) { $sessionId = (int) ($medicineRow['opd_pre_id'] ?? 0); if ($sessionId <= 0) { continue; } if (! isset($medicineMap[$sessionId])) { $medicineMap[$sessionId] = []; } $medicineMap[$sessionId][] = [ 'id' => (int) ($medicineRow['id'] ?? 0), 'med_id' => (int) ($medicineRow['med_id'] ?? 0), 'med_name' => trim((string) ($medicineRow['med_name'] ?? '')), 'med_type' => trim((string) ($medicineRow['med_type'] ?? '')), 'dosage' => trim((string) ($medicineRow['dosage'] ?? '')), 'dosage_label' => $this->resolveDoseMasterLabel($medicineRow['dosage'] ?? '', $doseMap), 'dosage_when' => trim((string) ($medicineRow['dosage_when'] ?? '')), 'dosage_when_label' => $this->resolveDoseMasterLabel($medicineRow['dosage_when'] ?? '', $whenMap), 'dosage_freq' => trim((string) ($medicineRow['dosage_freq'] ?? '')), 'dosage_freq_label' => $this->resolveDoseMasterLabel($medicineRow['dosage_freq'] ?? '', $freqMap), 'no_of_days' => trim((string) ($medicineRow['no_of_days'] ?? '')), 'qty' => trim((string) ($medicineRow['qty'] ?? '')), 'remark' => trim((string) ($medicineRow['remark'] ?? '')), ]; } } } } $scanMap = []; if ($this->db->tableExists('file_upload_data')) { $opdIds = array_map(static fn(array $r): int => (int) ($r['opd_id'] ?? 0), $opdRows); $opdIds = array_values(array_filter($opdIds)); if (!empty($opdIds)) { $fields = $this->db->getFieldNames('file_upload_data'); $builder = $this->db->table('file_upload_data') ->select('opd_id,full_path,file_ext,id,show_type') ->whereIn('opd_id', $opdIds) ->orderBy('id', 'DESC'); if (in_array('show_type', $fields, true)) { $builder->where('show_type', 0); } $files = $builder->get()->getResultArray(); foreach ($files as $file) { $opdId = (int) ($file['opd_id'] ?? 0); if ($opdId <= 0) { continue; } if (!isset($scanMap[$opdId])) { $scanMap[$opdId] = []; } if (count($scanMap[$opdId]) >= 2) { continue; } $raw = (string) ($file['full_path'] ?? ''); $publicPath = $raw; $pos = strpos($raw, '/uploads/', 1); if ($pos !== false) { $publicPath = substr($raw, $pos); } $ext = strtolower((string) ($file['file_ext'] ?? pathinfo($publicPath, PATHINFO_EXTENSION))); $ext = $ext !== '' && $ext[0] !== '.' ? '.' . $ext : $ext; $scanMap[$opdId][] = [ 'path' => $publicPath, 'is_pdf' => $ext === '.pdf', 'scan_type' => $this->detectScanTypeFromPath($publicPath), ]; } } } $out = []; foreach ($opdRows as $row) { $opdId = (int) ($row['opd_id'] ?? 0); $sessionId = (int) ($row['session_id'] ?? 0); $out[] = [ 'opd_id' => $opdId, 'opd_code' => (string) ($row['opd_code'] ?? $opdId), 'opd_date' => !empty($row['apointment_date']) ? date('d-m-Y', strtotime((string) $row['apointment_date'])) : '', 'complaints' => (string) ($row['complaints'] ?? ''), 'diagnosis' => (string) ($row['diagnosis'] ?? ''), 'session_id' => $sessionId, 'medicines' => $medicineMap[$sessionId] ?? [], 'scans' => $scanMap[$opdId] ?? [], ]; } return $out; } /** * @return array> */ private function getCurrentOpdScansForConsult(int $opdId): array { if ($opdId <= 0 || ! $this->db->tableExists('file_upload_data')) { return []; } $fields = $this->db->getFieldNames('file_upload_data'); $safeSelect = ['id', 'full_path', 'file_ext', 'insert_date']; foreach (['show_type', 'scan_type', 'document_type', 'content_description', 'ai_status', 'ai_alert_flag', 'ai_alert_text', 'extract_text', 'extracted_text'] as $optional) { if (in_array($optional, $fields, true)) { $safeSelect[] = $optional; } } $builder = $this->db->table('file_upload_data') ->select(implode(',', $safeSelect)) ->where('opd_id', $opdId) ->orderBy('id', 'DESC') ->limit(6); if (in_array('show_type', $fields, true)) { $builder->where('show_type', 0); } $rows = $builder->get()->getResultArray(); if (empty($rows)) { return []; } $out = []; foreach ($rows as $row) { $raw = str_replace('\\', '/', (string) ($row['full_path'] ?? '')); $publicPath = $raw; $pos = strpos($raw, '/uploads/'); if ($pos !== false) { $publicPath = substr($raw, $pos); } $ext = strtolower((string) ($row['file_ext'] ?? pathinfo($publicPath, PATHINFO_EXTENSION))); if ($ext !== '' && $ext[0] !== '.') { $ext = '.' . $ext; } $reportText = trim((string) ($row['content_description'] ?? '')); if ($reportText === '') { $reportText = trim((string) ($row['extracted_text'] ?? $row['extract_text'] ?? '')); } $out[] = [ 'id' => (int) ($row['id'] ?? 0), 'path' => $publicPath, 'is_pdf' => $ext === '.pdf', 'scan_type' => (string) ($row['scan_type'] ?? ''), 'document_type' => (string) ($row['document_type'] ?? ''), 'ai_status' => (string) ($row['ai_status'] ?? ''), 'ai_alert_flag' => (int) ($row['ai_alert_flag'] ?? 0), 'ai_alert_text' => (string) ($row['ai_alert_text'] ?? ''), 'report_text' => $reportText, 'insert_date' => !empty($row['insert_date']) ? date('d/m/Y H:i', strtotime((string) $row['insert_date'])) : '', ]; } return $out; } /** * @return array> */ private function getPatientScanHistoryForConsult(int $patientId): array { if ($patientId <= 0 || ! $this->db->tableExists('file_upload_data') || ! $this->db->tableExists('opd_master')) { return []; } $fields = $this->db->getFieldNames('file_upload_data'); $safeSelect = ['f.id', 'f.opd_id', 'f.full_path', 'f.file_ext', 'f.insert_date', 'o.opd_code', 'o.apointment_date']; foreach (['show_type', 'scan_type', 'document_type', 'content_description', 'ai_status', 'ai_alert_flag', 'ai_alert_text', 'extract_text', 'extracted_text'] as $optional) { if (in_array($optional, $fields, true)) { $safeSelect[] = 'f.' . $optional; } } $builder = $this->db->table('file_upload_data f') ->select(implode(',', $safeSelect)) ->join('opd_master o', 'o.opd_id = f.opd_id', 'left') ->where('o.p_id', $patientId) ->orderBy('f.id', 'DESC') ->limit(120); if (in_array('show_type', $fields, true)) { $builder->groupStart() ->where('f.show_type', 0) ->orWhere('f.show_type IS NULL', null, false) ->groupEnd(); } $rows = $builder->get()->getResultArray(); if (empty($rows)) { return []; } $out = []; foreach ($rows as $row) { $raw = str_replace('\\', '/', (string) ($row['full_path'] ?? '')); $publicPath = $raw; $pos = strpos($raw, '/uploads/'); if ($pos !== false) { $publicPath = substr($raw, $pos); } $ext = strtolower((string) ($row['file_ext'] ?? pathinfo($publicPath, PATHINFO_EXTENSION))); if ($ext !== '' && $ext[0] !== '.') { $ext = '.' . $ext; } $reportText = trim((string) ($row['content_description'] ?? '')); if ($reportText === '') { $reportText = trim((string) ($row['extracted_text'] ?? $row['extract_text'] ?? '')); } $out[] = [ 'id' => (int) ($row['id'] ?? 0), 'opd_id' => (int) ($row['opd_id'] ?? 0), 'opd_code' => (string) ($row['opd_code'] ?? ''), 'opd_date' => !empty($row['apointment_date']) ? date('d-m-Y', strtotime((string) $row['apointment_date'])) : '', 'path' => $publicPath, 'is_pdf' => $ext === '.pdf', 'scan_type' => (string) ($row['scan_type'] ?? ''), 'document_type' => (string) ($row['document_type'] ?? ''), 'ai_status' => (string) ($row['ai_status'] ?? ''), 'ai_alert_flag' => (int) ($row['ai_alert_flag'] ?? 0), 'ai_alert_text' => (string) ($row['ai_alert_text'] ?? ''), 'report_text' => $reportText, 'insert_date' => !empty($row['insert_date']) ? date('d/m/Y H:i', strtotime((string) $row['insert_date'])) : '', ]; } return $out; } private function detectScanTypeFromPath(string $path): string { $low = strtolower($path); if (str_contains($low, 'ecg')) { return 'ecg'; } if (str_contains($low, 'lab') || str_contains($low, 'report')) { return 'lab'; } return 'general'; } private function resolveLocalFilePathFromStoredPath(string $storedPath): ?string { $normalized = str_replace('\\', '/', trim($storedPath)); $uploadsPos = strpos($normalized, '/uploads/'); if ($uploadsPos === false) { return null; } $relative = substr($normalized, $uploadsPos + 1); $relative = str_replace(['../', '..\\'], '', $relative); $candidates = [ rtrim(FCPATH, '/\\') . '/' . $relative, rtrim(ROOTPATH, '/\\') . '/public/' . $relative, rtrim(ROOTPATH, '/\\') . '/' . $relative, ]; foreach ($candidates as $candidate) { $real = realpath($candidate); if ($real !== false && is_file($real)) { return $real; } } return null; } private function extractClinicalTextFromScan(string $filePath, string $scanType = ''): ?string { // Legacy external scan extraction has been removed from this module. // Keep returning null so existing fallback paths continue to work. unset($filePath, $scanType); return null; } public function opd_prescription_print(int $opdId, int $opdSessionId, int $printType = 0) { $printConfig = $this->resolvePrescriptionLayoutByDoctorField($opdId, $printType); $layoutMode = (string) ($printConfig['layout'] ?? 'content_only'); $templateKey = (string) ($printConfig['template'] ?? ''); $url = base_url('Opd/opd_lettre_pdf/' . (int) $opdId) . '?session_id=' . (int) $opdSessionId . '&layout=' . urlencode($layoutMode); if ($templateKey !== '') { $url .= '&template=' . urlencode($templateKey); } return redirect()->to($url); } /** * @return array{layout:string,template:string} */ private function resolvePrescriptionLayoutByDoctorField(int $opdId, int $printType): array { $allowed = ['full', 'meta_content', 'content_only', 'header_meta', 'meta_only']; $defaultByType = [ 0 => 'content_only', 1 => 'meta_content', 2 => 'full', ]; $fallback = $defaultByType[$printType] ?? 'content_only'; $fieldByType = [ 0 => 'rx_plain_paper', 1 => 'rx_blank_letter_head', 2 => 'rx_pre_print_letter_head_format', ]; $fieldName = $fieldByType[$printType] ?? ''; if ($fieldName === '') { return ['layout' => $fallback, 'template' => '']; } $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' => $fallback, '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' => $fallback, 'template' => $templateKey]; } return ['layout' => $fallback, 'template' => '']; } }