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; 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)) { $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, 'error_text' => $inserted > 0 ? ($inserted . ' medicine(s) added from Rx-Group.') : '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(), ]); } $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(), ]); } return $this->response->setJSON(['update' => 0, 'error_text' => 'AI unavailable and no local match found. Check AI settings.']); } $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 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(); 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'); } $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']); } $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(), ]); } 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 === '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); } 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 { unset($prompt, $maxOutputTokens); 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 ''; } 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' => '']; } }