12702 lines
508 KiB
PHP
12702 lines
508 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Controllers\BaseController;
|
|
use App\Libraries\AbdmWorkTaskService;
|
|
use App\Libraries\BridgeSyncService;
|
|
use App\Libraries\CsnotkTerminologyService;
|
|
use App\Models\OpdMedicineModel;
|
|
use CodeIgniter\I18n\Time;
|
|
|
|
class Opd_prescription extends BaseController
|
|
{
|
|
private string $lastAiProvider = 'none';
|
|
private string $lastAiServerError = '';
|
|
|
|
private function canAccessSnomedAdmin(): bool
|
|
{
|
|
$user = auth()->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<int, array{desc:string,days:int,date:string,value:string}>
|
|
*/
|
|
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')),
|
|
'complaint_snomed_json' => trim((string) $this->request->getPost('complaint_snomed_json')),
|
|
'diagnosis' => trim((string) $this->request->getPost('diagnosis')),
|
|
'diagnosis_json' => trim((string) $this->request->getPost('diagnosis_json')),
|
|
'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'] ?? ''),
|
|
];
|
|
$unmappedNabh = $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->hasAnyNabhField($unmappedNabh)) {
|
|
$payload['Prescriber_Remarks'] = $this->upsertNabhFieldsIntoRemarks(
|
|
(string) ($payload['Prescriber_Remarks'] ?? ''),
|
|
$unmappedNabh
|
|
);
|
|
}
|
|
|
|
if ($this->hasAnyPatientHistoryField($unmappedPatientHistory)) {
|
|
$payload['Prescriber_Remarks'] = $this->upsertPatientHistoryFieldsIntoRemarks(
|
|
(string) ($payload['Prescriber_Remarks'] ?? ''),
|
|
$unmappedPatientHistory
|
|
);
|
|
}
|
|
|
|
// Strip any legacy SNOMED lines previously embedded in remarks (no longer injected)
|
|
$payload['Prescriber_Remarks'] = $this->stripDiagnosisCodingFromRemarks(
|
|
(string) ($payload['Prescriber_Remarks'] ?? '')
|
|
);
|
|
|
|
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
|
|
);
|
|
}
|
|
}
|
|
|
|
// ABDM readiness is collected as non-blocking warnings; save proceeds regardless.
|
|
$abdmValidationErrors = $this->validateAbdmReadinessForConsult($payload, $patientRow, (array) $opdRow);
|
|
|
|
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);
|
|
|
|
// Queue background SNOMED coding (fail-open — do not block save response)
|
|
try {
|
|
if ($this->db->tableExists('opd_coding_queue') && $recordId > 0) {
|
|
$this->db->query(
|
|
"INSERT INTO opd_coding_queue (opd_id, opd_session_id, status, queued_at)"
|
|
. " VALUES (?, ?, 'pending', NOW())"
|
|
. " ON DUPLICATE KEY UPDATE status = 'pending', queued_at = NOW()",
|
|
[(int) $opdId, (int) $recordId]
|
|
);
|
|
}
|
|
} catch (\Throwable $queueEx) {
|
|
log_message('error', 'SNOMED coding queue insert failed: ' . $queueEx->getMessage());
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'opd_session_id' => $recordId,
|
|
'error_text' => 'Prescription saved',
|
|
'fhir_stored' => $fhirStored ? 1 : 0,
|
|
'abdm_warnings' => $abdmValidationErrors,
|
|
'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]);
|
|
}
|
|
|
|
/**
|
|
* SNOMED CT substance lookup for the medicine master form.
|
|
* Searches CSNOtk (substance/product semantic tags) for the given drug name term.
|
|
* Returns a ranked list of SNOMED CT concepts to pick from.
|
|
*
|
|
* GET /Opd_prescription/opd_medicince_snomed_lookup?term=amoxicillin&limit=10
|
|
*/
|
|
public function opd_medicince_snomed_lookup()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'rows' => []]);
|
|
}
|
|
|
|
$term = trim((string) $this->request->getGet('term'));
|
|
$limit = max(1, min(20, (int) ($this->request->getGet('limit') ?: 10)));
|
|
|
|
if ($term === '') {
|
|
return $this->response->setJSON(['update' => 1, 'rows' => [], 'message' => 'No term provided']);
|
|
}
|
|
|
|
try {
|
|
$svc = new \App\Libraries\CsnotkTerminologyService();
|
|
$rows = $svc->searchSubstance($term, $limit);
|
|
} catch (\Throwable $e) {
|
|
return $this->response->setJSON(['update' => 0, 'rows' => [], 'message' => 'SNOMED service error: ' . $e->getMessage()]);
|
|
}
|
|
|
|
// Normalise each row: extract clean display name + substance/product hint
|
|
$out = [];
|
|
foreach ($rows as $r) {
|
|
$conceptId = trim((string) ($r['concept_id'] ?? ''));
|
|
$fsn = trim((string) ($r['fsn'] ?? ''));
|
|
$term_ = trim((string) ($r['term'] ?? $fsn));
|
|
$hierarchy = trim((string) ($r['hierarchy'] ?? ''));
|
|
$source = trim((string) ($r['source'] ?? 'local'));
|
|
|
|
if ($conceptId === '' || $term_ === '') {
|
|
continue;
|
|
}
|
|
|
|
// Derive a clean INN-like name (term without parenthetical tag)
|
|
$displayTerm = $fsn !== '' ? preg_replace('/\s*\([^)]+\)\s*$/', '', $fsn) : $term_;
|
|
$displayTerm = trim((string) $displayTerm);
|
|
|
|
$out[] = [
|
|
'concept_id' => $conceptId,
|
|
'term' => $term_,
|
|
'fsn' => $fsn,
|
|
'display_term' => $displayTerm ?: $term_,
|
|
'hierarchy' => $hierarchy,
|
|
'source' => $source,
|
|
];
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'rows' => $out,
|
|
'count' => count($out),
|
|
'message' => count($out) === 0 ? 'No SNOMED CT substance concepts found for "' . $term . '"' : '',
|
|
]);
|
|
}
|
|
|
|
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<string, string>
|
|
*/
|
|
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<string, string>
|
|
*/
|
|
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<string, string>
|
|
*/
|
|
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', 'snomed_concept_id', 'snomed_term', 'loinc_code'] 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'] ?? ''),
|
|
'snomed_concept_id' => (string) ($row['snomed_concept_id'] ?? ''),
|
|
'snomed_term' => (string) ($row['snomed_term'] ?? ''),
|
|
'loinc_code' => (string) ($row['loinc_code'] ?? ''),
|
|
],
|
|
'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', 'snomed_concept_id', 'snomed_term', 'loinc_code'] 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_snomed_search()
|
|
{
|
|
$q = trim((string) $this->request->getGet('q'));
|
|
if ($q === '' || strlen($q) < 2) {
|
|
return $this->response->setJSON(['rows' => []]);
|
|
}
|
|
$svc = new \App\Libraries\CsnotkTerminologyService();
|
|
$rows = $svc->searchProcedure($q, 20);
|
|
return $this->response->setJSON(['rows' => $rows]);
|
|
}
|
|
|
|
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']);
|
|
$iSortOrder = $this->resolveFirstField($iFields, ['sort_id', 'sort_order', 'id', 'Code', 'code']);
|
|
|
|
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 ($iSortOrder !== null) {
|
|
$sql .= ', i.`' . $iSortOrder . '` ASC';
|
|
} elseif ($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(),
|
|
]);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────────────────────
|
|
|
|
// ─── Clinical Master Workspace (Complaints + Diagnosis/Disease) ────────
|
|
|
|
public function clinical_master_workspace(): string
|
|
{
|
|
return view('billing/opd_clinical_master_workspace');
|
|
}
|
|
|
|
// --- Complaints Master -------------------------------------------------
|
|
|
|
public function complaints_master_data(): \CodeIgniter\HTTP\ResponseInterface
|
|
{
|
|
if (! $this->db->tableExists('complaints_master')) {
|
|
return $this->response->setJSON(['data' => [], 'recordsTotal' => 0, 'recordsFiltered' => 0]);
|
|
}
|
|
|
|
$start = (int) ($this->request->getGet('start') ?? 0);
|
|
$length = (int) ($this->request->getGet('length') ?? 25);
|
|
$filter = trim((string) ($this->request->getGet('filter') ?? ''));
|
|
$snomed = (int) ($this->request->getGet('snomed_only') ?? 0);
|
|
|
|
$qb = $this->db->table('complaints_master');
|
|
|
|
if ($filter !== '') {
|
|
$qb->groupStart()
|
|
->like('Name', $filter)
|
|
->orLike('name_hinglish', $filter)
|
|
->orLike('keywords', $filter)
|
|
->groupEnd();
|
|
}
|
|
if ($snomed === 1) {
|
|
$qb->where('snomed_concept_id IS NOT NULL')->where("snomed_concept_id != ''");
|
|
}
|
|
|
|
$total = (clone $qb)->countAllResults(false);
|
|
$rows = $qb->select('Code, Name, show_in_short, name_hinglish, ai_hint, is_active, snomed_concept_id, snomed_term')
|
|
->orderBy('show_in_short', 'DESC')
|
|
->orderBy('Name', 'ASC')
|
|
->limit($length, $start)
|
|
->get()
|
|
->getResultArray();
|
|
|
|
return $this->response->setJSON([
|
|
'recordsTotal' => $total,
|
|
'recordsFiltered' => $total,
|
|
'data' => $rows,
|
|
]);
|
|
}
|
|
|
|
public function complaints_master_get(int $code): \CodeIgniter\HTTP\ResponseInterface
|
|
{
|
|
if (! $this->db->tableExists('complaints_master')) {
|
|
return $this->response->setStatusCode(404)->setJSON(['error' => 'Table not found']);
|
|
}
|
|
$row = $this->db->table('complaints_master')
|
|
->where('Code', $code)
|
|
->get(1)
|
|
->getRowArray();
|
|
if (! $row) {
|
|
return $this->response->setStatusCode(404)->setJSON(['error' => 'Not found']);
|
|
}
|
|
return $this->response->setJSON(['row' => $row]);
|
|
}
|
|
|
|
public function complaints_master_save(): \CodeIgniter\HTTP\ResponseInterface
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
|
|
}
|
|
if (! $this->db->tableExists('complaints_master')) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Table not found']);
|
|
}
|
|
|
|
$code = (int) ($this->request->getPost('Code') ?? 0);
|
|
$name = strtoupper(trim((string) ($this->request->getPost('Name') ?? '')));
|
|
$nameHinglish = trim((string) ($this->request->getPost('name_hinglish') ?? ''));
|
|
$keywords = trim((string) ($this->request->getPost('keywords') ?? ''));
|
|
$aiHint = trim((string) ($this->request->getPost('ai_hint') ?? ''));
|
|
$showInShort = (int) ($this->request->getPost('show_in_short') ?? 0);
|
|
$isActive = (int) ($this->request->getPost('is_active') ?? 1);
|
|
$snomedConceptId = trim((string) ($this->request->getPost('snomed_concept_id') ?? ''));
|
|
$snomedTerm = trim((string) ($this->request->getPost('snomed_term') ?? ''));
|
|
|
|
if ($name === '') {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Name is required']);
|
|
}
|
|
|
|
$payload = [
|
|
'Name' => $name,
|
|
'name_hinglish' => $nameHinglish !== '' ? $nameHinglish : null,
|
|
'keywords' => $keywords !== '' ? $keywords : null,
|
|
'ai_hint' => $aiHint !== '' ? $aiHint : null,
|
|
'show_in_short' => $showInShort ? 1 : 0,
|
|
'is_active' => $isActive ? 1 : 0,
|
|
'snomed_concept_id'=> $snomedConceptId !== '' ? $snomedConceptId : null,
|
|
'snomed_term' => $snomedTerm !== '' ? $snomedTerm : null,
|
|
'updated_at' => date('Y-m-d H:i:s'),
|
|
];
|
|
|
|
if ($code > 0) {
|
|
$this->db->table('complaints_master')->where('Code', $code)->update($payload);
|
|
$newCode = $code;
|
|
$msg = 'Complaint updated';
|
|
} else {
|
|
// auto Code
|
|
$maxCode = (int) ($this->db->table('complaints_master')->selectMax('Code')->get()->getRowArray()['Code'] ?? 0);
|
|
$newCode = $maxCode + 1;
|
|
$payload['Code'] = $newCode;
|
|
$payload['created_at'] = date('Y-m-d H:i:s');
|
|
$this->db->table('complaints_master')->insert($payload);
|
|
$msg = 'Complaint added';
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'error_text' => $msg,
|
|
'code' => $newCode,
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
public function complaints_master_remove(int $code): \CodeIgniter\HTTP\ResponseInterface
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
|
|
}
|
|
if (! $this->db->tableExists('complaints_master')) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Table not found']);
|
|
}
|
|
|
|
// Soft-delete: set is_active = 0
|
|
$this->db->table('complaints_master')
|
|
->where('Code', $code)
|
|
->update(['is_active' => 0, 'updated_at' => date('Y-m-d H:i:s')]);
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'error_text' => 'Complaint deactivated',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
// --- Disease/Diagnosis Master ------------------------------------------
|
|
|
|
public function disease_master_data(): \CodeIgniter\HTTP\ResponseInterface
|
|
{
|
|
if (! $this->db->tableExists('disease_master')) {
|
|
return $this->response->setJSON(['data' => [], 'recordsTotal' => 0, 'recordsFiltered' => 0]);
|
|
}
|
|
|
|
$start = (int) ($this->request->getGet('start') ?? 0);
|
|
$length = (int) ($this->request->getGet('length') ?? 25);
|
|
$filter = trim((string) ($this->request->getGet('filter') ?? ''));
|
|
$snomed = (int) ($this->request->getGet('snomed_only') ?? 0);
|
|
|
|
$fields = $this->db->getFieldNames('disease_master') ?? [];
|
|
$hasIsActive = in_array('is_active', $fields, true);
|
|
|
|
$qb = $this->db->table('disease_master');
|
|
|
|
if ($filter !== '') {
|
|
$qb->groupStart()->like('Name', $filter)->groupEnd();
|
|
}
|
|
if ($snomed === 1) {
|
|
$qb->where('snomed_concept_id IS NOT NULL')->where("snomed_concept_id != ''");
|
|
}
|
|
|
|
$total = (clone $qb)->countAllResults(false);
|
|
$select = 'Code, Name' . ($hasIsActive ? ', is_active' : '') . ', snomed_concept_id, snomed_term';
|
|
$rows = $qb->select($select)
|
|
->orderBy('Name', 'ASC')
|
|
->limit($length, $start)
|
|
->get()
|
|
->getResultArray();
|
|
|
|
return $this->response->setJSON([
|
|
'recordsTotal' => $total,
|
|
'recordsFiltered' => $total,
|
|
'data' => $rows,
|
|
]);
|
|
}
|
|
|
|
public function disease_master_get(int $code): \CodeIgniter\HTTP\ResponseInterface
|
|
{
|
|
if (! $this->db->tableExists('disease_master')) {
|
|
return $this->response->setStatusCode(404)->setJSON(['error' => 'Table not found']);
|
|
}
|
|
$row = $this->db->table('disease_master')
|
|
->where('Code', $code)
|
|
->get(1)
|
|
->getRowArray();
|
|
if (! $row) {
|
|
return $this->response->setStatusCode(404)->setJSON(['error' => 'Not found']);
|
|
}
|
|
return $this->response->setJSON(['row' => $row]);
|
|
}
|
|
|
|
public function disease_master_save(): \CodeIgniter\HTTP\ResponseInterface
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
|
|
}
|
|
if (! $this->db->tableExists('disease_master')) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Table not found']);
|
|
}
|
|
|
|
$code = (int) ($this->request->getPost('Code') ?? 0);
|
|
$name = strtoupper(trim((string) ($this->request->getPost('Name') ?? '')));
|
|
$snomedConceptId = trim((string) ($this->request->getPost('snomed_concept_id') ?? ''));
|
|
$snomedTerm = trim((string) ($this->request->getPost('snomed_term') ?? ''));
|
|
$isActive = (int) ($this->request->getPost('is_active') ?? 1);
|
|
|
|
if ($name === '') {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Name is required']);
|
|
}
|
|
|
|
$fields = $this->db->getFieldNames('disease_master') ?? [];
|
|
$hasIsActive = in_array('is_active', $fields, true);
|
|
|
|
$payload = [
|
|
'Name' => $name,
|
|
'snomed_concept_id' => $snomedConceptId !== '' ? $snomedConceptId : null,
|
|
'snomed_term' => $snomedTerm !== '' ? $snomedTerm : null,
|
|
];
|
|
if ($hasIsActive) {
|
|
$payload['is_active'] = $isActive ? 1 : 0;
|
|
}
|
|
|
|
if ($code > 0) {
|
|
$this->db->table('disease_master')->where('Code', $code)->update($payload);
|
|
$newCode = $code;
|
|
$msg = 'Diagnosis updated';
|
|
} else {
|
|
$maxCode = (int) ($this->db->table('disease_master')->selectMax('Code')->get()->getRowArray()['Code'] ?? 0);
|
|
$newCode = $maxCode + 1;
|
|
$payload['Code'] = $newCode;
|
|
$this->db->table('disease_master')->insert($payload);
|
|
$msg = 'Diagnosis added';
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'error_text' => $msg,
|
|
'code' => $newCode,
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
public function disease_master_remove(int $code): \CodeIgniter\HTTP\ResponseInterface
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
|
|
}
|
|
if (! $this->db->tableExists('disease_master')) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Table not found']);
|
|
}
|
|
|
|
$fields = $this->db->getFieldNames('disease_master') ?? [];
|
|
if (in_array('is_active', $fields, true)) {
|
|
$this->db->table('disease_master')->where('Code', $code)->update(['is_active' => 0]);
|
|
} else {
|
|
$this->db->table('disease_master')->where('Code', $code)->delete();
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'error_text' => 'Diagnosis deactivated',
|
|
'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('<div class="text-danger small">Access denied.</div>');
|
|
}
|
|
|
|
if ($opdId <= 0 || ! $this->db->tableExists('opd_master')) {
|
|
return $this->response->setBody('<div class="text-muted small">No scan history found.</div>');
|
|
}
|
|
|
|
$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('<div class="text-muted small">No scan history found.</div>');
|
|
}
|
|
|
|
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)
|
|
->whereIn('bundle_type', ['OPConsultRecord', '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)
|
|
->whereIn('bundle_type', ['OPConsultRecord', '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]);
|
|
}
|
|
|
|
/**
|
|
* POST Opd_prescription/fhir_bundle_regenerate
|
|
*
|
|
* Rebuilds the FHIR bundle for an OPD session using the current builder
|
|
* and overwrites the stored bundle in opd_fhir_documents.
|
|
* Accepts: opd_id, opd_session_id (0 = latest)
|
|
* Returns JSON: {ok:1, message, document_id, generated_at} or {ok:0, message}
|
|
*/
|
|
public function fhir_bundle_regenerate()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'message' => 'Invalid request']);
|
|
}
|
|
|
|
$opdId = (int) $this->request->getPost('opd_id');
|
|
$sessionId = (int) $this->request->getPost('opd_session_id');
|
|
|
|
return $this->response->setJSON($this->regenerateFhirBundleInternal($opdId, $sessionId));
|
|
}
|
|
|
|
/**
|
|
* GET Opd_prescription/fhir_bundle_regenerate
|
|
*
|
|
* Manual browser test wrapper. To execute regeneration, pass:
|
|
* ?opd_id={id}&opd_session_id={id-or-0}&run=1
|
|
*/
|
|
public function fhir_bundle_regenerate_manual()
|
|
{
|
|
$opdId = (int) $this->request->getGet('opd_id');
|
|
$sessionId = (int) $this->request->getGet('opd_session_id');
|
|
$run = trim((string) $this->request->getGet('run'));
|
|
|
|
if ($opdId <= 0 || $run !== '1') {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'message' => 'Manual test mode: call with ?opd_id={id}&opd_session_id=0&run=1 to execute regeneration.',
|
|
'example' => base_url('Opd_prescription/fhir_bundle_regenerate?opd_id=8&opd_session_id=0&run=1'),
|
|
]);
|
|
}
|
|
|
|
return $this->response->setJSON($this->regenerateFhirBundleInternal($opdId, $sessionId));
|
|
}
|
|
|
|
/**
|
|
* Shared core logic used by both POST-AJAX and manual GET regenerate endpoints.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function regenerateFhirBundleInternal(int $opdId, int $sessionId): array
|
|
{
|
|
|
|
if ($opdId <= 0) {
|
|
return ['ok' => 0, 'message' => 'opd_id is required'];
|
|
}
|
|
|
|
if (! $this->db->tableExists('opd_fhir_documents')) {
|
|
return ['ok' => 0, 'message' => 'FHIR bundle storage not available'];
|
|
}
|
|
|
|
// Load the OPD row
|
|
if (! $this->db->tableExists('opd_master')) {
|
|
return ['ok' => 0, 'message' => 'opd_master table not found'];
|
|
}
|
|
$opdRow = $this->db->table('opd_master')->where('opd_id', $opdId)->get(1)->getRowArray();
|
|
if (empty($opdRow)) {
|
|
return ['ok' => 0, 'message' => 'OPD record not found'];
|
|
}
|
|
|
|
// If no session given, find the latest prescription session
|
|
if ($sessionId <= 0 && $this->db->tableExists('opd_prescription')) {
|
|
$pRow = $this->db->table('opd_prescription')
|
|
->where('opd_id', $opdId)
|
|
->orderBy('id', 'DESC')
|
|
->get(1)->getRowArray();
|
|
$sessionId = (int) ($pRow['id'] ?? 0);
|
|
}
|
|
|
|
// Load patient row
|
|
$patientId = (int) ($opdRow['p_id'] ?? 0);
|
|
$patientRow = [];
|
|
if ($patientId > 0 && $this->db->tableExists('patient_master')) {
|
|
$patientRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray() ?? [];
|
|
}
|
|
if (empty($patientRow)) {
|
|
return ['ok' => 0, 'message' => 'Patient record not found'];
|
|
}
|
|
|
|
try {
|
|
// Rebuild bundle using current builder
|
|
$abhaField = $this->resolvePatientAbhaField();
|
|
$abhaAddress = $abhaField !== null ? trim((string) ($patientRow[$abhaField] ?? '')) : '';
|
|
|
|
$patFirstName = $this->sanitizePersonNamePart((string) ($patientRow['p_fname'] ?? ''));
|
|
$patLastName = $this->sanitizePersonNamePart((string) ($patientRow['p_lname'] ?? ''));
|
|
$patFullName = trim($patFirstName . ($patLastName !== '' ? ' ' . $patLastName : ''));
|
|
if ($patFullName === '') {
|
|
$patFullName = trim((string) ($opdRow['P_name'] ?? ''));
|
|
}
|
|
|
|
$patientFields = $this->db->getFieldNames('patient_master') ?? [];
|
|
$phoneField = $this->resolveFirstField($patientFields, ['p_contact', 'phone', 'mobile', 'contact_no', 'mobile_no', 'p_phone']);
|
|
$patPhone = $phoneField !== null ? trim((string) ($patientRow[$phoneField] ?? '')) : '';
|
|
|
|
$patient = [
|
|
'id' => (string) $patientId,
|
|
'name' => $patFullName,
|
|
'given_name' => $patFirstName,
|
|
'family_name' => $patLastName,
|
|
'gender' => $this->normalizeGender((string) ($patientRow['gender'] ?? '')),
|
|
'birthDate' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '',
|
|
'abhaAddress' => $abhaAddress,
|
|
'phone' => $patPhone,
|
|
];
|
|
|
|
$encounter = [
|
|
'id' => (string) $opdId,
|
|
'status' => 'finished',
|
|
'period_start' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
|
];
|
|
|
|
$medications = $this->getPrescriptionMedicines($sessionId);
|
|
$conditions = $this->getPrescriptionConditions($sessionId);
|
|
$clinicalContext = $sessionId > 0 ? $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');
|
|
$generatedAt = Time::now('Asia/Kolkata')->toDateTimeString();
|
|
|
|
// Overwrite the latest stored bundle for this opd_id / session
|
|
$existingBuilder = $this->db->table('opd_fhir_documents')
|
|
->where('opd_id', $opdId)
|
|
->whereIn('bundle_type', ['OPConsultRecord', 'MedicationRequestBundle']);
|
|
if ($sessionId > 0) {
|
|
$existingBuilder->where('opd_session_id', $sessionId);
|
|
}
|
|
$existing = $existingBuilder->orderBy('id', 'DESC')->get(1)->getRowArray();
|
|
|
|
if (! empty($existing)) {
|
|
$this->db->table('opd_fhir_documents')
|
|
->where('id', (int) $existing['id'])
|
|
->update([
|
|
'bundle_type' => 'OPConsultRecord',
|
|
'bundle_json' => $bundleJson,
|
|
'generated_by' => $generatedBy,
|
|
'generated_at' => $generatedAt,
|
|
]);
|
|
$documentId = (int) $existing['id'];
|
|
} else {
|
|
$this->db->table('opd_fhir_documents')->insert([
|
|
'opd_id' => $opdId,
|
|
'opd_session_id' => $sessionId,
|
|
'bundle_type' => 'OPConsultRecord',
|
|
'bundle_json' => $bundleJson,
|
|
'generated_by' => $generatedBy,
|
|
'generated_at' => $generatedAt,
|
|
]);
|
|
$documentId = (int) $this->db->insertID();
|
|
}
|
|
|
|
return [
|
|
'ok' => 1,
|
|
'message' => 'FHIR bundle regenerated successfully',
|
|
'document_id' => $documentId,
|
|
'generated_at' => $generatedAt,
|
|
'entry_count' => isset($bundle['entry']) ? count($bundle['entry']) : 0,
|
|
];
|
|
} catch (\Throwable $e) {
|
|
log_message('error', '[fhir_bundle_regenerate] ' . $e->getMessage());
|
|
return ['ok' => 0, 'message' => 'Regeneration failed: ' . $e->getMessage()];
|
|
}
|
|
}
|
|
|
|
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)
|
|
->whereIn('bundle_type', ['OPConsultRecord', '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),
|
|
]);
|
|
}
|
|
|
|
$prettyJson = (string) json_encode($bundle, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
|
|
$bundleType = (string) ($row['bundle_type'] ?? '');
|
|
if ($bundleType === '' || $bundleType === 'MedicationRequestBundle') {
|
|
$compositionProfiles = (array) ($bundle['entry'][0]['resource']['meta']['profile'] ?? []);
|
|
foreach ($compositionProfiles as $profile) {
|
|
if (stripos((string) $profile, 'OPConsultRecord') !== false) {
|
|
$bundleType = 'OPConsultRecord';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// AJAX callers (modal preview) get JSON directly
|
|
if ($this->request->isAJAX()) {
|
|
// Fetch live complaint and diagnosis terms from current prescription row
|
|
$liveComplaints = [];
|
|
$liveDiagnoses = [];
|
|
$storedSessionId = (int) ($row['opd_session_id'] ?? 0);
|
|
if ($this->db->tableExists('opd_prescription') && ($storedSessionId > 0 || $opdId > 0)) {
|
|
$prescBuilder = $this->db->table('opd_prescription');
|
|
if ($storedSessionId > 0) {
|
|
$prescBuilder->where('id', $storedSessionId);
|
|
} else {
|
|
$prescBuilder->where('opd_id', (int) $opdId)->orderBy('id', 'DESC');
|
|
}
|
|
$pRow = $prescBuilder->get(1)->getRowArray() ?? [];
|
|
if (! empty($pRow)) {
|
|
$sj = trim((string) ($pRow['complaint_snomed_json'] ?? ''));
|
|
if ($sj !== '' && $sj !== '[]') {
|
|
$sit = json_decode($sj, true);
|
|
if (is_array($sit)) { $liveComplaints = array_values($sit); }
|
|
}
|
|
$diagText = trim((string) ($pRow['diagnosis'] ?? ''));
|
|
$provText = trim((string) ($pRow['Provisional_diagnosis'] ?? ''));
|
|
if ($diagText !== '') {
|
|
$liveDiagnoses[] = [
|
|
'term' => $diagText,
|
|
'concept_id' => trim((string) ($pRow['diagnosis_snomed_id'] ?? '')),
|
|
'type' => 'confirmed',
|
|
];
|
|
}
|
|
if ($provText !== '') {
|
|
$liveDiagnoses[] = [
|
|
'term' => $provText,
|
|
'concept_id' => trim((string) ($pRow['provisional_diagnosis_snomed_id'] ?? '')),
|
|
'type' => 'provisional',
|
|
];
|
|
}
|
|
}
|
|
}
|
|
return $this->response->setJSON([
|
|
'status' => 'ok',
|
|
'bundle' => $bundle,
|
|
'document_id' => (int) ($row['id'] ?? 0),
|
|
'generated_at' => (string) ($row['generated_at'] ?? ''),
|
|
'bundle_type' => $bundleType,
|
|
'opd_id' => (int) $opdId,
|
|
'opd_session_id' => (int) $sessionId,
|
|
'live_complaints' => $liveComplaints,
|
|
'live_diagnoses' => $liveDiagnoses,
|
|
]);
|
|
}
|
|
|
|
// ── Full-page preview: also load patient / ABHA context ───────────────
|
|
$patientId = 0;
|
|
$patientName = '';
|
|
$abhaId = '';
|
|
$doctorName = '';
|
|
$visitDate = date('Y-m-d');
|
|
|
|
if ($this->db->tableExists('opd_master')) {
|
|
$opdRow = $this->db->table('opd_master')->where('opd_id', $opdId)->get(1)->getRowArray();
|
|
if (! empty($opdRow)) {
|
|
$patientId = (int) ($opdRow['p_id'] ?? 0);
|
|
$rawDate = (string) ($opdRow['opd_date'] ?? '');
|
|
if ($rawDate !== '') {
|
|
$ts = strtotime($rawDate);
|
|
if ($ts !== false) {
|
|
$visitDate = date('Y-m-d', $ts);
|
|
}
|
|
}
|
|
$docId2 = (int) ($opdRow['doc_id'] ?? 0);
|
|
if ($docId2 > 0 && $this->db->tableExists('doctor_master')) {
|
|
$dRow = $this->db->table('doctor_master')->where('id', $docId2)->get(1)->getRowArray();
|
|
if (! empty($dRow)) {
|
|
$doctorName = trim((string) ($dRow['p_fname'] ?? ''));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($patientId > 0 && $this->db->tableExists('patient_master')) {
|
|
$pFields = $this->db->getFieldNames('patient_master');
|
|
$pRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray();
|
|
if (! empty($pRow)) {
|
|
$fname = $this->sanitizePersonNamePart((string) ($pRow['p_fname'] ?? ''));
|
|
$lname = $this->sanitizePersonNamePart((string) ($pRow['p_lname'] ?? ''));
|
|
$patientName = trim($fname . ' ' . $lname);
|
|
foreach (['abha_number', 'abha_id', 'abha_address'] as $abhaField) {
|
|
if (in_array($abhaField, $pFields, true)) {
|
|
$val = trim((string) ($pRow[$abhaField] ?? ''));
|
|
if ($val !== '') {
|
|
$abhaId = $val;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$docId = (int) ($row['id'] ?? 0);
|
|
$generatedAt = htmlspecialchars((string) ($row['generated_at'] ?? ''), ENT_QUOTES, 'UTF-8');
|
|
$bundleType = htmlspecialchars($bundleType, ENT_QUOTES, 'UTF-8');
|
|
$entryCount = isset($bundle['entry']) && is_array($bundle['entry']) ? count($bundle['entry']) : 0;
|
|
$csrfTokName = csrf_token();
|
|
$csrfTokHash = csrf_hash();
|
|
$submitUrl = base_url('Opd_prescription/fhir_bundle_submit');
|
|
$gatewayUrl = base_url('AbdmGateway/gateway_status');
|
|
$regenerateUrl = base_url('Opd_prescription/fhir_bundle_regenerate');
|
|
|
|
// Count resource types inside the bundle
|
|
$resCounts = ['Composition' => 0, 'Patient' => 0, 'Practitioner' => 0,
|
|
'MedicationRequest' => 0, 'Condition' => 0, 'Observation' => 0,
|
|
'Encounter' => 0, 'AllergyIntolerance' => 0];
|
|
if (isset($bundle['entry']) && is_array($bundle['entry'])) {
|
|
foreach ($bundle['entry'] as $e) {
|
|
$rt = (string) ($e['resource']['resourceType'] ?? '');
|
|
if (isset($resCounts[$rt])) { $resCounts[$rt]++; }
|
|
}
|
|
}
|
|
|
|
// Encode data for JS
|
|
$jsBundle = json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
|
$jsContext = json_encode([
|
|
'opdId' => $opdId,
|
|
'sessionId' => $sessionId,
|
|
'patientId' => $patientId,
|
|
'patientName' => $patientName,
|
|
'abhaId' => $abhaId,
|
|
'visitDate' => $visitDate,
|
|
'doctorName' => $doctorName,
|
|
'docId' => $docId,
|
|
], JSON_UNESCAPED_UNICODE);
|
|
$jsPretty = json_encode(json_encode($bundle, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES));
|
|
|
|
// ── Build page ────────────────────────────────────────────────────────
|
|
$abhaDisplay = htmlspecialchars($abhaId ?: '(no ABHA)', ENT_QUOTES, 'UTF-8');
|
|
$pNameDisplay = htmlspecialchars($patientName ?: '(unknown)', ENT_QUOTES, 'UTF-8');
|
|
$docDisplay = htmlspecialchars($doctorName ?: '—', ENT_QUOTES, 'UTF-8');
|
|
$abhaClass = $abhaId !== '' ? 'bg-success' : 'bg-danger';
|
|
|
|
$html = '<!DOCTYPE html><html lang="en"><head>'
|
|
. '<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">'
|
|
. '<title>FHIR Preview — OPD #' . $opdId . '</title>'
|
|
. '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">'
|
|
. '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">'
|
|
. '<style>'
|
|
. 'body{background:#f0f2f5;font-size:.9rem}'
|
|
. '.card{border:none;box-shadow:0 1px 4px rgba(0,0,0,.1)}'
|
|
. 'pre{background:#1e1e2e;color:#cdd6f4;border-radius:8px;padding:1.2rem;font-size:.78rem;max-height:65vh;overflow:auto;tab-size:2}'
|
|
. '.check-item{padding:.4rem .6rem;border-radius:6px;margin-bottom:.35rem;display:flex;align-items:center;gap:.5rem}'
|
|
. '.check-item.pass{background:#d1fae5;color:#065f46}'
|
|
. '.check-item.fail{background:#fee2e2;color:#991b1b}'
|
|
. '.check-item.pending{background:#f1f5f9;color:#64748b}'
|
|
. '.step-badge{width:1.6rem;height:1.6rem;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-weight:700;font-size:.75rem;flex-shrink:0}'
|
|
. '#btnVerify:disabled,#btnSubmit:disabled{opacity:.5;cursor:not-allowed}'
|
|
. '</style>'
|
|
. '</head><body>'
|
|
. '<div class="container-xl py-3">'
|
|
|
|
// ── Header ──────────────────────────────────────────────────────
|
|
. '<div class="d-flex align-items-center justify-content-between mb-3 flex-wrap gap-2">'
|
|
. '<div>'
|
|
. '<h5 class="mb-0 fw-bold"><i class="bi bi-file-medical text-primary me-1"></i> FHIR Preview — OPD #' . $opdId . '</h5>'
|
|
. '<small class="text-muted">Doc #' . $docId . ' · Generated: ' . $generatedAt . ' · ' . $bundleType . ' · ' . $entryCount . ' entries</small>'
|
|
. '</div>'
|
|
. '<div class="d-flex gap-2 align-items-center">'
|
|
. '<button class="btn btn-sm btn-outline-warning" id="btnRegenerate" title="Rebuild bundle from prescription data (use after updating doctor or medication details)">'
|
|
. '<i class="bi bi-arrow-clockwise"></i> Regenerate'
|
|
. '</button>'
|
|
. '<a href="javascript:history.back()" class="btn btn-sm btn-outline-secondary"><i class="bi bi-arrow-left"></i> Back</a>'
|
|
. '</div>'
|
|
. '</div>'
|
|
|
|
// ── Patient card ────────────────────────────────────────────────
|
|
. '<div class="card mb-3"><div class="card-body py-2 px-3 d-flex flex-wrap gap-3 align-items-center">'
|
|
. '<div><div class="fw-bold">' . $pNameDisplay . '</div><div class="text-muted small">Patient ID: ' . $patientId . '</div></div>'
|
|
. '<div class="vr"></div>'
|
|
. '<div><span class="badge ' . $abhaClass . '">' . $abhaDisplay . '</span></div>'
|
|
. '<div class="vr"></div>'
|
|
. '<div><i class="bi bi-person-badge text-muted me-1"></i><small>Dr. ' . $docDisplay . '</small></div>'
|
|
. '<div class="vr"></div>'
|
|
. '<div><i class="bi bi-calendar3 text-muted me-1"></i><small>Visit: ' . htmlspecialchars($visitDate, ENT_QUOTES, 'UTF-8') . '</small></div>'
|
|
. '</div></div>'
|
|
|
|
// ── Resource summary row ─────────────────────────────────────────
|
|
. '<div class="row g-2 mb-3">';
|
|
|
|
$rIcons = ['Composition' => ['bi-file-text', 'primary'],
|
|
'Patient' => ['bi-person', 'success'],
|
|
'Encounter' => ['bi-calendar2-check', 'info'],
|
|
'Practitioner'=> ['bi-person-badge', 'secondary'],
|
|
'MedicationRequest' => ['bi-capsule', 'warning'],
|
|
'Condition' => ['bi-heart-pulse', 'danger'],
|
|
'Observation' => ['bi-activity', 'info'],
|
|
'AllergyIntolerance'=> ['bi-exclamation-triangle', 'danger']];
|
|
foreach ($rIcons as $rt => [$icon, $color]) {
|
|
$cnt = $resCounts[$rt] ?? 0;
|
|
$alpha = $cnt > 0 ? '' : ' opacity-50';
|
|
$html .= '<div class="col-6 col-sm-3 col-md-auto">'
|
|
. '<div class="card text-center px-3 py-2' . $alpha . '">'
|
|
. '<i class="bi ' . $icon . ' text-' . $color . '"></i>'
|
|
. '<div class="small fw-bold">' . $cnt . '</div>'
|
|
. '<div class="text-muted" style="font-size:.7rem">' . $rt . '</div>'
|
|
. '</div></div>';
|
|
}
|
|
$html .= '</div>';
|
|
|
|
// ── Main content: 2-col layout ───────────────────────────────────────
|
|
$html .= '<div class="row g-3">'
|
|
|
|
// ── LEFT: JSON view ──────────────────────────────────────────────
|
|
. '<div class="col-lg-7">'
|
|
. '<div class="card h-100"><div class="card-header d-flex justify-content-between align-items-center py-2">'
|
|
. '<strong class="small">FHIR R4 Bundle JSON</strong>'
|
|
. '<button class="btn btn-sm btn-outline-secondary" id="btnCopy"><i class="bi bi-clipboard"></i> Copy</button>'
|
|
. '</div><div class="card-body p-0"><pre id="jsonOut"></pre></div></div>'
|
|
. '</div>'
|
|
|
|
// ── RIGHT: Verify + Submit panel ─────────────────────────────────
|
|
. '<div class="col-lg-5">'
|
|
. '<div class="card mb-3"><div class="card-body">'
|
|
. '<div class="d-flex align-items-center gap-2 mb-3">'
|
|
. '<span class="step-badge bg-primary text-white">1</span>'
|
|
. '<strong>Verify Submission Readiness</strong>'
|
|
. '</div>'
|
|
. '<p class="text-muted small mb-3">Run a pre-flight check before submitting to ABDM bridge. All items must pass.</p>'
|
|
. '<div id="checkList">'
|
|
. '<div class="check-item pending"><i class="bi bi-circle text-muted"></i><span>ABHA ID / Address present</span></div>'
|
|
. '<div class="check-item pending"><i class="bi bi-circle text-muted"></i><span>Bundle type is "document"</span></div>'
|
|
. '<div class="check-item pending"><i class="bi bi-circle text-muted"></i><span>First entry is Composition</span></div>'
|
|
. '<div class="check-item pending"><i class="bi bi-circle text-muted"></i><span>Patient resource present</span></div>'
|
|
. '<div class="check-item pending"><i class="bi bi-circle text-muted"></i><span>At least one clinical resource</span></div>'
|
|
. '<div class="check-item pending"><i class="bi bi-circle text-muted"></i><span>Visit date valid</span></div>'
|
|
. '<div class="check-item pending" id="chkBridge"><i class="bi bi-circle text-muted"></i><span>Bridge connection reachable</span></div>'
|
|
. '</div>'
|
|
. '<button class="btn btn-primary w-100 mt-3" id="btnVerify">'
|
|
. '<i class="bi bi-shield-check me-1"></i> Verify Data'
|
|
. '</button>'
|
|
. '</div></div>'
|
|
|
|
. '<div class="card" id="submitCard" style="display:none">'
|
|
. '<div class="card-body">'
|
|
. '<div class="d-flex align-items-center gap-2 mb-3">'
|
|
. '<span class="step-badge bg-success text-white">2</span>'
|
|
. '<strong>Submit to ABDM Bridge</strong>'
|
|
. '</div>'
|
|
. '<p class="text-muted small mb-3">All checks passed. This will store the FHIR record in the bridge. ABDM links it when the patient opens their PHR app.</p>'
|
|
. '<button class="btn btn-success w-100" id="btnSubmit">'
|
|
. '<i class="bi bi-cloud-upload me-1"></i> Submit to ABDM'
|
|
. '</button>'
|
|
. '<div id="submitResult" class="mt-2"></div>'
|
|
. '</div></div>'
|
|
|
|
. '</div>' // end right col
|
|
. '</div>'; // end row
|
|
|
|
$html .= '</div>' // end container
|
|
. '<script>'
|
|
. 'const CTX=' . $jsContext . ';'
|
|
. 'const BUNDLE=' . $jsBundle . ';'
|
|
. 'const CSRF_NAME=' . json_encode($csrfTokName) . ';'
|
|
. 'const CSRF_HASH=' . json_encode($csrfTokHash) . ';'
|
|
. 'const SUBMIT_URL=' . json_encode($submitUrl) . ';'
|
|
. 'const GATEWAY_URL=' . json_encode($gatewayUrl) . ';'
|
|
. 'const REGENERATE_URL=' . json_encode($regenerateUrl) . ';'
|
|
|
|
// Render JSON
|
|
. 'document.getElementById("jsonOut").textContent=' . $jsPretty . ';'
|
|
|
|
// Copy button
|
|
. 'document.getElementById("btnCopy").addEventListener("click",function(){'
|
|
. ' navigator.clipboard.writeText(document.getElementById("jsonOut").textContent)'
|
|
. ' .then(()=>{this.innerHTML=\'<i class="bi bi-check2"></i> Copied\';setTimeout(()=>{this.innerHTML=\'<i class="bi bi-clipboard"></i> Copy\';},2000);})'
|
|
. ' .catch(()=>{});'
|
|
. '});'
|
|
|
|
// Verify button
|
|
. 'document.getElementById("btnVerify").addEventListener("click",async function(){'
|
|
. ' const btn=this;'
|
|
. ' btn.disabled=true;btn.innerHTML=\'<span class="spinner-border spinner-border-sm me-1"></span>Verifying\u2026\';'
|
|
. ' const checks=document.querySelectorAll("#checkList .check-item");'
|
|
. ' function setCheck(i,pass,msg){'
|
|
. ' checks[i].className="check-item "+(pass?"pass":"fail");'
|
|
. ' checks[i].innerHTML=(pass?\'<i class="bi bi-check-circle-fill text-success"></i>\':\'<i class="bi bi-x-circle-fill text-danger"></i>\')+\'<span>\'+msg+\'</span>\';'
|
|
. ' }'
|
|
|
|
// Check 1: ABHA
|
|
. ' const hasAbha=CTX.abhaId!=="";'
|
|
. ' setCheck(0,hasAbha,hasAbha?"ABHA: "+CTX.abhaId:"ABHA ID / Address missing — cannot push without ABHA");'
|
|
|
|
// Check 2: type=document
|
|
. ' const isDoc=BUNDLE.type==="document";'
|
|
. ' setCheck(1,isDoc,isDoc?"Bundle type: document":"Bundle type is \\""+BUNDLE.type+"\\" — must be \\"document\\"");'
|
|
|
|
// Check 3: First entry is Composition
|
|
. ' const firstRt=BUNDLE.entry&&BUNDLE.entry[0]&&BUNDLE.entry[0].resource?BUNDLE.entry[0].resource.resourceType:"";'
|
|
. ' const isComp=firstRt==="Composition";'
|
|
. ' setCheck(2,isComp,isComp?"First entry: Composition ✓":"First entry is "+firstRt+" — must be Composition");'
|
|
|
|
// Check 4: Patient resource
|
|
. ' const hasPatient=(BUNDLE.entry||[]).some(e=>e.resource&&e.resource.resourceType==="Patient");'
|
|
. ' setCheck(3,hasPatient,hasPatient?"Patient resource present":"Patient resource missing from bundle");'
|
|
|
|
// Check 5: Clinical resource
|
|
. ' const clinTypes=["MedicationRequest","Condition","Observation","AllergyIntolerance","DiagnosticReport"];'
|
|
. ' const clinCount=(BUNDLE.entry||[]).filter(e=>e.resource&&clinTypes.includes(e.resource.resourceType)).length;'
|
|
. ' setCheck(4,clinCount>0,"Clinical resources: "+clinCount+(clinCount>0?" present":" — bundle has no medications/conditions/observations"));'
|
|
|
|
// Check 6: visit date
|
|
. ' const dateOk=CTX.visitDate&&/^\d{4}-\d{2}-\d{2}$/.test(CTX.visitDate);'
|
|
. ' setCheck(5,dateOk,dateOk?"Visit date: "+CTX.visitDate:"Visit date missing or invalid");'
|
|
|
|
// Check 7: Bridge connectivity (AJAX)
|
|
. ' document.getElementById("chkBridge").className="check-item pending";'
|
|
. ' document.getElementById("chkBridge").innerHTML=\'<span class="spinner-border spinner-border-sm"></span><span>Checking bridge\u2026</span>\';'
|
|
. ' let bridgeOk=false;'
|
|
. ' try{'
|
|
. ' const r=await fetch(GATEWAY_URL,{headers:{"X-Requested-With":"XMLHttpRequest"},signal:AbortSignal.timeout(8000)});'
|
|
. ' const j=await r.json();'
|
|
. ' bridgeOk=r.ok&&(j.ok===1||j.status==="ok");'
|
|
. ' const info=bridgeOk?(j.authenticated_as?(" ("+j.authenticated_as+")"):""):" — "+( j.error_text||j.message||"unreachable");'
|
|
. ' setCheck(6,bridgeOk,bridgeOk?"Bridge connected"+info:"Bridge unreachable"+info);'
|
|
. ' }catch(e){setCheck(6,false,"Bridge check failed: "+e.message);}'
|
|
|
|
// Evaluate overall result
|
|
. ' const allPass=hasAbha&&isDoc&&isComp&&hasPatient&&clinCount>0&&dateOk&&bridgeOk;'
|
|
. ' btn.disabled=false;'
|
|
. ' if(allPass){'
|
|
. ' btn.className="btn btn-outline-success w-100 mt-3";'
|
|
. ' btn.innerHTML=\'<i class="bi bi-check-all me-1"></i>All Checks Passed — Re-verify\';'
|
|
. ' document.getElementById("submitCard").style.display="";'
|
|
. ' document.getElementById("btnSubmit").disabled=false;'
|
|
. ' }else{'
|
|
. ' btn.className="btn btn-warning w-100 mt-3";'
|
|
. ' btn.innerHTML=\'<i class="bi bi-exclamation-triangle me-1"></i>Issues Found — Fix & Re-verify\';'
|
|
. ' document.getElementById("submitCard").style.display="none";'
|
|
. ' }'
|
|
. '});'
|
|
|
|
// Submit button
|
|
. 'document.getElementById("btnSubmit").addEventListener("click",function(){'
|
|
. ' const btn=this,origHtml=btn.innerHTML;'
|
|
. ' if(!confirm("Submit FHIR bundle for OPD #' . $opdId . ' to ABDM gateway?"))return;'
|
|
. ' btn.disabled=true;btn.innerHTML=\'<span class="spinner-border spinner-border-sm me-1"></span>Submitting\u2026\';'
|
|
. ' const body=new URLSearchParams({opd_id:CTX.opdId,opd_session_id:CTX.sessionId});'
|
|
. ' body.append(CSRF_NAME,CSRF_HASH);'
|
|
. ' fetch(SUBMIT_URL,{method:"POST",headers:{"X-Requested-With":"XMLHttpRequest","Content-Type":"application/x-www-form-urlencoded"},body:body.toString()})'
|
|
. ' .then(r=>r.json()).then(res=>{'
|
|
. ' const el=document.getElementById("submitResult");'
|
|
. ' if(res.ok===1||res.ok==="1"){'
|
|
. ' btn.className="btn btn-outline-success w-100";'
|
|
. ' btn.innerHTML=\'<i class="bi bi-check-circle me-1"></i>Submitted \u2713\';'
|
|
. ' const qPart=res.queue_id?" · Queue: <code>"+res.queue_id+"</code>":"";'
|
|
. ' const idPart=res.bridge_id?" · Bridge ID: <strong>"+res.bridge_id+"</strong>":"";'
|
|
. ' el.innerHTML=\'<div class="alert alert-success mt-2 py-2">\u2705 \'+(res.message||"Submitted successfully.")+qPart+idPart+\'</div>\';'
|
|
. ' }else{'
|
|
. ' btn.disabled=false;btn.innerHTML=origHtml;'
|
|
. ' el.innerHTML=\'<div class="alert alert-danger mt-2 py-2">\u274c \'+(res.message||res.error_text||"Submission failed.")+(res.http_code?" (HTTP "+res.http_code+")":"")+\'</div>\';'
|
|
. ' }'
|
|
. ' }).catch(e=>{btn.disabled=false;btn.innerHTML=origHtml;'
|
|
. ' document.getElementById("submitResult").innerHTML=\'<div class="alert alert-danger mt-2 py-2">\u274c \'+e.message+\'</div>\';});'
|
|
. '});'
|
|
|
|
// Regenerate button
|
|
. 'document.getElementById("btnRegenerate").addEventListener("click",function(){'
|
|
. ' const btn=this,origHtml=btn.innerHTML;'
|
|
. ' if(!confirm("Rebuild FHIR bundle from current prescription data?\nThis will overwrite the stored bundle — any edits to the JSON will be lost."))return;'
|
|
. ' btn.disabled=true;btn.innerHTML=\'<span class="spinner-border spinner-border-sm me-1"></span>Rebuilding\u2026\';'
|
|
. ' const body=new URLSearchParams({opd_id:CTX.opdId,opd_session_id:CTX.sessionId});'
|
|
. ' body.append(CSRF_NAME,CSRF_HASH);'
|
|
. ' fetch(REGENERATE_URL,{method:"POST",headers:{"X-Requested-With":"XMLHttpRequest","Content-Type":"application/x-www-form-urlencoded"},body:body.toString()})'
|
|
. ' .then(r=>r.json()).then(res=>{'
|
|
. ' if(res.ok===1||res.ok==="1"){'
|
|
. ' btn.innerHTML=\'<i class="bi bi-check2 me-1"></i>Done — reloading\u2026\';'
|
|
. ' setTimeout(()=>location.reload(),900);'
|
|
. ' }else{'
|
|
. ' btn.disabled=false;btn.innerHTML=origHtml;'
|
|
. ' alert("\u274c Regeneration failed: "+(res.message||"unknown error"));'
|
|
. ' }'
|
|
. ' }).catch(e=>{btn.disabled=false;btn.innerHTML=origHtml;alert("\u274c "+e.message);});'
|
|
. '});'
|
|
|
|
. '</script>'
|
|
. '</body></html>';
|
|
|
|
return $this->response->setHeader('Content-Type', 'text/html; charset=UTF-8')->setBody($html);
|
|
}
|
|
|
|
public function fhir_bundle_submit()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'AJAX only']);
|
|
}
|
|
|
|
$opdId = (int) $this->request->getPost('opd_id');
|
|
$sessionId = (int) $this->request->getPost('opd_session_id');
|
|
|
|
if ($opdId <= 0) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'OPD ID required', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash()]);
|
|
}
|
|
|
|
if (! $this->db->tableExists('opd_fhir_documents')) {
|
|
return $this->response->setJSON(['ok' => 0, 'error_text' => 'FHIR bundle storage not available', 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash()]);
|
|
}
|
|
|
|
$builder = $this->db->table('opd_fhir_documents')
|
|
->where('opd_id', $opdId)
|
|
->whereIn('bundle_type', ['OPConsultRecord', 'MedicationRequestBundle']);
|
|
if ($sessionId > 0) {
|
|
$builder->where('opd_session_id', $sessionId);
|
|
}
|
|
$bundleRow = $builder->orderBy('id', 'DESC')->get(1)->getRowArray();
|
|
|
|
if (empty($bundleRow)) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'No FHIR bundle found. Save the prescription first.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$bundleJson = (string) ($bundleRow['bundle_json'] ?? '{}');
|
|
$bundle = json_decode($bundleJson, true);
|
|
if (! is_array($bundle)) {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'Stored FHIR bundle JSON is invalid.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
// Gather patient / OPD context for the push payload
|
|
$patientId = 0;
|
|
$patientName = '';
|
|
$abhaId = '';
|
|
$doctorName = '';
|
|
$visitDate = date('Y-m-d');
|
|
|
|
if ($this->db->tableExists('opd_master')) {
|
|
$opdRow = $this->db->table('opd_master')->where('opd_id', $opdId)->get(1)->getRowArray();
|
|
if (! empty($opdRow)) {
|
|
$patientId = (int) ($opdRow['p_id'] ?? 0);
|
|
$rawDate = (string) ($opdRow['opd_date'] ?? '');
|
|
if ($rawDate !== '') {
|
|
$ts = strtotime($rawDate);
|
|
if ($ts !== false) {
|
|
$visitDate = date('Y-m-d', $ts);
|
|
}
|
|
}
|
|
$docId2 = (int) ($opdRow['doc_id'] ?? 0);
|
|
if ($docId2 > 0 && $this->db->tableExists('doctor_master')) {
|
|
$dRow = $this->db->table('doctor_master')->where('id', $docId2)->get(1)->getRowArray();
|
|
if (! empty($dRow)) {
|
|
$doctorName = trim((string) ($dRow['p_fname'] ?? ''));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($patientId > 0 && $this->db->tableExists('patient_master')) {
|
|
$pFields = $this->db->getFieldNames('patient_master');
|
|
$pRow = $this->db->table('patient_master')->where('id', $patientId)->get(1)->getRowArray();
|
|
if (! empty($pRow)) {
|
|
$fname = $this->sanitizePersonNamePart((string) ($pRow['p_fname'] ?? ''));
|
|
$lname = $this->sanitizePersonNamePart((string) ($pRow['p_lname'] ?? ''));
|
|
$patientName = trim($fname . ' ' . $lname);
|
|
foreach (['abha_number', 'abha_id', 'abha_address'] as $abhaField) {
|
|
if (in_array($abhaField, $pFields, true)) {
|
|
$val = trim((string) ($pRow[$abhaField] ?? ''));
|
|
if ($val !== '') {
|
|
$abhaId = $val;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($abhaId === '') {
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'error_text' => 'ABHA address is required before pushing to ABDM. Update the patient record first.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
try {
|
|
$gatewayUrl = base_url('AbdmGateway/share_prescription_bundle');
|
|
$cookieHeader = (string) $this->request->getHeaderLine('Cookie');
|
|
$handoffTimeoutSec = $this->resolveFhirGatewayHandoffTimeoutSec();
|
|
|
|
$client = service('curlrequest', [
|
|
'timeout' => $handoffTimeoutSec,
|
|
'connect_timeout' => 10,
|
|
'http_errors' => false,
|
|
]);
|
|
|
|
$response = null;
|
|
$attemptError = null;
|
|
for ($attempt = 1; $attempt <= 2; $attempt++) {
|
|
try {
|
|
$response = $client->post($gatewayUrl, [
|
|
'headers' => array_filter([
|
|
'X-Requested-With' => 'XMLHttpRequest',
|
|
'Cookie' => $cookieHeader !== '' ? $cookieHeader : null,
|
|
]),
|
|
'form_params' => [
|
|
'opd_id' => $opdId,
|
|
'opd_session_id' => $sessionId,
|
|
'patient_id' => $patientId,
|
|
'abha_id' => $abhaId,
|
|
'consent_handle' => (string) ($this->request->getPost('consent_handle') ?? ''),
|
|
csrf_token() => csrf_hash(),
|
|
],
|
|
]);
|
|
$attemptError = null;
|
|
break;
|
|
} catch (\Throwable $attemptException) {
|
|
$attemptError = $attemptException;
|
|
if ($attempt < 2 && $this->isTransientFhirGatewayHandoffError($attemptException)) {
|
|
log_message('warning', '[fhir_bundle_submit] transient handoff error on attempt ' . $attempt . ' for OPD#' . $opdId . ': ' . $attemptException->getMessage());
|
|
continue;
|
|
}
|
|
throw $attemptException;
|
|
}
|
|
}
|
|
|
|
if ($response === null && $attemptError !== null) {
|
|
throw $attemptError;
|
|
}
|
|
|
|
$httpCode = (int) $response->getStatusCode();
|
|
$result = json_decode((string) $response->getBody(), true);
|
|
if (! is_array($result)) {
|
|
$result = [];
|
|
}
|
|
|
|
$ok = (int) ($result['ok'] ?? 0);
|
|
$queueId = (string) ($result['queue_id'] ?? '');
|
|
$bridgeId = (int) ($result['bridge_id'] ?? $result['id'] ?? 0);
|
|
$msg = (string) ($result['message'] ?? $result['error_text'] ?? ($ok === 1
|
|
? 'FHIR bundle submitted to ABDM gateway successfully.'
|
|
: 'Submission failed. Check gateway configuration.'));
|
|
|
|
return $this->response->setJSON([
|
|
'ok' => $ok,
|
|
'queue_id' => $queueId,
|
|
'bridge_id' => $bridgeId,
|
|
'message' => $msg,
|
|
'http_code' => $httpCode,
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', '[fhir_bundle_submit] OPD#' . $opdId . ': ' . $e->getMessage());
|
|
$isTimeout = $this->isTransientFhirGatewayHandoffError($e);
|
|
$friendly = $isTimeout
|
|
? 'Gateway handoff timed out. Bridge is slow/unreachable right now. Please retry submit in a minute.'
|
|
: 'Gateway handoff failed: ' . $e->getMessage();
|
|
return $this->response->setJSON([
|
|
'ok' => 0,
|
|
'message' => $friendly,
|
|
'retryable' => $isTimeout ? 1 : 0,
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
private function resolveFhirGatewayHandoffTimeoutSec(): int
|
|
{
|
|
$timeout = 120;
|
|
|
|
if ($this->db->tableExists('hospital_setting')) {
|
|
$row = $this->db->table('hospital_setting')
|
|
->select('s_value')
|
|
->whereIn('s_name', ['ABDM_FHIR_SUBMIT_TIMEOUT_SECONDS', 'ABDM_GATEWAY_TIMEOUT_SECONDS'])
|
|
->where('s_value !=', '')
|
|
->orderBy('s_name', 'ASC')
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
if (! empty($row['s_value'])) {
|
|
$timeout = (int) $row['s_value'];
|
|
}
|
|
}
|
|
|
|
if ($timeout < 30) {
|
|
$timeout = 30;
|
|
}
|
|
if ($timeout > 240) {
|
|
$timeout = 240;
|
|
}
|
|
|
|
return $timeout;
|
|
}
|
|
|
|
private function isTransientFhirGatewayHandoffError(\Throwable $e): bool
|
|
{
|
|
$msg = strtolower(trim($e->getMessage()));
|
|
if ($msg === '') {
|
|
return false;
|
|
}
|
|
|
|
$needles = [
|
|
'timed out',
|
|
'operation timed out',
|
|
'curl error 28',
|
|
'failed to connect',
|
|
'connection reset',
|
|
'empty reply from server',
|
|
];
|
|
|
|
foreach ($needles as $needle) {
|
|
if (str_contains($msg, $needle)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function fhir_complaint_recode()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
|
|
}
|
|
|
|
$opdId = (int) $this->request->getPost('opd_id');
|
|
$sessionId = (int) $this->request->getPost('opd_session_id');
|
|
$complaintText = trim((string) $this->request->getPost('complaint_text'));
|
|
$snomedId = trim((string) $this->request->getPost('snomed_concept_id'));
|
|
$snomedTerm = trim((string) $this->request->getPost('snomed_term'));
|
|
|
|
if ($opdId <= 0 || $complaintText === '') {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Missing required fields']);
|
|
}
|
|
if (! $this->db->tableExists('opd_prescription')) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Table not found']);
|
|
}
|
|
|
|
// Resolve the prescription row
|
|
$prescRow = null;
|
|
if ($sessionId > 0) {
|
|
$prescRow = $this->db->table('opd_prescription')
|
|
->where('id', $sessionId)->where('opd_id', $opdId)->get(1)->getRowArray();
|
|
}
|
|
if (empty($prescRow)) {
|
|
$prescRow = $this->db->table('opd_prescription')
|
|
->where('opd_id', $opdId)->orderBy('id', 'DESC')->get(1)->getRowArray();
|
|
}
|
|
if (empty($prescRow)) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Prescription record not found']);
|
|
}
|
|
|
|
$prescId = (int) $prescRow['id'];
|
|
$snomedJson = trim((string) ($prescRow['complaint_snomed_json'] ?? ''));
|
|
$items = [];
|
|
if ($snomedJson !== '' && $snomedJson !== '[]') {
|
|
$decoded = json_decode($snomedJson, true);
|
|
if (is_array($decoded)) {
|
|
$items = $decoded;
|
|
}
|
|
}
|
|
|
|
// Bootstrap from free-text if no structured items yet
|
|
if (empty($items)) {
|
|
$parts = preg_split('/[,;\n\r]+/', (string) ($prescRow['complaints'] ?? '')) ?: [];
|
|
foreach ($parts as $part) {
|
|
$part = trim((string) $part);
|
|
if ($part !== '' && preg_match('/^Complaint\s*(Onset|Duration|Severity)\s*:/i', $part) !== 1) {
|
|
$items[] = ['term' => $part, 'concept_id' => '', 'source' => 'local',
|
|
'hierarchy' => '', 'severity' => '', 'duration' => '', 'frequency' => '', 'date' => ''];
|
|
}
|
|
}
|
|
}
|
|
|
|
// Find and update matching complaint (case-insensitive)
|
|
$found = false;
|
|
foreach ($items as &$item) {
|
|
if (strtolower(trim((string) ($item['term'] ?? ''))) === strtolower($complaintText)) {
|
|
// Preserve original term (what doctor typed)
|
|
if (! isset($item['original_term']) || (string) ($item['original_term'] ?? '') === '') {
|
|
$item['original_term'] = $item['term'];
|
|
}
|
|
$item['concept_id'] = $snomedId;
|
|
$item['snomed_display'] = $snomedTerm ?: $item['term'];
|
|
// Do NOT overwrite term — keep doctor's original text
|
|
$item['source'] = $snomedId !== '' ? 'snomed' : ($item['source'] ?? 'local');
|
|
$found = true;
|
|
break;
|
|
}
|
|
}
|
|
unset($item);
|
|
|
|
if (! $found) {
|
|
$items[] = ['term' => $complaintText, 'original_term' => $complaintText,
|
|
'concept_id' => $snomedId, 'snomed_display' => $snomedTerm ?: $complaintText,
|
|
'source' => $snomedId !== '' ? 'snomed' : 'local',
|
|
'hierarchy' => '', 'severity' => '', 'duration' => '', 'frequency' => '', 'date' => ''];
|
|
}
|
|
|
|
// Also update getPrescriptionClinicalContext — use snomed_display for FHIR coding
|
|
// (handled in getPrescriptionClinicalContext via snomed_display field)
|
|
|
|
$newJson = (string) json_encode(array_values($items), JSON_UNESCAPED_UNICODE);
|
|
$this->db->table('opd_prescription')->where('id', $prescId)->update(['complaint_snomed_json' => $newJson]);
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'error_text' => 'Saved',
|
|
'presc_id' => $prescId,
|
|
'snomed_concept_id'=> $snomedId,
|
|
'snomed_term' => $snomedTerm,
|
|
'original_term' => $complaintText,
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
public function fhir_diagnosis_recode()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(403)->setJSON(['error_text' => 'Forbidden']);
|
|
}
|
|
$opdId = (int) $this->request->getPost('opd_id');
|
|
$sessionId = (int) $this->request->getPost('opd_session_id');
|
|
$diagType = trim((string) $this->request->getPost('diagnosis_type')); // confirmed|provisional
|
|
$diagText = trim((string) $this->request->getPost('diagnosis_text'));
|
|
$snomedId = trim((string) $this->request->getPost('snomed_concept_id'));
|
|
$snomedTerm = trim((string) $this->request->getPost('snomed_term'));
|
|
|
|
if ($opdId <= 0 || $diagText === '') {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid parameters']);
|
|
}
|
|
if (! $this->db->tableExists('opd_prescription')) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Table missing']);
|
|
}
|
|
|
|
$row = [];
|
|
if ($sessionId > 0) {
|
|
$row = $this->db->table('opd_prescription')
|
|
->where('opd_session_id', $sessionId)->where('opd_id', $opdId)
|
|
->get(1)->getRowArray() ?? [];
|
|
}
|
|
if (empty($row)) {
|
|
$row = $this->db->table('opd_prescription')
|
|
->where('opd_id', $opdId)->orderBy('id', 'DESC')
|
|
->get(1)->getRowArray() ?? [];
|
|
}
|
|
if (empty($row)) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Prescription not found']);
|
|
}
|
|
|
|
$prescId = (int) $row['id'];
|
|
$fields = $this->db->getFieldNames('opd_prescription') ?? [];
|
|
|
|
if ($diagType === 'provisional') {
|
|
$idCol = 'provisional_diagnosis_snomed_id';
|
|
$termCol = 'provisional_diagnosis_snomed_term';
|
|
$srcCol = 'provisional_diagnosis_snomed_source';
|
|
} else {
|
|
$idCol = 'diagnosis_snomed_id';
|
|
$termCol = 'diagnosis_snomed_term';
|
|
$srcCol = 'diagnosis_snomed_source';
|
|
}
|
|
|
|
$update = [];
|
|
if (in_array($idCol, $fields, true)) $update[$idCol] = $snomedId;
|
|
if (in_array($termCol, $fields, true)) $update[$termCol] = $snomedTerm !== '' ? $snomedTerm : $diagText;
|
|
if (in_array($srcCol, $fields, true)) $update[$srcCol] = $snomedId !== '' ? 'snomed' : 'local';
|
|
|
|
if (empty($update)) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'No columns to update']);
|
|
}
|
|
|
|
$this->db->table('opd_prescription')->where('id', $prescId)->update($update);
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'error_text' => 'Saved',
|
|
'presc_id' => $prescId,
|
|
'snomed_concept_id'=> $snomedId,
|
|
'snomed_term' => $snomedTerm,
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
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' => []]);
|
|
}
|
|
|
|
$out = [];
|
|
$seen = [];
|
|
$needle = mb_strtolower($this->normalizeAutocompleteSuggestionText($q));
|
|
|
|
// 1. SNOMED CT via CSNOtk (finding / disorder semantic tags)
|
|
$csnotkRows = (new CsnotkTerminologyService())->searchFinding($q, 20);
|
|
foreach ($csnotkRows as $row) {
|
|
$name = $this->normalizeAutocompleteSuggestionText((string) ($row['term'] ?? ''));
|
|
if ($name === '') {
|
|
continue;
|
|
}
|
|
$conceptId = trim((string) ($row['concept_id'] ?? ''));
|
|
$hierarchy = trim((string) ($row['hierarchy'] ?? ''));
|
|
$k = mb_strtoupper($name) . '|' . $conceptId;
|
|
if (isset($seen[$k])) {
|
|
continue;
|
|
}
|
|
$seen[$k] = true;
|
|
$out[] = [
|
|
'name' => $name,
|
|
'concept_id' => $conceptId,
|
|
'hierarchy' => $hierarchy,
|
|
'source' => 'snomed',
|
|
];
|
|
if (count($out) >= 20) {
|
|
return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]);
|
|
}
|
|
}
|
|
|
|
// 2. Local complaints_master fallback
|
|
if ($this->db->tableExists('complaints_master')) {
|
|
$localRows = $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();
|
|
|
|
foreach ($localRows as $row) {
|
|
$name = $this->normalizeAutocompleteSuggestionText((string) ($row['name'] ?? ''));
|
|
if ($name === '' || ($needle !== '' && mb_stripos($name, $needle) === false)) {
|
|
continue;
|
|
}
|
|
$k = mb_strtoupper($name) . '|';
|
|
if (isset($seen[$k])) {
|
|
continue;
|
|
}
|
|
$seen[$k] = true;
|
|
$out[] = [
|
|
'name' => $name,
|
|
'concept_id' => '',
|
|
'hierarchy' => '',
|
|
'source' => 'local',
|
|
'name_hinglish' => (string) ($row['name_hinglish'] ?? ''),
|
|
];
|
|
if (count($out) >= 20) {
|
|
return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 3. User autotype keywords
|
|
$keywordRows = $this->fetchAutotypeKeywords(
|
|
'complaints',
|
|
$q,
|
|
max(0, (int) ($this->getCurrentUserId() ?? 0)),
|
|
10
|
|
);
|
|
foreach ($keywordRows as $keyword) {
|
|
$name = $this->normalizeAutocompleteSuggestionText((string) $keyword);
|
|
if ($name === '' || ($needle !== '' && mb_stripos($name, $needle) === false)) {
|
|
continue;
|
|
}
|
|
$k = mb_strtoupper($name) . '|';
|
|
if (isset($seen[$k])) {
|
|
continue;
|
|
}
|
|
$seen[$k] = true;
|
|
$out[] = [
|
|
'name' => $name,
|
|
'concept_id' => '',
|
|
'hierarchy' => '',
|
|
'source' => 'keyword',
|
|
];
|
|
if (count($out) >= 20) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $this->response->setJSON(['rows' => array_slice($out, 0, 20)]);
|
|
}
|
|
|
|
/**
|
|
* Extract SNOMED codes from free-text complaint string.
|
|
* Parses duration/severity from each phrase, searches local SNOMED FULLTEXT.
|
|
*
|
|
* GET Opd_prescription/complaints_extract?q=headache+3+days+moderate,+fever+mild
|
|
*/
|
|
public function complaints_extract()
|
|
{
|
|
$q = trim((string) $this->request->getGet('q'));
|
|
if ($q === '') {
|
|
return $this->response->setJSON(['rows' => []]);
|
|
}
|
|
|
|
$phrases = $this->parseComplaintFreeText($q);
|
|
$svc = new CsnotkTerminologyService();
|
|
$out = [];
|
|
$seen = [];
|
|
|
|
foreach ($phrases as $p) {
|
|
$cleanTerm = trim($p['term']);
|
|
if (mb_strlen($cleanTerm) < 2) {
|
|
continue;
|
|
}
|
|
|
|
$results = $svc->searchFinding($cleanTerm, 3);
|
|
foreach ($results as $r) {
|
|
$cid = trim((string) ($r['concept_id'] ?? ''));
|
|
$dedupeKey = mb_strtoupper($r['term'] ?? '') . '|' . $cid;
|
|
if (isset($seen[$dedupeKey])) {
|
|
continue;
|
|
}
|
|
$seen[$dedupeKey] = true;
|
|
|
|
$out[] = [
|
|
'term' => (string) ($r['term'] ?? ''),
|
|
'concept_id' => $cid,
|
|
'semantic_tag' => (string) ($r['hierarchy'] ?? ''),
|
|
'confidence' => round(0.75, 3), // local FULLTEXT match scores high by default
|
|
'duration' => $p['duration'],
|
|
'severity' => $p['severity'],
|
|
'source_phrase'=> $p['original'],
|
|
];
|
|
}
|
|
|
|
if (count($out) >= 8) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return $this->response->setJSON(['rows' => $out]);
|
|
}
|
|
|
|
/**
|
|
* Parse a free-text complaint string into structured phrases.
|
|
* Extracts duration (e.g. "3 days", "since yesterday") and severity (mild/moderate/severe)
|
|
* from each comma/semicolon-delimited phrase.
|
|
*
|
|
* @return array<int,array{term:string,duration:string,severity:string,original:string}>
|
|
*/
|
|
private function parseComplaintFreeText(string $text): array
|
|
{
|
|
$durationRe = '/(?:since\s+|for\s+the\s+last\s+|for\s+last\s+|for\s+)?(\d+\s*(?:hour|day|week|month|year)s?|since\s+(?:yesterday|morning|evening|last\s+\w+)|past\s+\d+\s*(?:day|week|month|year)s?)/iu';
|
|
$severityRe = '/\b(mild|moderate|severe|slight|intense|chronic|acute|persistent|intermittent|recurrent)\b/iu';
|
|
|
|
// Split on common delimiters
|
|
$parts = preg_split('/[,;\/\n]|\s+and\s+|\s+with\s+/iu', $text) ?: [$text];
|
|
|
|
$result = [];
|
|
foreach ($parts as $part) {
|
|
$part = trim($part);
|
|
if ($part === '') {
|
|
continue;
|
|
}
|
|
$original = $part;
|
|
|
|
$duration = '';
|
|
if (preg_match($durationRe, $part, $m)) {
|
|
$duration = trim($m[0]);
|
|
$part = trim((string) preg_replace($durationRe, ' ', $part, 1));
|
|
}
|
|
|
|
$severity = '';
|
|
if (preg_match($severityRe, $part, $m)) {
|
|
$severity = mb_strtolower($m[1]);
|
|
$part = trim((string) preg_replace($severityRe, ' ', $part, 1));
|
|
}
|
|
|
|
$cleanTerm = trim((string) preg_replace('/\s+/', ' ', $part));
|
|
if (mb_strlen($cleanTerm) >= 2) {
|
|
$result[] = [
|
|
'term' => $cleanTerm,
|
|
'duration' => $duration,
|
|
'severity' => $severity,
|
|
'original' => $original,
|
|
];
|
|
}
|
|
}
|
|
|
|
// If parsing produced nothing usable, fall back to original string
|
|
if (empty($result)) {
|
|
$result[] = ['term' => $text, 'duration' => '', 'severity' => '', 'original' => $text];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
public function provisional_diagnosis_search()
|
|
{
|
|
$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']);
|
|
$iSortOrder = $this->resolveFirstField($iFields, ['sort_id', 'sort_order', 'id', 'Code', 'code']);
|
|
|
|
if ($pCode !== null && $pName !== null && $jProfileCode !== null && $jInvestCode !== null && $iCode !== null && $iName !== null) {
|
|
$orderExpr = 'p.`' . $pName . '` ASC';
|
|
if ($iSortOrder !== null) {
|
|
$orderExpr .= ', i.`' . $iSortOrder . '` ASC';
|
|
} elseif ($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'));
|
|
|
|
// Resolve profile shorthand names (CBC/LFT/KFT etc.) to canonical codes.
|
|
if (($code === '' || $code === '0') && $name !== '') {
|
|
$code = $this->resolveInvestigationCodeByText($name, $this->buildInvestigationLookupMaps());
|
|
}
|
|
|
|
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_add_bulk()
|
|
{
|
|
$startedAt = microtime(true);
|
|
$respond = function (array $payload, int $statusCode = 200) use ($startedAt) {
|
|
$payload['elapsed_ms'] = (int) round((microtime(true) - $startedAt) * 1000);
|
|
return $this->response->setStatusCode($statusCode)->setJSON($payload);
|
|
};
|
|
|
|
if (! $this->request->isAJAX()) {
|
|
return $respond(['update' => 0, 'error_text' => 'Invalid request'], 400);
|
|
}
|
|
|
|
$opdId = (int) $this->request->getPost('opd_id');
|
|
$sessionId = (int) $this->request->getPost('opd_session_id');
|
|
|
|
$rawTests = $this->request->getPost('tests');
|
|
$tests = [];
|
|
if (is_string($rawTests) && $rawTests !== '') {
|
|
$decoded = json_decode($rawTests, true);
|
|
if (is_array($decoded)) {
|
|
$tests = $decoded;
|
|
}
|
|
} elseif (is_array($rawTests)) {
|
|
$tests = $rawTests;
|
|
}
|
|
|
|
if ($opdId <= 0 || empty($tests)) {
|
|
return $respond(['update' => 0, 'error_text' => 'Invalid bulk payload']);
|
|
}
|
|
|
|
$table = $this->findExistingTable(['opd_prescription_investigation']);
|
|
if ($table === null) {
|
|
return $respond(['update' => 0, 'error_text' => 'Investigation table not found']);
|
|
}
|
|
|
|
$sessionId = $this->ensurePrescriptionSession($opdId, $sessionId);
|
|
if ($sessionId <= 0) {
|
|
return $respond(['update' => 0, 'error_text' => 'Unable to create prescription session']);
|
|
}
|
|
|
|
$fields = $this->db->getFieldNames($table);
|
|
$hasOpdPreId = in_array('opd_pre_id', $fields, true);
|
|
$hasOpdId = in_array('opd_id', $fields, true);
|
|
$hasCode = in_array('investigation_code', $fields, true);
|
|
$hasName = in_array('investigation_name', $fields, true);
|
|
|
|
$existingRowsBuilder = $this->db->table($table);
|
|
if ($hasOpdPreId) {
|
|
$existingRowsBuilder->where('opd_pre_id', $sessionId);
|
|
} elseif ($hasOpdId) {
|
|
$existingRowsBuilder->where('opd_id', $opdId);
|
|
} else {
|
|
return $respond(['update' => 0, 'error_text' => 'Invalid investigation table structure']);
|
|
}
|
|
|
|
$existingRows = $existingRowsBuilder->get()->getResultArray();
|
|
$existingByCode = [];
|
|
$existingByName = [];
|
|
foreach ($existingRows as $row) {
|
|
$exCode = trim((string) ($row['investigation_code'] ?? ''));
|
|
$exName = trim((string) ($row['investigation_name'] ?? ''));
|
|
if ($exCode !== '' && $exCode !== '0') {
|
|
$existingByCode[strtolower($exCode)] = true;
|
|
}
|
|
if ($exName !== '') {
|
|
$existingByName[strtolower($exName)] = true;
|
|
}
|
|
}
|
|
|
|
$invLookup = $this->buildInvestigationLookupMaps();
|
|
|
|
$seenPayload = [];
|
|
$insertRows = [];
|
|
$added = 0;
|
|
$skipped = 0;
|
|
|
|
foreach ($tests as $test) {
|
|
$name = trim((string) (($test['name'] ?? $test['investigation_name'] ?? '')));
|
|
$code = trim((string) (($test['code'] ?? $test['investigation_code'] ?? '')));
|
|
|
|
if ($name === '' && $code === '') {
|
|
$skipped++;
|
|
continue;
|
|
}
|
|
|
|
if (($code === '' || $code === '0') && $name !== '') {
|
|
$code = $this->resolveInvestigationCodeByText($name, $invLookup);
|
|
}
|
|
|
|
$payloadKey = strtolower($code . '|' . $name);
|
|
if (isset($seenPayload[$payloadKey])) {
|
|
$skipped++;
|
|
continue;
|
|
}
|
|
$seenPayload[$payloadKey] = true;
|
|
|
|
$duplicateByCode = ($code !== '' && isset($existingByCode[strtolower($code)]));
|
|
$duplicateByName = ($name !== '' && isset($existingByName[strtolower($name)]));
|
|
if ($duplicateByCode || $duplicateByName) {
|
|
$skipped++;
|
|
continue;
|
|
}
|
|
|
|
$row = [];
|
|
if ($hasOpdId) {
|
|
$row['opd_id'] = $opdId;
|
|
}
|
|
if ($hasOpdPreId) {
|
|
$row['opd_pre_id'] = $sessionId;
|
|
}
|
|
if ($hasCode) {
|
|
$row['investigation_code'] = ($code === '' || $code === '0') ? '' : $code;
|
|
}
|
|
if ($hasName) {
|
|
$row['investigation_name'] = $name;
|
|
}
|
|
$insertRows[] = $row;
|
|
|
|
if ($code !== '' && $code !== '0') {
|
|
$existingByCode[strtolower($code)] = true;
|
|
}
|
|
if ($name !== '') {
|
|
$existingByName[strtolower($name)] = true;
|
|
}
|
|
}
|
|
|
|
foreach ($insertRows as $row) {
|
|
try {
|
|
$this->db->table($table)->insert($row);
|
|
$added++;
|
|
$insertId = (int) $this->db->insertID();
|
|
$this->auditClinicalUpdate('opd_prescription_investigation', 'added', $insertId, null, $row);
|
|
} catch (\Throwable $e) {
|
|
$msg = strtolower($e->getMessage());
|
|
if (strpos($msg, 'duplicate entry') !== false) {
|
|
$skipped++;
|
|
continue;
|
|
}
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
$result = [
|
|
'update' => 1,
|
|
'opd_session_id' => $sessionId,
|
|
'added_count' => $added,
|
|
'skipped_count' => $skipped,
|
|
'total_count' => count($tests),
|
|
'error_text' => $added > 0 ? 'Investigations added' : 'No new investigations added',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
];
|
|
|
|
log_message(
|
|
'info',
|
|
'investigation_add_bulk opd_id={opd_id} session_id={session_id} total={total} added={added} skipped={skipped} elapsed_ms={elapsed_ms}',
|
|
[
|
|
'opd_id' => $opdId,
|
|
'session_id' => $sessionId,
|
|
'total' => count($tests),
|
|
'added' => $added,
|
|
'skipped' => $skipped,
|
|
'elapsed_ms' => (int) round((microtime(true) - $startedAt) * 1000),
|
|
]
|
|
);
|
|
|
|
return $respond($result);
|
|
}
|
|
|
|
private function buildInvestigationLookupMaps(): array
|
|
{
|
|
$out = [
|
|
'by_name' => [],
|
|
'by_short' => [],
|
|
'by_alias' => [],
|
|
];
|
|
|
|
if (! $this->db->tableExists('investigation')) {
|
|
return $out;
|
|
}
|
|
|
|
$invFields = $this->db->getFieldNames('investigation');
|
|
$invCodeField = $this->resolveFirstField($invFields, ['Code', 'code']);
|
|
$invNameField = $this->resolveFirstField($invFields, ['Name', 'name']);
|
|
$invShortField = $this->resolveFirstField($invFields, ['short_name', 'shortName', 'short']);
|
|
|
|
if ($invCodeField === null || $invNameField === null) {
|
|
return $out;
|
|
}
|
|
|
|
$select = $invCodeField . ' as code, ' . $invNameField . ' as name';
|
|
if ($invShortField !== null) {
|
|
$select .= ', ' . $invShortField . ' as short_name';
|
|
}
|
|
|
|
$rows = $this->db->table('investigation')->select($select)->get()->getResultArray();
|
|
foreach ($rows as $row) {
|
|
$code = trim((string) ($row['code'] ?? ''));
|
|
if ($code === '' || $code === '0') {
|
|
continue;
|
|
}
|
|
|
|
$name = trim((string) ($row['name'] ?? ''));
|
|
$short = trim((string) ($row['short_name'] ?? ''));
|
|
|
|
$nameKey = $this->normalizeInvestigationLookupKey($name);
|
|
if ($nameKey !== '' && ! isset($out['by_name'][$nameKey])) {
|
|
$out['by_name'][$nameKey] = $code;
|
|
}
|
|
|
|
$shortKey = $this->normalizeInvestigationLookupKey($short);
|
|
if ($shortKey !== '' && ! isset($out['by_short'][$shortKey])) {
|
|
$out['by_short'][$shortKey] = $code;
|
|
}
|
|
|
|
if (preg_match('/\(([A-Za-z0-9\-\s\.]{2,20})\)/', $name, $m) === 1) {
|
|
$aliasKey = $this->normalizeInvestigationLookupKey((string) ($m[1] ?? ''));
|
|
if ($aliasKey !== '' && ! isset($out['by_alias'][$aliasKey])) {
|
|
$out['by_alias'][$aliasKey] = $code;
|
|
}
|
|
}
|
|
|
|
$acronym = $this->buildInvestigationAcronym($name);
|
|
if ($acronym !== '' && ! isset($out['by_alias'][$acronym])) {
|
|
$out['by_alias'][$acronym] = $code;
|
|
}
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
private function resolveInvestigationCodeByText(string $text, array $lookup): string
|
|
{
|
|
$key = $this->normalizeInvestigationLookupKey($text);
|
|
if ($key === '') {
|
|
return '';
|
|
}
|
|
|
|
if (isset($lookup['by_name'][$key])) {
|
|
return (string) $lookup['by_name'][$key];
|
|
}
|
|
if (isset($lookup['by_short'][$key])) {
|
|
return (string) $lookup['by_short'][$key];
|
|
}
|
|
if (isset($lookup['by_alias'][$key])) {
|
|
return (string) $lookup['by_alias'][$key];
|
|
}
|
|
|
|
// Common profile aliases seen in legacy shortcuts.
|
|
$aliasFallback = [
|
|
'kft' => 'rft',
|
|
];
|
|
$fallback = $aliasFallback[$key] ?? '';
|
|
if ($fallback !== '' && isset($lookup['by_alias'][$fallback])) {
|
|
return (string) $lookup['by_alias'][$fallback];
|
|
}
|
|
if ($fallback !== '' && isset($lookup['by_short'][$fallback])) {
|
|
return (string) $lookup['by_short'][$fallback];
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function normalizeInvestigationLookupKey(string $value): string
|
|
{
|
|
$value = trim(strtolower($value));
|
|
if ($value === '') {
|
|
return '';
|
|
}
|
|
return (string) (preg_replace('/[^a-z0-9]+/', '', $value) ?? '');
|
|
}
|
|
|
|
private function buildInvestigationAcronym(string $name): string
|
|
{
|
|
$plain = preg_replace('/\([^)]*\)/', ' ', $name) ?? $name;
|
|
$plain = trim((string) preg_replace('/\s+/', ' ', $plain));
|
|
if ($plain === '') {
|
|
return '';
|
|
}
|
|
|
|
$stopWords = [
|
|
'and', 'of', 'for', 'the', 'test', 'panel', 'routine', 'view', 'complete', 'count',
|
|
];
|
|
|
|
$parts = preg_split('/\s+/', strtolower($plain)) ?: [];
|
|
$acronym = '';
|
|
foreach ($parts as $part) {
|
|
$part = trim((string) preg_replace('/[^a-z0-9]/', '', $part));
|
|
if ($part === '' || in_array($part, $stopWords, true)) {
|
|
continue;
|
|
}
|
|
$acronym .= substr($part, 0, 1);
|
|
}
|
|
|
|
return strlen($acronym) >= 2 ? $acronym : '';
|
|
}
|
|
|
|
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 investigation_remove_bulk()
|
|
{
|
|
$startedAt = microtime(true);
|
|
$respond = function (array $payload, int $statusCode = 200) use ($startedAt) {
|
|
$payload['elapsed_ms'] = (int) round((microtime(true) - $startedAt) * 1000);
|
|
return $this->response->setStatusCode($statusCode)->setJSON($payload);
|
|
};
|
|
|
|
if (! $this->request->isAJAX()) {
|
|
return $respond(['update' => 0, 'error_text' => 'Invalid request'], 400);
|
|
}
|
|
|
|
$rawIds = $this->request->getPost('ids');
|
|
$ids = [];
|
|
|
|
if (is_array($rawIds)) {
|
|
$ids = $rawIds;
|
|
} elseif (is_string($rawIds) && trim($rawIds) !== '') {
|
|
$decoded = json_decode($rawIds, true);
|
|
if (is_array($decoded)) {
|
|
$ids = $decoded;
|
|
} else {
|
|
$ids = explode(',', $rawIds);
|
|
}
|
|
}
|
|
|
|
$ids = array_values(array_unique(array_filter(array_map(static function ($id) {
|
|
return (int) $id;
|
|
}, $ids), static function ($id) {
|
|
return $id > 0;
|
|
})));
|
|
|
|
if (empty($ids)) {
|
|
return $respond(['update' => 0, 'error_text' => 'No investigations selected']);
|
|
}
|
|
|
|
$table = $this->findExistingTable(['opd_prescription_investigation']);
|
|
if ($table === null) {
|
|
return $respond(['update' => 0, 'error_text' => 'Investigation table not found']);
|
|
}
|
|
|
|
$fields = $this->db->getFieldNames($table);
|
|
if (! in_array('id', $fields, true)) {
|
|
return $respond(['update' => 0, 'error_text' => 'Investigation id field missing']);
|
|
}
|
|
|
|
$rows = $this->db->table($table)->whereIn('id', $ids)->get()->getResultArray();
|
|
$this->db->table($table)->whereIn('id', $ids)->delete();
|
|
|
|
foreach ($rows as $row) {
|
|
$this->auditClinicalUpdate('opd_prescription_investigation', 'removed', (int) ($row['id'] ?? 0), $row, null);
|
|
}
|
|
|
|
$result = [
|
|
'update' => 1,
|
|
'removed_count' => count($rows),
|
|
'requested_count' => count($ids),
|
|
'error_text' => 'Investigations removed',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
];
|
|
|
|
log_message(
|
|
'info',
|
|
'investigation_remove_bulk requested={requested} removed={removed} elapsed_ms={elapsed_ms}',
|
|
[
|
|
'requested' => count($ids),
|
|
'removed' => count($rows),
|
|
'elapsed_ms' => (int) round((microtime(true) - $startedAt) * 1000),
|
|
]
|
|
);
|
|
|
|
return $respond($result);
|
|
}
|
|
|
|
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<string, mixed> $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<int, bool>
|
|
*/
|
|
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<int, array{use_count:int,last_used_at:string}>
|
|
*/
|
|
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[] = '<span class="text-success">BP :</span> ' . esc($row['bp']) . ' / ' . esc((string) ($row['diastolic'] ?? ''));
|
|
}
|
|
if (!empty($row['pulse'])) {
|
|
$lines[] = '<span class="text-success">Pulse :</span> ' . esc($row['pulse']);
|
|
}
|
|
if (!empty($row['temp'])) {
|
|
$lines[] = '<span class="text-success">Temp :</span> ' . esc($row['temp']);
|
|
}
|
|
if (!empty($row['spo2'])) {
|
|
$lines[] = '<span class="text-success">SpO2 :</span> ' . esc($row['spo2']) . ' %';
|
|
}
|
|
|
|
return $this->response->setBody('<p class="text-primary">' . implode('<br>', $lines) . '</p>');
|
|
}
|
|
|
|
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('<p class="text-muted">No patient remarks found.</p>');
|
|
}
|
|
|
|
$out = '';
|
|
foreach ($rows as $row) {
|
|
$stamp = !empty($row['insert_datetime']) ? date('d-m-Y H:i', strtotime((string) $row['insert_datetime'])) : '';
|
|
$out .= '<p>' . nl2br(esc((string) ($row['remark'] ?? ''))) . '<br><small>Update By: ' . esc((string) ($row['insert_by'] ?? '')) . ' ' . esc($stamp) . '</small></p>';
|
|
}
|
|
|
|
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('<p class="text-muted">Medical purchase table not found.</p>');
|
|
}
|
|
|
|
$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('<p class="text-muted">No medicine purchase found.</p>');
|
|
}
|
|
|
|
$html = '<table class="table table-bordered table-sm"><thead><tr><th>Date</th><th>Invoice</th><th>Amount</th></tr></thead><tbody>';
|
|
foreach ($rows as $row) {
|
|
$html .= '<tr>'
|
|
. '<td>' . esc((string) ($row['date'] ?? $row['invoice_date'] ?? '')) . '</td>'
|
|
. '<td>' . esc((string) ($row['invoice_code'] ?? $row['invoice_no'] ?? $row['id'] ?? '')) . '</td>'
|
|
. '<td>' . esc((string) ($row['net_amount'] ?? $row['total_amount'] ?? '')) . '</td>'
|
|
. '</tr>';
|
|
}
|
|
$html .= '</tbody></table>';
|
|
|
|
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('<p class="text-muted">Old prescription data not found.</p>');
|
|
}
|
|
|
|
$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('<p class="text-muted mb-0">No old prescriptions found.</p>');
|
|
}
|
|
|
|
$medicineTable = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']);
|
|
if ($medicineTable === null) {
|
|
return $this->response->setBody('<p class="text-muted mb-0">Old medicine table not found.</p>');
|
|
}
|
|
|
|
$html = '<div class="old-rx-list">';
|
|
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 .= '<div class="card mb-2 old-rx-card">';
|
|
$html .= '<div class="card-header py-2 d-flex justify-content-between align-items-center">';
|
|
$html .= '<div>';
|
|
$html .= '<strong>OPD: ' . esc((string) ($row['opd_code'] ?? $row['opd_id'])) . '</strong>';
|
|
if ($dateLabel !== '') {
|
|
$html .= '<span class="text-muted ms-2">' . esc($dateLabel) . '</span>';
|
|
}
|
|
$html .= '</div>';
|
|
$html .= '<button type="button" class="btn btn-sm btn-outline-primary btn-old-rx-add-all">+ All</button>';
|
|
$html .= '</div>';
|
|
|
|
$html .= '<div class="card-body py-2">';
|
|
$html .= '<div class="small mb-1"><strong>Complaint:</strong> ' . esc((string) ($row['complaints'] ?? '')) . '</div>';
|
|
$html .= '<div class="small mb-2"><strong>Diagnosis:</strong> ' . esc((string) ($row['diagnosis'] ?? '')) . '</div>';
|
|
|
|
if (empty($medRows)) {
|
|
$html .= '<div class="text-muted small">No medicine row found.</div>';
|
|
} else {
|
|
$html .= '<table class="table table-sm table-bordered mb-0">';
|
|
$html .= '<thead><tr><th>Prescribed</th><th style="width:95px;">Dose</th><th style="width:58px;">+</th></tr></thead><tbody>';
|
|
|
|
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 .= '<tr>';
|
|
$html .= '<td>' . esc($medLabel !== '' ? $medLabel : '-') . '</td>';
|
|
$html .= '<td class="small">' . esc($doseLabel) . '</td>';
|
|
$html .= '<td class="text-center">'
|
|
. '<button type="button" class="btn btn-sm btn-outline-success btn-old-rx-add-one"'
|
|
. ' data-med-id="' . esc((string) ($med['med_id'] ?? 0), 'attr') . '"'
|
|
. ' data-med-name="' . esc($medName, 'attr') . '"'
|
|
. ' data-med-type="' . esc($medType, 'attr') . '"'
|
|
. ' data-dosage="' . esc((string) ($med['dosage'] ?? ''), 'attr') . '"'
|
|
. ' data-dosage-when="' . esc((string) ($med['dosage_when'] ?? ''), 'attr') . '"'
|
|
. ' data-dosage-freq="' . esc((string) ($med['dosage_freq'] ?? ''), 'attr') . '"'
|
|
. ' data-no-of-days="' . esc((string) ($med['no_of_days'] ?? ''), 'attr') . '"'
|
|
. ' data-qty="' . esc((string) ($med['qty'] ?? ''), 'attr') . '"'
|
|
. ' data-remark="' . esc((string) ($med['remark'] ?? ''), 'attr') . '">Add</button>'
|
|
. '</td>';
|
|
$html .= '</tr>';
|
|
}
|
|
|
|
$html .= '</tbody></table>';
|
|
}
|
|
|
|
$html .= '</div>';
|
|
$html .= '</div>';
|
|
}
|
|
$html .= '</div>';
|
|
|
|
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('<p class="text-muted">RX template table not found.</p>');
|
|
}
|
|
|
|
$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('<p class="text-muted">No RX groups found.</p>');
|
|
}
|
|
|
|
$html = '<ul class="list-group">';
|
|
foreach ($rows as $row) {
|
|
$html .= '<li class="list-group-item d-flex justify-content-between align-items-center">'
|
|
. '<span>' . esc((string) ($row['rx_group_name'] ?? 'RX Group')) . '</span>'
|
|
. '<span class="badge bg-secondary">#' . (int) ($row['id'] ?? 0) . ' (' . (int) ($row['med_count'] ?? 0) . ')</span>'
|
|
. '</li>';
|
|
}
|
|
$html .= '</ul>';
|
|
|
|
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('<p class="text-muted">No prescribed medicine table found.</p>');
|
|
}
|
|
|
|
$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('<p class="text-muted">No prescribed medicine found for this OPD session.</p>');
|
|
}
|
|
|
|
$html = '<table class="table table-bordered table-sm"><thead><tr><th>Type</th><th>Medicine</th><th>Dose</th><th>When</th><th>Freq</th><th>Days</th><th>Qty</th><th>Remark</th></tr></thead><tbody>';
|
|
foreach ($rows as $row) {
|
|
$html .= '<tr>'
|
|
. '<td>' . esc((string) ($row['med_type'] ?? '')) . '</td>'
|
|
. '<td>' . esc((string) ($row['med_name'] ?? '')) . '</td>'
|
|
. '<td>' . esc((string) ($row['dosage'] ?? '')) . '</td>'
|
|
. '<td>' . esc((string) ($row['dosage_when'] ?? '')) . '</td>'
|
|
. '<td>' . esc((string) ($row['dosage_freq'] ?? '')) . '</td>'
|
|
. '<td>' . esc((string) ($row['no_of_days'] ?? '')) . '</td>'
|
|
. '<td>' . esc((string) ($row['qty'] ?? '')) . '</td>'
|
|
. '<td>' . esc((string) ($row['remark'] ?? '')) . '</td>'
|
|
. '</tr>';
|
|
}
|
|
$html .= '</tbody></table>';
|
|
|
|
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<string, mixed> $row
|
|
* @return array<string, string>
|
|
*/
|
|
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<string, mixed> $payload
|
|
* @param array<int, string> $fields
|
|
* @param array<string, string> $input
|
|
* @return array<string, string>
|
|
*/
|
|
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<string, string>
|
|
*/
|
|
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<string, mixed> $payload
|
|
* @param array<int, string> $fields
|
|
* @param array<string, string> $input
|
|
* @return array<string, string>
|
|
*/
|
|
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<string, string> $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<string, mixed> $payload
|
|
* @param array<int, string> $fields
|
|
* @param array<string, string> $input
|
|
* @return array<string, string>
|
|
*/
|
|
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<string, string> $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<string, string> $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<string, string> $input
|
|
*/
|
|
private function hasAnyNabhField(array $input): bool
|
|
{
|
|
foreach ($input as $value) {
|
|
if (trim((string) $value) !== '') {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, string> $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<int, string>
|
|
*/
|
|
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<int, string> $candidateColumns
|
|
* @return array<int, string>
|
|
*/
|
|
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<int, string>
|
|
*/
|
|
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<int, string>
|
|
*/
|
|
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<string, int>
|
|
*/
|
|
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<int, array{id:string|int,name:string,selected:int}>
|
|
*/
|
|
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<int, array{id:string|int,name:string,selected:int}>
|
|
*/
|
|
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<string, bool>
|
|
*/
|
|
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<string, int> $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<int, int> $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<int, int>
|
|
*/
|
|
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<string, string> $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<string, mixed> $payload
|
|
* @param array<int, string> $fields
|
|
* @param array<string, string> $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<string, mixed> $patientRow
|
|
* @param array<string, mixed> $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] ?? '')) : '';
|
|
|
|
$patFirstName = $this->sanitizePersonNamePart((string) ($patientRow['p_fname'] ?? ''));
|
|
$patLastName = $this->sanitizePersonNamePart((string) ($patientRow['p_lname'] ?? ''));
|
|
$patFullName = trim($patFirstName . ($patLastName !== '' ? ' ' . $patLastName : ''));
|
|
if ($patFullName === '') {
|
|
$patFullName = trim((string) ($opdRow['P_name'] ?? ''));
|
|
}
|
|
|
|
// Resolve phone field (patient_master may use p_contact, phone, mobile, etc.)
|
|
$patientFields = $this->db->tableExists('patient_master') ? ($this->db->getFieldNames('patient_master') ?? []) : [];
|
|
$phoneField = $this->resolveFirstField($patientFields, ['p_contact', 'phone', 'mobile', 'contact_no', 'mobile_no', 'p_phone']);
|
|
$patPhone = $phoneField !== null ? trim((string) ($patientRow[$phoneField] ?? '')) : '';
|
|
|
|
$patient = [
|
|
'id' => (string) ($patientRow['id'] ?? ($opdRow['p_id'] ?? 0)),
|
|
'name' => $patFullName,
|
|
'given_name' => $patFirstName,
|
|
'family_name' => $patLastName,
|
|
'gender' => $this->normalizeGender((string) ($patientRow['gender'] ?? '')),
|
|
'birthDate' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '',
|
|
'abhaAddress' => $abhaAddress,
|
|
'phone' => $patPhone,
|
|
];
|
|
|
|
$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' => 'OPConsultRecord',
|
|
'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' => 'OPConsultRecord',
|
|
'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<int, array<string, mixed>>
|
|
*/
|
|
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<int, array<string, mixed>>
|
|
*/
|
|
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'] ?? ''));
|
|
|
|
// Do not perform reverse SNOMED lookups here; use only pre-stored codes to keep save fast.
|
|
$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<string, mixed> $opdRow
|
|
* @return array<string, mixed>
|
|
*/
|
|
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);
|
|
|
|
// Primary: use structured complaint_snomed_json (has SNOMED codes, severity, duration)
|
|
$complaints = [];
|
|
$snomedJson = trim((string) ($row['complaint_snomed_json'] ?? ''));
|
|
// Also use snomed_display for FHIR coding display
|
|
if ($snomedJson !== '' && $snomedJson !== '[]') {
|
|
$items = json_decode($snomedJson, true);
|
|
if (is_array($items)) {
|
|
$seen = [];
|
|
foreach ($items as $item) {
|
|
$text = trim((string) ($item['term'] ?? ''));
|
|
if ($text === '') {
|
|
continue;
|
|
}
|
|
$k = strtoupper($text);
|
|
if (isset($seen[$k])) {
|
|
continue;
|
|
}
|
|
$seen[$k] = true;
|
|
// Use original_term if available (preserves doctor's original text)
|
|
$displayText = trim((string) ($item['original_term'] ?? $text));
|
|
$snomedDisplay = trim((string) ($item['snomed_display'] ?? $text));
|
|
$complaints[] = [
|
|
'text' => $displayText,
|
|
'snomed_code' => trim((string) ($item['concept_id'] ?? '')),
|
|
'snomed_display' => $snomedDisplay,
|
|
'severity' => trim((string) ($item['severity'] ?? '')),
|
|
'duration' => trim((string) ($item['duration'] ?? '')),
|
|
'frequency' => trim((string) ($item['frequency'] ?? '')),
|
|
];
|
|
}
|
|
}
|
|
}
|
|
// Fallback: plain-text complaints field
|
|
if (empty($complaints)) {
|
|
$complaintsText = trim((string) ($row['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;
|
|
$complaints[] = [
|
|
'text' => $part,
|
|
'snomed_code' => '',
|
|
'snomed_display' => $part,
|
|
'severity' => '',
|
|
'duration' => '',
|
|
'frequency' => '',
|
|
];
|
|
}
|
|
}
|
|
}
|
|
|
|
$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', ['ABDM_HMS_NAME', 'ABDM_HFR_ID', '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['ABDM_HMS_NAME'] ?? $map['H_Name'] ?? $map['H_name'] ?? ''),
|
|
'hfr_id' => (string) ($map['ABDM_HFR_ID'] ?? $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<string, mixed> $payload
|
|
* @param array<string, mixed> $patientRow
|
|
* @param array<string, mixed> $opdRow
|
|
* @return array<int, string>
|
|
*/
|
|
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';
|
|
}
|
|
|
|
private function sanitizePersonNamePart(string $value): string
|
|
{
|
|
$value = trim($value);
|
|
if ($value === '') {
|
|
return '';
|
|
}
|
|
|
|
$normalized = strtolower($value);
|
|
if (in_array($normalized, ['0', '0.0', 'null', 'nil', 'none', '-'], true)) {
|
|
return '';
|
|
}
|
|
|
|
return $value;
|
|
}
|
|
|
|
/**
|
|
* @return string|int|null
|
|
*/
|
|
private function getCurrentUserId()
|
|
{
|
|
$user = auth()->user();
|
|
if ($user === null) {
|
|
return null;
|
|
}
|
|
|
|
return $user->id ?? null;
|
|
}
|
|
|
|
/**
|
|
* @return array<int, string>
|
|
*/
|
|
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<int, string> $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<int, string> $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<string, mixed>|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<int, string>
|
|
*/
|
|
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<string, mixed> $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<string, string>|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<string, mixed> $input
|
|
* @return array<string, string>
|
|
*/
|
|
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<int, string> $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<int, array<string, string>>
|
|
*/
|
|
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<int, array<string, string>>
|
|
*/
|
|
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<int, array<string, mixed>>
|
|
*/
|
|
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<int, array<string, mixed>>
|
|
*/
|
|
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<int, array<string, mixed>>
|
|
*/
|
|
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;
|
|
}
|
|
}
|