user(); if (! $user) { return false; } if (method_exists($user, 'can') && ( $user->can('admin.access') || $user->can('admin.settings') || $user->can('abdm.access') )) { return true; } return false; } private function canAccessPrescription(): bool { $user = auth()->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(); $patientProfileFilePath = '/assets/images/no_image.svg'; if (!empty($patient)) { $profilePicturePath = ''; if ($this->db->fieldExists('profile_picture', 'patient_master')) { $profilePicturePath = trim((string) ($patient[0]->profile_picture ?? '')); } $profileFileId = (int) ($patient[0]->profile_file_id ?? 0); if ($profileFileId > 0 && $this->db->tableExists('file_upload_data')) { $fileRow = $this->db->table('file_upload_data') ->select('full_path') ->where('id', $profileFileId) ->get(1) ->getRow(); $fullPath = trim((string) ($fileRow->full_path ?? '')); if ($fullPath !== '') { $pos = strpos($fullPath, '/uploads/', 1); $patientProfileFilePath = $pos !== false ? substr($fullPath, $pos) : $fullPath; } } elseif ($profilePicturePath !== '') { $pos = strpos($profilePicturePath, '/uploads/', 1); $patientProfileFilePath = $pos !== false ? substr($profilePicturePath, $pos) : $profilePicturePath; } } 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->Prescriber_Remarks = $this->stripNabhFieldsFromRemarks((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'] ?? ''); $historyInfo = $this->hydratePatientHistoryFields( $firstRow, (string) ($first->Prescriber_Remarks ?? '') ); $first->obstetric_history = (string) ($historyInfo['obstetric_history'] ?? ''); $first->menstrual_history = (string) ($historyInfo['menstrual_history'] ?? ''); $first->medical_surgical_history = (string) ($historyInfo['medical_surgical_history'] ?? ''); $first->family_history = (string) ($historyInfo['family_history'] ?? ''); $first->allergic_history = (string) ($historyInfo['allergic_history'] ?? ''); $first->vaccination_history = (string) ($historyInfo['vaccination_history'] ?? ''); $prescription[0] = $first; } } return view('billing/opd_prescription_basic', [ 'opd_id' => $opdId, 'opd_master' => $opdMaster, 'patient_master' => $patient, 'patient_profile_file_path' => $patientProfileFilePath, 'opd_prescription' => $prescription, 'next_visit_options' => $this->getNextVisitOptions(date('Y-m-d')), '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), ]); } /** * @return array */ private function getNextVisitOptions(string $baseDate): array { $fallbackDays = [3, 4, 5, 7, 10, 15, 20, 30, 60]; $fallbackDesc = [ 3 => '3 Days', 4 => '4 Days', 5 => '5 Days', 7 => '1 Week', 10 => '10 Days', 15 => '15 Days', 20 => '20 days', 30 => '1 Month', 60 => '2 Months', ]; $baseTs = strtotime($baseDate); if ($baseTs === false) { $baseTs = strtotime(date('Y-m-d')); } $rows = []; if ($this->db->tableExists('opd_nextvisit')) { $fields = $this->db->getFieldNames('opd_nextvisit') ?? []; $descField = $this->resolveFirstField($fields, ['next_visit_desc', 'visit_desc', 'description', 'nextvisit_desc', 'name']); $daysField = $this->resolveFirstField($fields, ['no_of_days', 'days', 'day_count']); if ($descField !== null && $daysField !== null) { $builder = $this->db->table('opd_nextvisit') ->select($descField . ' as visit_desc,' . $daysField . ' as no_of_days'); if (in_array('status', $fields, true)) { $builder->where('status', 1); } $rows = $builder ->orderBy($daysField, 'ASC') ->get() ->getResultArray(); } } $options = []; if (! empty($rows)) { foreach ($rows as $row) { $days = (int) ($row['no_of_days'] ?? 0); $desc = trim((string) ($row['visit_desc'] ?? '')); if ($days <= 0 || $desc === '') { continue; } $visitDate = date('d-m-Y', strtotime('+' . $days . ' day', $baseTs)); $value = $desc . ' (' . $visitDate . ')'; $options[] = [ 'desc' => $desc, 'days' => $days, 'date' => $visitDate, 'value' => $value, ]; } } if (empty($options)) { foreach ($fallbackDays as $days) { $desc = (string) ($fallbackDesc[$days] ?? ($days . ' Days')); $visitDate = date('d-m-Y', strtotime('+' . $days . ' day', $baseTs)); $value = $desc . ' (' . $visitDate . ')'; $options[] = [ 'desc' => $desc, 'days' => $days, 'date' => $visitDate, 'value' => $value, ]; } } return $options; } 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')), 'complaint_onset' => trim((string) $this->request->getPost('complaint_onset')), 'complaint_duration_days' => trim((string) $this->request->getPost('complaint_duration_days')), 'complaint_severity' => trim((string) $this->request->getPost('complaint_severity')), 'diagnosis' => trim((string) $this->request->getPost('diagnosis')), 'diagnosis_snomed_id' => trim((string) $this->request->getPost('diagnosis_snomed_id')), 'diagnosis_snomed_term' => trim((string) $this->request->getPost('diagnosis_snomed_term')), 'diagnosis_snomed_source' => trim((string) $this->request->getPost('diagnosis_snomed_source')), 'Provisional_diagnosis' => trim((string) $this->request->getPost('provisional_diagnosis')), 'provisional_diagnosis_snomed_id' => trim((string) $this->request->getPost('provisional_diagnosis_snomed_id')), 'provisional_diagnosis_snomed_term' => trim((string) $this->request->getPost('provisional_diagnosis_snomed_term')), 'provisional_diagnosis_snomed_source' => trim((string) $this->request->getPost('provisional_diagnosis_snomed_source')), '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')), 'obstetric_history' => trim((string) $this->request->getPost('obstetric_history')), 'menstrual_history' => trim((string) $this->request->getPost('menstrual_history')), 'medical_surgical_history' => trim((string) $this->request->getPost('medical_surgical_history')), 'family_history' => trim((string) $this->request->getPost('family_history')), 'allergic_history' => trim((string) $this->request->getPost('allergic_history')), 'vaccination_history' => trim((string) $this->request->getPost('vaccination_history')), '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 ); $provisionalText = trim((string) ($payload['Provisional_diagnosis'] ?? '')); $postedSnomedId = trim((string) ($payload['provisional_diagnosis_snomed_id'] ?? '')); $postedSnomedTerm = trim((string) ($payload['provisional_diagnosis_snomed_term'] ?? '')); $postedSnomedSource = trim((string) ($payload['provisional_diagnosis_snomed_source'] ?? '')); $diagnosisText = trim((string) ($payload['diagnosis'] ?? '')); $postedDiagnosisSnomedId = trim((string) ($payload['diagnosis_snomed_id'] ?? '')); $postedDiagnosisSnomedTerm = trim((string) ($payload['diagnosis_snomed_term'] ?? '')); $postedDiagnosisSnomedSource = trim((string) ($payload['diagnosis_snomed_source'] ?? '')); if ($postedDiagnosisSnomedId !== '') { $payload['diagnosis_snomed_id'] = $postedDiagnosisSnomedId; $payload['diagnosis_snomed_term'] = $postedDiagnosisSnomedTerm !== '' ? $postedDiagnosisSnomedTerm : $diagnosisText; $payload['diagnosis_snomed_source'] = $postedDiagnosisSnomedSource !== '' ? $postedDiagnosisSnomedSource : 'snomed'; } else { $payload['diagnosis_snomed_id'] = ''; $payload['diagnosis_snomed_term'] = ''; $payload['diagnosis_snomed_source'] = ''; if ($diagnosisText !== '' && strpos($diagnosisText, ',') === false) { $resolvedDiagnosis = $this->resolveProvisionalDiagnosisSnomedMatch($diagnosisText); if (! empty($resolvedDiagnosis['concept_id'])) { $payload['diagnosis_snomed_id'] = (string) $resolvedDiagnosis['concept_id']; $payload['diagnosis_snomed_term'] = (string) ($resolvedDiagnosis['term'] ?? $diagnosisText); $payload['diagnosis_snomed_source'] = (string) ($resolvedDiagnosis['source'] ?? 'snomed-exact'); } } } if ($postedSnomedId !== '') { $payload['provisional_diagnosis_snomed_id'] = $postedSnomedId; $payload['provisional_diagnosis_snomed_term'] = $postedSnomedTerm !== '' ? $postedSnomedTerm : $provisionalText; $payload['provisional_diagnosis_snomed_source'] = $postedSnomedSource !== '' ? $postedSnomedSource : 'snomed'; } else { $payload['provisional_diagnosis_snomed_id'] = ''; $payload['provisional_diagnosis_snomed_term'] = ''; $payload['provisional_diagnosis_snomed_source'] = ''; if ($provisionalText !== '' && strpos($provisionalText, ',') === false) { $resolved = $this->resolveProvisionalDiagnosisSnomedMatch($provisionalText); if (! empty($resolved['concept_id'])) { $payload['provisional_diagnosis_snomed_id'] = (string) $resolved['concept_id']; $payload['provisional_diagnosis_snomed_term'] = (string) ($resolved['term'] ?? $provisionalText); $payload['provisional_diagnosis_snomed_source'] = (string) ($resolved['source'] ?? 'snomed-exact'); } } } $this->mapWomenStructuredToExistingColumns($payload, $fields, $womenStructuredInput); $complaintStructuredInput = [ 'complaint_onset' => (string) ($payload['complaint_onset'] ?? ''), 'complaint_duration_days' => (string) ($payload['complaint_duration_days'] ?? ''), 'complaint_severity' => (string) ($payload['complaint_severity'] ?? ''), ]; $this->mapComplaintStructuredToExistingColumns($payload, $fields, $complaintStructuredInput); $payload['complaints'] = $this->upsertComplaintStructuredIntoComplaints( (string) ($payload['complaints'] ?? ''), $complaintStructuredInput ); $diagnosisCodingInput = [ 'diagnosis_snomed_id' => (string) ($payload['diagnosis_snomed_id'] ?? ''), 'diagnosis_snomed_term' => (string) ($payload['diagnosis_snomed_term'] ?? ''), 'diagnosis_snomed_source' => (string) ($payload['diagnosis_snomed_source'] ?? ''), ]; $unmappedDiagnosisCoding = $this->mapDiagnosisCodingToExistingColumns($payload, $fields, $diagnosisCodingInput); $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'] ?? ''), ]; $this->mapNabhFieldsToExistingColumns($payload, $fields, $nabhInput); $patientHistoryInput = [ 'obstetric_history' => (string) ($payload['obstetric_history'] ?? ''), 'menstrual_history' => (string) ($payload['menstrual_history'] ?? ''), 'medical_surgical_history' => (string) ($payload['medical_surgical_history'] ?? ''), 'family_history' => (string) ($payload['family_history'] ?? ''), 'allergic_history' => (string) ($payload['allergic_history'] ?? ''), 'vaccination_history' => (string) ($payload['vaccination_history'] ?? ''), ]; $unmappedPatientHistory = $this->mapPatientHistoryFieldsToExistingColumns($payload, $fields, $patientHistoryInput); // Keep prescription remarks user-controlled; do not auto-inject structured NABH lines. $payload['Prescriber_Remarks'] = $this->stripNabhFieldsFromRemarks((string) ($payload['Prescriber_Remarks'] ?? '')); $payload['Prescriber_Remarks'] = $this->stripPatientHistoryFieldsFromRemarks((string) ($payload['Prescriber_Remarks'] ?? '')); if ($this->hasAnyPatientHistoryField($unmappedPatientHistory)) { $payload['Prescriber_Remarks'] = $this->upsertPatientHistoryFieldsIntoRemarks( (string) ($payload['Prescriber_Remarks'] ?? ''), $unmappedPatientHistory ); } if ($this->hasAnyPatientHistoryField($unmappedDiagnosisCoding)) { $payload['Prescriber_Remarks'] = $this->upsertDiagnosisCodingIntoRemarks( (string) ($payload['Prescriber_Remarks'] ?? ''), $unmappedDiagnosisCoding ); } 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 ); } } $abdmValidationErrors = $this->validateAbdmReadinessForConsult($payload, $patientRow, (array) $opdRow); if (! empty($abdmValidationErrors)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => implode(' ', $abdmValidationErrors), 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } 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'])) { // JOIN through opd_master to guarantee per-doctor isolation $maxQueue = $this->db->query( 'SELECT COALESCE(MAX(pr.queue_no), 0) AS max_queue' . ' FROM opd_prescription pr' . ' INNER JOIN opd_master om ON om.opd_id = pr.opd_id' . ' WHERE pr.date_opd_visit = ? AND om.doc_id = ?', [date('Y-m-d'), (int) ($opdRow->doc_id ?? 0)] )->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]); } } $this->markOpdVisitedOnConsultCompletion((int) $opdId); 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 section_recent_entries() { $opdId = (int) $this->request->getGet('opd_id'); if ($opdId <= 0 || ! $this->db->tableExists('opd_master') || ! $this->db->tableExists('opd_prescription')) { return $this->response->setJSON([ 'update' => 0, 'sections' => [], 'error_text' => 'Recent entries not available', ]); } $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, 'sections' => [], 'error_text' => 'Patient not found', ]); } $sections = [ 'complaints' => $this->collectRecentPrescriptionEntries($patientId, $opdId, ['complaints']), 'diagnosis' => $this->collectRecentPrescriptionEntries($patientId, $opdId, ['diagnosis']), 'provisional_diagnosis' => $this->collectRecentPrescriptionEntries($patientId, $opdId, ['Provisional_diagnosis', 'provisional_diagnosis']), 'investigation' => $this->collectRecentInvestigationEntries($patientId, $opdId), ]; return $this->response->setJSON([ 'update' => 1, 'sections' => $sections, 'error_text' => 'Recent entries loaded', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function template_workspace() { return view('billing/opd_template_workspace'); } public function snomed_dashboard() { if (! $this->canAccessSnomedAdmin()) { return $this->response->setStatusCode(403)->setBody('Access denied'); } return view('billing/snomed_dashboard'); } public function snomed_release_status() { if (! $this->canAccessSnomedAdmin()) { return $this->response->setStatusCode(403)->setJSON(['error_text' => 'Access denied']); } $latest = []; if ($this->db->tableExists('snomed_release_log')) { $latest = $this->db->table('snomed_release_log') ->select('id,package_name,release_effective_time,rf2_md5,release_notes_md5,status,row_counts_json,error_text,imported_at,updated_at') ->orderBy('id', 'DESC') ->get(1) ->getRowArray() ?? []; } $tableCounts = [ 'concept' => $this->countTableRows('snomed_concept'), 'description' => $this->countTableRows('snomed_description'), 'language_refset' => $this->countTableRows('snomed_language_refset'), 'simple_map' => $this->countTableRows('snomed_map_simple'), 'extended_map' => $this->countTableRows('snomed_map_extended'), ]; return $this->response->setJSON([ 'latest_release' => $latest, 'table_counts' => $tableCounts, ]); } public function snomed_map_lookup() { if (! $this->canAccessPrescription() && ! $this->canAccessSnomedAdmin()) { return $this->response->setStatusCode(403)->setJSON(['rows' => [], 'error_text' => 'Access denied']); } $conceptId = trim((string) $this->request->getGet('concept_id')); $q = trim((string) $this->request->getGet('q')); if ($conceptId === '' && $q === '') { return $this->response->setJSON(['rows' => []]); } if ($conceptId === '') { $matched = $this->resolveProvisionalDiagnosisSnomedMatch($q); if (! empty($matched['concept_id'])) { $conceptId = (string) $matched['concept_id']; } } if ($conceptId === '' || ! preg_match('/^[0-9]{6,20}$/', $conceptId)) { return $this->response->setJSON(['rows' => []]); } $conceptTerm = $this->getSnomedPreferredTermByConcept($conceptId); $rows = $this->fetchSnomedIcdMapRows($conceptId); return $this->response->setJSON([ 'concept_id' => $conceptId, 'concept_term' => $conceptTerm, 'rows' => $rows, ]); } public function clinical_template_monitor() { if (! $this->ensureClinicalTemplateTable()) { return $this->response->setJSON(['rows' => []]); } $section = trim((string) $this->request->getGet('section')); $scope = strtolower(trim((string) $this->request->getGet('scope'))); $doctorId = (int) $this->request->getGet('doctor_id'); $docId = $this->getCurrentUserId(); $builder = $this->db->table('opd_clinical_templates') ->select('id,doc_id,section_key,template_name,template_text,is_active,created_at,updated_at') ->where('is_active', 1); if ($this->isTemplateSectionAllowed($section)) { $builder->where('section_key', $section); } if ($scope === 'master') { $builder->where('doc_id', 0); } elseif ($scope === 'doctor') { if ($doctorId > 0) { $builder->where('doc_id', $doctorId); } else { $builder->where('doc_id', $docId); } } else { $builder->groupStart() ->where('doc_id', 0) ->orWhere('doc_id', $docId) ->groupEnd(); } $rows = $builder ->orderBy('updated_at', 'DESC') ->orderBy('id', 'DESC') ->limit(300) ->get() ->getResultArray(); $doctorNameById = []; if ($this->db->tableExists('doctor_master')) { $doctorRows = $this->db->table('doctor_master')->select('id,p_fname')->get()->getResultArray(); foreach ($doctorRows as $doctorRow) { $doctorNameById[(int) ($doctorRow['id'] ?? 0)] = (string) ($doctorRow['p_fname'] ?? ''); } } foreach ($rows as &$row) { $row['scope_label'] = ((int) ($row['doc_id'] ?? -1) === 0) ? 'Master' : 'Doctor'; $row['doctor_name'] = ((int) ($row['doc_id'] ?? 0) > 0) ? ($doctorNameById[(int) ($row['doc_id'] ?? 0)] ?? '') : ''; } unset($row); return $this->response->setJSON(['rows' => $rows]); } public function clinical_template_update() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } if (! $this->ensureClinicalTemplateTable()) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Template storage unavailable']); } $id = (int) $this->request->getPost('id'); $section = trim((string) $this->request->getPost('section')); $templateName = trim((string) $this->request->getPost('template_name')); $templateText = trim((string) $this->request->getPost('template_text')); $templateScope = strtolower(trim((string) $this->request->getPost('template_scope'))); $templateDocId = (int) $this->request->getPost('template_doc_id'); if ($id <= 0 || ! $this->isTemplateSectionAllowed($section) || $templateName === '' || $templateText === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Template id, section, name and text are required']); } $targetDocId = $templateScope === 'master' ? 0 : ($templateDocId > 0 ? $templateDocId : $this->getCurrentUserId()); if ($targetDocId < 0) { $targetDocId = 0; } $current = $this->db->table('opd_clinical_templates')->where('id', $id)->where('is_active', 1)->get(1)->getRowArray(); if (empty($current)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Template not found']); } $currentDocId = (int) ($current['doc_id'] ?? -1); $myDocId = $this->getCurrentUserId(); if ($currentDocId !== 0 && $currentDocId !== $myDocId) { return $this->response->setJSON(['update' => 0, 'error_text' => 'You can edit only your own or master templates']); } $existing = $this->db->table('opd_clinical_templates') ->where('id !=', $id) ->where('doc_id', $targetDocId) ->where('section_key', $section) ->where('template_name', $templateName) ->where('is_active', 1) ->get(1) ->getRowArray(); if (!empty($existing)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Another template with same name already exists in selected scope']); } $this->db->table('opd_clinical_templates')->where('id', $id)->update([ 'doc_id' => $targetDocId, 'section_key' => $section, 'template_name' => $templateName, 'template_text' => $templateText, 'updated_at' => date('Y-m-d H:i:s'), ]); return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Template updated', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function clinical_template_doctors() { if (! $this->db->tableExists('doctor_master')) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames('doctor_master'); $idField = $this->resolveFirstField($fields, ['id', 'doc_id']); $nameField = $this->resolveFirstField($fields, ['p_fname', 'name', 'doc_name']); if ($idField === null || $nameField === null) { return $this->response->setJSON(['rows' => []]); } $builder = $this->db->table('doctor_master') ->select($idField . ' as id,' . $nameField . ' as name'); if (in_array('active', $fields, true)) { $builder->where('active', 1); } $rows = $builder->orderBy($nameField, 'ASC')->get()->getResultArray(); return $this->response->setJSON(['rows' => $rows]); } public function clinical_template_usage_analytics() { if (! $this->ensureClinicalTemplateTable() || ! $this->ensureClinicalTemplateUsageTable()) { return $this->response->setJSON([ 'top_rows' => [], 'section_rows' => [], 'summary' => ['total_uses' => 0, 'used_templates' => 0], ]); } $section = trim((string) $this->request->getGet('section')); $scope = strtolower(trim((string) $this->request->getGet('scope'))); $doctorId = (int) $this->request->getGet('doctor_id'); $currentDocId = (int) ($this->getCurrentUserId() ?? 0); $targetDocId = $scope === 'doctor' && $doctorId > 0 ? $doctorId : $currentDocId; $builder = $this->db->table('opd_clinical_template_usage u') ->select('u.template_id,u.section_key,u.use_count,u.last_used_at,t.template_name,t.doc_id') ->join('opd_clinical_templates t', 't.id = u.template_id', 'inner') ->where('u.doc_id', $targetDocId) ->where('t.is_active', 1); if ($this->isTemplateSectionAllowed($section)) { $builder->where('u.section_key', $section); } if ($scope === 'master') { $builder->where('t.doc_id', 0); } elseif ($scope === 'doctor') { $builder->where('t.doc_id', $targetDocId); } else { $builder->groupStart() ->where('t.doc_id', 0) ->orWhere('t.doc_id', $targetDocId) ->groupEnd(); } $usageRows = $builder->get()->getResultArray(); if (empty($usageRows)) { return $this->response->setJSON([ 'top_rows' => [], 'section_rows' => [], 'summary' => ['total_uses' => 0, 'used_templates' => 0], ]); } usort($usageRows, static function (array $a, array $b): int { $aUse = (int) ($a['use_count'] ?? 0); $bUse = (int) ($b['use_count'] ?? 0); if ($aUse !== $bUse) { return $bUse <=> $aUse; } return strcmp((string) ($b['last_used_at'] ?? ''), (string) ($a['last_used_at'] ?? '')); }); $totalUses = 0; $usedTemplates = []; $sectionAgg = []; foreach ($usageRows as $row) { $use = (int) ($row['use_count'] ?? 0); $sec = trim((string) ($row['section_key'] ?? '')); $totalUses += $use; $templateId = (int) ($row['template_id'] ?? 0); if ($templateId > 0) { $usedTemplates[$templateId] = true; } if ($sec === '') { continue; } if (! isset($sectionAgg[$sec])) { $sectionAgg[$sec] = ['section_key' => $sec, 'total_uses' => 0, 'template_count' => 0, 'template_ids' => []]; } $sectionAgg[$sec]['total_uses'] += $use; if ($templateId > 0 && ! isset($sectionAgg[$sec]['template_ids'][$templateId])) { $sectionAgg[$sec]['template_ids'][$templateId] = true; $sectionAgg[$sec]['template_count']++; } } $sectionRows = array_values(array_map(static function (array $row): array { unset($row['template_ids']); return $row; }, $sectionAgg)); usort($sectionRows, static function (array $a, array $b): int { $aUse = (int) ($a['total_uses'] ?? 0); $bUse = (int) ($b['total_uses'] ?? 0); if ($aUse !== $bUse) { return $bUse <=> $aUse; } return strcasecmp((string) ($a['section_key'] ?? ''), (string) ($b['section_key'] ?? '')); }); return $this->response->setJSON([ 'top_rows' => array_slice($usageRows, 0, 15), 'section_rows' => $sectionRows, 'summary' => [ 'total_uses' => $totalUses, 'used_templates' => count($usedTemplates), ], ]); } public function rx_group_panel() { return view('billing/opd_rx_group_workspace', ['initial_rx_id' => 0]); } public function save_rx_group_edit(int $rxId = 0) { return view('billing/opd_rx_group_workspace', ['initial_rx_id' => max(0, $rxId)]); } public function rx_group_data() { if (! $this->db->tableExists('opd_prescription_template')) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames('opd_prescription_template'); $idField = $this->resolveFirstField($fields, ['id']); $nameField = $this->resolveFirstField($fields, ['rx_group_name']); if ($idField === null || $nameField === null) { return $this->response->setJSON(['rows' => []]); } $userId = max(0, $this->getCurrentUserId()); $hasDocId = in_array('doc_id', $fields, true); $builder = $this->db->table('opd_prescription_template') ->select('MAX(' . $idField . ') as id,' . $nameField . ' as rx_group_name,COUNT(*) as row_count') ->where($nameField . ' <>', ''); if ($hasDocId) { $builder->select('SUM(CASE WHEN doc_id = 0 THEN 1 ELSE 0 END) as global_count', false); $builder->select('SUM(CASE WHEN doc_id = ' . (int) $userId . ' THEN 1 ELSE 0 END) as doctor_count', false); } if ($hasDocId) { $builder->groupStart() ->where('doc_id', $userId) ->orWhere('doc_id', 0) ->groupEnd(); } $rows = $builder ->groupBy($nameField) ->orderBy($nameField, 'ASC') ->limit(500) ->get() ->getResultArray(); foreach ($rows as &$row) { if (! $hasDocId) { $row['access_label'] = 'Global'; continue; } $globalCount = (int) ($row['global_count'] ?? 0); $doctorCount = (int) ($row['doctor_count'] ?? 0); if ($globalCount > 0 && $doctorCount > 0) { $row['access_label'] = 'Global + Doctor'; } elseif ($globalCount > 0) { $row['access_label'] = 'Global'; } elseif ($doctorCount > 0) { $row['access_label'] = 'Doctor'; } else { $row['access_label'] = 'Specific User'; } } unset($row); return $this->response->setJSON(['rows' => $rows]); } public function rx_group_apply_to_session() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $opdId = (int) $this->request->getPost('opd_id'); $sessionId = (int) $this->request->getPost('opd_session_id'); $rxGroupId = (int) $this->request->getPost('rx_group_id'); if ($opdId <= 0 || $rxGroupId <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid OPD or Rx-Group']); } $templateTable = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']); if ($templateTable === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx-Group medicine template not found']); } $sessionTable = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']); if ($sessionTable === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']); } $templateFields = $this->db->getFieldNames($templateTable); if (! in_array('rx_group_id', $templateFields, true)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid Rx-Group template structure']); } $templateRows = $this->db->table($templateTable) ->where('rx_group_id', $rxGroupId) ->orderBy('id', 'ASC') ->get() ->getResultArray(); if (empty($templateRows)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'No medicines found in selected Rx-Group']); } $sessionId = $this->ensurePrescriptionSession($opdId, $sessionId); if ($sessionId <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create prescription session']); } $sessionFields = $this->db->getFieldNames($sessionTable); $user = auth()->user(); $userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User'; $updateByText = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s'); $inserted = 0; $skipped = 0; foreach ($templateRows as $row) { $insert = []; if (in_array('opd_pre_id', $sessionFields, true)) { $insert['opd_pre_id'] = $sessionId; } if (in_array('opd_id', $sessionFields, true)) { $insert['opd_id'] = $opdId; } foreach (['med_id', 'med_name', 'med_type', 'dosage', 'dosage_when', 'dosage_freq', 'dosage_where', 'no_of_days', 'qty', 'remark', 'genericname'] as $field) { if (! in_array($field, $sessionFields, true) || ! array_key_exists($field, $row)) { continue; } $value = $row[$field]; if (in_array($field, ['med_name', 'remark'], true)) { $value = strtoupper((string) $value); } $insert[$field] = $value; } if (in_array('update_by', $sessionFields, true)) { $insert['update_by'] = $updateByText; } if (! empty($insert)) { if ($this->isDuplicateMedicineRow($sessionTable, $sessionFields, $sessionId, $insert)) { $skipped++; continue; } $this->db->table($sessionTable)->insert($insert); $inserted++; $medId = (int) ($insert['med_id'] ?? 0); if ($medId > 0) { $this->trackMedicineUsage($medId); } } } return $this->response->setJSON([ 'update' => 1, 'opd_session_id' => $sessionId, 'inserted_count' => $inserted, 'skipped_count' => $skipped, 'error_text' => $inserted > 0 ? ($inserted . ' medicine(s) added from Rx-Group.' . ($skipped > 0 ? (' ' . $skipped . ' duplicate(s) skipped.') : '')) : ($skipped > 0 ? ($skipped . ' duplicate medicine(s) already present.') : 'No medicine inserted from Rx-Group.'), 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function rx_group_get(int $rxId) { if (! $this->db->tableExists('opd_prescription_template')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group table not found']); } $fields = $this->db->getFieldNames('opd_prescription_template'); $builder = $this->db->table('opd_prescription_template')->where('id', (int) $rxId); if (in_array('doc_id', $fields, true)) { $userId = max(0, $this->getCurrentUserId()); $builder->groupStart() ->where('doc_id', $userId) ->orWhere('doc_id', 0) ->groupEnd(); } $row = $builder->get(1)->getRowArray() ?? []; if (empty($row)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group not found']); } return $this->response->setJSON(['update' => 1, 'row' => $row]); } public function rx_group_save() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } if (! $this->db->tableExists('opd_prescription_template')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group table not found']); } $id = (int) $this->request->getPost('id'); $name = trim((string) $this->request->getPost('rx_group_name')); if ($name === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx-Group Name is required']); } $fields = $this->db->getFieldNames('opd_prescription_template'); $data = []; if (in_array('rx_group_name', $fields, true)) { $data['rx_group_name'] = $name; } if (in_array('complaints', $fields, true)) { $data['complaints'] = trim((string) $this->request->getPost('complaints')); } if (in_array('diagnosis', $fields, true)) { $data['diagnosis'] = trim((string) $this->request->getPost('diagnosis')); } if (in_array('Finding_Examinations', $fields, true)) { $data['Finding_Examinations'] = trim((string) $this->request->getPost('finding_examinations')); } if (in_array('investigation', $fields, true)) { $data['investigation'] = trim((string) $this->request->getPost('investigation')); } $userId = max(0, $this->getCurrentUserId()); if (in_array('rx_group_name', $fields, true)) { $dupBuilder = $this->db->table('opd_prescription_template') ->select('id') ->where('rx_group_name', $name); if (in_array('doc_id', $fields, true)) { $dupBuilder->groupStart() ->where('doc_id', $userId) ->orWhere('doc_id', 0) ->groupEnd(); } if ($id > 0) { $dupBuilder->where('id <>', $id); } $exists = $dupBuilder->orderBy('id', 'DESC')->get(1)->getRowArray(); if (! empty($exists)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Rx-Group name already exists. Please use Edit for existing group.', 'existing_id' => (int) ($exists['id'] ?? 0), 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } } if (in_array('doc_id', $fields, true) && $id <= 0) { $data['doc_id'] = $userId; } $user = auth()->user(); if (in_array('update_by', $fields, true)) { $userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User'; $data['update_by'] = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s'); } if ($id > 0) { $this->db->table('opd_prescription_template')->where('id', $id)->update($data); $savedId = $id; $message = 'Rx-Group updated'; } else { $this->db->table('opd_prescription_template')->insert($data); $savedId = (int) $this->db->insertID(); $message = 'Rx-Group saved'; } return $this->response->setJSON([ 'update' => 1, 'insertid' => $savedId, 'error_text' => $message, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function rx_group_medicine(int $rxId) { $rxGroupId = max(0, $rxId); $rxGroupName = ''; if ($rxGroupId > 0 && $this->db->tableExists('opd_prescription_template')) { $templateFields = $this->db->getFieldNames('opd_prescription_template'); $nameField = $this->resolveFirstField($templateFields, ['rx_group_name']); if ($nameField !== null) { $row = $this->db->table('opd_prescription_template') ->select($nameField . ' as rx_group_name') ->where('id', $rxGroupId) ->get(1) ->getRowArray(); $rxGroupName = trim((string) ($row['rx_group_name'] ?? '')); } } return view('billing/rx_group_medicine_workspace', [ 'rx_group_id' => $rxGroupId, 'rx_group_name' => $rxGroupName, ]); } public function rx_group_medicine_list(int $rxId) { $table = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']); if ($table === null) { return $this->response->setJSON(['rows' => [], 'rx_group_name' => '']); } $fields = $this->db->getFieldNames($table); if (! in_array('rx_group_id', $fields, true)) { return $this->response->setJSON(['rows' => [], 'rx_group_name' => '']); } $rxGroupName = ''; if ($this->db->tableExists('opd_prescription_template')) { $templateFields = $this->db->getFieldNames('opd_prescription_template'); $nameField = $this->resolveFirstField($templateFields, ['rx_group_name']); if ($nameField !== null) { $groupRow = $this->db->table('opd_prescription_template') ->select($nameField . ' as rx_group_name') ->where('id', (int) $rxId) ->get(1) ->getRowArray(); $rxGroupName = trim((string) ($groupRow['rx_group_name'] ?? '')); } } $builder = $this->db->table($table)->where('rx_group_id', (int) $rxId); if (in_array('id', $fields, true)) { $builder->orderBy('id', 'DESC'); } $rows = $builder->get()->getResultArray(); if (empty($rows)) { return $this->response->setJSON(['rows' => [], 'rx_group_name' => $rxGroupName]); } $medGenericMap = []; $missingGenericMedIds = []; foreach ($rows as $row) { $existingGeneric = trim((string) ($row['genericname'] ?? '')); $medId = (int) ($row['med_id'] ?? 0); if ($existingGeneric === '' && $medId > 0) { $missingGenericMedIds[$medId] = true; } } if (! empty($missingGenericMedIds)) { $masterTable = $this->findExistingTable(['opd_med_master']); if ($masterTable !== null) { $masterFields = $this->db->getFieldNames($masterTable); $masterIdField = $this->resolveFirstField($masterFields, ['id']); $masterGenericField = $this->resolveFirstField($masterFields, ['genericname', 'generic_name']); $masterSaltField = $this->resolveFirstField($masterFields, ['salt_name', 'sal_name', 'salt', 'saltname']); // Prefer genericname (Generic Name) over salt_name (Substitute) $labelField = $masterGenericField ?? $masterSaltField; 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'] ?? '')) === '') { // Do NOT fall back to salt_name — that field stores Substitute (other brand names), // not the generic/salt composition. Use master lookup only. $fallback = $medGenericMap[(int) ($row['med_id'] ?? 0)] ?? ''; if ($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_delete(int $rxId) { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $rxId = max(0, $rxId); if ($rxId <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid Rx-Group ID']); } if (! $this->db->tableExists('opd_prescription_template')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group table not found']); } // Delete linked medicines first $medTable = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']); if ($medTable !== null) { $medFields = $this->db->getFieldNames($medTable); $fkField = $this->resolveFirstField($medFields, ['opd_prescription_template_id', 'rx_group_id', 'template_id']); if ($fkField !== null) { $this->db->table($medTable)->where($fkField, $rxId)->delete(); } } $this->db->table('opd_prescription_template')->where('id', $rxId)->delete(); return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Rx-Group deleted', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } 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'; } $query = $this->request->getGet(); $isDataTableRequest = is_array($query) && array_key_exists('draw', $query); if ($isDataTableRequest) { $draw = max(1, (int) ($query['draw'] ?? 1)); $start = max(0, (int) ($query['start'] ?? 0)); $length = (int) ($query['length'] ?? 25); if ($length < 1 || $length > 500) { $length = 25; } $searchValue = ''; if (isset($query['search']) && is_array($query['search'])) { $searchValue = trim((string) ($query['search']['value'] ?? '')); } $orderField = 'use_count'; $orderDir = 'desc'; $allowedOrderFields = ['id', 'item_name', 'use_count', 'last_used_at', 'genericname', 'company_name']; if (isset($query['order'][0]) && is_array($query['order'][0])) { $orderColIndex = (int) ($query['order'][0]['column'] ?? -1); $orderDirCandidate = strtolower(trim((string) ($query['order'][0]['dir'] ?? 'desc'))); if (in_array($orderDirCandidate, ['asc', 'desc'], true)) { $orderDir = $orderDirCandidate; } if ($orderColIndex >= 0 && isset($query['columns'][$orderColIndex]) && is_array($query['columns'][$orderColIndex])) { $candidate = trim((string) ($query['columns'][$orderColIndex]['data'] ?? '')); if (in_array($candidate, $allowedOrderFields, true)) { $orderField = $candidate; } } } $rows = $this->getOpdMedicineRows($filter, $scope, $showAll); $recordsTotal = count($rows); if ($searchValue !== '') { $needle = mb_strtolower($searchValue); $rows = array_values(array_filter($rows, static function (array $row) use ($needle): bool { $haystack = [ (string) ($row['id'] ?? ''), (string) ($row['item_name'] ?? ''), (string) ($row['genericname'] ?? ''), (string) ($row['company_name'] ?? ''), (string) ($row['last_used_at'] ?? ''), ]; foreach ($haystack as $value) { if (mb_stripos($value, $needle) !== false) { return true; } } return false; })); } $recordsFiltered = count($rows); usort($rows, static function (array $a, array $b) use ($orderField, $orderDir): int { $numericFields = ['id', 'use_count']; if (in_array($orderField, $numericFields, true)) { $cmp = ((int) ($a[$orderField] ?? 0)) <=> ((int) ($b[$orderField] ?? 0)); } else { $cmp = strcasecmp((string) ($a[$orderField] ?? ''), (string) ($b[$orderField] ?? '')); } if ($cmp === 0) { $cmp = strcasecmp((string) ($a['item_name'] ?? ''), (string) ($b['item_name'] ?? '')); } return $orderDir === 'desc' ? -$cmp : $cmp; }); $pageRows = array_slice($rows, $start, $length); return $this->response->setJSON([ 'draw' => $draw, 'recordsTotal' => $recordsTotal, 'recordsFiltered' => $recordsFiltered, 'data' => $pageRows, 'active_filter' => $filter, ]); } 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); } public function opd_medicince_coding_summary() { $rows = $this->getOpdMedicineRows('all', 'all', true); $summary = [ 'total' => 0, 'coding_missing' => 0, 'snomed_missing' => 0, 'atc_missing' => 0, 'both_missing' => 0, ]; foreach ($rows as $row) { $summary['total']++; $snomed = strtoupper(trim((string) ($row['snomed_code'] ?? ''))); $atc = strtoupper(trim((string) ($row['atc_code'] ?? ''))); $isSnomedMissing = ($snomed === ''); $isAtcMissing = ($atc === ''); if ($isSnomedMissing) { $summary['snomed_missing']++; } if ($isAtcMissing) { $summary['atc_missing']++; } if ($isSnomedMissing || $isAtcMissing) { $summary['coding_missing']++; } if ($isSnomedMissing && $isAtcMissing) { $summary['both_missing']++; } } return $this->response->setJSON([ 'update' => 1, 'summary' => $summary, ]); } 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']); $snomedField = $this->resolveFirstField($fields, ['snomed_code', 'medicine_snomed_code', 'snomed_ct_code', 'sctid']); $atcField = $this->resolveFirstField($fields, ['atc_code', 'who_atc_code']); 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'; } if ($snomedField !== null) { $select .= ',' . $snomedField . ' as snomed_code'; } if ($atcField !== null) { $select .= ',' . $atcField . ' as atc_code'; } $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); })); } elseif ($filter === 'coding_missing') { $rows = array_values(array_filter($rows, static function (array $row): bool { $snomed = strtoupper(trim((string) ($row['snomed_code'] ?? ''))); $atc = strtoupper(trim((string) ($row['atc_code'] ?? ''))); return $snomed === '' || $atc === ''; })); } elseif ($filter === 'snomed_missing') { $rows = array_values(array_filter($rows, static function (array $row): bool { $snomed = strtoupper(trim((string) ($row['snomed_code'] ?? ''))); return $snomed === ''; })); } elseif ($filter === 'atc_missing') { $rows = array_values(array_filter($rows, static function (array $row): bool { $atc = strtoupper(trim((string) ($row['atc_code'] ?? ''))); return $atc === ''; })); } 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); $candidateIds = []; foreach ($rows as $row) { $medId = (int) ($row['id'] ?? 0); if ($medId > 0) { $candidateIds[] = $medId; } } $candidateIds = array_values(array_unique($candidateIds)); $favoriteMap = $userId > 0 ? $this->getUserFavoriteMedicineIds($userId, $candidateIds) : []; $usageMap = $userId > 0 ? $this->getUserMedicineUsageMap($userId, $candidateIds) : []; 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')); $formulation = trim((string) $this->request->getPost('formulation')); if ($itemName === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']); } $medicineModel = new OpdMedicineModel($this->db); $duplicate = $medicineModel->findDuplicateByName($itemName, $formulation, $id); if (! empty($duplicate)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Duplicate medicine name with same formulation 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']); $snomedField = $this->resolveFirstField($fields, ['snomed_code', 'medicine_snomed_code', 'snomed_ct_code', 'sctid']); $atcField = $this->resolveFirstField($fields, ['atc_code', 'who_atc_code']); $data = []; if (in_array('item_name', $fields, true)) { $data['item_name'] = $itemName; } if (in_array('formulation', $fields, true)) { $data['formulation'] = $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 ($snomedField !== null) { $data[$snomedField] = trim((string) $this->request->getPost('snomed_code')); } if ($atcField !== null) { $data[$atcField] = strtoupper(trim((string) $this->request->getPost('atc_code'))); } foreach (['dosage', 'dosage_when', 'dosage_freq', 'dosage_where', 'no_of_days', 'qty', 'remark'] as $dosageField) { if (in_array($dosageField, $fields, true)) { $data[$dosageField] = trim((string) $this->request->getPost($dosageField)); } } if ($id > 0) { $this->db->table($table)->where('id', $id)->update($data); $savedId = $id; $message = 'Medicine updated'; } else { $this->db->table($table)->insert($data); $savedId = (int) $this->db->insertID(); $message = 'Medicine added'; } return $this->response->setJSON([ 'update' => 1, 'insertid' => $savedId, 'error_text' => $message, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_medicince_ai_details() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $name = trim((string) $this->request->getPost('item_name')); $formulation = trim((string) $this->request->getPost('formulation')); if ($name === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']); } $local = $this->lookupMedicineDetailsFromMaster($name, $formulation); if (($local['genericname'] ?? '') !== '' || ($local['salt_name'] ?? '') !== '') { return $this->response->setJSON([ 'update' => 1, 'provider' => 'local-master', 'match_confidence' => (string) ($local['match_confidence'] ?? 'partial'), 'matched_item_name' => (string) ($local['matched_item_name'] ?? ''), 'details' => [ 'genericname' => trim((string) ($local['genericname'] ?? '')), 'salt_name' => trim((string) ($local['salt_name'] ?? '')), 'dosage_restriction' => trim((string) ($local['dosage_restriction'] ?? '')), 'caution_note' => trim((string) ($local['caution_note'] ?? '')), ], 'error_text' => 'Medicine details found from local master data', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $serverDetails = $this->fetchMedicineDetailsFromAiServer($name, $formulation); if (($serverDetails['genericname'] ?? '') !== '' || ($serverDetails['salt_name'] ?? '') !== '') { return $this->response->setJSON([ 'update' => 1, 'provider' => 'ai-server', 'match_confidence' => 'ai-server', 'matched_item_name' => '', 'details' => [ 'genericname' => trim((string) ($serverDetails['genericname'] ?? '')), 'salt_name' => trim((string) ($serverDetails['salt_name'] ?? '')), 'dosage_restriction' => trim((string) ($serverDetails['dosage_restriction'] ?? '')), 'caution_note' => trim((string) ($serverDetails['caution_note'] ?? '')), ], 'error_text' => 'Medicine details prepared from AI server', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $prompt = "You are a medicine data enrichment assistant for OPD master data in India.\n" . "For this medicine, provide structured details based on public medical references and standard labeling guidance.\n" . "Medicine Name: {$name}\n" . "Formulation: {$formulation}\n" . "Return ONLY valid JSON object with keys: genericname, salt_name, dosage_restriction, caution_note.\n" . "Use short plain text values. If unsure for a field, keep empty string."; $this->lastAiProvider = 'none'; $aiText = $this->generateClinicalTextWithAi($prompt, 280); if ($aiText === null || trim($aiText) === '') { $fallback = $this->inferMedicineDetailsFromBrandName($name, $formulation); if (($fallback['genericname'] ?? '') !== '' || ($fallback['salt_name'] ?? '') !== '') { return $this->response->setJSON([ 'update' => 1, 'provider' => 'local-brand-fallback', 'match_confidence' => 'brand-fallback', 'matched_item_name' => '', 'details' => [ 'genericname' => trim((string) ($fallback['genericname'] ?? '')), 'salt_name' => trim((string) ($fallback['salt_name'] ?? '')), 'dosage_restriction' => trim((string) ($fallback['dosage_restriction'] ?? '')), 'caution_note' => trim((string) ($fallback['caution_note'] ?? '')), ], 'error_text' => 'AI unavailable. Fallback generic/salt suggestion prepared.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $aiServerHint = $this->lastAiServerError !== '' ? (' AI-server attempts: ' . $this->lastAiServerError) : ''; return $this->response->setJSON([ 'update' => 0, 'error_text' => 'AI unavailable and no local match found. Check AI settings (DIAGNOSIS_AI_SERVER_URL or DIAGNOSIS_AI_MEDICINE_ENDPOINT).' . $aiServerHint, ]); } $parsed = $this->parseJsonFromAiResponse($aiText); if ($parsed === null) { $fallback = $this->inferMedicineDetailsFromBrandName($name, $formulation); if (($fallback['genericname'] ?? '') !== '' || ($fallback['salt_name'] ?? '') !== '') { return $this->response->setJSON([ 'update' => 1, 'provider' => 'local-brand-fallback', 'match_confidence' => 'brand-fallback', 'matched_item_name' => '', 'details' => [ 'genericname' => trim((string) ($fallback['genericname'] ?? '')), 'salt_name' => trim((string) ($fallback['salt_name'] ?? '')), 'dosage_restriction' => trim((string) ($fallback['dosage_restriction'] ?? '')), 'caution_note' => trim((string) ($fallback['caution_note'] ?? '')), ], 'error_text' => 'AI parse failed. Fallback generic/salt suggestion prepared.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } return $this->response->setJSON(['update' => 0, 'error_text' => 'AI response parse failed. Try again.']); } return $this->response->setJSON([ 'update' => 1, 'provider' => $this->lastAiProvider, 'match_confidence' => 'ai', 'matched_item_name' => '', 'details' => [ 'genericname' => trim((string) ($parsed['genericname'] ?? '')), 'salt_name' => trim((string) ($parsed['salt_name'] ?? '')), 'dosage_restriction' => trim((string) ($parsed['dosage_restriction'] ?? '')), 'caution_note' => trim((string) ($parsed['caution_note'] ?? '')), ], 'error_text' => 'AI details prepared', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } /** * @return array */ private function fetchMedicineDetailsFromAiServer(string $itemName, string $formulation): array { $this->lastAiServerError = ''; $itemName = trim($itemName); if ($itemName === '') { return []; } $configuredEndpointsRaw = trim($this->readAiSettingValue([ 'DIAGNOSIS_AI_MEDICINE_ENDPOINT', 'DIAGNOSIS_AI_MEDICINE_DETAILS_ENDPOINT', 'AI_SERVER_MEDICINE_ENDPOINT', ])); $token = trim($this->readAiSettingValue(['DIAGNOSIS_AI_PARSE_TOKEN', 'AI_SERVER_TOKEN'])); if ($token === '') { $token = trim((string) getenv('DIAGNOSIS_AI_PARSE_TOKEN')); } if ($token === '') { $token = trim((string) getenv('AI_SERVER_TOKEN')); } $payload = [ 'item_name' => $itemName, 'medicine_name' => $itemName, 'name' => $itemName, 'formulation' => trim($formulation), 'mode' => 'medicine-details', ]; $headers = [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ]; if ($token !== '') { $headers['Authorization'] = 'Bearer ' . $token; } $baseCandidates = $this->resolveAiServerBaseCandidates(); $defaultPaths = [ '/medicine-details', '/medicine/details', '/medicine/enrich', '/medicine/lookup', '/api/medicine/details', '/api/medicine/enrich', '/api/medicine/lookup', '/api/v1/medicine/details', '/api/v1/medicine/enrich', '/api/v1/medicine/lookup', '/v1/medicine/details', '/v1/medicine/enrich', '/v1/medicine/lookup', ]; $endpointCandidates = []; if ($configuredEndpointsRaw !== '') { $parts = preg_split('/[\r\n,]+/', $configuredEndpointsRaw) ?: []; foreach ($parts as $part) { $candidate = trim((string) $part); if ($candidate === '') { continue; } // Normalize accidental Windows-style slashes from settings values. $candidate = str_replace('\\', '/', $candidate); if (preg_match('#^https?://#i', $candidate) === 1) { $endpointCandidates[] = rtrim($candidate, '/'); continue; } $path = '/' . ltrim($candidate, '/'); foreach ($baseCandidates as $base) { $endpointCandidates[] = rtrim($base, '/') . $path; } } } foreach ($baseCandidates as $base) { foreach ($defaultPaths as $path) { $endpointCandidates[] = rtrim($base, '/') . $path; } } $endpointCandidates = array_values(array_unique($endpointCandidates)); $endpointCandidates = array_values(array_filter($endpointCandidates, static function ($url): bool { $text = (string) $url; if ($text === '') { return false; } $lower = strtolower($text); if (strpos($text, '\\') !== false || strpos($lower, '%5c') !== false) { return false; } return true; })); $statusTrail = []; foreach ($endpointCandidates as $endpointUrl) { if (strpos($endpointUrl, '\\') !== false || strpos(strtolower($endpointUrl), '%5c') !== false) { $statusTrail[] = 'skipped-malformed @ ' . $endpointUrl; continue; } try { $options = [ 'timeout' => 20, 'http_errors' => false, ]; if (defined('ENVIRONMENT') && in_array((string) ENVIRONMENT, ['development', 'testing'], true)) { $options['verify'] = false; } $client = service('curlrequest', $options); $response = $client->post($endpointUrl, [ 'headers' => $headers, 'json' => $payload, ]); $status = (int) $response->getStatusCode(); $statusTrail[] = $status . ' @ ' . $endpointUrl; if ($status < 200 || $status >= 300) { continue; } $decoded = json_decode((string) $response->getBody(), true); if (!is_array($decoded)) { continue; } $details = is_array($decoded['details'] ?? null) ? $decoded['details'] : $decoded; $data = is_array($decoded['data'] ?? null) ? $decoded['data'] : []; $generic = trim((string) ( $details['genericname'] ?? $details['generic_name'] ?? $data['genericname'] ?? $data['generic_name'] ?? $data['medicine_name'] ?? '' )); $salt = trim((string) ( $details['salt_name'] ?? $details['salt'] ?? $data['salt_name'] ?? $data['salt'] ?? '' )); if ($salt === '' && is_array($data['brand_names'] ?? null)) { $salt = trim(implode(', ', array_slice(array_map('strval', $data['brand_names']), 0, 3))); } $dosage = trim((string) ( $details['dosage_restriction'] ?? $details['restriction'] ?? $data['dosage_restriction'] ?? $data['restriction'] ?? ((is_array($data['dosage'] ?? null) ? ($data['dosage']['adult_dose'] ?? '') : '')) ?? '' )); $caution = trim((string) ( $details['caution_note'] ?? $details['caution'] ?? $data['caution_note'] ?? $data['caution'] ?? '' )); if ($caution === '') { $pieces = []; if (is_array($data['contraindications'] ?? null) && !empty($data['contraindications'])) { $pieces[] = 'Contra: ' . implode(', ', array_slice(array_map('strval', $data['contraindications']), 0, 2)); } if (is_array($data['precautions'] ?? null) && !empty($data['precautions'])) { $pieces[] = 'Prec: ' . implode(', ', array_slice(array_map('strval', $data['precautions']), 0, 2)); } if (is_array($data['drug_interactions'] ?? null) && !empty($data['drug_interactions'])) { $pieces[] = 'Interacts: ' . implode(', ', array_slice(array_map('strval', $data['drug_interactions']), 0, 2)); } $caution = trim(implode(' | ', $pieces)); } $out = [ 'genericname' => $generic, 'salt_name' => $salt, 'dosage_restriction' => $this->formatMedicineNote($dosage, 420), 'caution_note' => $this->formatMedicineNote($caution, 520), ]; if ($out['genericname'] !== '' || $out['salt_name'] !== '') { $this->lastAiProvider = 'ai-server'; $this->lastAiServerError = ''; return $out; } $statusTrail[] = 'empty-payload @ ' . $endpointUrl; } catch (\Throwable $e) { $statusTrail[] = 'error @ ' . $endpointUrl . ' :: ' . $e->getMessage(); } } if (! empty($statusTrail)) { $this->lastAiServerError = implode(' | ', array_slice($statusTrail, 0, 5)); } return []; } private function formatMedicineNote(string $value, int $maxLen = 500): string { $text = trim($value); if ($text === '') { return ''; } // Normalize separators for UI readability while preserving meaning. $text = str_replace(["\r\n", "\n", "\r"], ' | ', $text); $text = preg_replace('/\s*\|\s*/', ' | ', $text) ?? $text; $text = preg_replace('/\s+/', ' ', $text) ?? $text; $text = trim($text, " \t\n\r\0\x0B|"); if ($maxLen < 80) { $maxLen = 80; } if (mb_strlen($text) <= $maxLen) { return $text; } $short = mb_substr($text, 0, $maxLen); $lastSep = mb_strrpos($short, ' | '); if ($lastSep !== false && $lastSep > (int) ($maxLen * 0.6)) { $short = mb_substr($short, 0, $lastSep); } return rtrim($short, " ,;|.") . '...'; } /** * @return array */ private function lookupMedicineDetailsFromMaster(string $name, string $formulation = ''): array { $table = $this->findExistingTable(['opd_med_master']); if ($table === null) { return []; } $fields = $this->db->getFieldNames($table); $nameField = $this->resolveFirstField($fields, ['item_name', 'med_name']); $formField = $this->resolveFirstField($fields, ['formulation', 'med_type']); $genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']); $saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']); $restrictionField = $this->resolveFirstField($fields, ['dosage_restriction', 'dose_restriction', 'restriction_note', 'restriction']); if ($nameField === null || ($genericField === null && $saltField === null)) { return []; } $name = trim($name); if ($name === '') { return []; } $exact = $this->db->table($table) ->where('UPPER(' . $nameField . ')', strtoupper($name)); if ($formField !== null && trim($formulation) !== '') { $exact->where('UPPER(' . $formField . ')', strtoupper(trim($formulation))); } $row = $exact->get(1)->getRowArray() ?? []; $matchConfidence = 'partial'; if (empty($row)) { $builder = $this->db->table($table); $builder->groupStart() ->like($nameField, $name) ->orLike($nameField, preg_replace('/\s+/', ' ', $name) ?? $name) ->groupEnd(); if ($formField !== null && trim($formulation) !== '') { $builder->like($formField, trim($formulation)); } $candidates = $builder->limit(15)->get()->getResultArray(); if (! empty($candidates)) { $target = strtoupper($name); usort($candidates, static function (array $a, array $b) use ($nameField, $target): int { $aName = strtoupper(trim((string) ($a[$nameField] ?? ''))); $bName = strtoupper(trim((string) ($b[$nameField] ?? ''))); $aScore = 0; $bScore = 0; if ($aName === $target) { $aScore += 1000; } elseif (str_starts_with($aName, $target)) { $aScore += 600; } elseif (str_contains($aName, $target)) { $aScore += 400; } if ($bName === $target) { $bScore += 1000; } elseif (str_starts_with($bName, $target)) { $bScore += 600; } elseif (str_contains($bName, $target)) { $bScore += 400; } if ($aScore !== $bScore) { return $bScore <=> $aScore; } return strlen($aName) <=> strlen($bName); }); $row = $candidates[0] ?? []; $picked = strtoupper(trim((string) ($row[$nameField] ?? ''))); $target = strtoupper($name); if ($picked === $target) { $matchConfidence = 'exact'; } elseif (str_starts_with($picked, $target) || str_contains($picked, $target)) { $matchConfidence = 'close'; } } } else { $matchConfidence = 'exact'; } if (empty($row)) { return []; } return [ 'genericname' => $genericField !== null ? trim((string) ($row[$genericField] ?? '')) : '', 'salt_name' => $saltField !== null ? trim((string) ($row[$saltField] ?? '')) : '', 'dosage_restriction' => $restrictionField !== null ? trim((string) ($row[$restrictionField] ?? '')) : '', 'caution_note' => '', 'match_confidence' => $matchConfidence, 'matched_item_name' => trim((string) ($row[$nameField] ?? '')), ]; } /** * @return array */ private function inferMedicineDetailsFromBrandName(string $name, string $formulation = ''): array { $text = strtoupper(trim($name)); if ($text === '') { return []; } preg_match('/(\d{2,4})\s*MG/i', $name, $m); $strength = isset($m[1]) ? trim((string) $m[1]) : ''; $map = [ 'DOLO' => 'Paracetamol', 'CROCIN' => 'Paracetamol', 'CALPOL' => 'Paracetamol', 'PARACIP' => 'Paracetamol', 'PANTOCID' => 'Pantoprazole', 'PAN' => 'Pantoprazole', 'AZEE' => 'Azithromycin', 'AZTOR' => 'Atorvastatin', 'MONTAIR' => 'Montelukast', 'LEVOCET' => 'Levocetirizine', 'AUGMENTIN' => 'Amoxicillin + Clavulanic Acid', ]; $generic = ''; foreach ($map as $brand => $gen) { if (str_contains($text, $brand)) { $generic = $gen; break; } } if ($generic === '') { return []; } $salt = $generic; if ($strength !== '') { if ($generic === 'Amoxicillin + Clavulanic Acid' && $strength === '625') { $salt = 'Amoxicillin 500 mg + Clavulanic Acid 125 mg'; } else { $salt = $generic . ' ' . $strength . ' mg'; } } $doseRestriction = ''; if ($generic === 'Paracetamol') { $doseRestriction = 'Adults: usually max 4 g/day total from all paracetamol-containing products.'; } elseif ($generic === 'Pantoprazole') { $doseRestriction = 'Usually once daily before food unless specifically advised otherwise.'; } elseif ($generic === 'Azithromycin') { $doseRestriction = 'Follow fixed daily schedule and complete full course.'; } return [ 'genericname' => $generic, 'salt_name' => $salt, 'dosage_restriction' => $doseRestriction, 'caution_note' => '', ]; } public function opd_medicince_remove(int $medId) { $table = $this->findExistingTable(['opd_med_master']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']); } $this->db->table($table)->where('id', (int) $medId)->delete(); return $this->response->setJSON(['update' => 1, 'error_text' => 'Medicine removed']); } public function opd_advice() { return view('billing/opd_advice_workspace', ['initial_advice_id' => 0]); } public function opd_advice_edit(int $adviceId) { return view('billing/opd_advice_workspace', ['initial_advice_id' => max(0, $adviceId)]); } public function opd_advice_data() { if (! $this->db->tableExists('opd_advice')) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames('opd_advice'); $idField = $this->resolveFirstField($fields, ['id', 'Code']); $adviceField = $this->resolveFirstField($fields, ['advice', 'Name', 'advice_txt']); $hindiField = $this->resolveFirstField($fields, ['advice_hindi', 'advice_local', 'hindi_advice', 'advice_hin']); if ($idField === null || $adviceField === null) { return $this->response->setJSON(['rows' => []]); } $select = $idField . ' as id,' . $adviceField . ' as advice'; if ($hindiField !== null) { $select .= ',' . $hindiField . ' as advice_hindi'; } $rows = $this->db->table('opd_advice') ->select($select) ->orderBy($adviceField, 'ASC') ->limit(5000) ->get() ->getResultArray(); return $this->response->setJSON(['rows' => $rows]); } public function opd_advice_get(int $adviceId) { if (! $this->db->tableExists('opd_advice')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice table not found']); } $fields = $this->db->getFieldNames('opd_advice'); $idField = $this->resolveFirstField($fields, ['id', 'Code']); if ($idField === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice ID field not found']); } $row = $this->db->table('opd_advice')->where($idField, (int) $adviceId)->get(1)->getRowArray() ?? []; if (empty($row)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice not found']); } return $this->response->setJSON(['update' => 1, 'row' => $row]); } public function opd_advice_save() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } if (! $this->db->tableExists('opd_advice')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice table not found']); } $fields = $this->db->getFieldNames('opd_advice'); $idField = $this->resolveFirstField($fields, ['id', 'Code']); $adviceField = $this->resolveFirstField($fields, ['advice', 'Name', 'advice_txt']); $hindiField = $this->resolveFirstField($fields, ['advice_hindi', 'advice_local', 'hindi_advice', 'advice_hin']); if ($idField === null || $adviceField === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice fields not found']); } $id = (int) $this->request->getPost('id'); $adviceText = trim((string) $this->request->getPost('advice')); $adviceHindi = trim((string) $this->request->getPost('advice_hindi')); if ($adviceText === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice text is required']); } $data = []; $data[$adviceField] = $adviceText; if ($hindiField !== null) { $data[$hindiField] = $adviceHindi; } if ($id > 0) { $this->db->table('opd_advice')->where($idField, $id)->update($data); $savedId = $id; $message = 'Advice updated'; } else { $this->db->table('opd_advice')->insert($data); $savedId = (int) $this->db->insertID(); $message = 'Advice added'; } return $this->response->setJSON([ 'update' => 1, 'insertid' => $savedId, 'error_text' => $message, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_advice_translate_ai() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $sourceText = trim((string) $this->request->getPost('advice')); $targetLang = strtolower(trim((string) $this->request->getPost('target_lang'))); if ($sourceText === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'English advice text is required']); } $supportedLanguages = [ 'hi' => 'Hindi', 'bn' => 'Bengali', 'mr' => 'Marathi', 'ta' => 'Tamil', 'te' => 'Telugu', 'gu' => 'Gujarati', 'kn' => 'Kannada', 'ml' => 'Malayalam', 'pa' => 'Punjabi', 'or' => 'Odia', 'as' => 'Assamese', 'ur' => 'Urdu', ]; if (! isset($supportedLanguages[$targetLang])) { $targetLang = 'hi'; } $targetLanguageName = $supportedLanguages[$targetLang]; $this->lastAiProvider = 'none'; $translatedText = $this->translateAdviceWithAiServer($sourceText, $targetLang); if ($translatedText === '') { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'AI server unavailable. Check DIAGNOSIS_AI_SERVER_URL and token settings.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } return $this->response->setJSON([ 'update' => 1, 'provider' => 'ai-server', 'target_lang' => $targetLang, 'target_language_name' => $targetLanguageName, 'translated_text' => $translatedText, 'error_text' => 'Advice translated', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_advice_remove(int $adviceId) { if (! $this->db->tableExists('opd_advice')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice table not found']); } $fields = $this->db->getFieldNames('opd_advice'); $idField = $this->resolveFirstField($fields, ['id', 'Code']); if ($idField === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice ID field not found']); } $this->db->table('opd_advice')->where($idField, (int) $adviceId)->delete(); return $this->response->setJSON(['update' => 1, 'error_text' => 'Advice removed']); } // ─── Investigation Master CRUD ───────────────────────────────────────── public function opd_invest_master() { $doctorModel = new \App\Models\DoctorModel($this->db); return view('billing/opd_invest_master_workspace', [ 'initial_inv_id' => 0, 'med_specs' => $doctorModel->getMedSpecs(), ]); } public function opd_invest_master_specs() { $specs = []; if ($this->db->tableExists('med_spec')) { $specs = $this->db->table('med_spec') ->select('id, SpecName') ->orderBy('SpecName', 'ASC') ->get()->getResultArray(); } return $this->response->setJSON(['specs' => $specs]); } public function opd_invest_master_data() { if (! $this->db->tableExists('investigation')) { return $this->response->setJSON(['draw' => 1, 'recordsTotal' => 0, 'recordsFiltered' => 0, 'data' => []]); } $fields = $this->db->getFieldNames('investigation'); $keyField = $this->resolveFirstField($fields, ['id', 'Code', 'code']); $codeField = $this->resolveFirstField($fields, ['Code', 'code']); $nameField = $this->resolveFirstField($fields, ['Name', 'name']); if ($keyField === null || $codeField === null || $nameField === null) { return $this->response->setJSON(['draw' => 1, 'recordsTotal' => 0, 'recordsFiltered' => 0, 'data' => []]); } $filter = trim((string) $this->request->getGet('filter')); $specId = (int) $this->request->getGet('spec_id'); $favOnly = (int) $this->request->getGet('fav_only'); $draw = (int) $this->request->getGet('draw'); $start = max(0, (int) $this->request->getGet('start')); $length = (int) $this->request->getGet('length'); if ($length <= 0) { $length = 25; } $selectCols = $keyField . ' as id, ' . $codeField . ' as code, ' . $nameField . ' as name'; foreach (['short_name', 'is_favourite', 'spec_ids', 'category_name'] as $f) { if (in_array($f, $fields, true)) { $selectCols .= ', ' . $f; } } $applyFilters = function ($b) use ($filter, $favOnly, $specId, $fields, $nameField, $codeField) { if ($filter !== '') { $b->groupStart() ->like($nameField, $filter) ->orLike($codeField, $filter); if (in_array('short_name', $fields, true)) { $b->orLike('short_name', $filter); } $b->groupEnd(); } if ($favOnly && in_array('is_favourite', $fields, true)) { $b->where('is_favourite', 1); } if ($specId > 0 && in_array('spec_ids', $fields, true)) { $b->where("FIND_IN_SET({$specId}, spec_ids) > 0", null, false); } }; $total = $this->db->table('investigation')->countAllResults(); $countBuilder = $this->db->table('investigation'); $applyFilters($countBuilder); $filtered = $countBuilder->countAllResults(); $builder = $this->db->table('investigation')->select($selectCols); $applyFilters($builder); if (in_array('is_favourite', $fields, true)) { $builder->orderBy('is_favourite', 'DESC'); } $builder->orderBy($nameField, 'ASC'); $builder->limit($length, $start); $rows = $builder->get()->getResultArray(); return $this->response->setJSON([ 'draw' => $draw, 'recordsTotal' => $total, 'recordsFiltered' => $filtered, 'data' => $rows, ]); } public function opd_invest_master_get(int $invId) { if ($invId <= 0 || ! $this->db->tableExists('investigation')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Not found']); } $fields = $this->db->getFieldNames('investigation'); $keyField = $this->resolveFirstField($fields, ['id', 'Code', 'code']); $codeField = $this->resolveFirstField($fields, ['Code', 'code']); $nameField = $this->resolveFirstField($fields, ['Name', 'name']); if ($keyField === null || $codeField === null || $nameField === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid table structure']); } $row = $this->db->table('investigation')->where($keyField, $invId)->limit(1)->get()->getRowArray(); if (! $row) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Not found']); } return $this->response->setJSON([ 'update' => 1, 'row' => [ 'id' => (int) ($row[$keyField] ?? 0), 'code' => (string) ($row[$codeField ?? 'code'] ?? ''), 'name' => (string) ($row[$nameField ?? 'name'] ?? ''), 'short_name' => (string) ($row['short_name'] ?? ''), 'category_name' => (string) ($row['category_name'] ?? ''), 'is_favourite' => (int) ($row['is_favourite'] ?? 0), 'spec_ids' => (string) ($row['spec_ids'] ?? ''), ], 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_invest_master_save() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0]); } if (! $this->db->tableExists('investigation')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation table not found']); } $id = (int) $this->request->getPost('id'); $name = trim((string) $this->request->getPost('name')); if ($name === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation name is required']); } $fields = $this->db->getFieldNames('investigation'); $keyField = $this->resolveFirstField($fields, ['id', 'Code', 'code']); $codeField = $this->resolveFirstField($fields, ['Code', 'code']); $nameField = $this->resolveFirstField($fields, ['Name', 'name']); if ($keyField === null || $codeField === null || $nameField === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid table structure']); } $code = trim((string) $this->request->getPost('code')); if ($code === '') { $code = strtoupper(preg_replace('/[^A-Z0-9]/i', '', substr($name, 0, 8))) . rand(100, 999); } $data = [ $nameField => $name, $codeField => $code, ]; foreach (['short_name', 'category_name', 'spec_ids'] as $f) { if (in_array($f, $fields, true)) { $data[$f] = trim((string) $this->request->getPost($f)); } } if (in_array('is_favourite', $fields, true)) { $data['is_favourite'] = ((int) $this->request->getPost('is_favourite')) ? 1 : 0; } if ($id > 0) { $this->db->table('investigation')->where($keyField, $id)->update($data); $savedId = $id; $message = 'Investigation updated'; } else { $this->db->table('investigation')->insert($data); $savedId = (int) $this->db->insertID(); $message = 'Investigation added'; } return $this->response->setJSON([ 'update' => 1, 'insertid' => $savedId, 'error_text' => $message, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_invest_master_remove(int $invId) { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0]); } if ($invId <= 0 || ! $this->db->tableExists('investigation')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Not found']); } $fields = $this->db->getFieldNames('investigation'); $keyField = $this->resolveFirstField($fields, ['id', 'Code', 'code']); if ($keyField === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid key field']); } $this->db->table('investigation')->where($keyField, $invId)->delete(); return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Investigation deleted', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_invest_master_toggle_fav(int $invId) { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0]); } if ($invId <= 0 || ! $this->db->tableExists('investigation')) { return $this->response->setJSON(['update' => 0]); } $fields = $this->db->getFieldNames('investigation'); $keyField = $this->resolveFirstField($fields, ['id', 'Code', 'code']); if ($keyField === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid key field']); } if (! in_array('is_favourite', $fields, true)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Run database migration first']); } $row = $this->db->table('investigation')->where($keyField, $invId)->limit(1)->get()->getRowArray(); if (! $row) { return $this->response->setJSON(['update' => 0]); } $newVal = ((int) ($row['is_favourite'] ?? 0)) === 1 ? 0 : 1; $this->db->table('investigation')->where($keyField, $invId)->update(['is_favourite' => $newVal]); return $this->response->setJSON([ 'update' => 1, 'is_favourite' => $newVal, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_invest_shortcuts_manager() { return view('billing/opd_invest_shortcuts_manager'); } public function opd_invest_shortcuts_all() { if (! $this->db->tableExists('investigation')) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames('investigation'); $keyField = $this->resolveFirstField($fields, ['id', 'Code', 'code']); $codeField = $this->resolveFirstField($fields, ['Code', 'code']); $nameField = $this->resolveFirstField($fields, ['Name', 'name']); if ($keyField === null || $codeField === null || $nameField === null) { return $this->response->setJSON(['rows' => []]); } $selectCols = $keyField . ' as id, ' . $codeField . ' as code, ' . $nameField . ' as name'; foreach (['short_name', 'sort_id'] as $f) { if (in_array($f, $fields, true)) { $selectCols .= ', ' . $f; } } $rows = $this->db->table('investigation') ->select($selectCols) ->orderBy($nameField, 'ASC') ->get()->getResultArray(); $rows = array_map(static function (array $r): array { return [ 'id' => (int) ($r['id'] ?? 0), 'code' => (string) ($r['code'] ?? ''), 'name' => (string) ($r['name'] ?? ''), 'short_name' => (string) ($r['short_name'] ?? ''), 'sort_id' => (int) ($r['sort_id'] ?? 0), ]; }, $rows); return $this->response->setJSON([ 'rows' => $rows, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_invest_shortcuts_save_item() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0]); } if (! $this->db->tableExists('investigation')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation table not found']); } $id = (int) $this->request->getPost('id'); if ($id <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid id']); } $fields = $this->db->getFieldNames('investigation'); $keyField = $this->resolveFirstField($fields, ['id', 'Code', 'code']); if ($keyField === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid table structure']); } $data = []; if (in_array('short_name', $fields, true)) { $data['short_name'] = trim((string) $this->request->getPost('short_name')); } if (in_array('sort_id', $fields, true)) { $data['sort_id'] = max(0, (int) $this->request->getPost('sort_id')); } if (empty($data)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Nothing to update']); } $this->db->table('investigation')->where($keyField, $id)->update($data); return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Saved', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_invest_profile_master() { return view('billing/opd_invest_profile_master_workspace'); } private function buildInvestigationProfileRows(string $filter = ''): array { if (! $this->db->tableExists('invprofiles') || ! $this->db->tableExists('invtprofiles') || ! $this->db->tableExists('investigation')) { return []; } $pFields = $this->db->getFieldNames('invprofiles'); $jFields = $this->db->getFieldNames('invtprofiles'); $iFields = $this->db->getFieldNames('investigation'); $pCode = $this->resolveFirstField($pFields, ['Code', 'code', 'id']); $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) { return []; } $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 ' . 'LEFT JOIN `invtprofiles` j ON p.`' . $pCode . '` = j.`' . $jProfileCode . '` ' . 'LEFT JOIN `investigation` i ON i.`' . $iCode . '` = j.`' . $jInvestCode . '` '; $params = []; if ($filter !== '') { $sql .= 'WHERE p.`' . $pName . '` LIKE ? OR i.`' . $iName . '` LIKE ? '; $params[] = '%' . $filter . '%'; $params[] = '%' . $filter . '%'; } $sql .= 'ORDER BY p.`' . $pName . '` ASC'; if ($jPrintOrder !== null) { $sql .= ', j.`' . $jPrintOrder . '` ASC'; } $rows = $this->db->query($sql, $params)->getResultArray(); $map = []; foreach ($rows as $row) { $profileCode = (int) ($row['profile_code'] ?? 0); $profileName = trim((string) ($row['profile_name'] ?? '')); if ($profileCode <= 0 || $profileName === '') { continue; } if (! isset($map[$profileCode])) { $map[$profileCode] = [ 'profile_code' => $profileCode, 'profile_name' => $profileName, 'tests' => [], ]; } $testCode = trim((string) ($row['investigation_code'] ?? '')); $testName = trim((string) ($row['investigation_name'] ?? '')); if ($testCode !== '' && $testName !== '') { $map[$profileCode]['tests'][] = [ 'code' => $testCode, 'name' => $testName, ]; } } return array_values($map); } public function opd_invest_profile_master_list() { $filter = trim((string) $this->request->getGet('filter')); return $this->response->setJSON(['rows' => $this->buildInvestigationProfileRows($filter)]); } public function opd_invest_profile_master_data() { $filter = trim((string) $this->request->getGet('filter')); $list = $this->buildInvestigationProfileRows($filter); $draw = (int) $this->request->getGet('draw'); $start = max(0, (int) $this->request->getGet('start')); $length = (int) $this->request->getGet('length'); if ($length <= 0) { $length = 25; } $paged = array_slice($list, $start, $length); foreach ($paged as &$row) { $row['test_count'] = count($row['tests'] ?? []); $row['test_names'] = implode(', ', array_map(static function ($item) { return (string) ($item['name'] ?? ''); }, $row['tests'] ?? [])); } unset($row); return $this->response->setJSON([ 'draw' => $draw, 'recordsTotal' => count($list), 'recordsFiltered' => count($list), 'data' => $paged, ]); } public function opd_invest_profile_master_get(int $profileCode) { $rows = $this->buildInvestigationProfileRows(); foreach ($rows as $row) { if ((int) ($row['profile_code'] ?? 0) === $profileCode) { return $this->response->setJSON([ 'update' => 1, 'row' => $row, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } } return $this->response->setJSON(['update' => 0, 'error_text' => 'Profile not found']); } public function opd_invest_profile_master_save() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0]); } if (! $this->db->tableExists('invprofiles') || ! $this->db->tableExists('invtprofiles') || ! $this->db->tableExists('investigation')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Profile tables not found']); } $profileCode = (int) $this->request->getPost('profile_code'); $profileName = trim((string) $this->request->getPost('profile_name')); $investigationCodes = array_values(array_filter(array_map('intval', explode(',', (string) $this->request->getPost('investigation_codes'))))); if ($profileName === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Profile name is required']); } if (empty($investigationCodes)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Select at least one investigation']); } $pFields = $this->db->getFieldNames('invprofiles'); $jFields = $this->db->getFieldNames('invtprofiles'); $pCode = $this->resolveFirstField($pFields, ['Code', 'code', 'id']); $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']); if ($pCode === null || $pName === null || $jProfileCode === null || $jInvestCode === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid profile table structure']); } if ($profileCode > 0) { $this->db->table('invprofiles')->where($pCode, $profileCode)->update([$pName => $profileName]); $savedCode = $profileCode; $this->db->table('invtprofiles')->where($jProfileCode, $savedCode)->delete(); } else { $this->db->table('invprofiles')->insert([$pName => $profileName]); $savedCode = (int) $this->db->insertID(); } $order = 1; foreach ($investigationCodes as $code) { $joinRow = [ $jProfileCode => $savedCode, $jInvestCode => $code, ]; if ($jPrintOrder !== null) { $joinRow[$jPrintOrder] = $order; } $this->db->table('invtprofiles')->insert($joinRow); $order++; } return $this->response->setJSON([ 'update' => 1, 'insertid' => $savedCode, 'error_text' => $profileCode > 0 ? 'Profile updated' : 'Profile added', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function opd_invest_profile_master_remove(int $profileCode) { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0]); } if (! $this->db->tableExists('invprofiles') || ! $this->db->tableExists('invtprofiles')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Profile tables not found']); } $pFields = $this->db->getFieldNames('invprofiles'); $jFields = $this->db->getFieldNames('invtprofiles'); $pCode = $this->resolveFirstField($pFields, ['Code', 'code', 'id']); $jProfileCode = $this->resolveFirstField($jFields, ['ProfileCode', 'profile_code']); if ($pCode === null || $jProfileCode === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid profile table structure']); } $this->db->table('invtprofiles')->where($jProfileCode, $profileCode)->delete(); $this->db->table('invprofiles')->where($pCode, $profileCode)->delete(); return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Profile deleted', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } // ───────────────────────────────────────────────────────────────────────── 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) { $rowSessionId = (int) ($row['opd_session_id'] ?? 0); $result[] = [ 'id' => (int) ($row['id'] ?? 0), 'opd_session_id' => $rowSessionId, 'generated_at' => (string) ($row['generated_at'] ?? ''), 'generated_by' => (string) ($row['generated_by'] ?? ''), 'download_url' => base_url('Opd_prescription/fhir_bundle/' . (int) $opdId . '/' . $rowSessionId), 'preview_url' => base_url('Opd_prescription/fhir_bundle_preview/' . (int) $opdId . '/' . $rowSessionId), 'preview_latest_url' => base_url('Opd_prescription/fhir_bundle_preview/' . (int) $opdId), ]; } return $this->response->setJSON(['rows' => $result]); } public function fhir_bundle_preview(int $opdId, int $sessionId = 0) { if (! $this->db->tableExists('opd_fhir_documents')) { return $this->response->setStatusCode(404)->setJSON([ 'status' => 'error', 'message' => '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)->setJSON([ 'status' => 'error', 'message' => 'FHIR bundle not found.', 'opd_id' => (int) $opdId, 'opd_session_id' => (int) $sessionId, ]); } $bundleJson = (string) ($row['bundle_json'] ?? '{}'); $bundle = json_decode($bundleJson, true); if (! is_array($bundle)) { return $this->response->setStatusCode(500)->setJSON([ 'status' => 'error', 'message' => 'Stored FHIR bundle JSON is invalid.', 'document_id' => (int) ($row['id'] ?? 0), ]); } return $this->response->setJSON([ 'status' => 'ok', 'document_id' => (int) ($row['id'] ?? 0), 'opd_id' => (int) ($row['opd_id'] ?? 0), 'opd_session_id' => (int) ($row['opd_session_id'] ?? 0), 'bundle_type' => (string) ($row['bundle_type'] ?? ''), 'generated_at' => (string) ($row['generated_at'] ?? ''), 'generated_by' => (string) ($row['generated_by'] ?? ''), 'bundle' => $bundle, ]); } 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 === '') { return $this->response->setJSON(['rows' => []]); } $rows = []; if ($this->db->tableExists('complaints_master')) { $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(); } $keywordRows = $this->fetchAutotypeKeywords( 'complaints', $q, max(0, (int) ($this->getCurrentUserId() ?? 0)), 20 ); $out = []; $seen = []; $needle = mb_strtolower($this->normalizeAutocompleteSuggestionText($q)); foreach ($rows as $row) { $name = $this->normalizeAutocompleteSuggestionText((string) ($row['name'] ?? '')); if ($name === '') { continue; } if ($needle !== '' && mb_stripos($name, $needle) === false) { continue; } $k = mb_strtoupper($name); if (isset($seen[$k])) { continue; } $seen[$k] = true; $row['name'] = $name; $out[] = $row; } foreach ($keywordRows as $keyword) { $name = $this->normalizeAutocompleteSuggestionText((string) $keyword); if ($name === '') { continue; } if ($needle !== '' && mb_stripos($name, $needle) === false) { continue; } $k = mb_strtoupper($name); if (isset($seen[$k])) { continue; } $seen[$k] = true; $out[] = [ 'code' => '', 'name' => $name, 'name_hinglish' => '', 'show_in_short' => 0, 'ai_hint' => '', ]; } return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); } public function provisional_diagnosis_search() { if (! $this->canAccessPrescription()) { return $this->response->setStatusCode(403)->setJSON(['rows' => []]); } $q = trim((string) $this->request->getGet('q')); if ($q === '') { return $this->response->setJSON(['rows' => []]); } $out = []; $seen = []; $needle = mb_strtolower($this->normalizeAutocompleteSuggestionText($q)); $csnotkRows = (new CsnotkTerminologyService())->searchDiagnosis($q, 20); foreach ($csnotkRows as $row) { $name = $this->normalizeAutocompleteSuggestionText((string) ($row['term'] ?? '')); if ($name === '') { continue; } if ($needle !== '' && mb_stripos($name, $needle) === false) { continue; } $conceptId = trim((string) ($row['concept_id'] ?? '')); $source = trim((string) ($row['source'] ?? 'csnotk')); $k = mb_strtoupper($name) . '|' . $conceptId; if (isset($seen[$k])) { continue; } $seen[$k] = true; $out[] = [ 'name' => $name, 'snomed_concept_id' => $conceptId, 'snomed_term' => $name, 'source' => $source, ]; if (count($out) >= 20) { return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); } } $snomedRows = $this->fetchSnomedDiagnosisRows($q, 20); foreach ($snomedRows as $row) { $name = $this->normalizeAutocompleteSuggestionText((string) ($row['term'] ?? '')); if ($name === '') { continue; } if ($needle !== '' && mb_stripos($name, $needle) === false) { continue; } $k = mb_strtoupper($name); if (isset($seen[$k])) { continue; } $seen[$k] = true; $out[] = [ 'name' => $name, 'snomed_concept_id' => (string) ($row['concept_id'] ?? ''), 'snomed_term' => $name, 'source' => 'snomed', ]; if (count($out) >= 20) { return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); } } if (! $this->db->tableExists('disease_master')) { return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); } $fields = $this->db->getFieldNames('disease_master') ?? []; $nameField = in_array('Name', $fields, true) ? 'Name' : (in_array('name', $fields, true) ? 'name' : null); if ($nameField === null) { return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); } $rows = $this->db->table('disease_master') ->select($nameField . ' as name') ->like($nameField, $q) ->orderBy($nameField, 'ASC') ->limit(20) ->get() ->getResultArray(); $keywordRows = $this->fetchAutotypeKeywords( 'provisional_diagnosis', $q, max(0, (int) ($this->getCurrentUserId() ?? 0)), 20 ); foreach ($rows as $row) { $name = $this->normalizeAutocompleteSuggestionText((string) ($row['name'] ?? '')); if ($name === '') { continue; } if ($needle !== '' && mb_stripos($name, $needle) === false) { continue; } $k = mb_strtoupper($name); if (isset($seen[$k])) { continue; } $seen[$k] = true; $out[] = [ 'name' => $name, 'snomed_concept_id' => '', 'snomed_term' => '', 'source' => 'disease_master', ]; } foreach ($keywordRows as $keyword) { $name = $this->normalizeAutocompleteSuggestionText((string) $keyword); if ($name === '') { continue; } if ($needle !== '' && mb_stripos($name, $needle) === false) { continue; } $k = mb_strtoupper($name); if (isset($seen[$k])) { continue; } $seen[$k] = true; $out[] = [ 'name' => $name, 'snomed_concept_id' => '', 'snomed_term' => '', 'source' => 'keyword', ]; } return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); } private function fetchSnomedDiagnosisRows(string $query, int $limit = 20): array { $q = trim($query); if ($q === '') { return []; } if (! $this->db->tableExists('snomed_concept') || ! $this->db->tableExists('snomed_description')) { return []; } $normalized = mb_strtolower($this->normalizeAutocompleteSuggestionText($q)); if ($normalized === '') { return []; } $builder = $this->db->table('snomed_description d') ->select('d.concept_id, d.term, MAX(CASE WHEN lr.acceptability_id = "900000000000548007" THEN 2 WHEN lr.acceptability_id = "900000000000549004" THEN 1 ELSE 0 END) AS accept_rank', false) ->join('snomed_concept c', 'c.concept_id = d.concept_id AND c.active = 1', 'inner') ->join('snomed_language_refset lr', 'lr.referenced_component_id = d.description_id AND lr.active = 1 AND lr.refset_id IN ("900000000000508004", "900000000000509007")', 'left') ->where('d.active', 1) ->where('d.language_code', 'en') ->groupStart() ->like('d.term_normalized', $normalized, 'after') ->orLike('d.term_normalized', ' ' . $normalized, 'both') ->groupEnd() ->groupBy('d.description_id') ->orderBy('accept_rank', 'DESC') ->orderBy('CHAR_LENGTH(d.term)', 'ASC', false) ->orderBy('d.term', 'ASC') ->limit(max(1, min(100, $limit))); return $builder->get()->getResultArray(); } private function resolveProvisionalDiagnosisSnomedMatch(string $text): array { $term = trim($text); if ($term === '') { return []; } if (! $this->db->tableExists('snomed_description')) { return []; } $row = $this->db->table('snomed_description d') ->select('d.concept_id, d.term') ->join('snomed_concept c', 'c.concept_id = d.concept_id AND c.active = 1', 'inner') ->where('d.active', 1) ->where('d.language_code', 'en') ->where('d.term', $term) ->orderBy('d.term', 'ASC') ->get(1) ->getRowArray(); if (! is_array($row) || empty($row['concept_id'])) { return []; } return [ 'concept_id' => (string) ($row['concept_id'] ?? ''), 'term' => (string) ($row['term'] ?? $term), 'source' => 'snomed-exact', ]; } private function getSnomedPreferredTermByConcept(string $conceptId): string { if (! $this->db->tableExists('snomed_description')) { return ''; } $preferred = $this->db->table('snomed_description d') ->select('d.term') ->join('snomed_language_refset lr', 'lr.referenced_component_id = d.description_id AND lr.active = 1 AND lr.acceptability_id = "900000000000548007"', 'inner') ->where('d.active', 1) ->where('d.concept_id', $conceptId) ->where('d.language_code', 'en') ->orderBy('d.term', 'ASC') ->get(1) ->getRowArray(); if (is_array($preferred) && ! empty($preferred['term'])) { return trim((string) $preferred['term']); } $fallback = $this->db->table('snomed_description') ->select('term') ->where('active', 1) ->where('concept_id', $conceptId) ->where('language_code', 'en') ->orderBy('term', 'ASC') ->get(1) ->getRowArray(); return trim((string) ($fallback['term'] ?? '')); } private function fetchSnomedIcdMapRows(string $conceptId): array { $out = []; $seen = []; if ($this->db->tableExists('snomed_map_extended')) { $extended = $this->db->table('snomed_map_extended') ->select('map_target,map_group,map_priority,map_rule,map_advice') ->where('active', 1) ->where('referenced_component_id', $conceptId) ->where('map_target IS NOT NULL', null, false) ->where('map_target <>', '') ->orderBy('map_group', 'ASC') ->orderBy('map_priority', 'ASC') ->limit(50) ->get() ->getResultArray(); foreach ($extended as $row) { $code = trim((string) ($row['map_target'] ?? '')); if ($code === '' || isset($seen['E:' . $code])) { continue; } $seen['E:' . $code] = true; $out[] = [ 'icd_code' => $code, 'source' => 'extended_map', 'map_group' => (int) ($row['map_group'] ?? 0), 'map_priority' => (int) ($row['map_priority'] ?? 0), 'map_rule' => (string) ($row['map_rule'] ?? ''), 'map_advice' => (string) ($row['map_advice'] ?? ''), ]; } } if ($this->db->tableExists('snomed_map_simple')) { $simple = $this->db->table('snomed_map_simple') ->select('map_target') ->where('active', 1) ->where('referenced_component_id', $conceptId) ->where('map_target IS NOT NULL', null, false) ->where('map_target <>', '') ->orderBy('map_target', 'ASC') ->limit(50) ->get() ->getResultArray(); foreach ($simple as $row) { $code = trim((string) ($row['map_target'] ?? '')); if ($code === '' || isset($seen['E:' . $code]) || isset($seen['S:' . $code])) { continue; } $seen['S:' . $code] = true; $out[] = [ 'icd_code' => $code, 'source' => 'simple_map', 'map_group' => 0, 'map_priority' => 0, 'map_rule' => '', 'map_advice' => '', ]; } } return $out; } private function countTableRows(string $table): int { if (! preg_match('/^[a-zA-Z0-9_]+$/', $table)) { return 0; } if (! $this->db->tableExists($table)) { return 0; } $row = $this->db->query('SELECT COUNT(*) AS row_count FROM ' . $table)->getRowArray(); return (int) ($row['row_count'] ?? 0); } public function finding_exam_search() { if (! $this->canAccessPrescription()) { return $this->response->setStatusCode(403)->setJSON(['rows' => []]); } $q = trim((string) $this->request->getGet('q')); if ($q === '' || ! $this->db->tableExists('opd_prescription')) { return $this->response->setJSON(['rows' => []]); } $rows = $this->db->table('opd_prescription') ->select('Finding_Examinations as finding_examinations') ->where('Finding_Examinations IS NOT NULL', null, false) ->where('Finding_Examinations <>', '') ->like('Finding_Examinations', $q) ->groupBy('Finding_Examinations') ->orderBy('id', 'DESC') ->limit(20) ->get() ->getResultArray(); $keywordRows = $this->fetchAutotypeKeywords( 'finding_examinations', $q, max(0, (int) ($this->getCurrentUserId() ?? 0)), 20 ); $out = []; $seen = []; $needle = mb_strtolower($this->normalizeAutocompleteSuggestionText($q)); foreach ($rows as $row) { $text = $this->normalizeAutocompleteSuggestionText((string) ($row['finding_examinations'] ?? '')); if ($text === '') { continue; } if ($needle !== '' && mb_stripos($text, $needle) === false) { continue; } $k = mb_strtoupper($text); if (isset($seen[$k])) { continue; } $seen[$k] = true; $out[] = ['finding_examinations' => $text]; } foreach ($keywordRows as $keyword) { $text = $this->normalizeAutocompleteSuggestionText((string) $keyword); if ($text === '') { continue; } if ($needle !== '' && mb_stripos($text, $needle) === false) { continue; } $k = mb_strtoupper($text); if (isset($seen[$k])) { continue; } $seen[$k] = true; $out[] = ['finding_examinations' => $text]; } return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]); } private function normalizeAutocompleteSuggestionText(string $text): string { $text = str_replace(["\r\n", "\r", "\n"], ' ', $text); $text = preg_replace('/\s+/', ' ', $text) ?? $text; $parts = preg_split('/\s*,\s*/', $text) ?: []; $cleanParts = []; foreach ($parts as $part) { $part = trim((string) $part); if ($part === '') { continue; } $part = preg_replace('/\s+/', ' ', $part) ?? $part; $part = preg_replace_callback('/\b([A-Za-z])\s*\/\s*([A-Za-z])\b/u', static function (array $m): string { return mb_strtoupper($m[1]) . '/' . mb_strtoupper($m[2]); }, $part) ?? $part; // If commas split out punctuation-only fragments like "+", attach to previous token. if (preg_match('/^[+-]{1,3}$/', $part) && $cleanParts !== []) { $lastIndex = count($cleanParts) - 1; $cleanParts[$lastIndex] = rtrim($cleanParts[$lastIndex]) . ' ' . $part; continue; } if (preg_match('/^(.+?)([+-]{1,3})$/u', $part, $m) === 1) { $base = trim((string) $m[1]); $sign = trim((string) $m[2]); if ($base !== '') { $part = $base . ' ' . $sign; } } $cleanParts[] = $part; } $normalized = implode(', ', $cleanParts); return rtrim(trim($normalized), ', '); } public function autotype_keyword_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']); } $section = trim((string) $this->request->getPost('section')); $keyword = trim((string) $this->request->getPost('keyword')); $scope = strtolower(trim((string) $this->request->getPost('scope'))); if ($scope === '') { $scope = 'doctor'; } if (! $this->isAutotypeKeywordSectionAllowed($section)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unsupported section']); } $keyword = $this->normalizeAutocompleteSuggestionText($keyword); if (! $this->isUsableAutotypeKeyword($keyword)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Please enter a meaningful keyword (not only commas/symbols).']); } if (! $this->ensureAutotypeKeywordTable()) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword storage unavailable']); } $docId = max(0, (int) ($this->getCurrentUserId() ?? 0)); if ($scope === 'master') { $user = auth()->user(); $canMaster = $user && method_exists($user, 'can') && ($user->can('doctor_work.template_workspace.access') || $user->can('doctor_work.access')); if (! $canMaster) { return $this->response->setJSON(['update' => 0, 'error_text' => 'No permission to save master keyword']); } $docId = 0; } $table = $this->db->table('opd_autotype_keyword_master'); $existing = $table ->where('doc_id', $docId) ->where('section_key', $section) ->where('keyword', $keyword) ->get(1) ->getRowArray(); if (! empty($existing)) { $table->where('id', (int) ($existing['id'] ?? 0))->update([ 'keyword' => $keyword, 'is_active' => 1, 'updated_at' => date('Y-m-d H:i:s'), ]); } else { $dupId = $this->findNormalizedAutotypeKeywordId($docId, $section, $keyword, 0); if ($dupId > 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword already exists in this scope']); } $table->insert([ 'doc_id' => $docId, 'section_key' => $section, 'keyword' => $keyword, 'is_active' => 1, 'created_at' => date('Y-m-d H:i:s'), 'updated_at' => date('Y-m-d H:i:s'), ]); } return $this->response->setJSON([ 'update' => 1, 'error_text' => $docId === 0 ? 'Keyword saved in master list' : 'Keyword saved for current doctor', 'keyword' => $keyword, 'section' => $section, 'scope' => $docId === 0 ? 'master' : 'doctor', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function autotype_keyword_list() { if (! $this->canAccessPrescription()) { return $this->response->setStatusCode(403)->setJSON(['rows' => []]); } $section = trim((string) $this->request->getGet('section')); $scope = strtolower(trim((string) $this->request->getGet('scope'))); $q = trim((string) $this->request->getGet('q')); $docId = max(0, (int) ($this->getCurrentUserId() ?? 0)); $canMaster = $this->canManageMasterKeyword(); if ($scope === '') { $scope = 'all'; } if (! $this->ensureAutotypeKeywordTable()) { return $this->response->setJSON(['rows' => []]); } $builder = $this->db->table('opd_autotype_keyword_master') ->select('id,doc_id,section_key,keyword,updated_at') ->where('is_active', 1); if ($this->isAutotypeKeywordSectionAllowed($section)) { $builder->where('section_key', $section); } if ($scope === 'master') { $builder->where('doc_id', 0); } elseif ($scope === 'doctor') { $builder->where('doc_id', $docId); } else { $builder->groupStart() ->where('doc_id', 0) ->orWhere('doc_id', $docId) ->groupEnd(); } if ($q !== '') { $builder->like('keyword', $q); } $rows = $builder ->orderBy('doc_id', 'DESC') ->orderBy('updated_at', 'DESC') ->orderBy('keyword', 'ASC') ->limit(200) ->get() ->getResultArray(); $out = []; foreach ($rows as $row) { $id = (int) ($row['id'] ?? 0); $rowDocId = (int) ($row['doc_id'] ?? 0); $isMaster = $rowDocId === 0; $canEdit = $isMaster ? $canMaster : ($rowDocId === $docId); $out[] = [ 'id' => $id, 'doc_id' => $rowDocId, 'section_key' => (string) ($row['section_key'] ?? ''), 'keyword' => (string) ($row['keyword'] ?? ''), 'scope' => $isMaster ? 'master' : 'doctor', 'scope_label' => $isMaster ? 'Master' : 'My', 'updated_at' => (string) ($row['updated_at'] ?? ''), 'can_edit' => $canEdit ? 1 : 0, 'can_delete' => $canEdit ? 1 : 0, ]; } return $this->response->setJSON(['rows' => $out]); } public function autotype_keyword_update() { 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']); } $id = (int) $this->request->getPost('id'); $keyword = trim((string) $this->request->getPost('keyword')); $keyword = $this->normalizeAutocompleteSuggestionText($keyword); if ($id <= 0 || ! $this->isUsableAutotypeKeyword($keyword)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Please enter a meaningful keyword (not only commas/symbols).']); } if (! $this->ensureAutotypeKeywordTable()) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword storage unavailable']); } $table = $this->db->table('opd_autotype_keyword_master'); $current = $table->where('id', $id)->where('is_active', 1)->get(1)->getRowArray(); if (empty($current)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword not found']); } $docId = max(0, (int) ($this->getCurrentUserId() ?? 0)); $rowDocId = (int) ($current['doc_id'] ?? 0); if ($rowDocId === 0) { if (! $this->canManageMasterKeyword()) { return $this->response->setJSON(['update' => 0, 'error_text' => 'No permission to edit master keyword']); } } elseif ($rowDocId !== $docId) { return $this->response->setJSON(['update' => 0, 'error_text' => 'You can edit only your own keyword']); } $section = (string) ($current['section_key'] ?? ''); if (! $this->isAutotypeKeywordSectionAllowed($section)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unsupported section']); } $dup = $table ->where('doc_id', $rowDocId) ->where('section_key', $section) ->where('keyword', $keyword) ->where('id <>', $id) ->where('is_active', 1) ->get(1) ->getRowArray(); if (! empty($dup)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword already exists in this scope']); } $dupNormalizedId = $this->findNormalizedAutotypeKeywordId($rowDocId, $section, $keyword, $id); if ($dupNormalizedId > 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword already exists in this scope']); } $table->where('id', $id)->update([ 'keyword' => $keyword, 'updated_at' => date('Y-m-d H:i:s'), ]); return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Keyword updated', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function autotype_keyword_remove() { 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']); } $id = (int) $this->request->getPost('id'); if ($id <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword id is required']); } if (! $this->ensureAutotypeKeywordTable()) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword storage unavailable']); } $table = $this->db->table('opd_autotype_keyword_master'); $current = $table->where('id', $id)->where('is_active', 1)->get(1)->getRowArray(); if (empty($current)) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Keyword not found']); } $docId = max(0, (int) ($this->getCurrentUserId() ?? 0)); $rowDocId = (int) ($current['doc_id'] ?? 0); if ($rowDocId === 0) { if (! $this->canManageMasterKeyword()) { return $this->response->setJSON(['update' => 0, 'error_text' => 'No permission to remove master keyword']); } } elseif ($rowDocId !== $docId) { return $this->response->setJSON(['update' => 0, 'error_text' => 'You can remove only your own keyword']); } $table->where('id', $id)->update([ 'is_active' => 0, 'updated_at' => date('Y-m-d H:i:s'), ]); return $this->response->setJSON([ 'update' => 1, 'error_text' => 'Keyword removed', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } private function isUsableAutotypeKeyword(string $keyword): bool { if (mb_strlen($keyword) < 2) { return false; } // Must contain at least one letter/number to avoid symbol/comma-only junk keywords. return preg_match('/[\p{L}\p{N}]/u', $keyword) === 1; } private function findNormalizedAutotypeKeywordId(int $docId, string $section, string $normalizedKeyword, int $ignoreId = 0): int { if (! $this->db->tableExists('opd_autotype_keyword_master')) { return 0; } $builder = $this->db->table('opd_autotype_keyword_master') ->select('id, keyword') ->where('doc_id', $docId) ->where('section_key', $section) ->where('is_active', 1); if ($ignoreId > 0) { $builder->where('id <>', $ignoreId); } $rows = $builder->get()->getResultArray(); if ($rows === []) { return 0; } $needle = mb_strtoupper($this->normalizeAutocompleteSuggestionText($normalizedKeyword)); foreach ($rows as $row) { $candidate = mb_strtoupper($this->normalizeAutocompleteSuggestionText((string) ($row['keyword'] ?? ''))); if ($candidate === '' || $candidate !== $needle) { continue; } return (int) ($row['id'] ?? 0); } return 0; } // Legacy-compatible wrappers used by older autocomplete clients. public function get_complaints() { $q = trim((string) ($this->request->getGet('term') ?? '')); if ($q === '' || ! $this->db->tableExists('complaints_master')) { return $this->response->setJSON([]); } $rows = $this->db->table('complaints_master') ->select('Name as value') ->where('is_active', 1) ->like('Name', $q) ->orderBy('Name', 'ASC') ->limit(20) ->get() ->getResultArray(); $out = []; foreach ($rows as $row) { $value = trim((string) ($row['value'] ?? '')); if ($value === '') { continue; } $out[] = ['label' => $value, 'value' => $value]; } return $this->response->setJSON($out); } public function get_disease() { $q = trim((string) ($this->request->getGet('term') ?? '')); if ($q === '' || ! $this->db->tableExists('disease_master')) { return $this->response->setJSON([]); } $fields = $this->db->getFieldNames('disease_master') ?? []; $nameField = in_array('Name', $fields, true) ? 'Name' : (in_array('name', $fields, true) ? 'name' : null); if ($nameField === null) { return $this->response->setJSON([]); } $rows = $this->db->table('disease_master') ->select($nameField . ' as value') ->like($nameField, $q) ->orderBy($nameField, 'ASC') ->limit(20) ->get() ->getResultArray(); $out = []; foreach ($rows as $row) { $value = trim((string) ($row['value'] ?? '')); if ($value === '') { continue; } $out[] = ['label' => $value, 'value' => $value]; } return $this->response->setJSON($out); } public function get_finding_exam() { $q = trim((string) ($this->request->getGet('term') ?? '')); if ($q === '' || ! $this->db->tableExists('opd_prescription')) { return $this->response->setJSON([]); } $rows = $this->db->table('opd_prescription') ->select('Finding_Examinations as value') ->where('Finding_Examinations IS NOT NULL', null, false) ->where('Finding_Examinations <>', '') ->like('Finding_Examinations', $q) ->groupBy('Finding_Examinations') ->orderBy('id', 'DESC') ->limit(20) ->get() ->getResultArray(); $out = []; foreach ($rows as $row) { $value = trim((string) ($row['value'] ?? '')); if ($value === '') { continue; } $out[] = ['label' => $value, 'value' => $value]; } return $this->response->setJSON($out); } 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'); $adviceHindi = trim((string) $this->request->getPost('advice_hindi')); 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); if ($adviceHindi === '' && $this->db->tableExists('opd_advice')) { $masterFields = $this->db->getFieldNames('opd_advice'); $masterIdField = $this->resolveFirstField($masterFields, ['id', 'advice_id']); $masterAdviceField = $this->resolveFirstField($masterFields, ['advice', 'advice_txt', 'advice_text']); $masterHindiField = $this->resolveFirstField($masterFields, ['advice_hindi', 'advice_local', 'hindi_advice', 'advice_hin']); if ($masterAdviceField !== null && $masterHindiField !== null) { $masterBuilder = $this->db->table('opd_advice') ->select($masterAdviceField . ' as advice_text, ' . $masterHindiField . ' as advice_hindi' . ($masterIdField !== null ? (', ' . $masterIdField . ' as advice_id') : '')); if ($masterIdField !== null && $adviceId > 0) { $masterBuilder->where($masterIdField, $adviceId); } else { $normalizedAdviceText = preg_replace('/\s+/', ' ', $adviceText) ?? $adviceText; $masterBuilder->groupStart() ->where($masterAdviceField, $adviceText) ->orWhere($masterAdviceField, $normalizedAdviceText) ->groupEnd(); } $masterRow = $masterBuilder->get(1)->getRowArray() ?? []; $adviceHindi = trim((string) ($masterRow['advice_hindi'] ?? '')); } } $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; } if ($adviceHindi !== '') { if (in_array('advice_txt_hindi', $fields, true)) { $insert['advice_txt_hindi'] = $adviceHindi; } elseif (in_array('advice_hindi', $fields, true)) { $insert['advice_hindi'] = $adviceHindi; } } $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')); $favOnly = (int) $this->request->getGet('fav_only'); $specId = (int) $this->request->getGet('spec_id'); if (! $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; $hasFav = in_array('is_favourite', $fields, true); $hasSpec = in_array('spec_ids', $fields, true); if ($codeField === null || $nameField === null) { return $this->response->setJSON(['rows' => []]); } // When no search term but fav/spec filter applied, still return results if ($q === '' && $favOnly === 0 && $specId === 0) { return $this->response->setJSON(['rows' => []]); } $selectCols = $codeField . ' as code, ' . $nameField . ' as name'; if ($hasFav) { $selectCols .= ', is_favourite'; } $builder = $this->db->table('investigation')->select($selectCols); if ($q !== '') { $builder->groupStart()->like($nameField, $q); if ($shortField !== null) { $builder->orLike($shortField, $q); } $builder->groupEnd(); } if ($favOnly && $hasFav) { $builder->where('is_favourite', 1); } if ($specId > 0 && $hasSpec) { $builder->where("FIND_IN_SET({$specId}, spec_ids) > 0", null, false); } if ($hasFav) { $builder->orderBy('is_favourite', 'DESC'); } $builder->orderBy('LOWER(' . $this->db->protectIdentifiers($nameField) . ')', 'ASC', false); $rows = $builder->limit(40)->get()->getResultArray(); return $this->response->setJSON(['rows' => $rows]); } public function investigation_shortcuts() { $profiles = []; $shortTests = []; if ($this->db->tableExists('investigation')) { $invFields = $this->db->getFieldNames('investigation'); $pickField = static function (array $fields, array $candidates): ?string { $map = []; foreach ($fields as $field) { $map[strtolower((string) $field)] = (string) $field; } foreach ($candidates as $candidate) { $key = strtolower((string) $candidate); if (isset($map[$key])) { return $map[$key]; } } return null; }; $codeField = $pickField($invFields, ['Code', 'code']); $nameField = $pickField($invFields, ['Name', 'name']); $shortField = $pickField($invFields, ['short_name', 'shortName', 'short']); $sortField = $pickField($invFields, ['sort_id', 'sort_order', 'id']); if ($codeField !== null && $nameField !== null && $shortField !== null && $sortField !== null) { // Keep legacy CI3 behavior: show only mostly-used tests and group/order by short_name, sort_id. $shortTests = $this->db->table('investigation') ->select($codeField . ' as code,' . $nameField . ' as name,' . $shortField . ' as short_name,' . $sortField . ' as sort_id') ->where($shortField . ' !=', '') ->where($shortField . ' is not null', null, false) ->orderBy($shortField, 'ASC') ->orderBy($sortField, 'ASC') ->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, ]); } // DEBUG ENDPOINT - Remove after verifying data public function debug_investigation_fields() { if ($this->db->tableExists('investigation')) { $fields = $this->db->getFieldNames('investigation'); $total = $this->db->table('investigation')->countAll(); $withShortName = $this->db->table('investigation') ->where("short_name != ''", null, false) ->where('short_name IS NOT NULL', null, false) ->countAll(); $sample = $this->db->table('investigation') ->where("short_name != ''", null, false) ->where('short_name IS NOT NULL', null, false) ->orderBy('short_name', 'ASC') ->orderBy('sort_id', 'ASC') ->limit(10) ->get() ->getResultArray(); return $this->response->setJSON([ 'table_fields' => $fields, 'total_investigations' => $total, 'with_short_name' => $withShortName, 'sample_data' => $sample, ]); } return $this->response->setJSON(['error' => 'investigation table not found']); } 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')); // Profile chips may send only investigation name; resolve code from master to avoid unique-key collisions. if ($code === '' && $name !== '' && $this->db->tableExists('investigation')) { $invFields = $this->db->getFieldNames('investigation'); $invCodeField = $this->resolveFirstField($invFields, ['Code', 'code']); $invNameField = $this->resolveFirstField($invFields, ['Name', 'name']); if ($invCodeField !== null && $invNameField !== null) { $resolved = $this->db->table('investigation') ->select($invCodeField . ' as code') ->where($invNameField, $name) ->limit(1) ->get() ->getRowArray(); if (! empty($resolved['code'])) { $code = trim((string) $resolved['code']); } } } 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; } $builder = $this->db->table($table); if (in_array('opd_pre_id', $fields, true)) { $builder->where('opd_pre_id', $sessionId); } elseif (in_array('opd_id', $fields, true)) { $builder->where('opd_id', $opdId); } if ($code !== '' && in_array('investigation_code', $fields, true)) { $builder->where('investigation_code', $code); } elseif ($name !== '' && in_array('investigation_name', $fields, true)) { $builder->where('investigation_name', $name); } $already = $builder->get(1)->getRowArray(); if (! empty($already)) { return $this->response->setJSON([ 'update' => 1, 'opd_session_id' => $sessionId, 'error_text' => 'Investigation already added', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } try { $this->db->table($table)->insert($insert); } catch (\Throwable $e) { $msg = strtolower($e->getMessage()); if (strpos($msg, 'duplicate entry') !== false) { return $this->response->setJSON([ 'update' => 1, 'opd_session_id' => $sessionId, 'error_text' => 'Investigation already added', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } throw $e; } $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'))); $limit = (int) ($this->request->getGet('limit') ?? 10); if ($limit <= 0) { $limit = 10; } $limit = min(10, $limit); if (! in_array($scope, ['active', 'favorite', 'all'], true)) { $scope = 'active'; } if ($q === '') { return $this->response->setJSON(['rows' => []]); } $qUpper = strtoupper($q); $qLoose = trim((string) (preg_replace('/[^A-Z0-9]+/', ' ', $qUpper) ?? $qUpper)); $qNormalized = trim((string) (preg_replace('/[^A-Z0-9]+/', '', $qUpper) ?? $qUpper)); $qTokens = array_values(array_filter(preg_split('/\s+/', $qLoose) ?: [], static fn(string $token): bool => $token !== '')); $qFirstToken = $qTokens[0] ?? ''; $qAlpha = trim((string) (preg_replace('/[^A-Z]+/', '', $qUpper) ?? $qUpper)); $qSoundex = $qAlpha !== '' ? soundex($qAlpha) : ''; $qMetaphone = $qAlpha !== '' ? metaphone($qAlpha) : ''; $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; $dosageField = $this->resolveFirstField($fields, ['dosage']); $whenField = $this->resolveFirstField($fields, ['dosage_when']); $freqField = $this->resolveFirstField($fields, ['dosage_freq']); $whereField = $this->resolveFirstField($fields, ['dosage_where']); $daysField = $this->resolveFirstField($fields, ['no_of_days']); $qtyField = $this->resolveFirstField($fields, ['qty']); $remarkField = $this->resolveFirstField($fields, ['remark']); 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'; } if ($dosageField !== null) { $select .= ',' . $dosageField . ' as dosage'; } if ($whenField !== null) { $select .= ',' . $whenField . ' as dosage_when'; } if ($freqField !== null) { $select .= ',' . $freqField . ' as dosage_freq'; } if ($whereField !== null) { $select .= ',' . $whereField . ' as dosage_where'; } if ($daysField !== null) { $select .= ',' . $daysField . ' as no_of_days'; } if ($qtyField !== null) { $select .= ',' . $qtyField . ' as qty'; } if ($remarkField !== null) { $select .= ',' . $remarkField . ' as remark'; } $userId = (int) ($this->getCurrentUserId() ?? 0); $cacheKey = 'medicine_search_v2_' . md5(json_encode([ 'q' => strtoupper($q), 'scope' => $scope, 'user' => $userId, 'limit' => $limit, ])); $cachedRows = cache($cacheKey); if (is_array($cachedRows)) { return $this->response->setJSON(['rows' => $cachedRows]); } $builder = $this->db->table($table)->select($select); if ($q !== '') { $builder->groupStart() ->like($nameField, $q, 'both'); if ($formField !== null) { $builder->orLike($formField, $q, 'both'); } if ($qFirstToken !== '' && mb_strlen($qFirstToken) >= 1) { $builder->orLike($nameField, $qFirstToken, 'after'); } // Phonetic candidates can differ by one letter (e.g. CETLAY/CETLEY), // so include an alpha-prefix fallback to widen SQL fetch before PHP ranking. if ($qAlpha !== '') { if (mb_strlen($qAlpha) >= 3) { $builder->orLike($nameField, substr($qAlpha, 0, 3), 'after'); } elseif (mb_strlen($qAlpha) >= 2) { $builder->orLike($nameField, substr($qAlpha, 0, 2), 'after'); } } // Normalize separators so variants like "utor", "u-tor", "u tor" match each other. if ($qNormalized !== '') { $qNormalizedSql = str_replace("'", "''", strtoupper($qNormalized)); $nameNormalizedSql = "REPLACE(REPLACE(UPPER(" . $nameField . "), '-', ''), ' ', '')"; $builder->orWhere($nameNormalizedSql . " LIKE '%" . $qNormalizedSql . "%'", null, false); if ($formField !== null) { $formNormalizedSql = "REPLACE(REPLACE(UPPER(" . $formField . "), '-', ''), ' ', '')"; $builder->orWhere($formNormalizedSql . " LIKE '%" . $qNormalizedSql . "%'", null, false); } } $builder->groupEnd(); } $rows = $builder ->orderBy($nameField, 'ASC') ->limit(500) ->get() ->getResultArray(); if (empty($rows)) { return $this->response->setJSON(['rows' => []]); } $favoriteMap = $userId > 0 ? $this->getUserFavoriteMedicineIds($userId) : []; $usageMap = $userId > 0 ? $this->getUserMedicineUsageMap($userId) : []; foreach ($rows as &$row) { $medId = (int) ($row['id'] ?? 0); $name = trim((string) ($row['med_name'] ?? '')); $nameUpper = strtoupper($name); $nameLoose = trim((string) (preg_replace('/[^A-Z0-9]+/', ' ', $nameUpper) ?? $nameUpper)); $nameNormalized = trim((string) (preg_replace('/[^A-Z0-9]+/', '', $nameUpper) ?? $nameUpper)); $nameAlpha = trim((string) (preg_replace('/[^A-Z]+/', '', $nameUpper) ?? $nameUpper)); $nameSoundex = $nameAlpha !== '' ? soundex($nameAlpha) : ''; $nameMetaphone = $nameAlpha !== '' ? metaphone($nameAlpha) : ''; $tokenMatch = false; if (! empty($qTokens)) { $tokenMatch = true; foreach ($qTokens as $token) { if ($token === '' || ! str_contains($nameLoose, $token)) { $tokenMatch = false; break; } } } $phoneticMatch = false; if ($qAlpha !== '' && $nameAlpha !== '') { $phoneticMatch = ($qSoundex !== '' && $qSoundex === $nameSoundex) || ($qMetaphone !== '' && $qMetaphone === $nameMetaphone); } $nearMatch = false; if ($qNormalized !== '' && $nameNormalized !== '') { $dist = levenshtein($qNormalized, $nameNormalized); $nearMatch = $dist >= 0 && $dist <= 2; } $matchesSearch = false; if ($qUpper !== '') { $matchesSearch = str_contains($nameUpper, $qUpper) || ($qNormalized !== '' && str_contains($nameNormalized, $qNormalized)) || $tokenMatch || $phoneticMatch || $nearMatch; } if (! $matchesSearch) { $row['_skip'] = 1; } $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; } if ($qNormalized !== '' && str_starts_with($nameNormalized, $qNormalized)) { $score += 180; } elseif ($qNormalized !== '' && str_contains($nameNormalized, $qNormalized)) { $score += 120; } if ($tokenMatch) { $score += 140; if ($qFirstToken !== '' && str_starts_with($nameLoose, $qFirstToken)) { $score += 40; } } if ($phoneticMatch) { $score += 110; } if ($nearMatch) { $score += 70; } } $row['is_favorite'] = $isFavorite ? 1 : 0; $row['use_count'] = $useCount; $row['last_used_at'] = $lastUsedAt; $row['_score'] = $score; } unset($row); $rows = array_values(array_filter($rows, static fn(array $row): bool => (int) ($row['_skip'] ?? 0) !== 1)); 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, $limit); foreach ($rows as &$row) { unset($row['_score']); unset($row['_skip']); } unset($row); cache()->save($cacheKey, $rows, 120); return $this->response->setJSON(['rows' => $rows]); } public function medicine_substitutes() { $table = $this->findExistingTable(['opd_med_master']); if ($table === null) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames($table); $idField = $this->resolveFirstField($fields, ['id']); $nameField = $this->resolveFirstField($fields, ['item_name', 'med_name']); $formField = $this->resolveFirstField($fields, ['formulation']); $genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']); $saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']); $companyField = $this->resolveFirstField($fields, ['company_name', 'company']); if ($idField === null || $nameField === null || ($genericField === null && $saltField === null)) { return $this->response->setJSON(['rows' => []]); } $medId = (int) $this->request->getGet('med_id'); $medName = trim((string) $this->request->getGet('med_name')); $current = []; if ($medId > 0) { $current = $this->db->table($table)->where($idField, $medId)->get(1)->getRowArray() ?? []; } if (empty($current) && $medName !== '') { $current = $this->db->table($table)->where('UPPER(' . $nameField . ')', strtoupper($medName))->get(1)->getRowArray() ?? []; if (! empty($current)) { $medId = (int) ($current[$idField] ?? 0); } } if (empty($current)) { return $this->response->setJSON(['rows' => []]); } $generic = $genericField !== null ? trim((string) ($current[$genericField] ?? '')) : ''; $salt = $saltField !== null ? trim((string) ($current[$saltField] ?? '')) : ''; if ($generic === '' && $salt === '') { return $this->response->setJSON(['rows' => []]); } $select = $idField . ' as id,' . $nameField . ' as med_name'; if ($formField !== null) { $select .= ',' . $formField . ' as med_type'; } if ($genericField !== null) { $select .= ',' . $genericField . ' as genericname'; } if ($saltField !== null) { $select .= ',' . $saltField . ' as salt_name'; } if ($companyField !== null) { $select .= ',' . $companyField . ' as company_name'; } $builder = $this->db->table($table)->select($select); $builder->groupStart(); if ($genericField !== null && $generic !== '') { $builder->where($genericField, $generic); } if ($saltField !== null && $salt !== '') { if ($genericField !== null && $generic !== '') { $builder->orWhere($saltField, $salt); } else { $builder->where($saltField, $salt); } } $builder->groupEnd(); if ($medId > 0) { $builder->where($idField . ' !=', $medId); } $rows = $builder->orderBy($nameField, 'ASC')->limit(25)->get()->getResultArray(); return $this->response->setJSON(['rows' => $rows]); } public function medicine_favorite_toggle() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $userId = (int) ($this->getCurrentUserId() ?? 0); $medId = (int) $this->request->getPost('med_id'); if ($userId <= 0 || $medId <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid user or medicine']); } if (! $this->db->tableExists('opd_medicine_favorites')) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine favorites table not found. Run migration first.']); } $exists = $this->db->table('opd_medicine_favorites') ->where('user_id', $userId) ->where('med_id', $medId) ->get(1) ->getRowArray(); if ($exists) { $this->db->table('opd_medicine_favorites') ->where('user_id', $userId) ->where('med_id', $medId) ->delete(); return $this->response->setJSON([ 'update' => 1, 'is_favorite' => 0, 'error_text' => 'Removed from favorites', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $this->db->table('opd_medicine_favorites')->insert([ 'user_id' => $userId, 'med_id' => $medId, 'created_at' => date('Y-m-d H:i:s'), ]); return $this->response->setJSON([ 'update' => 1, 'is_favorite' => 1, 'error_text' => 'Added to favorites', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } public function medicine_list($sessionId) { $table = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']); if ($table === null) { return $this->response->setJSON(['rows' => []]); } $fields = $this->db->getFieldNames($table); $builder = $this->db->table($table); if (in_array('opd_pre_id', $fields, true)) { if ((int) $sessionId <= 0) { return $this->response->setJSON(['rows' => []]); } $builder->where('opd_pre_id', (int) $sessionId); } else { return $this->response->setJSON(['rows' => []]); } if (in_array('id', $fields, true)) { $builder->orderBy('id', 'DESC'); } $rows = $builder->get()->getResultArray(); $doseMap = $this->getDoseMasterLabelMap('opd_dose_shed', ['dose_shed_id', 'id'], ['dose_show_sign', 'dose_sign', 'dose_sign_desc', 'name']); $whenMap = $this->getDoseMasterLabelMap('opd_dose_when', ['dose_when_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']); $freqMap = $this->getDoseMasterLabelMap('opd_dose_frequency', ['dose_freq_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']); $whereMap = $this->getDoseMasterLabelMap('opd_dose_where', ['dose_where_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']); foreach ($rows as &$row) { $row['dosage_label'] = $this->resolveDoseMasterLabel($row['dosage'] ?? '', $doseMap); $row['dosage_when_label'] = $this->resolveDoseMasterLabel($row['dosage_when'] ?? '', $whenMap); $row['dosage_freq_label'] = $this->resolveDoseMasterLabel($row['dosage_freq'] ?? '', $freqMap); $row['dosage_where_label'] = $this->resolveDoseMasterLabel($row['dosage_where'] ?? '', $whereMap); } unset($row); return $this->response->setJSON(['rows' => $rows]); } public function medicine_add() { if (!$this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']); } $opdId = (int) $this->request->getPost('opd_id'); $sessionId = (int) $this->request->getPost('opd_session_id'); $medName = trim((string) $this->request->getPost('med_name')); $medType = trim((string) $this->request->getPost('med_type')); $medId = (int) $this->request->getPost('med_id'); if ($opdId <= 0 || $medName === '') { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']); } $table = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']); if ($table === null) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']); } $sessionId = $this->ensurePrescriptionSession($opdId, $sessionId); if ($sessionId <= 0) { return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create prescription session']); } $masterInfo = [ 'med_id' => 0, 'is_new' => false, 'details_updated' => false, 'details_source' => '', ]; if ($medId <= 0) { $masterInfo = $this->ensureMedicineMasterRecordFromConsult($medName, $medType); $medId = (int) ($masterInfo['med_id'] ?? 0); } $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('dosage_where', $fields, true)) { $insert['dosage_where'] = trim((string) $this->request->getPost('dosage_where')); } 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'))); } $this->applyDoseMasterStringFields($insert, $fields); $user = auth()->user(); if (in_array('update_by', $fields, true)) { $userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User'; $insert['update_by'] = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s'); } if ($this->isDuplicateMedicineRow($table, $fields, $sessionId, $insert)) { return $this->response->setJSON([ 'update' => 1, 'opd_session_id' => $sessionId, 'error_text' => 'Medicine already exists in this prescription. Duplicate skipped.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $this->db->table($table)->insert($insert); $insertId = (int) $this->db->insertID(); $this->auditClinicalUpdate('opd_prescription_medicine', 'added', $insertId, null, $insert); if ($medId > 0) { $this->trackMedicineUsage($medId); $this->syncMedicineMasterDefaults($medId, [ 'dosage' => $insert['dosage'] ?? '', 'dosage_when' => $insert['dosage_when'] ?? '', 'dosage_freq' => $insert['dosage_freq'] ?? '', 'dosage_where' => $insert['dosage_where'] ?? '', 'no_of_days' => $insert['no_of_days'] ?? '', 'qty' => $insert['qty'] ?? '', 'remark' => $insert['remark'] ?? '', ]); } $message = 'Medicine added'; if (! empty($masterInfo['is_new'])) { $message .= ' | New medicine saved in OPD master'; } if (! empty($masterInfo['details_updated'])) { $src = trim((string) ($masterInfo['details_source'] ?? 'master')); if ($src === '') { $src = 'master'; } $message .= ' | Master details updated (' . $src . ')'; } return $this->response->setJSON([ 'update' => 1, 'opd_session_id' => $sessionId, 'new_master_medicine' => ! empty($masterInfo['is_new']) ? 1 : 0, 'master_details_updated' => ! empty($masterInfo['details_updated']) ? 1 : 0, 'master_details_source' => (string) ($masterInfo['details_source'] ?? ''), 'error_text' => $message, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } /** * @return array{med_id:int,is_new:bool,details_updated:bool,details_source:string} */ private function ensureMedicineMasterRecordFromConsult(string $medName, string $medType = ''): array { $medName = trim($medName); $medType = trim($medType); if ($medName === '') { return ['med_id' => 0, 'is_new' => false, 'details_updated' => false, 'details_source' => '']; } $table = $this->findExistingTable(['opd_med_master']); if ($table === null) { return ['med_id' => 0, 'is_new' => false, 'details_updated' => false, 'details_source' => '']; } $fields = $this->db->getFieldNames($table); if (! in_array('id', $fields, true)) { return ['med_id' => 0, 'is_new' => false, 'details_updated' => false, 'details_source' => '']; } $medId = $this->resolveMedicineMasterIdByName($medName); $isNew = false; if ($medId <= 0) { $insert = []; if (in_array('item_name', $fields, true)) { $insert['item_name'] = strtoupper($medName); } elseif (in_array('med_name', $fields, true)) { $insert['med_name'] = strtoupper($medName); } if (in_array('formulation', $fields, true) && $medType !== '') { $insert['formulation'] = $medType; } if (! empty($insert)) { $this->db->table($table)->insert($insert); $medId = (int) $this->db->insertID(); $isNew = $medId > 0; } } if ($medId <= 0) { return ['med_id' => 0, 'is_new' => false, 'details_updated' => false, 'details_source' => '']; } $detailsUpdated = false; $detailsSource = ''; $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']); $row = $this->db->table($table)->where('id', $medId)->get(1)->getRowArray() ?? []; $hasDetails = false; if ($genericField !== null && trim((string) ($row[$genericField] ?? '')) !== '') { $hasDetails = true; } if ($saltField !== null && trim((string) ($row[$saltField] ?? '')) !== '') { $hasDetails = true; } if ($restrictionField !== null && trim((string) ($row[$restrictionField] ?? '')) !== '') { $hasDetails = true; } if (! $hasDetails && ($genericField !== null || $saltField !== null || $restrictionField !== null)) { $details = $this->lookupMedicineDetailsFromMaster($medName, $medType); if (($details['genericname'] ?? '') !== '' || ($details['salt_name'] ?? '') !== '' || ($details['dosage_restriction'] ?? '') !== '') { $detailsSource = 'local-master'; } if (($details['genericname'] ?? '') === '' && ($details['salt_name'] ?? '') === '' && ($details['dosage_restriction'] ?? '') === '') { $fallback = $this->inferMedicineDetailsFromBrandName($medName, $medType); if (($fallback['genericname'] ?? '') !== '' || ($fallback['salt_name'] ?? '') !== '' || ($fallback['dosage_restriction'] ?? '') !== '') { $details = $fallback; $detailsSource = 'brand-fallback'; } } $update = []; if ($genericField !== null && trim((string) ($details['genericname'] ?? '')) !== '') { $update[$genericField] = trim((string) ($details['genericname'] ?? '')); } if ($saltField !== null && trim((string) ($details['salt_name'] ?? '')) !== '') { $update[$saltField] = trim((string) ($details['salt_name'] ?? '')); } if ($restrictionField !== null && trim((string) ($details['dosage_restriction'] ?? '')) !== '') { $update[$restrictionField] = trim((string) ($details['dosage_restriction'] ?? '')); } if (! empty($update)) { $this->db->table($table)->where('id', $medId)->update($update); $detailsUpdated = true; } } return [ 'med_id' => $medId, 'is_new' => $isNew, 'details_updated' => $detailsUpdated, 'details_source' => $detailsSource, ]; } 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'), ]); } /** * Persist last-used dosage defaults back to master for quick reuse in consult form. * * @param array $defaults */ private function syncMedicineMasterDefaults(int $medId, array $defaults): void { if ($medId <= 0) { return; } $table = $this->findExistingTable(['opd_med_master']); if ($table === null) { return; } $fields = $this->db->getFieldNames($table); if (! in_array('id', $fields, true)) { return; } $fieldMap = [ 'dosage' => $this->resolveFirstField($fields, ['dosage']), 'dosage_when' => $this->resolveFirstField($fields, ['dosage_when']), 'dosage_freq' => $this->resolveFirstField($fields, ['dosage_freq']), 'dosage_where' => $this->resolveFirstField($fields, ['dosage_where']), 'no_of_days' => $this->resolveFirstField($fields, ['no_of_days']), 'qty' => $this->resolveFirstField($fields, ['qty']), 'remark' => $this->resolveFirstField($fields, ['remark']), ]; $update = []; foreach ($fieldMap as $payloadKey => $dbField) { if ($dbField === null) { continue; } $value = trim((string) ($defaults[$payloadKey] ?? '')); if ($value === '') { continue; } $update[$dbField] = $value; } if (empty($update)) { return; } $this->db->table($table)->where('id', $medId)->update($update); } /** * @return array */ private function getUserFavoriteMedicineIds(int $userId, array $medIds = []): array { if ($userId <= 0 || ! $this->db->tableExists('opd_medicine_favorites')) { return []; } $builder = $this->db->table('opd_medicine_favorites') ->select('med_id') ->where('user_id', $userId); $medIds = array_values(array_unique(array_filter(array_map('intval', $medIds), static fn (int $id): bool => $id > 0))); if (! empty($medIds)) { $builder->whereIn('med_id', $medIds); } $rows = $builder->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 $medIds = []): array { if ($userId <= 0 || ! $this->db->tableExists('opd_medicine_usage')) { return []; } $builder = $this->db->table('opd_medicine_usage') ->select('med_id,use_count,last_used_at') ->where('user_id', $userId); $medIds = array_values(array_unique(array_filter(array_map('intval', $medIds), static fn (int $id): bool => $id > 0))); if (! empty($medIds)) { $builder->whereIn('med_id', $medIds); } $rows = $builder->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')), 'dosage_where' => trim((string) $this->request->getPost('dosage_where')), '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; } } $this->applyDoseMasterStringFields($update, $fields); 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']); } $masterInfo = [ 'med_id' => 0, 'is_new' => false, 'details_updated' => false, 'details_source' => '', ]; $resolvedMedName = trim((string) ($update['med_name'] ?? $current['med_name'] ?? '')); $resolvedMedType = trim((string) ($update['med_type'] ?? $current['med_type'] ?? '')); $resolvedMedId = (int) ($current['med_id'] ?? 0); if ($resolvedMedId <= 0) { $masterInfo = $this->ensureMedicineMasterRecordFromConsult($resolvedMedName, $resolvedMedType); $resolvedMedId = (int) ($masterInfo['med_id'] ?? 0); } if ($resolvedMedId > 0 && in_array('med_id', $fields, true)) { $update['med_id'] = $resolvedMedId; } $candidate = array_replace($current, $update); $sessionId = (int) ($candidate['opd_pre_id'] ?? 0); if ($sessionId > 0 && $this->isDuplicateMedicineRow($table, $fields, $sessionId, $candidate, $id)) { return $this->response->setJSON([ 'update' => 0, 'error_text' => 'Same medicine row already exists. Duplicate update blocked.', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } $user = auth()->user(); if (in_array('update_by', $fields, true)) { $userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User'; $update['update_by'] = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s'); } $this->db->table($table)->where('id', $id)->update($update); $this->clinicalAuditTrail->logChangedFields('opd_prescription_medicine', $id, $current, array_replace($current, $update), $this->getCurrentUserId()); $masterMedId = (int) ($update['med_id'] ?? $current['med_id'] ?? 0); if ($masterMedId <= 0) { $masterMedId = $this->resolveMedicineMasterIdByName((string) ($update['med_name'] ?? $current['med_name'] ?? '')); } if ($masterMedId > 0) { $merged = array_replace($current, $update); $this->trackMedicineUsage($masterMedId); $this->syncMedicineMasterDefaults($masterMedId, [ 'dosage' => $merged['dosage'] ?? '', 'dosage_when' => $merged['dosage_when'] ?? '', 'dosage_freq' => $merged['dosage_freq'] ?? '', 'dosage_where' => $merged['dosage_where'] ?? '', 'no_of_days' => $merged['no_of_days'] ?? '', 'qty' => $merged['qty'] ?? '', 'remark' => $merged['remark'] ?? '', ]); } $message = 'Medicine updated'; if (! empty($masterInfo['is_new'])) { $message .= ' | New medicine saved in OPD master'; } if (! empty($masterInfo['details_updated'])) { $src = trim((string) ($masterInfo['details_source'] ?? 'master')); if ($src === '') { $src = 'master'; } $message .= ' | Master details updated (' . $src . ')'; } return $this->response->setJSON([ 'update' => 1, 'new_master_medicine' => ! empty($masterInfo['is_new']) ? 1 : 0, 'master_details_updated' => ! empty($masterInfo['details_updated']) ? 1 : 0, 'master_details_source' => (string) ($masterInfo['details_source'] ?? ''), 'error_text' => $message, 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); } private function isDuplicateMedicineRow(string $table, array $fields, int $sessionId, array $row, int $excludeId = 0): bool { if ($sessionId <= 0 || ! in_array('opd_pre_id', $fields, true)) { return false; } $builder = $this->db->table($table)->where('opd_pre_id', $sessionId); if ($excludeId > 0 && in_array('id', $fields, true)) { $builder->where('id !=', $excludeId); } foreach (['med_id', 'med_type', 'dosage', 'dosage_when', 'dosage_freq', 'no_of_days', 'qty', 'remark'] as $field) { if (! in_array($field, $fields, true)) { continue; } $value = isset($row[$field]) ? trim((string) $row[$field]) : ''; if ($value === '') { $builder->groupStart()->where($field, '')->orWhere($field, null)->groupEnd(); } else { $builder->where($field, $value); } } $candidates = $builder->get()->getResultArray(); if (empty($candidates)) { return false; } if (! in_array('med_name', $fields, true)) { return true; } $needle = $this->normalizeMedicineMatchKey((string) ($row['med_name'] ?? '')); if ($needle === '') { foreach ($candidates as $candidate) { $name = trim((string) ($candidate['med_name'] ?? '')); if ($name === '') { return true; } } return false; } foreach ($candidates as $candidate) { $current = $this->normalizeMedicineMatchKey((string) ($candidate['med_name'] ?? '')); if ($current !== '' && $current === $needle) { return true; } } return false; } private function normalizeMedicineMatchKey(string $value): string { $key = strtoupper(trim($value)); $key = preg_replace('/[^A-Z0-9]+/', '', $key) ?? $key; return trim((string) $key); } 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']); if ($medicineTable === null) { return $this->response->setBody('

Old medicine table not found.

'); } $html = '
'; foreach ($rows as $row) { $sessionId = (int) ($row['session_id'] ?? 0); $dateLabel = ! empty($row['apointment_date']) ? date('d/m/Y', strtotime((string) $row['apointment_date'])) : ''; $medRows = []; if ($sessionId > 0) { $medRows = $this->db->table($medicineTable) ->select('id,med_id,med_type,med_name,dosage,dosage_when,dosage_freq,no_of_days,qty,remark') ->where('opd_pre_id', $sessionId) ->orderBy('id', 'ASC') ->get() ->getResultArray(); } $html .= '
'; $html .= '
'; $html .= '
'; $html .= 'OPD: ' . esc((string) ($row['opd_code'] ?? $row['opd_id'])) . ''; if ($dateLabel !== '') { $html .= '' . esc($dateLabel) . ''; } $html .= '
'; $html .= ''; $html .= '
'; $html .= '
'; $html .= '
Complaint: ' . esc((string) ($row['complaints'] ?? '')) . '
'; $html .= '
Diagnosis: ' . esc((string) ($row['diagnosis'] ?? '')) . '
'; if (empty($medRows)) { $html .= '
No medicine row found.
'; } else { $html .= ''; $html .= ''; foreach ($medRows as $med) { $medType = trim((string) ($med['med_type'] ?? '')); $medName = trim((string) ($med['med_name'] ?? '')); $medLabel = trim($medType . ' ' . $medName); $doseLabel = trim( trim((string) ($med['dosage'] ?? '')) . ' ' . trim((string) ($med['dosage_when'] ?? '')) . ' ' . trim((string) ($med['dosage_freq'] ?? '')) ); if ($doseLabel === '') { $doseLabel = '-'; } $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; } $html .= '
PrescribedDose+
' . esc($medLabel !== '' ? $medLabel : '-') . '' . esc($doseLabel) . '' . '' . '
'; } $html .= '
'; $html .= '
'; } $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); if ($idField === null) { return []; } $labelField = null; $secondaryLabelField = null; $localLabelField = null; if ($table === 'opd_dose_shed') { $labelField = $this->resolveFirstField($fields, ['dose_show_sign', 'dose_sign', 'dose_sign_desc', 'name']); $localLabelField = $this->resolveFirstField($fields, ['dose_show_desc', 'dose_sign_hindi', 'dose_sign_desc']); } else { $labelField = $this->resolveFirstField($fields, ['dose_sign', 'dose_sign_desc', 'name']); $secondaryLabelField = $this->resolveFirstField($fields, ['dose_sign_desc', 'name']); $localLabelField = $this->resolveFirstField($fields, ['dose_sign_hindi', 'dose_sign_desc']); } if ($labelField === null) { $labelField = $this->resolveFirstField($fields, $labelCandidates); } if ($labelField === null) { return []; } $select = [$idField . ' as id', $labelField . ' as label']; if ($secondaryLabelField !== null && $secondaryLabelField !== $labelField) { $select[] = $secondaryLabelField . ' as secondary_label'; } if ($localLabelField !== null && $localLabelField !== $labelField) { $select[] = $localLabelField . ' as local_label'; } return $this->db->table($table) ->select(implode(',', $select)) ->where($labelField . ' !=', '') ->orderBy($idField, '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 === '' || $id === '0' || $label === '') { continue; } $map[$id] = $label; } return $map; } private function getDoseMasterLocalLabelMap(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['local_label'] ?? ($row['secondary_label'] ?? ''))); if ($id === '' || $id === '0' || $label === '') { continue; } $map[$id] = $label; } return $map; } private function resolveDoseMasterLabel($value, array $map): string { $raw = trim((string) $value); if ($raw === '' || $raw === '0') { return ''; } return $map[$raw] ?? $raw; } private function applyDoseMasterStringFields(array &$payload, array $fields): void { $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']); $stringFields = [ 'dosage_str' => $this->resolveDoseMasterLabel($payload['dosage'] ?? '', $doseMap), 'dosage_when_str' => $this->resolveDoseMasterLabel($payload['dosage_when'] ?? '', $whenMap), 'dosage_freq_str' => $this->resolveDoseMasterLabel($payload['dosage_freq'] ?? '', $freqMap), 'dosage_where_str' => $this->resolveDoseMasterLabel($payload['dosage_where'] ?? '', $whereMap), ]; foreach ($stringFields as $field => $value) { if ($value === '' || ! in_array($field, $fields, true)) { continue; } $payload[$field] = $value; } } 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 = $this->stripNabhFieldsFromRemarks($remarks); $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 stripNabhFieldsFromRemarks(string $remarks): 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; return trim($clean); } /** * @param array $row * @return array */ private function hydratePatientHistoryFields(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 = [ 'obstetric_history' => $pick(['obstetric_history', 'obstetric_hist', 'obstetric_details']), 'menstrual_history' => $pick(['menstrual_history', 'menstrual_hist', 'menstrual_details']), 'medical_surgical_history' => $pick(['medical_surgical_history', 'medical_surgical_hist', 'past_medical_surgical_history', 'medical_history', 'surgical_history']), 'family_history' => $pick(['family_history', 'family_hist']), 'allergic_history' => $pick(['allergic_history', 'allergy_history', 'allergy_hist']), 'vaccination_history' => $pick(['vaccination_history', 'vaccination_hist', 'immunization_history']), ]; $parsed = $this->extractPatientHistoryFieldsFromRemarks($remarks); foreach ($data as $key => $value) { if ($value === '' && ! empty($parsed[$key])) { $data[$key] = (string) $parsed[$key]; } } return $data; } /** * @param array $payload * @param array $fields * @param array $input * @return array */ private function mapPatientHistoryFieldsToExistingColumns(array &$payload, array $fields, array $input): array { $mapping = [ 'obstetric_history' => ['obstetric_history', 'obstetric_hist', 'obstetric_details'], 'menstrual_history' => ['menstrual_history', 'menstrual_hist', 'menstrual_details'], 'medical_surgical_history' => ['medical_surgical_history', 'medical_surgical_hist', 'past_medical_surgical_history', 'medical_history', 'surgical_history'], 'family_history' => ['family_history', 'family_hist'], 'allergic_history' => ['allergic_history', 'allergy_history', 'allergy_hist'], 'vaccination_history' => ['vaccination_history', 'vaccination_hist', 'immunization_history'], ]; $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; } /** * @return array */ private function extractPatientHistoryFieldsFromRemarks(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 [ 'obstetric_history' => $extract('/^\s*OBSTETRIC\s*HISTORY\s*:\s*(.+)$/im', $remarks), 'menstrual_history' => $extract('/^\s*MENSTRUAL\s*HISTORY\s*:\s*(.+)$/im', $remarks), 'medical_surgical_history' => $extract('/^\s*MEDICAL\s*\/\s*SURGICAL\s*HISTORY\s*:\s*(.+)$/im', $remarks), 'family_history' => $extract('/^\s*FAMILY\s*HISTORY\s*:\s*(.+)$/im', $remarks), 'allergic_history' => $extract('/^\s*ALLERGIC\s*HISTORY\s*:\s*(.+)$/im', $remarks), 'vaccination_history' => $extract('/^\s*VACCINATION\s*:\s*(.+)$/im', $remarks), ]; } private function stripPatientHistoryFieldsFromRemarks(string $remarks): string { $clean = trim($remarks); $clean = preg_replace('/\n?\s*OBSTETRIC\s*HISTORY\s*:\s*.+$/im', '', $clean) ?? $clean; $clean = preg_replace('/\n?\s*MENSTRUAL\s*HISTORY\s*:\s*.+$/im', '', $clean) ?? $clean; $clean = preg_replace('/\n?\s*MEDICAL\s*\/\s*SURGICAL\s*HISTORY\s*:\s*.+$/im', '', $clean) ?? $clean; $clean = preg_replace('/\n?\s*FAMILY\s*HISTORY\s*:\s*.+$/im', '', $clean) ?? $clean; $clean = preg_replace('/\n?\s*ALLERGIC\s*HISTORY\s*:\s*.+$/im', '', $clean) ?? $clean; $clean = preg_replace('/\n?\s*VACCINATION\s*:\s*.+$/im', '', $clean) ?? $clean; return trim($clean); } /** * @param array $payload * @param array $fields * @param array $input * @return array */ private function mapComplaintStructuredToExistingColumns(array &$payload, array $fields, array $input): array { $mapping = [ 'complaint_onset' => ['complaint_onset', 'symptom_onset', 'onset'], 'complaint_duration_days' => ['complaint_duration_days', 'symptom_duration_days', 'duration_days', 'complaint_duration'], 'complaint_severity' => ['complaint_severity', 'symptom_severity', 'severity'], ]; $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; } /** * @param array $input */ private function upsertComplaintStructuredIntoComplaints(string $complaints, array $input): string { $clean = $this->stripComplaintStructuredFromComplaints($complaints); $lines = []; if (trim((string) ($input['complaint_onset'] ?? '')) !== '') { $lines[] = 'Complaint Onset: ' . trim((string) ($input['complaint_onset'] ?? '')); } if (trim((string) ($input['complaint_duration_days'] ?? '')) !== '') { $lines[] = 'Complaint Duration (Days): ' . trim((string) ($input['complaint_duration_days'] ?? '')); } if (trim((string) ($input['complaint_severity'] ?? '')) !== '') { $lines[] = 'Complaint Severity: ' . trim((string) ($input['complaint_severity'] ?? '')); } if (empty($lines)) { return $clean; } if ($clean !== '') { return $clean . "\n" . implode("\n", $lines); } return implode("\n", $lines); } private function stripComplaintStructuredFromComplaints(string $complaints): string { $clean = trim($complaints); $clean = preg_replace('/\n?\s*Complaint\s*Onset\s*:\s*.+$/im', '', $clean) ?? $clean; $clean = preg_replace('/\n?\s*Complaint\s*Duration\s*\(\s*Days\s*\)\s*:\s*.+$/im', '', $clean) ?? $clean; $clean = preg_replace('/\n?\s*Complaint\s*Severity\s*:\s*.+$/im', '', $clean) ?? $clean; return trim($clean); } /** * @param array $payload * @param array $fields * @param array $input * @return array */ private function mapDiagnosisCodingToExistingColumns(array &$payload, array $fields, array $input): array { $mapping = [ 'diagnosis_snomed_id' => ['diagnosis_snomed_id', 'diagnosis_sctid', 'diagnosis_concept_id'], 'diagnosis_snomed_term' => ['diagnosis_snomed_term', 'diagnosis_sct_term', 'diagnosis_concept_term'], 'diagnosis_snomed_source' => ['diagnosis_snomed_source', 'diagnosis_sct_source', 'diagnosis_concept_source'], ]; $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; } /** * @param array $input */ private function upsertDiagnosisCodingIntoRemarks(string $remarks, array $input): string { $clean = $this->stripDiagnosisCodingFromRemarks($remarks); $lines = []; if (trim((string) ($input['diagnosis_snomed_id'] ?? '')) !== '') { $lines[] = 'Diagnosis SNOMED ID: ' . trim((string) ($input['diagnosis_snomed_id'] ?? '')); } if (trim((string) ($input['diagnosis_snomed_term'] ?? '')) !== '') { $lines[] = 'Diagnosis SNOMED Term: ' . trim((string) ($input['diagnosis_snomed_term'] ?? '')); } if (trim((string) ($input['diagnosis_snomed_source'] ?? '')) !== '') { $lines[] = 'Diagnosis SNOMED Source: ' . trim((string) ($input['diagnosis_snomed_source'] ?? '')); } if (empty($lines)) { return $clean; } if ($clean !== '') { return $clean . "\n" . implode("\n", $lines); } return implode("\n", $lines); } private function stripDiagnosisCodingFromRemarks(string $remarks): string { $clean = trim($remarks); $clean = preg_replace('/\n?\s*Diagnosis\s*SNOMED\s*ID\s*:\s*.+$/im', '', $clean) ?? $clean; $clean = preg_replace('/\n?\s*Diagnosis\s*SNOMED\s*Term\s*:\s*.+$/im', '', $clean) ?? $clean; $clean = preg_replace('/\n?\s*Diagnosis\s*SNOMED\s*Source\s*:\s*.+$/im', '', $clean) ?? $clean; return trim($clean); } /** * @param array $input */ private function upsertPatientHistoryFieldsIntoRemarks(string $remarks, array $input): string { $clean = $this->stripPatientHistoryFieldsFromRemarks($remarks); $labels = [ 'obstetric_history' => 'OBSTETRIC HISTORY', 'menstrual_history' => 'MENSTRUAL HISTORY', 'medical_surgical_history' => 'MEDICAL / SURGICAL HISTORY', 'family_history' => 'FAMILY HISTORY', 'allergic_history' => 'ALLERGIC HISTORY', 'vaccination_history' => 'VACCINATION', ]; $lines = []; foreach ($labels as $key => $label) { $value = trim((string) ($input[$key] ?? '')); if ($value !== '') { $lines[] = $label . ': ' . $value; } } if (empty($lines)) { return $clean; } if ($clean !== '') { return $clean . "\n" . implode("\n", $lines); } return implode("\n", $lines); } /** * @param array $input */ private function hasAnyPatientHistoryField(array $input): bool { foreach ($input as $value) { if (trim((string) $value) !== '') { return true; } } return false; } 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 ensureAutotypeKeywordTable(): bool { if ($this->db->tableExists('opd_autotype_keyword_master')) { return true; } try { $sql = "CREATE TABLE IF NOT EXISTS opd_autotype_keyword_master ( id INT AUTO_INCREMENT PRIMARY KEY, doc_id INT NOT NULL DEFAULT 0, section_key VARCHAR(80) NOT NULL, keyword VARCHAR(255) NOT NULL, is_active TINYINT NOT NULL DEFAULT 1, created_at DATETIME NULL, updated_at DATETIME NULL, UNIQUE KEY uniq_doc_section_keyword (doc_id, section_key, keyword(120)), INDEX idx_section_keyword (section_key, keyword(120)), INDEX idx_doc_active (doc_id, is_active) )"; $this->db->query($sql); } catch (\Throwable $e) { // Fallback for stricter MySQL/InnoDB index length limits. try { $fallbackSql = "CREATE TABLE IF NOT EXISTS opd_autotype_keyword_master ( id INT AUTO_INCREMENT PRIMARY KEY, doc_id INT NOT NULL DEFAULT 0, section_key VARCHAR(60) NOT NULL, keyword VARCHAR(180) NOT NULL, is_active TINYINT NOT NULL DEFAULT 1, created_at DATETIME NULL, updated_at DATETIME NULL, UNIQUE KEY uniq_doc_section_keyword (doc_id, section_key, keyword), INDEX idx_section_keyword (section_key, keyword), INDEX idx_doc_active (doc_id, is_active) )"; $this->db->query($fallbackSql); } catch (\Throwable $e2) { return false; } } return $this->db->tableExists('opd_autotype_keyword_master'); } private function isAutotypeKeywordSectionAllowed(string $section): bool { return in_array($section, ['complaints', 'provisional_diagnosis', 'finding_examinations'], true); } /** * @return array */ private function fetchAutotypeKeywords(string $section, string $q, int $docId, int $limit = 20): array { if ($q === '' || ! $this->isAutotypeKeywordSectionAllowed($section) || ! $this->ensureAutotypeKeywordTable()) { return []; } $builder = $this->db->table('opd_autotype_keyword_master') ->select('keyword,doc_id') ->where('is_active', 1) ->where('section_key', $section) ->like('keyword', $q) ->orderBy('doc_id', 'DESC') ->orderBy('updated_at', 'DESC') ->limit(max(1, $limit)); if ($docId > 0) { $builder->groupStart() ->where('doc_id', 0) ->orWhere('doc_id', $docId) ->groupEnd(); } else { $builder->where('doc_id', 0); } $rows = $builder->get()->getResultArray(); if (empty($rows)) { return []; } $out = []; $seen = []; foreach ($rows as $row) { $keyword = trim((string) ($row['keyword'] ?? '')); if ($keyword === '') { continue; } $k = strtoupper($keyword); if (isset($seen[$k])) { continue; } $seen[$k] = true; $out[] = $keyword; } return $out; } private function canManageMasterKeyword(): bool { $user = auth()->user(); if (! $user || ! method_exists($user, 'can')) { return false; } return $user->can('doctor_work.template_workspace.access') || $user->can('doctor_work.access'); } private function isTemplateSectionAllowed(string $section): bool { return in_array($section, ['finding_examinations', 'diagnosis', 'provisional_diagnosis', 'prescriber_remarks', 'advice'], true); } /** * @param array $candidateColumns * @return array */ private function collectRecentPrescriptionEntries(int $patientId, int $currentOpdId, array $candidateColumns, int $limit = 8): array { if ($patientId <= 0 || ! $this->db->tableExists('opd_prescription')) { return []; } $fields = $this->db->getFieldNames('opd_prescription') ?? []; $column = $this->resolveFirstField($fields, $candidateColumns); if ($column === null) { return []; } $rows = $this->db->table('opd_prescription') ->select($column . ' as section_text') ->where('p_id', $patientId) ->where('opd_id !=', $currentOpdId) ->where($column . ' IS NOT NULL', null, false) ->where($column . ' !=', '') ->orderBy('id', 'DESC') ->limit(40) ->get() ->getResultArray(); $out = []; $seen = []; foreach ($rows as $row) { $sectionText = trim((string) ($row['section_text'] ?? '')); if ($sectionText === '') { continue; } $parts = $this->splitRecentEntryText($sectionText); foreach ($parts as $part) { $normalized = $this->normalizeRecentEntryText($part); if ($normalized === '') { continue; } $key = strtolower($normalized); if (isset($seen[$key])) { continue; } $seen[$key] = true; $out[] = $normalized; if (count($out) >= $limit) { return $out; } } } return $out; } /** * @return array */ private function collectRecentInvestigationEntries(int $patientId, int $currentOpdId, int $limit = 8): array { $out = $this->collectRecentPrescriptionEntries($patientId, $currentOpdId, ['investigation'], $limit); $seen = []; foreach ($out as $item) { $seen[strtolower($item)] = true; } if (! $this->db->tableExists('opd_prescription_investigation') || ! $this->db->tableExists('opd_master')) { return array_slice($out, 0, $limit); } $fields = $this->db->getFieldNames('opd_prescription_investigation') ?? []; $nameField = $this->resolveFirstField($fields, ['investigation_name', 'name', 'test_name']); $opdField = $this->resolveFirstField($fields, ['opd_id', 'opdid']); if ($nameField === null || $opdField === null) { return array_slice($out, 0, $limit); } $builder = $this->db->table('opd_prescription_investigation i') ->select('i.`' . $nameField . '` as investigation_name') ->join('opd_master o', 'o.opd_id = i.`' . $opdField . '`', 'inner') ->where('o.p_id', $patientId) ->where('i.`' . $opdField . '` !=', $currentOpdId) ->where('i.`' . $nameField . '` IS NOT NULL', null, false) ->where('i.`' . $nameField . '` !=', '') ->limit(60); $idField = $this->resolveFirstField($fields, ['id', 'investigation_id']); if ($idField !== null) { $builder->orderBy('i.`' . $idField . '`', 'DESC'); } else { $builder->orderBy('i.`' . $opdField . '`', 'DESC'); } $rows = $builder->get()->getResultArray(); foreach ($rows as $row) { $name = $this->normalizeRecentEntryText((string) ($row['investigation_name'] ?? '')); if ($name === '') { continue; } $key = strtolower($name); if (isset($seen[$key])) { continue; } $seen[$key] = true; $out[] = $name; if (count($out) >= $limit) { break; } } return array_slice($out, 0, $limit); } /** * @return array */ private function splitRecentEntryText(string $text): array { $normalized = trim(str_replace(["\r\n", "\r"], "\n", $text)); if ($normalized === '') { return []; } $parts = preg_split('/[\n;]+/', $normalized) ?: []; if (count($parts) <= 1 && substr_count($normalized, ',') > 0 && strlen($normalized) <= 220) { $parts = preg_split('/,/', $normalized) ?: []; } if (empty($parts)) { return [$normalized]; } return $parts; } private function normalizeRecentEntryText(string $value, int $maxLen = 110): string { $text = trim($value); if ($text === '') { return ''; } $text = preg_replace('/^[\-\*\x{2022}\s]+/u', '', $text) ?? $text; $text = preg_replace('/\s+/', ' ', $text) ?? $text; $text = trim($text); if ($text === '') { return ''; } if (strlen($text) > $maxLen) { $text = rtrim(substr($text, 0, $maxLen - 3)) . '...'; } return $text; } 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)) { // JOIN through opd_master to guarantee per-doctor isolation $maxQueue = $this->db->query( 'SELECT COALESCE(MAX(pr.queue_no), 0) AS max_queue' . ' FROM opd_prescription pr' . ' INNER JOIN opd_master om ON om.opd_id = pr.opd_id' . ' WHERE pr.date_opd_visit = ? AND om.doc_id = ?', [date('Y-m-d'), (int) ($opdRow->doc_id ?? 0)] )->getRow(); $insert['queue_no'] = (int) ($maxQueue->max_queue ?? 0) + 1; } $this->db->table('opd_prescription')->insert($insert); return (int) $this->db->insertID(); } public function create_opd_queue(int $opdId) { if (! $this->canAccessPrescription()) { return $this->response->setStatusCode(403)->setBody('Access denied'); } if ($opdId <= 0) { return $this->response->setBody('Invalid OPD ID'); } $sessionId = $this->ensurePrescriptionSession($opdId, 0); if ($sessionId <= 0) { return $this->response->setBody('Unable to create queue'); } return $this->response->setBody('Queue Created'); } 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', 'period_start' => Time::now('Asia/Kolkata')->toDateTimeString(), 'period_end' => Time::now('Asia/Kolkata')->toDateTimeString(), ]; $medications = $this->getPrescriptionMedicines($sessionId); $conditions = $this->getPrescriptionConditions($sessionId); $clinicalContext = $this->getPrescriptionClinicalContext($sessionId, $opdRow); $bundle = $this->fhirR4Builder->buildPrescriptionBundle($patient, $encounter, $medications, $conditions, $clinicalContext); $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(); $masterTable = $this->findExistingTable(['opd_med_master']); $masterById = []; if ($masterTable !== null) { $masterFields = $this->db->getFieldNames($masterTable) ?? []; $masterNameField = $this->resolveFirstField($masterFields, ['item_name', 'med_name']); $masterGenericField = $this->resolveFirstField($masterFields, ['genericname', 'generic_name']); $masterTypeField = $this->resolveFirstField($masterFields, ['formulation']); $masterSnomedField = $this->resolveFirstField($masterFields, ['snomed_code', 'medicine_snomed_code', 'snomed_ct_code', 'sctid']); $masterAtcField = $this->resolveFirstField($masterFields, ['atc_code', 'who_atc_code']); $medIds = []; foreach ($rows as $row) { $mid = (int) ($row['med_id'] ?? 0); if ($mid > 0) { $medIds[$mid] = $mid; } } if (! empty($medIds)) { $select = ['id']; if ($masterNameField !== null) { $select[] = $masterNameField . ' as item_name'; } if ($masterGenericField !== null) { $select[] = $masterGenericField . ' as genericname'; } if ($masterTypeField !== null) { $select[] = $masterTypeField . ' as formulation'; } if ($masterSnomedField !== null) { $select[] = $masterSnomedField . ' as snomed_code'; } if ($masterAtcField !== null) { $select[] = $masterAtcField . ' as atc_code'; } $masterRows = $this->db->table($masterTable) ->select(implode(',', $select)) ->whereIn('id', array_values($medIds)) ->get() ->getResultArray(); foreach ($masterRows as $mrow) { $mid = (int) ($mrow['id'] ?? 0); if ($mid > 0) { $masterById[$mid] = $mrow; } } } } $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') : '', ]); $mid = (int) ($row['med_id'] ?? 0); $master = $mid > 0 ? ($masterById[$mid] ?? []) : []; $drugName = trim((string) ($row['med_name'] ?? '')); if ($drugName === '') { $drugName = trim((string) ($master['item_name'] ?? '')); } $genericName = trim((string) ($master['genericname'] ?? '')); $medType = trim((string) ($row['med_type'] ?? '')); if ($medType === '') { $medType = trim((string) ($master['formulation'] ?? '')); } $medications[] = [ 'drug_name' => $drugName, 'status' => 'active', 'dosage' => implode(' | ', $dosageParts), 'route_text' => trim((string) ($row['dosage_where'] ?? '')), 'frequency_text' => trim((string) ($row['dosage_freq'] ?? '')), 'med_id' => $mid, 'generic_name' => $genericName, 'med_type' => $medType, 'snomed_code' => trim((string) ($master['snomed_code'] ?? '')), 'atc_code' => strtoupper(trim((string) ($master['atc_code'] ?? ''))), ]; } return $medications; } /** * @return array> */ private function getPrescriptionConditions(int $sessionId): array { if (! $this->db->tableExists('opd_prescription') || $sessionId <= 0) { return []; } $fields = $this->db->getFieldNames('opd_prescription') ?? []; $selectFields = ['id']; foreach (['diagnosis', 'diagnosis_snomed_id', 'diagnosis_snomed_term', 'Provisional_diagnosis', 'provisional_diagnosis_snomed_id', 'provisional_diagnosis_snomed_term'] as $field) { if (in_array($field, $fields, true)) { $selectFields[] = $field; } } $row = $this->db->table('opd_prescription') ->select(implode(',', $selectFields)) ->where('id', $sessionId) ->get(1) ->getRowArray(); if (! is_array($row) || empty($row)) { return []; } $diagnosis = trim((string) ($row['diagnosis'] ?? '')); $diagnosisSnomedId = trim((string) ($row['diagnosis_snomed_id'] ?? '')); $diagnosisSnomedTerm = trim((string) ($row['diagnosis_snomed_term'] ?? '')); $provisional = trim((string) ($row['Provisional_diagnosis'] ?? '')); $snomedId = trim((string) ($row['provisional_diagnosis_snomed_id'] ?? '')); $snomedTerm = trim((string) ($row['provisional_diagnosis_snomed_term'] ?? '')); if ($diagnosisSnomedId === '' && $diagnosis !== '' && strpos($diagnosis, ',') === false) { $resolvedDiagnosis = $this->resolveProvisionalDiagnosisSnomedMatch($diagnosis); if (! empty($resolvedDiagnosis['concept_id'])) { $diagnosisSnomedId = (string) ($resolvedDiagnosis['concept_id'] ?? ''); $diagnosisSnomedTerm = (string) ($resolvedDiagnosis['term'] ?? $diagnosis); } } if ($snomedId === '' && $provisional !== '' && strpos($provisional, ',') === false) { $resolved = $this->resolveProvisionalDiagnosisSnomedMatch($provisional); if (! empty($resolved['concept_id'])) { $snomedId = (string) ($resolved['concept_id'] ?? ''); $snomedTerm = (string) ($resolved['term'] ?? $provisional); } } $out = []; $seen = []; if ($diagnosis !== '') { $k = strtoupper($diagnosis) . '|' . $diagnosisSnomedId; $seen[$k] = true; $out[] = [ 'text' => $diagnosis, 'verification_status' => 'confirmed', 'snomed_code' => $diagnosisSnomedId, 'snomed_display' => $diagnosisSnomedTerm !== '' ? $diagnosisSnomedTerm : $diagnosis, ]; } if ($provisional !== '') { $k = strtoupper($provisional) . '|' . $snomedId; if (! isset($seen[$k])) { $seen[$k] = true; $out[] = [ 'text' => $provisional, 'verification_status' => 'provisional', 'snomed_code' => $snomedId, 'snomed_display' => $snomedTerm !== '' ? $snomedTerm : $provisional, ]; } } return $out; } /** * @param array $opdRow * @return array */ private function getPrescriptionClinicalContext(int $sessionId, array $opdRow): array { if ($sessionId <= 0 || ! $this->db->tableExists('opd_prescription')) { return []; } $row = $this->db->table('opd_prescription')->where('id', $sessionId)->get(1)->getRowArray() ?? []; if (empty($row)) { return []; } $remarks = (string) ($row['Prescriber_Remarks'] ?? ''); $nabh = $this->hydrateNabhPrescriptionFields($row, $remarks); $complaintsText = trim((string) ($row['complaints'] ?? '')); $complaints = []; if ($complaintsText !== '') { $parts = preg_split('/[,;\n\r]+/', $complaintsText) ?: []; $seen = []; foreach ($parts as $part) { $part = trim((string) $part); if ($part === '' || preg_match('/^Complaint\s*(Onset|Duration|Severity)\s*:/i', $part) === 1) { continue; } $k = strtoupper($part); if (isset($seen[$k])) { continue; } $seen[$k] = true; $complaint = [ 'text' => $part, ]; $resolvedComplaint = $this->resolveProvisionalDiagnosisSnomedMatch($part); if (! empty($resolvedComplaint['code'])) { $complaint['snomed_code'] = (string) $resolvedComplaint['code']; $complaint['snomed_display'] = (string) ($resolvedComplaint['display'] ?? $part); } $complaints[] = $complaint; } } $observations = []; $observationMap = [ 'pulse' => ['loinc' => '8867-4', 'display' => 'Heart rate', 'unit' => '/min', 'ucum' => '/min'], 'spo2' => ['loinc' => '59408-5', 'display' => 'Oxygen saturation in Arterial blood by Pulse oximetry', 'unit' => '%', 'ucum' => '%'], 'bp' => ['loinc' => '8480-6', 'display' => 'Systolic blood pressure', 'unit' => 'mmHg', 'ucum' => 'mm[Hg]'], 'diastolic' => ['loinc' => '8462-4', 'display' => 'Diastolic blood pressure', 'unit' => 'mmHg', 'ucum' => 'mm[Hg]'], 'temp' => ['loinc' => '8310-5', 'display' => 'Body temperature', 'unit' => 'Cel', 'ucum' => 'Cel'], 'rr_min' => ['loinc' => '9279-1', 'display' => 'Respiratory rate', 'unit' => '/min', 'ucum' => '/min'], 'height' => ['loinc' => '8302-2', 'display' => 'Body height', 'unit' => 'cm', 'ucum' => 'cm'], 'weight' => ['loinc' => '29463-7', 'display' => 'Body weight', 'unit' => 'kg', 'ucum' => 'kg'], ]; foreach ($observationMap as $field => $meta) { $value = $this->parseNumericFromText((string) ($row[$field] ?? '')); if ($value === null) { continue; } $observations[] = [ 'loinc' => (string) ($meta['loinc'] ?? ''), 'display' => (string) ($meta['display'] ?? ''), 'value' => $value, 'unit' => (string) ($meta['unit'] ?? ''), 'ucum' => (string) ($meta['ucum'] ?? ''), ]; } $allergies = []; $allergyStatus = strtolower(trim((string) ($nabh['drug_allergy_status'] ?? ''))); $allergyDetails = trim((string) ($nabh['drug_allergy_details'] ?? '')); $adrHistory = trim((string) ($nabh['adr_history'] ?? '')); if ($allergyStatus === 'known' && $allergyDetails !== '') { $allergies[] = [ 'code_text' => $allergyDetails, 'reaction_text' => $adrHistory, 'clinical_status' => 'active', 'verification_status' => 'confirmed', ]; } $serviceRequests = []; $investigationText = trim((string) ($row['investigation'] ?? '')); if ($investigationText !== '') { $tests = preg_split('/[,;\n\r]+/', $investigationText) ?: []; $seenTests = []; foreach ($tests as $test) { $test = trim((string) $test); if ($test === '') { continue; } $k = strtoupper($test); if (isset($seenTests[$k])) { continue; } $seenTests[$k] = true; $serviceRequests[] = [ 'code_text' => $test, 'status' => 'active', 'intent' => 'order', ]; } } $appointments = []; $nextVisit = trim((string) ($row['next_visit'] ?? '')); $referTo = trim((string) ($row['refer_to'] ?? '')); if ($nextVisit !== '' || $referTo !== '') { $descParts = []; if ($nextVisit !== '') { $descParts[] = 'Follow-up: ' . $nextVisit; } if ($referTo !== '') { $descParts[] = 'Refer to: ' . $referTo; } $appointments[] = [ 'status' => 'proposed', 'description' => implode(' | ', $descParts), ]; } $hospitalProfile = $this->getHospitalProfileForFhir(); $practitionerName = trim((string) ($opdRow['doc_name'] ?? '')); $practitionerId = trim((string) ($opdRow['doc_id'] ?? '')); $doctorIdentity = $this->getDoctorAbdmIdentity((int) ($opdRow['doc_id'] ?? 0)); return [ 'complaints' => $complaints, 'observations' => $observations, 'allergies' => $allergies, 'service_requests' => $serviceRequests, 'appointments' => $appointments, 'practitioner' => [ 'id' => $practitionerId !== '' ? $practitionerId : 'doctor-unknown', 'name' => $practitionerName, 'registration_number' => trim((string) ($doctorIdentity['registration_number'] ?? ($hospitalProfile['doctor_registration'] ?? ''))), ], 'organization' => [ 'id' => trim((string) ($hospitalProfile['hfr_id'] ?? '')) !== '' ? preg_replace('/[^A-Za-z0-9\-]/', '-', (string) ($hospitalProfile['hfr_id'] ?? '')) : 'facility-unknown', 'name' => (string) ($hospitalProfile['name'] ?? ''), 'hfr_id' => (string) ($hospitalProfile['hfr_id'] ?? ''), ], ]; } /** * @return array{name:string,hfr_id:string,doctor_registration:string} */ private function getHospitalProfileForFhir(): array { $defaults = [ 'name' => '', 'hfr_id' => '', 'doctor_registration' => '', ]; if (! $this->db->tableExists('hospital_setting')) { return $defaults; } $rows = $this->db->table('hospital_setting') ->select('s_name,s_value') ->whereIn('s_name', ['H_name', 'H_HFR_ID', 'HPR_ID', 'DOCTOR_REG_NO']) ->get() ->getResultArray(); $map = []; foreach ($rows as $row) { $key = trim((string) ($row['s_name'] ?? '')); if ($key === '') { continue; } $map[$key] = trim((string) ($row['s_value'] ?? '')); } return [ 'name' => (string) ($map['H_name'] ?? ''), 'hfr_id' => (string) ($map['H_HFR_ID'] ?? ''), 'doctor_registration' => (string) (($map['HPR_ID'] ?? $map['DOCTOR_REG_NO'] ?? '')), ]; } /** * @return array{hpr_id:string,registration_number:string} */ private function getDoctorAbdmIdentity(int $docId): array { $out = [ 'hpr_id' => '', 'registration_number' => '', ]; if ($docId <= 0 || ! $this->db->tableExists('doctor_master')) { return $out; } $fields = $this->db->getFieldNames('doctor_master') ?? []; $select = ['id']; foreach (['hpr_id', 'hpr_no', 'hpr_number', 'doctor_reg_no', 'registration_no', 'reg_no', 'doc_reg_no'] as $field) { if (in_array($field, $fields, true)) { $select[] = $field; } } $row = $this->db->table('doctor_master') ->select(implode(',', $select)) ->where('id', $docId) ->get(1) ->getRowArray() ?? []; if (empty($row)) { return $out; } foreach (['hpr_id', 'hpr_no', 'hpr_number'] as $field) { if (! empty($row[$field])) { $out['hpr_id'] = strtoupper(trim((string) $row[$field])); break; } } foreach (['doctor_reg_no', 'registration_no', 'reg_no', 'doc_reg_no'] as $field) { if (! empty($row[$field])) { $out['registration_number'] = trim((string) $row[$field]); break; } } if ($out['registration_number'] === '' && $out['hpr_id'] !== '') { $out['registration_number'] = $out['hpr_id']; } return $out; } /** * @param array $payload * @param array $patientRow * @param array $opdRow * @return array */ private function validateAbdmReadinessForConsult(array $payload, array $patientRow, array $opdRow): array { $errors = []; $abhaField = $this->resolvePatientAbhaField(); $abha = $abhaField !== null ? trim((string) ($patientRow[$abhaField] ?? '')) : ''; if ($abha === '') { $errors[] = 'ABDM requires ABHA Address before saving consult.'; } $diagnosisCode = trim((string) ($payload['diagnosis_snomed_id'] ?? '')); $provisionalCode = trim((string) ($payload['provisional_diagnosis_snomed_id'] ?? '')); if ($diagnosisCode === '' && $provisionalCode === '') { $errors[] = 'ABDM requires at least one SNOMED-coded diagnosis (Diagnosis or Provisional Diagnosis).'; } $complaints = trim((string) ($payload['complaints'] ?? '')); if ($complaints === '') { $errors[] = 'ABDM OP consult requires chief complaints.'; } $hasAnyVital = false; foreach (['bp', 'diastolic', 'pulse', 'temp', 'spo2', 'rr_min', 'height', 'weight'] as $vitalField) { if ($this->parseNumericFromText((string) ($payload[$vitalField] ?? '')) !== null) { $hasAnyVital = true; break; } } if (! $hasAnyVital) { $errors[] = 'ABDM OP consult requires at least one numeric vital observation.'; } $hospitalProfile = $this->getHospitalProfileForFhir(); if (trim((string) ($hospitalProfile['hfr_id'] ?? '')) === '') { $errors[] = 'Hospital HFR ID is required for ABDM document structure.'; } $doctorIdentity = $this->getDoctorAbdmIdentity((int) ($opdRow['doc_id'] ?? 0)); $doctorReg = trim((string) ($doctorIdentity['registration_number'] ?? '')); $fallbackDoctorReg = trim((string) ($hospitalProfile['doctor_registration'] ?? '')); if ($doctorReg === '' && $fallbackDoctorReg === '') { $errors[] = 'Doctor HPR/registration ID is required for ABDM practitioner mapping.'; } return $errors; } private function normalizeGender(string $value): string { $value = trim($value); if ($value === '1' || strtolower($value) === 'male') { return 'male'; } if ($value === '2' || strtolower($value) === 'female') { return 'female'; } return 'unknown'; } /** * @return string|int|null */ private function getCurrentUserId() { $user = auth()->user(); if ($user === null) { return null; } return $user->id ?? null; } /** * @return array */ private function getComplaintMatchesFromText(string $text): array { $pieces = preg_split('/[,;\n\r]+/', strtolower($text)) ?: []; $out = []; $directMap = [ 'kamar' => 'PAIN BACK', 'back pain' => 'PAIN BACK', 'pet dard' => 'PAIN IN ABDOMEN', 'body pain' => 'BODYACHE', 'kamzori' => 'WEAKNESS', 'weakness' => 'WEAKNESS', 'bukhar' => 'FEVER', 'khansi' => 'COUGH DRY', 'chakkar' => 'GIDDINESS', 'ulti' => 'VOMITING', 'thakan' => 'TIREDNESS', ]; foreach ($pieces as $piece) { $token = trim((string) $piece); if ($token === '') { continue; } $matchedName = null; foreach ($directMap as $key => $value) { if (str_contains($token, $key)) { $matchedName = $value; break; } } if ($matchedName !== null) { $out[strtoupper($matchedName)] = $matchedName; continue; } if (! $this->db->tableExists('complaints_master')) { $out[strtoupper($token)] = strtoupper($token); continue; } $row = $this->db->table('complaints_master') ->select('Name') ->where('is_active', 1) ->groupStart() ->like('Name', $token) ->orLike('name_hinglish', $token) ->orLike('keywords', $token) ->groupEnd() ->orderBy('show_in_short', 'DESC') ->orderBy('Code', 'ASC') ->get(1) ->getRowArray(); if (! empty($row['Name'])) { $name = trim((string) $row['Name']); $out[strtoupper($name)] = $name; } } return array_values($out); } /** * @param array $settingCandidates */ private function readAiSettingValue(array $settingCandidates): string { if ($this->db->tableExists('hospital_setting') && ! empty($settingCandidates)) { $row = $this->db->table('hospital_setting') ->select('s_value') ->whereIn('s_name', $settingCandidates) ->where('s_value !=', '') ->orderBy('s_name', 'ASC') ->get(1) ->getRowArray(); if (! empty($row['s_value'])) { return trim((string) $row['s_value']); } } return ''; } /** * @param array $complaints */ private function generateComplaintDraftWithAi(array $complaints, string $currentText): ?string { if (empty($complaints)) { return null; } $prompt = "You are a clinical documentation assistant for Indian OPD. Rewrite chief complaints in concise professional style. Input complaints: " . implode(', ', $complaints) . " Additional raw doctor notes: " . $currentText . " Output only one plain text sentence starting with 'Chief complaints:' and no markdown."; return $this->generateClinicalTextWithAi($prompt, 180); } private function buildClinicalPrompt(string $mode, string $text): string { if ($mode === 'hinglish_to_english') { return "You are a clinical OPD assistant. Convert Hinglish medical text to clear professional English. Keep all clinical meaning unchanged. Do not add new diagnosis. Return plain text only, no markdown. Input: " . $text; } if ($mode === 'local_to_medical') { return "You are a medical documentation specialist for Indian OPD. Convert the following patient complaints written in Hindi, Hinglish, or a mix of local language and English into precise clinical English with correct standard medical terminology. Rules: - Replace colloquial terms with proper medical terms (e.g., bukhar → pyrexia/fever, khansi → cough, saans phoolna → dyspnoea, sir dard → cephalalgia/headache, body dard → myalgia, thakaan → fatigue, chakkar → vertigo/giddiness, ulti → vomiting/emesis, jalan → burning sensation, thandi lagana → chills/rigors, kamzori → weakness/asthenia). - Preserve all durations, severity, and context exactly as mentioned. - Output concise, professional clinical English suitable for OPD documentation. - Return plain text only, no bullet points, no markdown. Input: " . $text; } if ($mode === 'local_to_patient_voice') { return "You are a translator for Indian hospital OPD. The following text is a patient describing their symptoms in Hindi, Hinglish, or mixed language. Translate it into natural, first-person English exactly as the patient would say it — conversational, not clinical. Rules: - Use first-person (\"I feel\", \"I have\", \"I am\", \"I've had\"). - Preserve the exact meaning, durations, and sequence. - Do NOT use medical jargon or clinical terms — keep it as natural spoken English. - Example: \"Teen din se bukhar hai\" → \"I've had a fever for three days.\" - Example: \"Thand lag rahi hai aur jukham hai\" → \"I'm feeling cold and I have a cold.\" - Return plain text only, no bullet points, no markdown. Input: " . $text; } if ($mode === 'diagnosis') { return "You are a clinical OPD assistant. Rewrite diagnosis text into concise professional medical English. Preserve meaning, do not invent findings. Return plain text only. Input: " . $text; } if ($mode === 'finding') { return "You are a clinical OPD assistant. Rewrite findings/examination notes into concise clinical English. Preserve original meaning and sequence. Return plain text only. Input: " . $text; } return "You are a clinical OPD assistant. Rewrite this OPD note into concise, professional English suitable for prescription documentation. Preserve meaning, do not add new facts. Return plain text only. Input: " . $text; } private function buildClinicalAutotypeLocalFallback(string $mode, string $text): string { $source = trim((string) $text); if ($source === '') { return ''; } if ($mode === 'hinglish_to_english') { $dictionary = [ 'bukhar' => 'fever', 'khansi' => 'cough', 'pet dard' => 'abdominal pain', 'sir dard' => 'headache', 'ulti' => 'vomiting', 'kamzori' => 'weakness', 'saans' => 'breath', 'saans phoolna' => 'breathlessness', 'jukaam' => 'cold', 'chakkar' => 'giddiness', 'dast' => 'loose motions', 'jalan' => 'burning sensation', ]; $converted = mb_strtolower($source); foreach ($dictionary as $from => $to) { $converted = preg_replace('/\b' . preg_quote($from, '/') . '\b/u', $to, $converted) ?? $converted; } return $this->normalizeClinicalSentenceFlow($converted); } if ($mode === 'local_to_medical') { $dictionary = [ 'bukhar' => 'fever', 'khansi' => 'cough', 'pet dard' => 'abdominal pain', 'sir dard' => 'headache', 'sar dard' => 'headache', 'ulti' => 'vomiting', 'kamzori' => 'weakness', 'saans phoolna' => 'dyspnoea', 'saans' => 'breathing', 'jukaam' => 'rhinitis', 'chakkar' => 'vertigo', 'dast' => 'loose motions', 'jalan' => 'burning sensation', 'thakaan' => 'fatigue', 'body dard' => 'myalgia', 'badan dard' => 'myalgia', 'thandi lagna' => 'chills', 'thandi lagana' => 'chills', 'kaanp' => 'rigors', 'neend' => 'sleep', 'bhookh' => 'appetite', 'paani' => 'water intake', 'peshab' => 'urination', 'seena dard' => 'chest pain', 'ghabrahat' => 'palpitations', ]; $converted = mb_strtolower($source); foreach ($dictionary as $from => $to) { $converted = preg_replace('/\b' . preg_quote($from, '/') . '\b/u', $to, $converted) ?? $converted; } return $this->normalizeClinicalSentenceFlow($converted); } if ($mode === 'local_to_patient_voice') { // Basic word-swap into first-person English; AI path is strongly preferred for this mode $dictionary = [ 'teen din se' => 'for three days I have had', 'do din se' => 'for two days I have had', 'ek din se' => 'since yesterday I have had', 'subah se' => 'since morning I have had', 'raat se' => 'since last night I have had', 'bukhar hai' => 'a fever', 'bukhar' => 'fever', 'khansi hai' => 'a cough', 'khansi' => 'a cough', 'jukham hai' => 'a cold', 'jukham' => 'a cold', 'thand lag rahi hai' => 'I am feeling cold', 'thandi lag rahi hai' => 'I am feeling cold', 'thand lag raha hai' => 'I am feeling cold', 'chakkar aa raha hai' => 'I am feeling dizzy', 'chakkar' => 'dizziness', 'ulti ho rahi hai' => 'I am vomiting', 'ulti' => 'vomiting', 'pet dard hai' => 'I have abdominal pain', 'pet dard' => 'abdominal pain', 'sir dard hai' => 'I have a headache', 'sir dard' => 'a headache', 'saans phool rahi hai' => 'I am having difficulty breathing', 'saans' => 'breathing', 'kamzori' => 'weakness', 'thakaan' => 'tiredness', 'dast' => 'loose motions', 'jalan' => 'a burning sensation', 'aur' => 'and', 'hai' => '', 'hain' => '', ]; $converted = mb_strtolower($source); foreach ($dictionary as $from => $to) { $converted = str_replace($from, $to, $converted); } $converted = preg_replace('/\s+/', ' ', trim($converted)) ?? $converted; $converted = ucfirst($converted); return $converted; } return $this->normalizeClinicalSentenceFlow($source); } /** * @return array|null */ private function parseJsonFromAiResponse(string $raw): ?array { $text = trim($raw); if ($text === '') { return null; } $decoded = json_decode($text, true); if (is_array($decoded)) { return $decoded; } if (preg_match('/\{(?:[^{}]|(?R))*\}/s', $text, $match) === 1) { $decoded = json_decode((string) ($match[0] ?? ''), true); if (is_array($decoded)) { return $decoded; } } return null; } private function normalizeClinicalSentenceFlow(string $text): string { $normalized = trim($text); if ($normalized === '') { return ''; } $normalized = preg_replace('/\s+/', ' ', $normalized) ?? $normalized; $normalized = str_replace([' ,', ' .', ' ;', ' :'], [',', '.', ';', ':'], $normalized); $normalized = str_replace(["\r\n", "\r"], "\n", $normalized); $chunks = preg_split('/(?<=[\.!?;])\s+|\n+/', $normalized) ?: []; $sentences = []; foreach ($chunks as $chunk) { $chunk = trim((string) $chunk); if ($chunk === '') { continue; } $chunk = ucfirst($chunk); if (! preg_match('/[\.!?]$/', $chunk)) { $chunk .= '.'; } $sentences[] = $chunk; } if (empty($sentences)) { return ucfirst($normalized); } return implode(' ', $sentences); } private function generateClinicalTextWithAi(string $prompt, int $maxOutputTokens = 300): ?string { $prompt = trim($prompt); if ($prompt === '') { return null; } $apiKey = trim($this->readAiSettingValue([ 'GEMINI_API_KEY', 'DIAGNOSIS_AI_GEMINI_API_KEY', 'DIAGNOSIS_GEMINI_API_KEY', ])); if ($apiKey === '') { $apiKey = trim((string) getenv('GEMINI_API_KEY')); } if ($apiKey === '') { return null; } $modelCandidatesRaw = trim($this->readAiSettingValue([ 'DIAGNOSIS_AI_GEMINI_MODELS', 'DIAGNOSIS_AI_GEMINI_MODEL', 'GEMINI_MODEL_CANDIDATES', 'GEMINI_MODEL', ])); $models = []; if ($modelCandidatesRaw !== '') { $parts = preg_split('/[,\s]+/', $modelCandidatesRaw) ?: []; foreach ($parts as $part) { $model = trim((string) $part); if ($model !== '') { $models[] = $model; } } } if (empty($models)) { $models = ['gemini-2.0-flash', 'gemini-1.5-flash']; } $maxOutputTokens = max(64, min(2048, (int) $maxOutputTokens)); foreach ($models as $model) { try { $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . rawurlencode($model) . ':generateContent?key=' . rawurlencode($apiKey); $payload = [ 'contents' => [[ 'parts' => [[ 'text' => $prompt, ]], ]], 'generationConfig' => [ 'temperature' => 0.2, 'maxOutputTokens' => $maxOutputTokens, ], ]; $options = [ 'timeout' => 30, 'http_errors' => false, ]; if (defined('ENVIRONMENT') && in_array((string) ENVIRONMENT, ['development', 'testing'], true)) { $options['verify'] = false; } $client = service('curlrequest', $options); $response = $client->post($url, [ 'headers' => [ 'Accept' => 'application/json', 'Content-Type' => 'application/json', ], 'body' => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), ]); $code = (int) $response->getStatusCode(); if ($code < 200 || $code >= 300) { continue; } $decoded = json_decode((string) $response->getBody(), true); if (!is_array($decoded)) { continue; } $out = ''; $candidates = $decoded['candidates'] ?? []; if (is_array($candidates)) { foreach ($candidates as $candidate) { $parts = $candidate['content']['parts'] ?? []; if (!is_array($parts)) { continue; } foreach ($parts as $part) { $piece = trim((string) ($part['text'] ?? '')); if ($piece !== '') { $out .= ($out === '' ? '' : "\n") . $piece; } } } } $out = trim($out); if ($out !== '') { $this->lastAiProvider = 'gemini'; return $out; } } catch (\Throwable $e) { continue; } } return null; } private function resolveAiServerBaseUrl(): string { $base = rtrim($this->readAiSettingValue(['DIAGNOSIS_AI_SERVER_URL']), '/'); if ($base !== '') { return $base; } $candidates = [ trim($this->readAiSettingValue(['DIAGNOSIS_AI_PARSE_ENDPOINT'])), trim($this->readAiSettingValue(['DIAGNOSIS_AI_OCR_ENDPOINT'])), ]; foreach ($candidates as $url) { if ($url === '') { continue; } $parts = @parse_url($url); if (! is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) { continue; } $base = (string) $parts['scheme'] . '://' . (string) $parts['host']; if (! empty($parts['port'])) { $base .= ':' . (int) $parts['port']; } return rtrim($base, '/'); } return ''; } /** * @return array */ private function resolveAiServerBaseCandidates(): array { $bases = []; $direct = trim($this->resolveAiServerBaseUrl()); if ($direct !== '') { $bases[] = rtrim($direct, '/'); } $derivedFromEndpoint = [ trim($this->readAiSettingValue(['DIAGNOSIS_AI_PARSE_ENDPOINT'])), trim($this->readAiSettingValue(['DIAGNOSIS_AI_OCR_ENDPOINT'])), ]; foreach ($derivedFromEndpoint as $endpointUrl) { if ($endpointUrl === '') { continue; } $parts = @parse_url($endpointUrl); if (! is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) { continue; } $origin = (string) $parts['scheme'] . '://' . (string) $parts['host']; if (! empty($parts['port'])) { $origin .= ':' . (int) $parts['port']; } $origin = rtrim($origin, '/'); $bases[] = $origin; $path = trim((string) ($parts['path'] ?? '')); if ($path !== '' && $path !== '/') { $dirPath = trim((string) dirname($path)); if ($dirPath !== '' && $dirPath !== '.') { $dirPath = '/' . trim($dirPath, '/'); $bases[] = $origin . $dirPath; $segments = array_values(array_filter(explode('/', trim($dirPath, '/')), static fn($x) => $x !== '')); if (count($segments) > 1) { $running = ''; foreach ($segments as $index => $segment) { if ($index >= count($segments) - 1) { break; } $running .= '/' . $segment; $bases[] = $origin . $running; } } } } } $bases[] = 'http://139.59.13.39:8000'; return array_values(array_unique(array_filter(array_map(static fn($v) => rtrim((string) $v, '/'), $bases)))); } private function translateAdviceWithAiServer(string $sourceText, string $targetLang): string { $base = $this->resolveAiServerBaseUrl(); if ($base === '') { return ''; } $token = trim($this->readAiSettingValue(['DIAGNOSIS_AI_PARSE_TOKEN'])); try { $options = [ 'timeout' => 20, 'http_errors' => false, ]; if (defined('ENVIRONMENT') && in_array((string) ENVIRONMENT, ['development', 'testing'], true)) { $options['verify'] = false; } $headers = ['Accept' => 'application/json']; if ($token !== '') { $headers['Authorization'] = 'Bearer ' . $token; } $client = service('curlrequest', $options); $url = $base . '/translate?text=' . rawurlencode($sourceText) . '&target_lang=' . rawurlencode($targetLang); $response = $client->get($url, ['headers' => $headers]); if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) { return ''; } $decoded = json_decode((string) $response->getBody(), true); $translated = trim((string) ($decoded['translated_text'] ?? '')); if ($translated === '') { return ''; } $this->lastAiProvider = 'ai-server'; return $translated; } catch (\Throwable $e) { return ''; } } /** * @param array $input */ private function buildFullClinicalDraftPrompt(array $input): string { $payload = json_encode($input, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); if ($payload === false) { $payload = '{}'; } return "You are a senior OPD clinical documentation assistant. Using the complete OPD snapshot JSON, produce concise professional drafts. Do not invent facts. Use only the data provided. Return ONLY valid minified JSON object with exactly these keys: finding_examinations, diagnosis, provisional_diagnosis, prescriber_remarks, investigation, advice. Each value must be plain text string (no markdown). OPD SNAPSHOT JSON: " . $payload; } /** * @return array|null */ private function extractFullDraftJson(?string $aiText): ?array { if ($aiText === null || trim($aiText) === '') { return null; } $candidate = trim($aiText); if (!str_starts_with($candidate, '{') || !str_ends_with($candidate, '}')) { if (preg_match('/\{.*\}/s', $candidate, $match) === 1) { $candidate = (string) ($match[0] ?? ''); } } $decoded = json_decode($candidate, true); if (!is_array($decoded)) { return null; } $keys = ['finding_examinations', 'diagnosis', 'provisional_diagnosis', 'prescriber_remarks', 'investigation', 'advice']; $result = []; foreach ($keys as $key) { $result[$key] = trim((string) ($decoded[$key] ?? '')); } return $result; } /** * @param array $input * @return array */ private function buildFullDraftFallback(array $input): array { $vitals = $this->joinParts([ ($input['bp'] ?? '') !== '' || ($input['diastolic'] ?? '') !== '' ? 'BP ' . trim((string) ($input['bp'] ?? '')) . '/' . trim((string) ($input['diastolic'] ?? '')) : '', ($input['pulse'] ?? '') !== '' ? 'Pulse ' . trim((string) $input['pulse']) . '/min' : '', ($input['temp'] ?? '') !== '' ? 'Temp ' . trim((string) $input['temp']) : '', ($input['spo2'] ?? '') !== '' ? 'SpO2 ' . trim((string) $input['spo2']) . '%' : '', ($input['rr_min'] ?? '') !== '' ? 'RR ' . trim((string) $input['rr_min']) . '/min' : '', ], '; '); $exam = $this->joinParts([ ($input['pallor'] ?? '') !== '' ? 'Pallor: ' . trim((string) $input['pallor']) : '', ($input['icterus'] ?? '') !== '' ? 'Icterus: ' . trim((string) $input['icterus']) : '', ($input['cyanosis'] ?? '') !== '' ? 'Cyanosis: ' . trim((string) $input['cyanosis']) : '', ($input['clubbing'] ?? '') !== '' ? 'Clubbing: ' . trim((string) $input['clubbing']) : '', ($input['edema'] ?? '') !== '' ? 'Edema: ' . trim((string) $input['edema']) : '', ($input['pain_value'] ?? '') !== '' ? 'Pain scale: ' . trim((string) $input['pain_value']) . '/4' : '', ], '; '); $complications = $this->joinParts([ !empty($input['pregnancy']) ? 'Pregnancy' : '', !empty($input['lactation']) ? 'Lactation' : '', !empty($input['liver_insufficiency']) ? 'Liver insufficiency' : '', !empty($input['renal_insufficiency']) ? 'Renal insufficiency' : '', !empty($input['pulmonary_insufficiency']) ? 'Pulmonary insufficiency' : '', !empty($input['corona_suspected']) ? 'Corona suspected' : '', !empty($input['dengue']) ? 'Dengue' : '', ], ', '); $addiction = $this->joinParts([ !empty($input['is_smoking']) ? 'Smoking' : '', !empty($input['is_alcohol']) ? 'Alcohol' : '', !empty($input['is_drug_abuse']) ? 'Drug abuse' : '', ], ', '); $women = $this->joinParts([ ($input['women_related_problems'] ?? '') !== '' ? 'Women related: ' . trim((string) $input['women_related_problems']) : '', ($input['women_lmp'] ?? '') !== '' ? 'LMP days before: ' . trim((string) $input['women_lmp']) : '', ($input['women_last_baby'] ?? '') !== '' ? 'Last baby: ' . trim((string) $input['women_last_baby']) : '', ($input['women_pregnancy_related'] ?? '') !== '' ? 'Pregnancy related: ' . trim((string) $input['women_pregnancy_related']) : '', ], '; '); $finding = $this->joinParts([ trim((string) ($input['finding_examinations'] ?? '')), $vitals !== '' ? 'Vitals: ' . $vitals : '', $exam !== '' ? 'General examination: ' . $exam : '', ], "\n"); $remarks = $this->joinParts([ trim((string) ($input['prescriber_remarks'] ?? '')), $complications !== '' ? 'Complication: ' . $complications : '', $addiction !== '' ? 'Addiction: ' . $addiction : '', $women, ], "\n"); $diagnosis = trim((string) ($input['diagnosis'] ?? '')); if ($diagnosis === '' && trim((string) ($input['complaints'] ?? '')) !== '') { $diagnosis = 'Provisional diagnosis to correlate with complaints: ' . trim((string) ($input['complaints'] ?? '')); } $provisional = trim((string) ($input['provisional_diagnosis'] ?? '')); if ($provisional === '') { $provisional = $diagnosis; } $investigation = trim((string) ($input['investigation'] ?? '')); $advice = $this->joinParts([ trim((string) ($input['advice'] ?? '')), trim((string) ($input['next_visit'] ?? '')) !== '' ? 'Next visit: ' . trim((string) ($input['next_visit'] ?? '')) : '', trim((string) ($input['refer_to'] ?? '')) !== '' ? 'Refer to: ' . trim((string) ($input['refer_to'] ?? '')) : '', ], "\n"); return [ 'finding_examinations' => $finding, 'diagnosis' => $diagnosis, 'provisional_diagnosis' => $provisional, 'prescriber_remarks' => $remarks, 'investigation' => $investigation, 'advice' => $advice, ]; } /** * @param array $parts */ private function joinParts(array $parts, string $separator = ', '): string { $filtered = []; foreach ($parts as $part) { $part = trim((string) $part); if ($part !== '') { $filtered[] = $part; } } return implode($separator, $filtered); } /** * @return array> */ private function buildPatientHistoryAlerts(int $patientId, int $currentOpdId): array { if ($patientId <= 0 || ! $this->db->tableExists('opd_prescription') || ! $this->db->tableExists('opd_master')) { return []; } $alerts = []; $rows = $this->db->table('opd_prescription p') ->select('p.*,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), ]; } $historyFields = $this->hydratePatientHistoryFields($row, (string) ($row['Prescriber_Remarks'] ?? '')); $historyLabels = [ 'obstetric_history' => 'Obstetric History', 'menstrual_history' => 'Menstrual History', 'medical_surgical_history' => 'Medical / Surgical History', 'family_history' => 'Family History', 'allergic_history' => 'Allergic History', 'vaccination_history' => 'Vaccination', ]; foreach ($historyLabels as $key => $label) { $value = trim((string) ($historyFields[$key] ?? '')); if ($value === '') { continue; } $alerts[] = [ 'type' => $label, 'message' => $dateLabel . ': ' . $value, ]; } } 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) { $this->createOpdPrescriptionWorkTask($opdId, $opdSessionId); $this->markOpdVisitedOnConsultCompletion((int) $opdId); $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); } private function markOpdVisitedOnConsultCompletion(int $opdId): void { if ($opdId <= 0 || ! $this->db->tableExists('opd_master')) { return; } // Only promote waiting/booked OPD to visited; leave cancelled/visited untouched. $this->db->table('opd_master') ->where('opd_id', $opdId) ->where('opd_status', 1) ->update(['opd_status' => 2]); } /** * @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' => '']; } private function createOpdPrescriptionWorkTask(int $opdId, int $opdSessionId): void { if ($opdId <= 0 || ! $this->db->tableExists('opd_master') || ! $this->db->tableExists('patient_master')) { return; } $opdRow = $this->db->table('opd_master')->select('opd_id,p_id')->where('opd_id', $opdId)->get(1)->getRowArray(); if (empty($opdRow)) { return; } $patientId = (int) ($opdRow['p_id'] ?? 0); if ($patientId <= 0) { return; } $pmFields = $this->db->getFieldNames('patient_master') ?? []; $abhaCols = array_filter(['abha_id', 'abha_no', 'abha_address', 'abha'], fn ($c) => in_array($c, $pmFields, true)); $abhaSelect = $abhaCols ? (', ' . implode(', ', $abhaCols)) : ''; $patient = $this->db->table('patient_master') ->select('p_fname' . $abhaSelect) ->where('id', $patientId) ->get(1) ->getRowArray(); $abhaId = trim((string) ($patient['abha_id'] ?? $patient['abha_no'] ?? $patient['abha_address'] ?? $patient['abha'] ?? '')); if (! $this->isValidAbhaNumber($abhaId)) { return; } $taskService = new AbdmWorkTaskService(); $taskService->createOrRefreshTask( 'opd_prescription_publish', 'opd_prescription', 'opd', (string) $opdId, $patientId, trim((string) ($patient['p_fname'] ?? '')), $abhaId, 'submit', [ 'opd_id' => $opdId, 'opd_session_id' => $opdSessionId, 'trigger' => 'opd.prescription.printed', ] ); } private function isValidAbhaNumber(string $abhaId): bool { return preg_match('/^\d{14}$/', trim($abhaId)) === 1; } }