devsofttech_hms_ci4/app/Controllers/Opd_prescription.php

7790 lines
307 KiB
PHP

<?php
namespace App\Controllers;
use App\Controllers\BaseController;
use App\Libraries\BridgeSyncService;
use App\Models\OpdMedicineModel;
use CodeIgniter\I18n\Time;
class Opd_prescription extends BaseController
{
private string $lastAiProvider = 'none';
private string $lastAiServerError = '';
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();
if (!empty($patient)) {
$patient[0]->str_age = get_age_1($patient[0]->dob ?? null, $patient[0]->age ?? '', $patient[0]->age_in_month ?? '', $patient[0]->estimate_dob ?? '', $opdRow->apointment_date ?? null);
}
$prescription = [];
if ($this->db->tableExists('opd_prescription')) {
$prescription = $this->db->table('opd_prescription')
->where('opd_id', $opdId)
->orderBy('id', 'DESC')
->get(1)
->getResult();
if (!empty($prescription)) {
$first = $prescription[0];
$firstRow = (array) $first;
$womenText = '';
if (isset($first->women_related_problems)) {
$womenText = trim((string) $first->women_related_problems);
}
if ($womenText === '') {
$womenText = $this->extractWomenRelatedProblemsFromRemarks((string) ($first->Prescriber_Remarks ?? ''));
}
$womenStructured = $this->parseWomenStructuredDetails($womenText);
$first->women_related_problems = (string) ($womenStructured['women_related_problems'] ?? '');
$first->women_lmp = trim((string) (
$firstRow['women_lmp']
?? $firstRow['lmp']
?? $firstRow['lmp_date']
?? ($womenStructured['women_lmp'] ?? '')
));
$first->women_last_baby = trim((string) (
$firstRow['women_last_baby']
?? $firstRow['last_baby']
?? $firstRow['last_baby_details']
?? ($womenStructured['women_last_baby'] ?? '')
));
$first->women_pregnancy_related = trim((string) (
$firstRow['women_pregnancy_related']
?? $firstRow['pregnancy_related']
?? $firstRow['pregnancy_related_problem']
?? ($womenStructured['women_pregnancy_related'] ?? '')
));
$nabhInfo = $this->hydrateNabhPrescriptionFields(
$firstRow,
(string) ($first->Prescriber_Remarks ?? '')
);
$first->drug_allergy_status = (string) ($nabhInfo['drug_allergy_status'] ?? '');
$first->drug_allergy_details = (string) ($nabhInfo['drug_allergy_details'] ?? '');
$first->adr_history = (string) ($nabhInfo['adr_history'] ?? '');
$first->current_medications = (string) ($nabhInfo['current_medications'] ?? '');
$prescription[0] = $first;
}
}
return view('billing/opd_prescription_basic', [
'opd_id' => $opdId,
'opd_master' => $opdMaster,
'patient_master' => $patient,
'opd_prescription' => $prescription,
'addiction_flags' => $this->getPatientAddictionFlags($patientId),
'co_morbidities' => $this->getCoMorbiditiesWithSelection($patientId),
'co_morbidities_prefilled_from_history' => $this->hasHistoricalCoMorbiditySelection($patientId),
'history_alerts' => $this->buildPatientHistoryAlerts((int) ($opdRow->p_id ?? 0), $opdId),
'left_remarks' => $this->fetchPatientRemarks((int) ($opdRow->p_id ?? 0), 8),
]);
}
public function vitals_get(int $opdId)
{
if (! $this->canAccessPrescription()) {
return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'Access denied']);
}
if ($opdId <= 0 || ! $this->db->tableExists('opd_prescription')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'OPD prescription table not found']);
}
$row = $this->db->table('opd_prescription')
->where('opd_id', (int) $opdId)
->orderBy('id', 'DESC')
->get(1)
->getRowArray() ?? [];
$vitals = [
'pulse' => (string) ($row['pulse'] ?? ''),
'spo2' => (string) ($row['spo2'] ?? ''),
'bp' => (string) ($row['bp'] ?? ''),
'diastolic' => (string) ($row['diastolic'] ?? ''),
'temp' => (string) ($row['temp'] ?? ''),
'rr_min' => (string) ($row['rr_min'] ?? ''),
'height' => (string) ($row['height'] ?? ''),
'weight' => (string) ($row['weight'] ?? ''),
'waist' => (string) ($row['waist'] ?? ''),
];
return $this->response->setJSON([
'update' => 1,
'opd_session_id' => (int) ($row['id'] ?? 0),
'vitals' => $vitals,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function vitals_save()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
if (! $this->canAccessPrescription()) {
return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'Access denied']);
}
$opdId = (int) $this->request->getPost('opd_id');
$sessionId = (int) $this->request->getPost('opd_session_id');
if ($opdId <= 0 || ! $this->db->tableExists('opd_prescription')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'OPD prescription table not found']);
}
$sessionId = $this->ensurePrescriptionSession($opdId, $sessionId);
if ($sessionId <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create prescription session']);
}
$fields = $this->db->getFieldNames('opd_prescription');
$input = [
'pulse' => trim((string) $this->request->getPost('pulse')),
'spo2' => trim((string) $this->request->getPost('spo2')),
'bp' => trim((string) $this->request->getPost('bp')),
'diastolic' => trim((string) $this->request->getPost('diastolic')),
'temp' => trim((string) $this->request->getPost('temp')),
'rr_min' => trim((string) $this->request->getPost('rr_min')),
'height' => trim((string) $this->request->getPost('height')),
'weight' => trim((string) $this->request->getPost('weight')),
'waist' => trim((string) $this->request->getPost('waist')),
];
$update = [];
foreach ($input as $field => $value) {
if (in_array($field, $fields, true)) {
$update[$field] = $value;
}
}
if (empty($update)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'No vital fields available']);
}
$this->db->table('opd_prescription')->where('id', $sessionId)->update($update);
return $this->response->setJSON([
'update' => 1,
'opd_session_id' => $sessionId,
'error_text' => 'Vitals saved successfully',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function opd_prescription_save()
{
if (!$this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON([
'update' => 0,
'error_text' => 'Invalid request',
]);
}
if (!$this->db->tableExists('opd_prescription')) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Table opd_prescription not found.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$opdId = (int) $this->request->getPost('opd_id');
$sessionId = (int) $this->request->getPost('opd_session_id');
$sql = "select * from opd_master where opd_id=" . $opdId;
$query = $this->db->query($sql);
$opdMaster = $query->getResult();
if (empty($opdMaster)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'OPD not found',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$opdRow = $opdMaster[0];
$fields = $this->db->getFieldNames('opd_prescription');
$patientRow = $this->db->table('patient_master')->where('id', (int) ($opdRow->p_id ?? 0))->get(1)->getRowArray() ?? [];
$abhaAddress = trim((string) $this->request->getPost('abha_address'));
if ($abhaAddress !== '' && ! $this->isValidAbhaAddress($abhaAddress)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Invalid ABHA Address format.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$abhaField = $this->resolvePatientAbhaField();
if ($abhaField !== null && $abhaAddress !== '') {
$currentAbha = trim((string) ($patientRow[$abhaField] ?? ''));
if ($currentAbha !== $abhaAddress) {
$this->db->table('patient_master')->where('id', (int) ($opdRow->p_id ?? 0))->update([$abhaField => $abhaAddress]);
$this->auditClinicalUpdate('patient_master', $abhaField, (int) ($opdRow->p_id ?? 0), $currentAbha, $abhaAddress);
$patientRow[$abhaField] = $abhaAddress;
}
}
$payload = [
'complaints' => trim((string) $this->request->getPost('complaints')),
'diagnosis' => trim((string) $this->request->getPost('diagnosis')),
'Provisional_diagnosis' => trim((string) $this->request->getPost('provisional_diagnosis')),
'Finding_Examinations' => trim((string) $this->request->getPost('finding_examinations')),
'Prescriber_Remarks' => trim((string) $this->request->getPost('prescriber_remarks')),
'women_related_problems' => trim((string) $this->request->getPost('women_related_problems')),
'women_lmp' => trim((string) $this->request->getPost('women_lmp')),
'women_last_baby' => trim((string) $this->request->getPost('women_last_baby')),
'women_pregnancy_related' => trim((string) $this->request->getPost('women_pregnancy_related')),
'drug_allergy_status' => trim((string) $this->request->getPost('drug_allergy_status')),
'drug_allergy_details' => trim((string) $this->request->getPost('drug_allergy_details')),
'adr_history' => trim((string) $this->request->getPost('adr_history')),
'current_medications' => trim((string) $this->request->getPost('current_medications')),
'investigation' => trim((string) $this->request->getPost('investigation')),
'advice' => trim((string) $this->request->getPost('advice')),
'next_visit' => trim((string) $this->request->getPost('next_visit')),
'refer_to' => trim((string) $this->request->getPost('refer_to')),
'bp' => trim((string) $this->request->getPost('bp')),
'diastolic' => trim((string) $this->request->getPost('diastolic')),
'pulse' => trim((string) $this->request->getPost('pulse')),
'temp' => trim((string) $this->request->getPost('temp')),
'spo2' => trim((string) $this->request->getPost('spo2')),
'rr_min' => trim((string) $this->request->getPost('rr_min')),
'height' => trim((string) $this->request->getPost('height')),
'weight' => trim((string) $this->request->getPost('weight')),
'waist' => trim((string) $this->request->getPost('waist')),
'pallor' => trim((string) $this->request->getPost('pallor')),
'Icterus' => trim((string) $this->request->getPost('icterus')),
'cyanosis' => trim((string) $this->request->getPost('cyanosis')),
'clubbing' => trim((string) $this->request->getPost('clubbing')),
'edema' => trim((string) $this->request->getPost('edema')),
'pain_value' => trim((string) $this->request->getPost('pain_value')),
'pregnancy' => (int) $this->request->getPost('pregnancy'),
'lactation' => (int) $this->request->getPost('lactation'),
'liver_insufficiency' => (int) $this->request->getPost('liver_insufficiency'),
'renal_insufficiency' => (int) $this->request->getPost('renal_insufficiency'),
'pulmonary_insufficiency' => (int) $this->request->getPost('pulmonary_insufficiency'),
'corona_suspected' => (int) $this->request->getPost('corona_suspected'),
'dengue' => (int) $this->request->getPost('dengue'),
];
$allergyStatusRaw = trim((string) ($payload['drug_allergy_status'] ?? ''));
$allergyStatusNorm = mb_strtolower(preg_replace('/\s+/', ' ', $allergyStatusRaw) ?? $allergyStatusRaw);
$allergyDetails = trim((string) ($payload['drug_allergy_details'] ?? ''));
if ($allergyStatusRaw === '') {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Drug Allergy Status is required as per NABH documentation.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
if (in_array($allergyStatusNorm, ['known', 'yes', 'present'], true) && $allergyDetails === '') {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Drug Allergy Details are required when Drug Allergy Status is Known.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$womenStructuredInput = [
'women_lmp' => (string) ($payload['women_lmp'] ?? ''),
'women_last_baby' => (string) ($payload['women_last_baby'] ?? ''),
'women_pregnancy_related' => (string) ($payload['women_pregnancy_related'] ?? ''),
];
$payload['women_related_problems'] = $this->buildWomenCombinedText(
(string) ($payload['women_related_problems'] ?? ''),
$womenStructuredInput
);
$this->mapWomenStructuredToExistingColumns($payload, $fields, $womenStructuredInput);
$nabhInput = [
'drug_allergy_status' => (string) ($payload['drug_allergy_status'] ?? ''),
'drug_allergy_details' => (string) ($payload['drug_allergy_details'] ?? ''),
'adr_history' => (string) ($payload['adr_history'] ?? ''),
'current_medications' => (string) ($payload['current_medications'] ?? ''),
];
$unmappedNabh = $this->mapNabhFieldsToExistingColumns($payload, $fields, $nabhInput);
if (!in_array('women_related_problems', $fields, true)) {
$womenText = trim((string) ($payload['women_related_problems'] ?? ''));
if ($womenText !== '') {
$payload['Prescriber_Remarks'] = $this->upsertWomenRelatedProblemsIntoRemarks(
(string) ($payload['Prescriber_Remarks'] ?? ''),
$womenText
);
}
}
if (!empty(array_filter($unmappedNabh, static fn ($v) => trim((string) $v) !== ''))) {
$payload['Prescriber_Remarks'] = $this->upsertNabhFieldsIntoRemarks(
(string) ($payload['Prescriber_Remarks'] ?? ''),
$unmappedNabh
);
}
foreach ($payload as $value) {
if (mb_strlen($value) > 4000) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Each section allows up to 4000 characters.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
}
$insertBase = [
'opd_id' => $opdId,
'p_id' => (int) ($opdRow->p_id ?? 0),
'doc_id' => (int) ($opdRow->doc_id ?? 0),
'date_opd_visit' => date('Y-m-d'),
'visit_status' => 0,
'session_id' => 0,
];
$allowedData = [];
foreach (array_merge($insertBase, $payload) as $key => $value) {
if (in_array($key, $fields, true)) {
$allowedData[$key] = $value;
}
}
if (in_array('queue_no', $fields, true) && !isset($allowedData['queue_no'])) {
$maxQueue = $this->db->table('opd_prescription')
->selectMax('queue_no', 'max_queue')
->where('date_opd_visit', date('Y-m-d'))
->where('doc_id', (int) ($opdRow->doc_id ?? 0))
->get()
->getRow();
$allowedData['queue_no'] = (int) ($maxQueue->max_queue ?? 0) + 1;
}
$table = $this->db->table('opd_prescription');
$beforeRow = [];
if ($sessionId > 0) {
$beforeRow = $table->where('id', $sessionId)->where('opd_id', $opdId)->get(1)->getRowArray() ?? [];
$table->where('id', $sessionId)->where('opd_id', $opdId)->update($allowedData);
$recordId = $sessionId;
} else {
$table->insert($allowedData);
$recordId = (int) $this->db->insertID();
}
$afterRow = $this->db->table('opd_prescription')->where('id', $recordId)->where('opd_id', $opdId)->get(1)->getRowArray() ?? [];
if (!empty($afterRow)) {
$this->clinicalAuditTrail->logChangedFields('opd_prescription', $recordId, $beforeRow, $afterRow, $this->getCurrentUserId());
}
$fhirStored = $this->storePrescriptionFhirBundle((int) $opdId, (int) $recordId, $patientRow, (array) $opdRow);
$this->syncPatientAddictionFlags((int) ($opdRow->p_id ?? 0), [
'is_smoking' => (int) $this->request->getPost('is_smoking'),
'is_alcohol' => (int) $this->request->getPost('is_alcohol'),
'is_drug_abuse' => (int) $this->request->getPost('is_drug_abuse'),
]);
$morbidityIds = $this->parseNumericList((string) $this->request->getPost('morbidities_list'));
$this->syncPatientMorbidities((int) ($opdRow->p_id ?? 0), $morbidityIds);
if (in_array('Prescriber_Remarks', $fields, true)) {
$coMorbiditiesText = trim((string) $this->request->getPost('morbidities_text'));
$remarksNow = (string) ($allowedData['Prescriber_Remarks'] ?? ($afterRow['Prescriber_Remarks'] ?? ''));
$updatedRemarks = $this->upsertCoMorbiditiesIntoRemarks($remarksNow, $coMorbiditiesText);
if ($updatedRemarks !== $remarksNow) {
$this->db->table('opd_prescription')
->where('id', $recordId)
->where('opd_id', $opdId)
->update(['Prescriber_Remarks' => $updatedRemarks]);
}
}
return $this->response->setJSON([
'update' => 1,
'opd_session_id' => $recordId,
'error_text' => 'Prescription saved',
'fhir_stored' => $fhirStored ? 1 : 0,
'saved_at' => date('d-m-Y H:i:s'),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function clinical_autotype()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$text = trim((string) $this->request->getPost('text'));
$mode = trim((string) $this->request->getPost('mode'));
if ($mode === '') {
$mode = 'autotype';
}
if ($text === '') {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Please enter text first.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$prompt = $this->buildClinicalPrompt($mode, $text);
$this->lastAiProvider = 'none';
$draft = $this->generateClinicalTextWithAi($prompt);
if ($draft !== null && trim($draft) !== '') {
$provider = $this->lastAiProvider === 'ai-server' ? 'ai-server' : 'ai';
$providerLabel = $provider === 'ai-server' ? 'AI Server' : 'AI';
return $this->response->setJSON([
'update' => 1,
'draft_text' => trim($draft),
'error_text' => 'AI text prepared using ' . $providerLabel,
'ai_mode' => $provider,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$fallback = $text;
$fallback = $this->buildClinicalAutotypeLocalFallback($mode, $text);
$fallbackMessage = 'External AI unavailable. Local rewrite applied.';
if (trim($fallback) === trim($text)) {
$fallbackMessage = 'External AI unavailable. Original text kept.';
}
return $this->response->setJSON([
'update' => 1,
'draft_text' => $fallback,
'error_text' => $fallbackMessage,
'ai_mode' => 'fallback',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function ai_full_draft()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$input = [
'complaints' => trim((string) $this->request->getPost('complaints')),
'finding_examinations' => trim((string) $this->request->getPost('finding_examinations')),
'diagnosis' => trim((string) $this->request->getPost('diagnosis')),
'provisional_diagnosis' => trim((string) $this->request->getPost('provisional_diagnosis')),
'prescriber_remarks' => trim((string) $this->request->getPost('prescriber_remarks')),
'investigation' => trim((string) $this->request->getPost('investigation')),
'advice' => trim((string) $this->request->getPost('advice')),
'next_visit' => trim((string) $this->request->getPost('next_visit')),
'refer_to' => trim((string) $this->request->getPost('refer_to')),
'bp' => trim((string) $this->request->getPost('bp')),
'diastolic' => trim((string) $this->request->getPost('diastolic')),
'pulse' => trim((string) $this->request->getPost('pulse')),
'temp' => trim((string) $this->request->getPost('temp')),
'spo2' => trim((string) $this->request->getPost('spo2')),
'rr_min' => trim((string) $this->request->getPost('rr_min')),
'height' => trim((string) $this->request->getPost('height')),
'weight' => trim((string) $this->request->getPost('weight')),
'waist' => trim((string) $this->request->getPost('waist')),
'pallor' => trim((string) $this->request->getPost('pallor')),
'icterus' => trim((string) $this->request->getPost('icterus')),
'cyanosis' => trim((string) $this->request->getPost('cyanosis')),
'clubbing' => trim((string) $this->request->getPost('clubbing')),
'edema' => trim((string) $this->request->getPost('edema')),
'pain_value' => trim((string) $this->request->getPost('pain_value')),
'pregnancy' => (int) $this->request->getPost('pregnancy'),
'lactation' => (int) $this->request->getPost('lactation'),
'liver_insufficiency' => (int) $this->request->getPost('liver_insufficiency'),
'renal_insufficiency' => (int) $this->request->getPost('renal_insufficiency'),
'pulmonary_insufficiency' => (int) $this->request->getPost('pulmonary_insufficiency'),
'corona_suspected' => (int) $this->request->getPost('corona_suspected'),
'dengue' => (int) $this->request->getPost('dengue'),
'is_smoking' => (int) $this->request->getPost('is_smoking'),
'is_alcohol' => (int) $this->request->getPost('is_alcohol'),
'is_drug_abuse' => (int) $this->request->getPost('is_drug_abuse'),
'women_related_problems' => trim((string) $this->request->getPost('women_related_problems')),
'women_lmp' => trim((string) $this->request->getPost('women_lmp')),
'women_last_baby' => trim((string) $this->request->getPost('women_last_baby')),
'women_pregnancy_related' => trim((string) $this->request->getPost('women_pregnancy_related')),
];
if ($input['complaints'] === '' && $input['finding_examinations'] === '' && $input['diagnosis'] === '') {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Add complaints/finding/diagnosis first, then use AI Draft.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$prompt = $this->buildFullClinicalDraftPrompt($input);
$this->lastAiProvider = 'none';
$draftText = $this->generateClinicalTextWithAi($prompt);
$drafts = $this->extractFullDraftJson($draftText);
if ($drafts === null) {
$drafts = $this->buildFullDraftFallback($input);
$aiMode = 'fallback';
$message = 'AI unavailable. Template draft prepared from entered data.';
} else {
$aiMode = $this->lastAiProvider === 'ai-server' ? 'ai-server' : 'ai';
$message = 'Full clinical draft prepared using ' . ($aiMode === 'ai-server' ? 'AI Server' : 'AI') . '.';
}
return $this->response->setJSON([
'update' => 1,
'drafts' => $drafts,
'ai_mode' => $aiMode,
'error_text' => $message,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function local_clinical_assist()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$input = [
'complaints' => trim((string) $this->request->getPost('complaints')),
'finding_examinations' => trim((string) $this->request->getPost('finding_examinations')),
'diagnosis' => trim((string) $this->request->getPost('diagnosis')),
'investigation' => trim((string) $this->request->getPost('investigation')),
'advice' => trim((string) $this->request->getPost('advice')),
'bp' => trim((string) $this->request->getPost('bp')),
'diastolic' => trim((string) $this->request->getPost('diastolic')),
'pulse' => trim((string) $this->request->getPost('pulse')),
'temp' => trim((string) $this->request->getPost('temp')),
'spo2' => trim((string) $this->request->getPost('spo2')),
'rr_min' => trim((string) $this->request->getPost('rr_min')),
'pain_value' => trim((string) $this->request->getPost('pain_value')),
'pregnancy' => (int) $this->request->getPost('pregnancy'),
'lactation' => (int) $this->request->getPost('lactation'),
'dengue' => (int) $this->request->getPost('dengue'),
'corona_suspected' => (int) $this->request->getPost('corona_suspected'),
];
if ($input['complaints'] === '' && $input['finding_examinations'] === '') {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Enter complaints or findings first for clinical assist.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$assist = $this->buildLocalClinicalAssist($input);
return $this->response->setJSON([
'update' => 1,
'assist' => $assist,
'error_text' => 'Local clinical assist prepared. Final decision remains with doctor.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function section_template_save()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$section = trim((string) $this->request->getPost('section'));
$templateName = trim((string) $this->request->getPost('template_name'));
$templateText = trim((string) $this->request->getPost('template_text'));
$templateScope = strtolower(trim((string) $this->request->getPost('template_scope')));
$templateDocId = (int) $this->request->getPost('template_doc_id');
if ($templateName === '' || $templateText === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Template name and text are required']);
}
if (! $this->isTemplateSectionAllowed($section)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unsupported section']);
}
if (! $this->ensureClinicalTemplateTable()) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to access template storage']);
}
$docId = $this->getCurrentUserId();
if ($templateScope === 'master') {
$docId = 0;
} elseif ($templateDocId > 0) {
$docId = $templateDocId;
} elseif ($docId < 0) {
$docId = 0;
}
$table = $this->db->table('opd_clinical_templates');
$existing = $table
->where('doc_id', $docId)
->where('section_key', $section)
->where('template_name', $templateName)
->where('is_active', 1)
->get(1)
->getRowArray();
$now = date('Y-m-d H:i:s');
if (! empty($existing)) {
$table->where('id', (int) ($existing['id'] ?? 0))->update([
'template_text' => $templateText,
'updated_at' => $now,
]);
} else {
$table->insert([
'doc_id' => $docId,
'section_key' => $section,
'template_name' => $templateName,
'template_text' => $templateText,
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
]);
}
return $this->response->setJSON([
'update' => 1,
'error_text' => ($docId === 0 ? 'Master template saved' : 'My template saved'),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function section_template_list()
{
$section = trim((string) $this->request->getGet('section'));
if (! $this->isTemplateSectionAllowed($section)) {
return $this->response->setJSON(['rows' => []]);
}
if (! $this->ensureClinicalTemplateTable()) {
return $this->response->setJSON(['rows' => []]);
}
$docId = $this->getCurrentUserId();
$rows = $this->db->table('opd_clinical_templates')
->select('id,template_name,template_text,doc_id,section_key')
->where('section_key', $section)
->where('is_active', 1)
->groupStart()
->where('doc_id', $docId)
->orWhere('doc_id', 0)
->groupEnd()
->orderBy('doc_id', 'DESC')
->orderBy('template_name', 'ASC')
->limit(100)
->get()
->getResultArray();
$usageByTemplateId = [];
if (! empty($rows) && $this->ensureClinicalTemplateUsageTable() && $docId !== null) {
$templateIds = array_map(static fn($row) => (int) ($row['id'] ?? 0), $rows);
$templateIds = array_values(array_filter($templateIds, static fn($id) => $id > 0));
if (! empty($templateIds)) {
$usageRows = $this->db->table('opd_clinical_template_usage')
->select('template_id,use_count,last_used_at')
->where('doc_id', (int) $docId)
->where('section_key', $section)
->whereIn('template_id', $templateIds)
->get()
->getResultArray();
foreach ($usageRows as $usageRow) {
$templateId = (int) ($usageRow['template_id'] ?? 0);
if ($templateId <= 0) {
continue;
}
$usageByTemplateId[$templateId] = [
'use_count' => (int) ($usageRow['use_count'] ?? 0),
'last_used_at' => (string) ($usageRow['last_used_at'] ?? ''),
];
}
}
}
foreach ($rows as &$row) {
$templateId = (int) ($row['id'] ?? 0);
$usage = $usageByTemplateId[$templateId] ?? ['use_count' => 0, 'last_used_at' => ''];
$row['use_count'] = (int) ($usage['use_count'] ?? 0);
$row['last_used_at'] = (string) ($usage['last_used_at'] ?? '');
}
unset($row);
usort($rows, static function (array $a, array $b): int {
$aUse = (int) ($a['use_count'] ?? 0);
$bUse = (int) ($b['use_count'] ?? 0);
if ($aUse !== $bUse) {
return $bUse <=> $aUse;
}
$aDoc = (int) ($a['doc_id'] ?? 0);
$bDoc = (int) ($b['doc_id'] ?? 0);
if ($aDoc !== $bDoc) {
return $bDoc <=> $aDoc;
}
$aLast = trim((string) ($a['last_used_at'] ?? ''));
$bLast = trim((string) ($b['last_used_at'] ?? ''));
if ($aLast !== $bLast) {
return strcmp($bLast, $aLast);
}
return strcasecmp((string) ($a['template_name'] ?? ''), (string) ($b['template_name'] ?? ''));
});
return $this->response->setJSON(['rows' => $rows]);
}
public function section_template_track_usage()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$section = trim((string) $this->request->getPost('section'));
$templateId = (int) $this->request->getPost('template_id');
if ($templateId <= 0 || ! $this->isTemplateSectionAllowed($section)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Invalid template usage payload',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
if (! $this->ensureClinicalTemplateTable() || ! $this->ensureClinicalTemplateUsageTable()) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Usage storage unavailable',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$docId = $this->getCurrentUserId();
if ($docId === null) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'User not found',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$templateRow = $this->db->table('opd_clinical_templates')
->select('id,section_key,is_active')
->where('id', $templateId)
->where('is_active', 1)
->get(1)
->getRowArray();
if (empty($templateRow)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Template not found',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
if (trim((string) ($templateRow['section_key'] ?? '')) !== $section) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Section mismatch for template',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$usageTable = $this->db->table('opd_clinical_template_usage');
$existing = $usageTable
->where('doc_id', (int) $docId)
->where('template_id', $templateId)
->where('section_key', $section)
->get(1)
->getRowArray();
$now = date('Y-m-d H:i:s');
if (! empty($existing)) {
$usageTable->where('id', (int) ($existing['id'] ?? 0))->update([
'use_count' => ((int) ($existing['use_count'] ?? 0)) + 1,
'last_used_at' => $now,
'updated_at' => $now,
]);
} else {
$usageTable->insert([
'doc_id' => (int) $docId,
'template_id' => $templateId,
'section_key' => $section,
'use_count' => 1,
'last_used_at' => $now,
'created_at' => $now,
'updated_at' => $now,
]);
}
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Template usage tracked',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function section_past_data()
{
$section = trim((string) $this->request->getGet('section'));
$opdId = (int) $this->request->getGet('opd_id');
if ($opdId <= 0 || ! $this->isTemplateSectionAllowed($section) || ! $this->db->tableExists('opd_master') || ! $this->db->tableExists('opd_prescription')) {
return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Past data not found']);
}
$field = $this->resolveSectionColumn($section);
if ($field === null) {
return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Unsupported section']);
}
$fields = $this->db->getFieldNames('opd_prescription');
if (! in_array($field, $fields, true)) {
return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Past data column missing']);
}
$opdRow = $this->db->table('opd_master')->select('p_id')->where('opd_id', $opdId)->get(1)->getRowArray();
$patientId = (int) ($opdRow['p_id'] ?? 0);
if ($patientId <= 0) {
return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Patient not found']);
}
$row = $this->db->table('opd_prescription')
->select($field . ' as section_text,opd_id,id')
->where('p_id', $patientId)
->where('opd_id !=', $opdId)
->where($field . ' IS NOT NULL', null, false)
->where($field . ' !=', '')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
if (empty($row)) {
return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'No past data found']);
}
return $this->response->setJSON([
'update' => 1,
'past_text' => (string) ($row['section_text'] ?? ''),
'past_opd_id' => (int) ($row['opd_id'] ?? 0),
'error_text' => 'Past data loaded',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function template_workspace()
{
return view('billing/opd_template_workspace');
}
public function clinical_template_monitor()
{
if (! $this->ensureClinicalTemplateTable()) {
return $this->response->setJSON(['rows' => []]);
}
$section = trim((string) $this->request->getGet('section'));
$scope = strtolower(trim((string) $this->request->getGet('scope')));
$doctorId = (int) $this->request->getGet('doctor_id');
$docId = $this->getCurrentUserId();
$builder = $this->db->table('opd_clinical_templates')
->select('id,doc_id,section_key,template_name,template_text,is_active,created_at,updated_at')
->where('is_active', 1);
if ($this->isTemplateSectionAllowed($section)) {
$builder->where('section_key', $section);
}
if ($scope === 'master') {
$builder->where('doc_id', 0);
} elseif ($scope === 'doctor') {
if ($doctorId > 0) {
$builder->where('doc_id', $doctorId);
} else {
$builder->where('doc_id', $docId);
}
} else {
$builder->groupStart()
->where('doc_id', 0)
->orWhere('doc_id', $docId)
->groupEnd();
}
$rows = $builder
->orderBy('updated_at', 'DESC')
->orderBy('id', 'DESC')
->limit(300)
->get()
->getResultArray();
$doctorNameById = [];
if ($this->db->tableExists('doctor_master')) {
$doctorRows = $this->db->table('doctor_master')->select('id,p_fname')->get()->getResultArray();
foreach ($doctorRows as $doctorRow) {
$doctorNameById[(int) ($doctorRow['id'] ?? 0)] = (string) ($doctorRow['p_fname'] ?? '');
}
}
foreach ($rows as &$row) {
$row['scope_label'] = ((int) ($row['doc_id'] ?? -1) === 0) ? 'Master' : 'Doctor';
$row['doctor_name'] = ((int) ($row['doc_id'] ?? 0) > 0) ? ($doctorNameById[(int) ($row['doc_id'] ?? 0)] ?? '') : '';
}
unset($row);
return $this->response->setJSON(['rows' => $rows]);
}
public function clinical_template_update()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
if (! $this->ensureClinicalTemplateTable()) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Template storage unavailable']);
}
$id = (int) $this->request->getPost('id');
$section = trim((string) $this->request->getPost('section'));
$templateName = trim((string) $this->request->getPost('template_name'));
$templateText = trim((string) $this->request->getPost('template_text'));
$templateScope = strtolower(trim((string) $this->request->getPost('template_scope')));
$templateDocId = (int) $this->request->getPost('template_doc_id');
if ($id <= 0 || ! $this->isTemplateSectionAllowed($section) || $templateName === '' || $templateText === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Template id, section, name and text are required']);
}
$targetDocId = $templateScope === 'master' ? 0 : ($templateDocId > 0 ? $templateDocId : $this->getCurrentUserId());
if ($targetDocId < 0) {
$targetDocId = 0;
}
$current = $this->db->table('opd_clinical_templates')->where('id', $id)->where('is_active', 1)->get(1)->getRowArray();
if (empty($current)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Template not found']);
}
$currentDocId = (int) ($current['doc_id'] ?? -1);
$myDocId = $this->getCurrentUserId();
if ($currentDocId !== 0 && $currentDocId !== $myDocId) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'You can edit only your own or master templates']);
}
$existing = $this->db->table('opd_clinical_templates')
->where('id !=', $id)
->where('doc_id', $targetDocId)
->where('section_key', $section)
->where('template_name', $templateName)
->where('is_active', 1)
->get(1)
->getRowArray();
if (!empty($existing)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Another template with same name already exists in selected scope']);
}
$this->db->table('opd_clinical_templates')->where('id', $id)->update([
'doc_id' => $targetDocId,
'section_key' => $section,
'template_name' => $templateName,
'template_text' => $templateText,
'updated_at' => date('Y-m-d H:i:s'),
]);
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Template updated',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function clinical_template_doctors()
{
if (! $this->db->tableExists('doctor_master')) {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames('doctor_master');
$idField = $this->resolveFirstField($fields, ['id', 'doc_id']);
$nameField = $this->resolveFirstField($fields, ['p_fname', 'name', 'doc_name']);
if ($idField === null || $nameField === null) {
return $this->response->setJSON(['rows' => []]);
}
$builder = $this->db->table('doctor_master')
->select($idField . ' as id,' . $nameField . ' as name');
if (in_array('active', $fields, true)) {
$builder->where('active', 1);
}
$rows = $builder->orderBy($nameField, 'ASC')->get()->getResultArray();
return $this->response->setJSON(['rows' => $rows]);
}
public function clinical_template_usage_analytics()
{
if (! $this->ensureClinicalTemplateTable() || ! $this->ensureClinicalTemplateUsageTable()) {
return $this->response->setJSON([
'top_rows' => [],
'section_rows' => [],
'summary' => ['total_uses' => 0, 'used_templates' => 0],
]);
}
$section = trim((string) $this->request->getGet('section'));
$scope = strtolower(trim((string) $this->request->getGet('scope')));
$doctorId = (int) $this->request->getGet('doctor_id');
$currentDocId = (int) ($this->getCurrentUserId() ?? 0);
$targetDocId = $scope === 'doctor' && $doctorId > 0 ? $doctorId : $currentDocId;
$builder = $this->db->table('opd_clinical_template_usage u')
->select('u.template_id,u.section_key,u.use_count,u.last_used_at,t.template_name,t.doc_id')
->join('opd_clinical_templates t', 't.id = u.template_id', 'inner')
->where('u.doc_id', $targetDocId)
->where('t.is_active', 1);
if ($this->isTemplateSectionAllowed($section)) {
$builder->where('u.section_key', $section);
}
if ($scope === 'master') {
$builder->where('t.doc_id', 0);
} elseif ($scope === 'doctor') {
$builder->where('t.doc_id', $targetDocId);
} else {
$builder->groupStart()
->where('t.doc_id', 0)
->orWhere('t.doc_id', $targetDocId)
->groupEnd();
}
$usageRows = $builder->get()->getResultArray();
if (empty($usageRows)) {
return $this->response->setJSON([
'top_rows' => [],
'section_rows' => [],
'summary' => ['total_uses' => 0, 'used_templates' => 0],
]);
}
usort($usageRows, static function (array $a, array $b): int {
$aUse = (int) ($a['use_count'] ?? 0);
$bUse = (int) ($b['use_count'] ?? 0);
if ($aUse !== $bUse) {
return $bUse <=> $aUse;
}
return strcmp((string) ($b['last_used_at'] ?? ''), (string) ($a['last_used_at'] ?? ''));
});
$totalUses = 0;
$usedTemplates = [];
$sectionAgg = [];
foreach ($usageRows as $row) {
$use = (int) ($row['use_count'] ?? 0);
$sec = trim((string) ($row['section_key'] ?? ''));
$totalUses += $use;
$templateId = (int) ($row['template_id'] ?? 0);
if ($templateId > 0) {
$usedTemplates[$templateId] = true;
}
if ($sec === '') {
continue;
}
if (! isset($sectionAgg[$sec])) {
$sectionAgg[$sec] = ['section_key' => $sec, 'total_uses' => 0, 'template_count' => 0, 'template_ids' => []];
}
$sectionAgg[$sec]['total_uses'] += $use;
if ($templateId > 0 && ! isset($sectionAgg[$sec]['template_ids'][$templateId])) {
$sectionAgg[$sec]['template_ids'][$templateId] = true;
$sectionAgg[$sec]['template_count']++;
}
}
$sectionRows = array_values(array_map(static function (array $row): array {
unset($row['template_ids']);
return $row;
}, $sectionAgg));
usort($sectionRows, static function (array $a, array $b): int {
$aUse = (int) ($a['total_uses'] ?? 0);
$bUse = (int) ($b['total_uses'] ?? 0);
if ($aUse !== $bUse) {
return $bUse <=> $aUse;
}
return strcasecmp((string) ($a['section_key'] ?? ''), (string) ($b['section_key'] ?? ''));
});
return $this->response->setJSON([
'top_rows' => array_slice($usageRows, 0, 15),
'section_rows' => $sectionRows,
'summary' => [
'total_uses' => $totalUses,
'used_templates' => count($usedTemplates),
],
]);
}
public function rx_group_panel()
{
return view('billing/opd_rx_group_workspace', ['initial_rx_id' => 0]);
}
public function save_rx_group_edit(int $rxId = 0)
{
return view('billing/opd_rx_group_workspace', ['initial_rx_id' => max(0, $rxId)]);
}
public function rx_group_data()
{
if (! $this->db->tableExists('opd_prescription_template')) {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames('opd_prescription_template');
$idField = $this->resolveFirstField($fields, ['id']);
$nameField = $this->resolveFirstField($fields, ['rx_group_name']);
if ($idField === null || $nameField === null) {
return $this->response->setJSON(['rows' => []]);
}
$userId = max(0, $this->getCurrentUserId());
$hasDocId = in_array('doc_id', $fields, true);
$builder = $this->db->table('opd_prescription_template')
->select('MAX(' . $idField . ') as id,' . $nameField . ' as rx_group_name,COUNT(*) as row_count')
->where($nameField . ' <>', '');
if ($hasDocId) {
$builder->select('SUM(CASE WHEN doc_id = 0 THEN 1 ELSE 0 END) as global_count', false);
$builder->select('SUM(CASE WHEN doc_id = ' . (int) $userId . ' THEN 1 ELSE 0 END) as doctor_count', false);
}
if ($hasDocId) {
$builder->groupStart()
->where('doc_id', $userId)
->orWhere('doc_id', 0)
->groupEnd();
}
$rows = $builder
->groupBy($nameField)
->orderBy($nameField, 'ASC')
->limit(500)
->get()
->getResultArray();
foreach ($rows as &$row) {
if (! $hasDocId) {
$row['access_label'] = 'Global';
continue;
}
$globalCount = (int) ($row['global_count'] ?? 0);
$doctorCount = (int) ($row['doctor_count'] ?? 0);
if ($globalCount > 0 && $doctorCount > 0) {
$row['access_label'] = 'Global + Doctor';
} elseif ($globalCount > 0) {
$row['access_label'] = 'Global';
} elseif ($doctorCount > 0) {
$row['access_label'] = 'Doctor';
} else {
$row['access_label'] = 'Specific User';
}
}
unset($row);
return $this->response->setJSON(['rows' => $rows]);
}
public function rx_group_apply_to_session()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$opdId = (int) $this->request->getPost('opd_id');
$sessionId = (int) $this->request->getPost('opd_session_id');
$rxGroupId = (int) $this->request->getPost('rx_group_id');
if ($opdId <= 0 || $rxGroupId <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid OPD or Rx-Group']);
}
$templateTable = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']);
if ($templateTable === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx-Group medicine template not found']);
}
$sessionTable = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']);
if ($sessionTable === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']);
}
$templateFields = $this->db->getFieldNames($templateTable);
if (! in_array('rx_group_id', $templateFields, true)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid Rx-Group template structure']);
}
$templateRows = $this->db->table($templateTable)
->where('rx_group_id', $rxGroupId)
->orderBy('id', 'ASC')
->get()
->getResultArray();
if (empty($templateRows)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'No medicines found in selected Rx-Group']);
}
$sessionId = $this->ensurePrescriptionSession($opdId, $sessionId);
if ($sessionId <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create prescription session']);
}
$sessionFields = $this->db->getFieldNames($sessionTable);
$user = auth()->user();
$userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User';
$updateByText = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s');
$inserted = 0;
$skipped = 0;
foreach ($templateRows as $row) {
$insert = [];
if (in_array('opd_pre_id', $sessionFields, true)) {
$insert['opd_pre_id'] = $sessionId;
}
if (in_array('opd_id', $sessionFields, true)) {
$insert['opd_id'] = $opdId;
}
foreach (['med_id', 'med_name', 'med_type', 'dosage', 'dosage_when', 'dosage_freq', 'dosage_where', 'no_of_days', 'qty', 'remark', 'genericname'] as $field) {
if (! in_array($field, $sessionFields, true) || ! array_key_exists($field, $row)) {
continue;
}
$value = $row[$field];
if (in_array($field, ['med_name', 'remark'], true)) {
$value = strtoupper((string) $value);
}
$insert[$field] = $value;
}
if (in_array('update_by', $sessionFields, true)) {
$insert['update_by'] = $updateByText;
}
if (! empty($insert)) {
if ($this->isDuplicateMedicineRow($sessionTable, $sessionFields, $sessionId, $insert)) {
$skipped++;
continue;
}
$this->db->table($sessionTable)->insert($insert);
$inserted++;
$medId = (int) ($insert['med_id'] ?? 0);
if ($medId > 0) {
$this->trackMedicineUsage($medId);
}
}
}
return $this->response->setJSON([
'update' => 1,
'opd_session_id' => $sessionId,
'inserted_count' => $inserted,
'skipped_count' => $skipped,
'error_text' => $inserted > 0
? ($inserted . ' medicine(s) added from Rx-Group.' . ($skipped > 0 ? (' ' . $skipped . ' duplicate(s) skipped.') : ''))
: ($skipped > 0 ? ($skipped . ' duplicate medicine(s) already present.') : 'No medicine inserted from Rx-Group.'),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function rx_group_get(int $rxId)
{
if (! $this->db->tableExists('opd_prescription_template')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group table not found']);
}
$fields = $this->db->getFieldNames('opd_prescription_template');
$builder = $this->db->table('opd_prescription_template')->where('id', (int) $rxId);
if (in_array('doc_id', $fields, true)) {
$userId = max(0, $this->getCurrentUserId());
$builder->groupStart()
->where('doc_id', $userId)
->orWhere('doc_id', 0)
->groupEnd();
}
$row = $builder->get(1)->getRowArray() ?? [];
if (empty($row)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group not found']);
}
return $this->response->setJSON(['update' => 1, 'row' => $row]);
}
public function rx_group_save()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
if (! $this->db->tableExists('opd_prescription_template')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group table not found']);
}
$id = (int) $this->request->getPost('id');
$name = trim((string) $this->request->getPost('rx_group_name'));
if ($name === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx-Group Name is required']);
}
$fields = $this->db->getFieldNames('opd_prescription_template');
$data = [];
if (in_array('rx_group_name', $fields, true)) {
$data['rx_group_name'] = $name;
}
if (in_array('complaints', $fields, true)) {
$data['complaints'] = trim((string) $this->request->getPost('complaints'));
}
if (in_array('diagnosis', $fields, true)) {
$data['diagnosis'] = trim((string) $this->request->getPost('diagnosis'));
}
if (in_array('Finding_Examinations', $fields, true)) {
$data['Finding_Examinations'] = trim((string) $this->request->getPost('finding_examinations'));
}
if (in_array('investigation', $fields, true)) {
$data['investigation'] = trim((string) $this->request->getPost('investigation'));
}
$userId = max(0, $this->getCurrentUserId());
if (in_array('rx_group_name', $fields, true)) {
$dupBuilder = $this->db->table('opd_prescription_template')
->select('id')
->where('rx_group_name', $name);
if (in_array('doc_id', $fields, true)) {
$dupBuilder->groupStart()
->where('doc_id', $userId)
->orWhere('doc_id', 0)
->groupEnd();
}
if ($id > 0) {
$dupBuilder->where('id <>', $id);
}
$exists = $dupBuilder->orderBy('id', 'DESC')->get(1)->getRowArray();
if (! empty($exists)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Rx-Group name already exists. Please use Edit for existing group.',
'existing_id' => (int) ($exists['id'] ?? 0),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
}
if (in_array('doc_id', $fields, true) && $id <= 0) {
$data['doc_id'] = $userId;
}
$user = auth()->user();
if (in_array('update_by', $fields, true)) {
$userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User';
$data['update_by'] = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s');
}
if ($id > 0) {
$this->db->table('opd_prescription_template')->where('id', $id)->update($data);
$savedId = $id;
$message = 'Rx-Group updated';
} else {
$this->db->table('opd_prescription_template')->insert($data);
$savedId = (int) $this->db->insertID();
$message = 'Rx-Group saved';
}
return $this->response->setJSON([
'update' => 1,
'insertid' => $savedId,
'error_text' => $message,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function rx_group_medicine(int $rxId)
{
$rxGroupId = max(0, $rxId);
$rxGroupName = '';
if ($rxGroupId > 0 && $this->db->tableExists('opd_prescription_template')) {
$templateFields = $this->db->getFieldNames('opd_prescription_template');
$nameField = $this->resolveFirstField($templateFields, ['rx_group_name']);
if ($nameField !== null) {
$row = $this->db->table('opd_prescription_template')
->select($nameField . ' as rx_group_name')
->where('id', $rxGroupId)
->get(1)
->getRowArray();
$rxGroupName = trim((string) ($row['rx_group_name'] ?? ''));
}
}
return view('billing/rx_group_medicine_workspace', [
'rx_group_id' => $rxGroupId,
'rx_group_name' => $rxGroupName,
]);
}
public function rx_group_medicine_list(int $rxId)
{
$table = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']);
if ($table === null) {
return $this->response->setJSON(['rows' => [], 'rx_group_name' => '']);
}
$fields = $this->db->getFieldNames($table);
if (! in_array('rx_group_id', $fields, true)) {
return $this->response->setJSON(['rows' => [], 'rx_group_name' => '']);
}
$rxGroupName = '';
if ($this->db->tableExists('opd_prescription_template')) {
$templateFields = $this->db->getFieldNames('opd_prescription_template');
$nameField = $this->resolveFirstField($templateFields, ['rx_group_name']);
if ($nameField !== null) {
$groupRow = $this->db->table('opd_prescription_template')
->select($nameField . ' as rx_group_name')
->where('id', (int) $rxId)
->get(1)
->getRowArray();
$rxGroupName = trim((string) ($groupRow['rx_group_name'] ?? ''));
}
}
$builder = $this->db->table($table)->where('rx_group_id', (int) $rxId);
if (in_array('id', $fields, true)) {
$builder->orderBy('id', 'DESC');
}
$rows = $builder->get()->getResultArray();
if (empty($rows)) {
return $this->response->setJSON(['rows' => [], 'rx_group_name' => $rxGroupName]);
}
$medGenericMap = [];
$missingGenericMedIds = [];
foreach ($rows as $row) {
$existingGeneric = trim((string) ($row['genericname'] ?? ''));
$medId = (int) ($row['med_id'] ?? 0);
if ($existingGeneric === '' && $medId > 0) {
$missingGenericMedIds[$medId] = true;
}
}
if (! empty($missingGenericMedIds)) {
$masterTable = $this->findExistingTable(['opd_med_master']);
if ($masterTable !== null) {
$masterFields = $this->db->getFieldNames($masterTable);
$masterIdField = $this->resolveFirstField($masterFields, ['id']);
$masterGenericField = $this->resolveFirstField($masterFields, ['genericname', 'generic_name']);
$masterSaltField = $this->resolveFirstField($masterFields, ['salt_name', 'sal_name', 'salt', 'saltname']);
$labelField = $masterSaltField ?? $masterGenericField;
if ($masterIdField !== null && $labelField !== null) {
$medRows = $this->db->table($masterTable)
->select($masterIdField . ' as med_id,' . $labelField . ' as generic_fallback')
->whereIn($masterIdField, array_map('intval', array_keys($missingGenericMedIds)))
->get()
->getResultArray();
foreach ($medRows as $medRow) {
$id = (int) ($medRow['med_id'] ?? 0);
$label = trim((string) ($medRow['generic_fallback'] ?? ''));
if ($id > 0 && $label !== '') {
$medGenericMap[$id] = $label;
}
}
}
}
}
$doseMap = $this->getDoseMasterLabelMap('opd_dose_shed', ['dose_shed_id', 'id'], ['dose_show_sign', 'dose_sign', 'dose_sign_desc', 'name']);
$whenMap = $this->getDoseMasterLabelMap('opd_dose_when', ['dose_when_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']);
$freqMap = $this->getDoseMasterLabelMap('opd_dose_frequency', ['dose_freq_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']);
$whereMap = $this->getDoseMasterLabelMap('opd_dose_where', ['dose_where_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']);
foreach ($rows as &$row) {
if (trim((string) ($row['genericname'] ?? '')) === '') {
$inlineSalt = trim((string) ($row['salt_name'] ?? ''));
if ($inlineSalt !== '') {
$row['genericname'] = $inlineSalt;
}
$fallback = $medGenericMap[(int) ($row['med_id'] ?? 0)] ?? '';
if (($row['genericname'] ?? '') === '' && $fallback !== '') {
$row['genericname'] = $fallback;
}
}
$row['dosage_label'] = $this->resolveDoseMasterLabel($row['dosage'] ?? '', $doseMap);
$row['dosage_when_label'] = $this->resolveDoseMasterLabel($row['dosage_when'] ?? '', $whenMap);
$row['dosage_freq_label'] = $this->resolveDoseMasterLabel($row['dosage_freq'] ?? '', $freqMap);
$row['dosage_where_label'] = $this->resolveDoseMasterLabel($row['dosage_where'] ?? '', $whereMap);
}
unset($row);
return $this->response->setJSON(['rows' => $rows, 'rx_group_name' => $rxGroupName]);
}
public function rx_group_medicine_get(int $itemId)
{
$table = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx medicine table not found']);
}
$row = $this->db->table($table)->where('id', (int) $itemId)->get(1)->getRowArray() ?? [];
if (empty($row)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx medicine not found']);
}
return $this->response->setJSON(['update' => 1, 'row' => $row]);
}
public function rx_group_medicine_save(int $rxId)
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
if (! $this->db->tableExists('opd_prescription_template')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group table not found']);
}
$rxExists = $this->db->table('opd_prescription_template')->where('id', (int) $rxId)->get(1)->getRowArray();
if (empty($rxExists)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group not found']);
}
$table = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx medicine table not found']);
}
$itemId = (int) $this->request->getPost('item_id');
$medId = (int) $this->request->getPost('med_id');
$medName = trim((string) $this->request->getPost('med_name'));
if ($medName === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']);
}
$genericName = trim((string) $this->request->getPost('genericname'));
$medType = trim((string) $this->request->getPost('med_type'));
$masterTable = $this->findExistingTable(['opd_med_master']);
if ($masterTable !== null) {
$masterFields = $this->db->getFieldNames($masterTable);
$masterNameField = $this->resolveFirstField($masterFields, ['item_name', 'med_name']);
$masterTypeField = $this->resolveFirstField($masterFields, ['formulation']);
$masterGenericField = $this->resolveFirstField($masterFields, ['genericname', 'generic_name']);
$masterSaltField = $this->resolveFirstField($masterFields, ['salt_name', 'sal_name', 'salt', 'saltname']);
if ($medId <= 0 && $masterNameField !== null) {
$existingMaster = $this->db->table($masterTable)->where($masterNameField, $medName)->get(1)->getRowArray();
if (! empty($existingMaster)) {
$medId = (int) ($existingMaster['id'] ?? 0);
}
}
if ($medId > 0) {
$updateMaster = [];
if ($masterTypeField !== null && $medType !== '') {
$updateMaster[$masterTypeField] = $medType;
}
if ($masterGenericField !== null && $genericName !== '') {
$updateMaster[$masterGenericField] = $genericName;
}
if ($masterSaltField !== null && $genericName !== '') {
$updateMaster[$masterSaltField] = $genericName;
}
if (! empty($updateMaster)) {
$this->db->table($masterTable)->where('id', $medId)->update($updateMaster);
}
} elseif ($masterNameField !== null) {
$insertMaster = [$masterNameField => $medName];
if ($masterTypeField !== null) {
$insertMaster[$masterTypeField] = $medType;
}
if ($masterGenericField !== null) {
$insertMaster[$masterGenericField] = $genericName;
}
if ($masterSaltField !== null) {
$insertMaster[$masterSaltField] = $genericName;
}
$this->db->table($masterTable)->insert($insertMaster);
$medId = (int) $this->db->insertID();
}
}
$fields = $this->db->getFieldNames($table);
$data = [];
if (in_array('rx_group_id', $fields, true)) {
$data['rx_group_id'] = (int) $rxId;
}
if (in_array('med_id', $fields, true)) {
$data['med_id'] = $medId;
}
if (in_array('med_name', $fields, true)) {
$data['med_name'] = strtoupper($medName);
}
if (in_array('med_type', $fields, true)) {
$data['med_type'] = $medType;
}
if (in_array('dosage', $fields, true)) {
$data['dosage'] = trim((string) $this->request->getPost('dosage'));
}
if (in_array('dosage_when', $fields, true)) {
$data['dosage_when'] = trim((string) $this->request->getPost('dosage_when'));
}
if (in_array('dosage_freq', $fields, true)) {
$data['dosage_freq'] = trim((string) $this->request->getPost('dosage_freq'));
}
if (in_array('dosage_where', $fields, true)) {
$data['dosage_where'] = trim((string) $this->request->getPost('dosage_where'));
}
if (in_array('no_of_days', $fields, true)) {
$data['no_of_days'] = trim((string) $this->request->getPost('no_of_days'));
}
if (in_array('qty', $fields, true)) {
$data['qty'] = trim((string) $this->request->getPost('qty'));
}
if (in_array('remark', $fields, true)) {
$data['remark'] = strtoupper(trim((string) $this->request->getPost('remark')));
}
if (in_array('genericname', $fields, true)) {
$data['genericname'] = $genericName;
}
$user = auth()->user();
if (in_array('update_by', $fields, true)) {
$userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User';
$data['update_by'] = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s');
}
if ($itemId > 0) {
$this->db->table($table)->where('id', $itemId)->update($data);
$savedId = $itemId;
$message = 'Rx group medicine updated';
} else {
$this->db->table($table)->insert($data);
$savedId = (int) $this->db->insertID();
$message = 'Rx group medicine added';
}
return $this->response->setJSON([
'update' => 1,
'insertid' => $savedId,
'error_text' => $message,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function rx_group_medicine_remove(int $itemId)
{
$table = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx medicine table not found']);
}
$this->db->table($table)->where('id', (int) $itemId)->delete();
return $this->response->setJSON(['update' => 1, 'error_text' => 'Rx group medicine removed']);
}
public function rx_group_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);
}
private function getOpdMedicineRows(string $filter = '', string $scope = 'all', bool $showAll = false): array
{
$table = $this->findExistingTable(['opd_med_master']);
if ($table === null) {
return [];
}
$fields = $this->db->getFieldNames($table);
$idField = $this->resolveFirstField($fields, ['id']);
$nameField = $this->resolveFirstField($fields, ['item_name', 'med_name']);
$genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']);
$saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']);
$companyField = $this->resolveFirstField($fields, ['company_name', 'company']);
$restrictionField = $this->resolveFirstField($fields, ['dosage_restriction', 'dose_restriction', 'restriction_note', 'restriction']);
if ($idField === null || $nameField === null) {
return [];
}
$select = $idField . ' as id,' . $nameField . ' as item_name';
if (in_array('formulation', $fields, true)) {
$select .= ',formulation';
}
if ($genericField !== null) {
$select .= ',' . $genericField . ' as genericname';
}
if ($saltField !== null) {
$select .= ',' . $saltField . ' as salt_name';
}
if ($companyField !== null) {
$select .= ',' . $companyField . ' as company_name';
}
if ($restrictionField !== null) {
$select .= ',' . $restrictionField . ' as dosage_restriction';
}
$rows = $this->db->table($table)
->select($select)
->orderBy($nameField, 'ASC')
->limit(5000)
->get()
->getResultArray();
$unclearTokens = ['na', 'n/a', 'same', '-', '--', 'unknown', '?'];
$normalize = static function ($value): string {
$text = trim((string) $value);
if ($text === '') {
return '';
}
$text = preg_replace('/\s+/', ' ', $text) ?? $text;
return mb_strtolower($text);
};
if ($filter === 'generic_issue') {
$rows = array_values(array_filter($rows, static function (array $row) use ($normalize, $unclearTokens): bool {
$name = $normalize($row['item_name'] ?? '');
$generic = $normalize($row['genericname'] ?? '');
$salt = $normalize($row['salt_name'] ?? '');
$genericUnclear = ($generic === '' || in_array($generic, $unclearTokens, true) || ($name !== '' && $generic === $name));
$saltUnclear = ($salt === '' || in_array($salt, $unclearTokens, true) || ($name !== '' && $salt === $name));
return $genericUnclear && $saltUnclear;
}));
} elseif ($filter === 'generic_same_name') {
$rows = array_values(array_filter($rows, static function (array $row) use ($normalize): bool {
$name = $normalize($row['item_name'] ?? '');
$generic = $normalize($row['genericname'] ?? '');
$salt = $normalize($row['salt_name'] ?? '');
if ($name === '') {
return false;
}
return ($generic !== '' && $generic === $name) || ($salt !== '' && $salt === $name);
}));
} elseif ($filter === 'company_blank') {
$rows = array_values(array_filter($rows, static function (array $row) use ($normalize, $unclearTokens): bool {
$company = $normalize($row['company_name'] ?? '');
return $company === '' || in_array($company, $unclearTokens, true);
}));
}
foreach ($rows as &$row) {
$generic = trim((string) ($row['genericname'] ?? ''));
if ($generic === '') {
$salt = trim((string) ($row['salt_name'] ?? ''));
if ($salt !== '') {
$row['genericname'] = $salt;
}
}
}
unset($row);
$userId = (int) ($this->getCurrentUserId() ?? 0);
$favoriteMap = $userId > 0 ? $this->getUserFavoriteMedicineIds($userId) : [];
$usageMap = $userId > 0 ? $this->getUserMedicineUsageMap($userId) : [];
foreach ($rows as &$row) {
$medId = (int) ($row['id'] ?? 0);
$row['is_favorite'] = isset($favoriteMap[$medId]) ? 1 : 0;
$row['use_count'] = (int) ($usageMap[$medId]['use_count'] ?? 0);
$row['last_used_at'] = (string) ($usageMap[$medId]['last_used_at'] ?? '');
}
unset($row);
if (! $showAll) {
if ($scope === 'favorite') {
$rows = array_values(array_filter($rows, static fn(array $row): bool => (int) ($row['is_favorite'] ?? 0) === 1));
} elseif ($scope === 'active') {
$rows = array_values(array_filter($rows, static fn(array $row): bool => ((int) ($row['is_favorite'] ?? 0) === 1) || ((int) ($row['use_count'] ?? 0) > 0)));
}
}
return $rows;
}
public function opd_medicince_company_suggest()
{
if (! $this->db->tableExists('med_company')) {
return $this->response->setJSON(['rows' => []]);
}
$term = trim((string) $this->request->getGet('term'));
$fields = $this->db->getFieldNames('med_company');
$nameField = $this->resolveFirstField($fields, ['company_name', 'name', 'short_name']);
if ($nameField === null) {
return $this->response->setJSON(['rows' => []]);
}
$builder = $this->db->table('med_company')->select($nameField . ' as company_name')->orderBy($nameField, 'ASC');
if ($term !== '') {
$builder->like($nameField, $term);
}
$rows = $builder->limit(200)->get()->getResultArray();
$names = [];
foreach ($rows as $row) {
$name = trim((string) ($row['company_name'] ?? ''));
if ($name === '') {
continue;
}
$names[$name] = ['company_name' => $name];
}
return $this->response->setJSON(['rows' => array_values($names)]);
}
public function opd_medicince_get(int $medId)
{
$table = $this->findExistingTable(['opd_med_master']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']);
}
$row = $this->db->table($table)->where('id', (int) $medId)->get(1)->getRowArray() ?? [];
if (empty($row)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine not found']);
}
return $this->response->setJSON(['update' => 1, 'row' => $row]);
}
public function opd_medicince_save()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$table = $this->findExistingTable(['opd_med_master']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']);
}
$id = (int) $this->request->getPost('id');
$itemName = trim((string) $this->request->getPost('item_name'));
if ($itemName === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']);
}
$medicineModel = new OpdMedicineModel($this->db);
$duplicate = $medicineModel->findDuplicateByName($itemName, $id);
if (! empty($duplicate)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Duplicate medicine name found (ID: ' . (int) ($duplicate['id'] ?? 0) . '). Please use existing entry or merge duplicates.',
'duplicate_id' => (int) ($duplicate['id'] ?? 0),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$fields = $this->db->getFieldNames($table);
$genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']);
$saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']);
$companyField = $this->resolveFirstField($fields, ['company_name', 'company']);
$restrictionField = $this->resolveFirstField($fields, ['dosage_restriction', 'dose_restriction', 'restriction_note', 'restriction']);
$data = [];
if (in_array('item_name', $fields, true)) {
$data['item_name'] = $itemName;
}
if (in_array('formulation', $fields, true)) {
$data['formulation'] = trim((string) $this->request->getPost('formulation'));
}
if ($genericField !== null) {
$data[$genericField] = trim((string) $this->request->getPost('genericname'));
}
if ($saltField !== null) {
$data[$saltField] = trim((string) $this->request->getPost('salt_name'));
}
if ($companyField !== null) {
$data[$companyField] = trim((string) $this->request->getPost('company_name'));
}
if ($restrictionField !== null) {
$data[$restrictionField] = trim((string) $this->request->getPost('dosage_restriction'));
}
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'] as $f) {
if (in_array($f, $fields, true)) {
$selectCols .= ', ' . $f;
}
}
$applyFilters = function ($b) use ($filter, $favOnly, $specId, $fields, $nameField, $codeField) {
if ($filter !== '') {
$b->groupStart()
->like($nameField, $filter)
->orLike($codeField, $filter);
if (in_array('short_name', $fields, true)) {
$b->orLike('short_name', $filter);
}
$b->groupEnd();
}
if ($favOnly && in_array('is_favourite', $fields, true)) {
$b->where('is_favourite', 1);
}
if ($specId > 0 && in_array('spec_ids', $fields, true)) {
$b->where("FIND_IN_SET({$specId}, spec_ids) > 0", null, false);
}
};
$total = $this->db->table('investigation')->countAllResults();
$countBuilder = $this->db->table('investigation');
$applyFilters($countBuilder);
$filtered = $countBuilder->countAllResults();
$builder = $this->db->table('investigation')->select($selectCols);
$applyFilters($builder);
if (in_array('is_favourite', $fields, true)) {
$builder->orderBy('is_favourite', 'DESC');
}
$builder->orderBy($nameField, 'ASC');
$builder->limit($length, $start);
$rows = $builder->get()->getResultArray();
return $this->response->setJSON([
'draw' => $draw,
'recordsTotal' => $total,
'recordsFiltered' => $filtered,
'data' => $rows,
]);
}
public function opd_invest_master_get(int $invId)
{
if ($invId <= 0 || ! $this->db->tableExists('investigation')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Not found']);
}
$fields = $this->db->getFieldNames('investigation');
$keyField = $this->resolveFirstField($fields, ['id', 'Code', 'code']);
$codeField = $this->resolveFirstField($fields, ['Code', 'code']);
$nameField = $this->resolveFirstField($fields, ['Name', 'name']);
if ($keyField === null || $codeField === null || $nameField === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid table structure']);
}
$row = $this->db->table('investigation')->where($keyField, $invId)->limit(1)->get()->getRowArray();
if (! $row) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Not found']);
}
return $this->response->setJSON([
'update' => 1,
'row' => [
'id' => (int) ($row[$keyField] ?? 0),
'code' => (string) ($row[$codeField ?? 'code'] ?? ''),
'name' => (string) ($row[$nameField ?? 'name'] ?? ''),
'short_name' => (string) ($row['short_name'] ?? ''),
'category_name' => (string) ($row['category_name'] ?? ''),
'is_favourite' => (int) ($row['is_favourite'] ?? 0),
'spec_ids' => (string) ($row['spec_ids'] ?? ''),
],
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function opd_invest_master_save()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
}
if (! $this->db->tableExists('investigation')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation table not found']);
}
$id = (int) $this->request->getPost('id');
$name = trim((string) $this->request->getPost('name'));
if ($name === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation name is required']);
}
$fields = $this->db->getFieldNames('investigation');
$keyField = $this->resolveFirstField($fields, ['id', 'Code', 'code']);
$codeField = $this->resolveFirstField($fields, ['Code', 'code']);
$nameField = $this->resolveFirstField($fields, ['Name', 'name']);
if ($keyField === null || $codeField === null || $nameField === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid table structure']);
}
$code = trim((string) $this->request->getPost('code'));
if ($code === '') {
$code = strtoupper(preg_replace('/[^A-Z0-9]/i', '', substr($name, 0, 8))) . rand(100, 999);
}
$data = [
$nameField => $name,
$codeField => $code,
];
foreach (['short_name', 'category_name', 'spec_ids'] as $f) {
if (in_array($f, $fields, true)) {
$data[$f] = trim((string) $this->request->getPost($f));
}
}
if (in_array('is_favourite', $fields, true)) {
$data['is_favourite'] = ((int) $this->request->getPost('is_favourite')) ? 1 : 0;
}
if ($id > 0) {
$this->db->table('investigation')->where($keyField, $id)->update($data);
$savedId = $id;
$message = 'Investigation updated';
} else {
$this->db->table('investigation')->insert($data);
$savedId = (int) $this->db->insertID();
$message = 'Investigation added';
}
return $this->response->setJSON([
'update' => 1,
'insertid' => $savedId,
'error_text' => $message,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function opd_invest_master_remove(int $invId)
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
}
if ($invId <= 0 || ! $this->db->tableExists('investigation')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Not found']);
}
$fields = $this->db->getFieldNames('investigation');
$keyField = $this->resolveFirstField($fields, ['id', 'Code', 'code']);
if ($keyField === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid key field']);
}
$this->db->table('investigation')->where($keyField, $invId)->delete();
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Investigation deleted',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function opd_invest_master_toggle_fav(int $invId)
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
}
if ($invId <= 0 || ! $this->db->tableExists('investigation')) {
return $this->response->setJSON(['update' => 0]);
}
$fields = $this->db->getFieldNames('investigation');
$keyField = $this->resolveFirstField($fields, ['id', 'Code', 'code']);
if ($keyField === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid key field']);
}
if (! in_array('is_favourite', $fields, true)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Run database migration first']);
}
$row = $this->db->table('investigation')->where($keyField, $invId)->limit(1)->get()->getRowArray();
if (! $row) {
return $this->response->setJSON(['update' => 0]);
}
$newVal = ((int) ($row['is_favourite'] ?? 0)) === 1 ? 0 : 1;
$this->db->table('investigation')->where($keyField, $invId)->update(['is_favourite' => $newVal]);
return $this->response->setJSON([
'update' => 1,
'is_favourite' => $newVal,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function opd_invest_profile_master()
{
return view('billing/opd_invest_profile_master_workspace');
}
private function buildInvestigationProfileRows(string $filter = ''): array
{
if (! $this->db->tableExists('invprofiles') || ! $this->db->tableExists('invtprofiles') || ! $this->db->tableExists('investigation')) {
return [];
}
$pFields = $this->db->getFieldNames('invprofiles');
$jFields = $this->db->getFieldNames('invtprofiles');
$iFields = $this->db->getFieldNames('investigation');
$pCode = $this->resolveFirstField($pFields, ['Code', 'code', 'id']);
$pName = $this->resolveFirstField($pFields, ['Name', 'name']);
$jProfileCode = $this->resolveFirstField($jFields, ['ProfileCode', 'profile_code']);
$jInvestCode = $this->resolveFirstField($jFields, ['InvestigationCode', 'investigation_code']);
$jPrintOrder = $this->resolveFirstField($jFields, ['printOrder', 'print_order', 'id']);
$iCode = $this->resolveFirstField($iFields, ['Code', 'code']);
$iName = $this->resolveFirstField($iFields, ['Name', 'name']);
if ($pCode === null || $pName === null || $jProfileCode === null || $jInvestCode === null || $iCode === null || $iName === null) {
return [];
}
$sql = 'SELECT '
. 'p.`' . $pCode . '` AS profile_code, '
. 'p.`' . $pName . '` AS profile_name, '
. 'i.`' . $iCode . '` AS investigation_code, '
. 'i.`' . $iName . '` AS investigation_name '
. 'FROM `invprofiles` p '
. 'LEFT JOIN `invtprofiles` j ON p.`' . $pCode . '` = j.`' . $jProfileCode . '` '
. 'LEFT JOIN `investigation` i ON i.`' . $iCode . '` = j.`' . $jInvestCode . '` ';
$params = [];
if ($filter !== '') {
$sql .= 'WHERE p.`' . $pName . '` LIKE ? OR i.`' . $iName . '` LIKE ? ';
$params[] = '%' . $filter . '%';
$params[] = '%' . $filter . '%';
}
$sql .= 'ORDER BY p.`' . $pName . '` ASC';
if ($jPrintOrder !== null) {
$sql .= ', j.`' . $jPrintOrder . '` ASC';
}
$rows = $this->db->query($sql, $params)->getResultArray();
$map = [];
foreach ($rows as $row) {
$profileCode = (int) ($row['profile_code'] ?? 0);
$profileName = trim((string) ($row['profile_name'] ?? ''));
if ($profileCode <= 0 || $profileName === '') {
continue;
}
if (! isset($map[$profileCode])) {
$map[$profileCode] = [
'profile_code' => $profileCode,
'profile_name' => $profileName,
'tests' => [],
];
}
$testCode = trim((string) ($row['investigation_code'] ?? ''));
$testName = trim((string) ($row['investigation_name'] ?? ''));
if ($testCode !== '' && $testName !== '') {
$map[$profileCode]['tests'][] = [
'code' => $testCode,
'name' => $testName,
];
}
}
return array_values($map);
}
public function opd_invest_profile_master_list()
{
$filter = trim((string) $this->request->getGet('filter'));
return $this->response->setJSON(['rows' => $this->buildInvestigationProfileRows($filter)]);
}
public function opd_invest_profile_master_data()
{
$filter = trim((string) $this->request->getGet('filter'));
$list = $this->buildInvestigationProfileRows($filter);
$draw = (int) $this->request->getGet('draw');
$start = max(0, (int) $this->request->getGet('start'));
$length = (int) $this->request->getGet('length');
if ($length <= 0) {
$length = 25;
}
$paged = array_slice($list, $start, $length);
foreach ($paged as &$row) {
$row['test_count'] = count($row['tests'] ?? []);
$row['test_names'] = implode(', ', array_map(static function ($item) {
return (string) ($item['name'] ?? '');
}, $row['tests'] ?? []));
}
unset($row);
return $this->response->setJSON([
'draw' => $draw,
'recordsTotal' => count($list),
'recordsFiltered' => count($list),
'data' => $paged,
]);
}
public function opd_invest_profile_master_get(int $profileCode)
{
$rows = $this->buildInvestigationProfileRows();
foreach ($rows as $row) {
if ((int) ($row['profile_code'] ?? 0) === $profileCode) {
return $this->response->setJSON([
'update' => 1,
'row' => $row,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
}
return $this->response->setJSON(['update' => 0, 'error_text' => 'Profile not found']);
}
public function opd_invest_profile_master_save()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
}
if (! $this->db->tableExists('invprofiles') || ! $this->db->tableExists('invtprofiles') || ! $this->db->tableExists('investigation')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Profile tables not found']);
}
$profileCode = (int) $this->request->getPost('profile_code');
$profileName = trim((string) $this->request->getPost('profile_name'));
$investigationCodes = array_values(array_filter(array_map('intval', explode(',', (string) $this->request->getPost('investigation_codes')))));
if ($profileName === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Profile name is required']);
}
if (empty($investigationCodes)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Select at least one investigation']);
}
$pFields = $this->db->getFieldNames('invprofiles');
$jFields = $this->db->getFieldNames('invtprofiles');
$pCode = $this->resolveFirstField($pFields, ['Code', 'code', 'id']);
$pName = $this->resolveFirstField($pFields, ['Name', 'name']);
$jProfileCode = $this->resolveFirstField($jFields, ['ProfileCode', 'profile_code']);
$jInvestCode = $this->resolveFirstField($jFields, ['InvestigationCode', 'investigation_code']);
$jPrintOrder = $this->resolveFirstField($jFields, ['printOrder', 'print_order', 'id']);
if ($pCode === null || $pName === null || $jProfileCode === null || $jInvestCode === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid profile table structure']);
}
if ($profileCode > 0) {
$this->db->table('invprofiles')->where($pCode, $profileCode)->update([$pName => $profileName]);
$savedCode = $profileCode;
$this->db->table('invtprofiles')->where($jProfileCode, $savedCode)->delete();
} else {
$this->db->table('invprofiles')->insert([$pName => $profileName]);
$savedCode = (int) $this->db->insertID();
}
$order = 1;
foreach ($investigationCodes as $code) {
$joinRow = [
$jProfileCode => $savedCode,
$jInvestCode => $code,
];
if ($jPrintOrder !== null) {
$joinRow[$jPrintOrder] = $order;
}
$this->db->table('invtprofiles')->insert($joinRow);
$order++;
}
return $this->response->setJSON([
'update' => 1,
'insertid' => $savedCode,
'error_text' => $profileCode > 0 ? 'Profile updated' : 'Profile added',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function opd_invest_profile_master_remove(int $profileCode)
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
}
if (! $this->db->tableExists('invprofiles') || ! $this->db->tableExists('invtprofiles')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Profile tables not found']);
}
$pFields = $this->db->getFieldNames('invprofiles');
$jFields = $this->db->getFieldNames('invtprofiles');
$pCode = $this->resolveFirstField($pFields, ['Code', 'code', 'id']);
$jProfileCode = $this->resolveFirstField($jFields, ['ProfileCode', 'profile_code']);
if ($pCode === null || $jProfileCode === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid profile table structure']);
}
$this->db->table('invtprofiles')->where($jProfileCode, $profileCode)->delete();
$this->db->table('invprofiles')->where($pCode, $profileCode)->delete();
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Profile deleted',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
// ─────────────────────────────────────────────────────────────────────────
public function opd_medicince_duplicate_report()
{
$medicineModel = new OpdMedicineModel($this->db);
if (! $medicineModel->tableExists()) {
return $this->response->setJSON(['rows' => []]);
}
return $this->response->setJSON([
'rows' => $medicineModel->getDuplicateGroups(),
'update' => 1,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function opd_medicince_merge_duplicates()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$keepId = (int) $this->request->getPost('keep_id');
$mergeRaw = trim((string) $this->request->getPost('merge_ids'));
$mergeIds = [];
if ($mergeRaw !== '') {
foreach (explode(',', $mergeRaw) as $token) {
$value = (int) trim($token);
if ($value > 0) {
$mergeIds[] = $value;
}
}
}
$medicineModel = new OpdMedicineModel($this->db);
$merged = $medicineModel->mergeDuplicates($keepId, $mergeIds);
if (! ($merged['ok'] ?? false)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => (string) ($merged['error'] ?? 'Unable to merge duplicates'),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Merged ' . (int) ($merged['merged_count'] ?? 0) . ' duplicate medicine(s).',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function opd_medicince_autofix_duplicates()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$medicineModel = new OpdMedicineModel($this->db);
if (! $medicineModel->tableExists()) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Medicine table not found',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$groups = $medicineModel->getDuplicateGroups();
if (empty($groups)) {
return $this->response->setJSON([
'update' => 1,
'error_text' => 'No duplicate medicine names found.',
'merged_groups' => 0,
'merged_rows' => 0,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$mergedGroups = 0;
$mergedRows = 0;
foreach ($groups as $group) {
$keepId = (int) ($group['keep_id'] ?? 0);
$mergeIds = is_array($group['merge_ids'] ?? null) ? $group['merge_ids'] : [];
if ($keepId <= 0 || empty($mergeIds)) {
continue;
}
$result = $medicineModel->mergeDuplicates($keepId, $mergeIds);
if (! ($result['ok'] ?? false)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => (string) ($result['error'] ?? 'Auto-fix stopped due to merge failure'),
'merged_groups' => $mergedGroups,
'merged_rows' => $mergedRows,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$mergedGroups++;
$mergedRows += (int) ($result['merged_count'] ?? 0);
}
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Auto-fix completed. Merged ' . $mergedRows . ' duplicate medicine row(s) in ' . $mergedGroups . ' group(s).',
'merged_groups' => $mergedGroups,
'merged_rows' => $mergedRows,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function scan_text_extract()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$filePath = trim((string) $this->request->getPost('file_path'));
$scanType = trim((string) $this->request->getPost('scan_type'));
if ($filePath === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Scan file path is required']);
}
$localPath = $this->resolveLocalFilePathFromStoredPath($filePath);
if ($localPath === null || !is_file($localPath)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Scan file not found']);
}
$extracted = $this->extractClinicalTextFromScan($localPath, $scanType);
if ($extracted === null || trim($extracted) === '') {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Unable to extract report text from this scan currently.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
return $this->response->setJSON([
'update' => 1,
'extracted_text' => trim($extracted),
'error_text' => 'Scan text extracted',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function patient_scan_history(int $opdId)
{
if (! $this->canAccessPrescription()) {
return $this->response->setStatusCode(403)->setBody('<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)
->where('bundle_type', 'MedicationRequestBundle');
if ($sessionId > 0) {
$builder->where('opd_session_id', (int) $sessionId);
}
$row = $builder->orderBy('id', 'DESC')->get(1)->getRowArray();
if (empty($row)) {
return $this->response->setStatusCode(404)->setBody('FHIR bundle not found.');
}
$bundleJson = (string) ($row['bundle_json'] ?? '{}');
$fileSession = (int) ($row['opd_session_id'] ?? 0);
$filename = 'opd_' . (int) $opdId . '_session_' . $fileSession . '_fhir.json';
return $this->response
->setHeader('Content-Type', 'application/fhir+json; charset=utf-8')
->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
->setBody($bundleJson);
}
public function fhir_bundle_history(int $opdId, int $sessionId = 0)
{
if (! $this->db->tableExists('opd_fhir_documents')) {
return $this->response->setJSON(['rows' => []]);
}
$builder = $this->db->table('opd_fhir_documents')
->where('opd_id', (int) $opdId)
->where('bundle_type', 'MedicationRequestBundle');
if ($sessionId > 0) {
$builder->where('opd_session_id', (int) $sessionId);
}
$rows = $builder->orderBy('id', 'DESC')->limit(5)->get()->getResultArray();
$result = [];
foreach ($rows as $row) {
$result[] = [
'id' => (int) ($row['id'] ?? 0),
'opd_session_id' => (int) ($row['opd_session_id'] ?? 0),
'generated_at' => (string) ($row['generated_at'] ?? ''),
'generated_by' => (string) ($row['generated_by'] ?? ''),
'download_url' => base_url('Opd_prescription/fhir_bundle/' . (int) $opdId . '/' . (int) ($row['opd_session_id'] ?? 0)),
];
}
return $this->response->setJSON(['rows' => $result]);
}
public function advice_search()
{
$q = trim((string) $this->request->getGet('q'));
if (! $this->db->tableExists('opd_advice')) {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames('opd_advice');
$idField = in_array('id', $fields, true) ? 'id' : (in_array('Code', $fields, true) ? 'Code' : null);
$textField = in_array('advice', $fields, true) ? 'advice' : (in_array('Name', $fields, true) ? 'Name' : null);
if ($idField === null || $textField === null) {
return $this->response->setJSON(['rows' => []]);
}
$builder = $this->db->table('opd_advice')
->select($idField . ' as id,' . $textField . ' as label');
if ($q !== '') {
$builder->like($textField, $q);
}
$rows = $builder
->orderBy($idField, 'DESC')
->limit(20)
->get()
->getResultArray();
return $this->response->setJSON(['rows' => $rows]);
}
public function complaints_search()
{
$q = trim((string) $this->request->getGet('q'));
if ($q === '' || ! $this->db->tableExists('complaints_master')) {
return $this->response->setJSON(['rows' => []]);
}
$rows = $this->db->table('complaints_master')
->select('Code as code, Name as name, name_hinglish, show_in_short, ai_hint')
->where('is_active', 1)
->groupStart()
->like('Name', $q)
->orLike('name_hinglish', $q)
->orLike('keywords', $q)
->groupEnd()
->orderBy('show_in_short', 'DESC')
->orderBy('Name', 'ASC')
->limit(20)
->get()
->getResultArray();
return $this->response->setJSON(['rows' => $rows]);
}
public function complaints_parse()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$text = trim((string) $this->request->getPost('text'));
if ($text === '') {
return $this->response->setJSON(['update' => 0, 'rows' => [], 'error_text' => 'Enter complaint text']);
}
$matched = $this->getComplaintMatchesFromText($text);
return $this->response->setJSON([
'update' => 1,
'rows' => $matched,
'error_text' => empty($matched) ? 'No standard complaints matched.' : 'Complaint terms matched',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function complaints_ai_draft()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$currentText = trim((string) $this->request->getPost('current_text'));
$complaints = $this->request->getPost('complaints');
$complaints = is_array($complaints) ? $complaints : [];
$normalized = [];
foreach ($complaints as $item) {
$item = trim((string) $item);
if ($item === '') {
continue;
}
$normalized[strtoupper($item)] = $item;
}
if ($currentText !== '') {
foreach ($this->getComplaintMatchesFromText($currentText) as $item) {
$normalized[strtoupper($item)] = $item;
}
}
if (! empty($normalized) && $this->db->tableExists('complaints_master')) {
$names = array_values($normalized);
$hintRows = $this->db->table('complaints_master')
->select('Name, ai_hint')
->whereIn('Name', $names)
->get()
->getResultArray();
foreach ($hintRows as $hintRow) {
$name = trim((string) ($hintRow['Name'] ?? ''));
$hint = trim((string) ($hintRow['ai_hint'] ?? ''));
if ($name !== '' && $hint !== '') {
$normalized[strtoupper($name)] = $name . ' (' . $hint . ')';
}
}
}
$finalComplaints = array_values($normalized);
$this->lastAiProvider = 'none';
$aiDraft = $this->generateComplaintDraftWithAi($finalComplaints, $currentText);
if ($aiDraft !== null && trim($aiDraft) !== '') {
$provider = $this->lastAiProvider === 'ai-server' ? 'ai-server' : 'ai';
$providerLabel = $provider === 'ai-server' ? 'AI Server' : 'AI';
return $this->response->setJSON([
'update' => 1,
'draft_text' => trim($aiDraft),
'error_text' => 'AI draft prepared using ' . $providerLabel,
'ai_mode' => $provider,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$line = '';
if (! empty($finalComplaints)) {
$line = 'Chief complaints: ' . implode(', ', $finalComplaints) . '.';
}
$draft = trim($line . ' ' . $currentText);
if ($draft === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Please add complaint terms first.']);
}
return $this->response->setJSON([
'update' => 1,
'draft_text' => $draft,
'error_text' => 'Complaint draft prepared (template mode)',
'ai_mode' => 'template',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function advice_list($opdId, $sessionId = 0)
{
$table = $this->findExistingTable(['opd_prescription_advice']);
if ($table === null) {
return $this->response->setJSON(['rows' => []]);
}
$builder = $this->db->table($table);
$fields = $this->db->getFieldNames($table);
if (in_array('opd_pre_id', $fields, true)) {
if ((int) $sessionId <= 0) {
return $this->response->setJSON(['rows' => []]);
}
$builder->where('opd_pre_id', (int) $sessionId);
} elseif (in_array('opd_id', $fields, true)) {
$builder->where('opd_id', (int) $opdId);
} else {
return $this->response->setJSON(['rows' => []]);
}
if (in_array('id', $fields, true)) {
$builder->orderBy('id', 'DESC');
}
$rows = $builder->get()->getResultArray();
return $this->response->setJSON(['rows' => $rows]);
}
public function advice_add()
{
if (!$this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$opdId = (int) $this->request->getPost('opd_id');
$sessionId = (int) $this->request->getPost('opd_session_id');
$adviceText = trim((string) $this->request->getPost('advice_text'));
$adviceId = (int) $this->request->getPost('advice_id');
$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');
$codeField = $this->resolveFirstField($invFields, ['Code', 'code']);
$nameField = $this->resolveFirstField($invFields, ['Name', 'name']);
$shortField = $this->resolveFirstField($invFields, ['short_name', 'shortName', 'short']);
$sortField = $this->resolveFirstField($invFields, ['sort_id', 'sort_order', 'id']);
if ($codeField !== null && $nameField !== null && $shortField !== null) {
$builder = $this->db->table('investigation')
->select($codeField . ' as code,' . $nameField . ' as name,' . $shortField . ' as short_name')
->where($shortField . ' !=', '')
->where($shortField . ' is not null', null, false)
->limit(200);
if ($sortField !== null) {
$builder->orderBy($shortField, 'ASC')->orderBy($sortField, 'ASC');
} else {
$builder->orderBy($shortField, 'ASC')->orderBy($nameField, 'ASC');
}
$shortTests = $builder->get()->getResultArray();
}
}
if ($this->db->tableExists('invprofiles') && $this->db->tableExists('invtprofiles') && $this->db->tableExists('investigation')) {
$pFields = $this->db->getFieldNames('invprofiles');
$jFields = $this->db->getFieldNames('invtprofiles');
$iFields = $this->db->getFieldNames('investigation');
$pCode = $this->resolveFirstField($pFields, ['Code', 'code']);
$pName = $this->resolveFirstField($pFields, ['Name', 'name']);
$jProfileCode = $this->resolveFirstField($jFields, ['ProfileCode', 'profile_code']);
$jInvestCode = $this->resolveFirstField($jFields, ['InvestigationCode', 'investigation_code']);
$jPrintOrder = $this->resolveFirstField($jFields, ['printOrder', 'print_order', 'id']);
$iCode = $this->resolveFirstField($iFields, ['Code', 'code']);
$iName = $this->resolveFirstField($iFields, ['Name', 'name']);
if ($pCode !== null && $pName !== null && $jProfileCode !== null && $jInvestCode !== null && $iCode !== null && $iName !== null) {
$orderExpr = 'p.`' . $pName . '` ASC';
if ($jPrintOrder !== null) {
$orderExpr .= ', j.`' . $jPrintOrder . '` ASC';
}
$sql = 'SELECT '
. 'p.`' . $pCode . '` AS profile_code, '
. 'p.`' . $pName . '` AS profile_name, '
. 'i.`' . $iCode . '` AS investigation_code, '
. 'i.`' . $iName . '` AS investigation_name '
. 'FROM `invprofiles` p '
. 'JOIN `invtprofiles` j ON p.`' . $pCode . '` = j.`' . $jProfileCode . '` '
. 'JOIN `investigation` i ON i.`' . $iCode . '` = j.`' . $jInvestCode . '` '
. 'ORDER BY ' . $orderExpr;
$rows = $this->db->query($sql)->getResultArray();
$profileMap = [];
foreach ($rows as $row) {
$profileCode = trim((string) ($row['profile_code'] ?? ''));
$profileName = trim((string) ($row['profile_name'] ?? ''));
$testCode = trim((string) ($row['investigation_code'] ?? ''));
$testName = trim((string) ($row['investigation_name'] ?? ''));
if ($profileCode === '' || $profileName === '' || $testName === '') {
continue;
}
if (!isset($profileMap[$profileCode])) {
$profileMap[$profileCode] = [
'profile_code' => $profileCode,
'profile_name' => $profileName,
'tests' => [],
'_seen' => [],
];
}
$key = strtolower($testCode . '|' . $testName);
if (isset($profileMap[$profileCode]['_seen'][$key])) {
continue;
}
$profileMap[$profileCode]['_seen'][$key] = true;
$profileMap[$profileCode]['tests'][] = [
'code' => $testCode,
'name' => $testName,
];
}
foreach ($profileMap as $item) {
unset($item['_seen']);
$profiles[] = $item;
}
}
}
return $this->response->setJSON([
'update' => 1,
'profiles' => $profiles,
'short_tests' => $shortTests,
]);
}
public function investigation_list($opdId, $sessionId = 0)
{
$table = $this->findExistingTable(['opd_prescription_investigation']);
if ($table === null) {
return $this->response->setJSON(['rows' => []]);
}
$builder = $this->db->table($table);
$fields = $this->db->getFieldNames($table);
if (in_array('opd_pre_id', $fields, true)) {
if ((int) $sessionId <= 0) {
return $this->response->setJSON(['rows' => []]);
}
$builder->where('opd_pre_id', (int) $sessionId);
} elseif (in_array('opd_id', $fields, true)) {
$builder->where('opd_id', (int) $opdId);
} else {
return $this->response->setJSON(['rows' => []]);
}
if (in_array('investigation_name', $fields, true)) {
$builder->orderBy('investigation_name', 'ASC');
} elseif (in_array('id', $fields, true)) {
$builder->orderBy('id', 'DESC');
}
$rows = $builder->get()->getResultArray();
return $this->response->setJSON(['rows' => $rows]);
}
public function investigation_add()
{
if (!$this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$opdId = (int) $this->request->getPost('opd_id');
$sessionId = (int) $this->request->getPost('opd_session_id');
$code = trim((string) $this->request->getPost('investigation_code'));
$name = trim((string) $this->request->getPost('investigation_name'));
// Profile chips may send only investigation name; resolve code from master to avoid unique-key collisions.
if ($code === '' && $name !== '' && $this->db->tableExists('investigation')) {
$invFields = $this->db->getFieldNames('investigation');
$invCodeField = $this->resolveFirstField($invFields, ['Code', 'code']);
$invNameField = $this->resolveFirstField($invFields, ['Name', 'name']);
if ($invCodeField !== null && $invNameField !== null) {
$resolved = $this->db->table('investigation')
->select($invCodeField . ' as code')
->where($invNameField, $name)
->limit(1)
->get()
->getRowArray();
if (! empty($resolved['code'])) {
$code = trim((string) $resolved['code']);
}
}
}
if ($opdId <= 0 || ($code === '' && $name === '')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation is required']);
}
$table = $this->findExistingTable(['opd_prescription_investigation']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation table not found']);
}
$sessionId = $this->ensurePrescriptionSession($opdId, $sessionId);
if ($sessionId <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create prescription session']);
}
$fields = $this->db->getFieldNames($table);
$insert = [];
if (in_array('opd_id', $fields, true)) {
$insert['opd_id'] = $opdId;
}
if (in_array('opd_pre_id', $fields, true)) {
$insert['opd_pre_id'] = $sessionId;
}
if (in_array('investigation_code', $fields, true)) {
$insert['investigation_code'] = $code;
}
if (in_array('investigation_name', $fields, true)) {
$insert['investigation_name'] = $name;
}
$builder = $this->db->table($table);
if (in_array('opd_pre_id', $fields, true)) {
$builder->where('opd_pre_id', $sessionId);
} elseif (in_array('opd_id', $fields, true)) {
$builder->where('opd_id', $opdId);
}
if ($code !== '' && in_array('investigation_code', $fields, true)) {
$builder->where('investigation_code', $code);
} elseif ($name !== '' && in_array('investigation_name', $fields, true)) {
$builder->where('investigation_name', $name);
}
$already = $builder->get(1)->getRowArray();
if (! empty($already)) {
return $this->response->setJSON([
'update' => 1,
'opd_session_id' => $sessionId,
'error_text' => 'Investigation already added',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
try {
$this->db->table($table)->insert($insert);
} catch (\Throwable $e) {
$msg = strtolower($e->getMessage());
if (strpos($msg, 'duplicate entry') !== false) {
return $this->response->setJSON([
'update' => 1,
'opd_session_id' => $sessionId,
'error_text' => 'Investigation already added',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
throw $e;
}
$insertId = (int) $this->db->insertID();
$this->auditClinicalUpdate('opd_prescription_investigation', 'added', $insertId, null, $insert);
return $this->response->setJSON([
'update' => 1,
'opd_session_id' => $sessionId,
'error_text' => 'Investigation added',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function investigation_remove($id)
{
$table = $this->findExistingTable(['opd_prescription_investigation']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation table not found']);
}
$fields = $this->db->getFieldNames($table);
if (!in_array('id', $fields, true)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation id field missing']);
}
$row = $this->db->table($table)->where('id', (int) $id)->get(1)->getRowArray() ?? [];
$this->db->table($table)->where('id', (int) $id)->delete();
$this->auditClinicalUpdate('opd_prescription_investigation', 'removed', (int) $id, $row, null);
return $this->response->setJSON(['update' => 1, 'error_text' => 'Investigation removed']);
}
public function medicine_search()
{
$q = trim((string) $this->request->getGet('q'));
$scope = strtolower(trim((string) $this->request->getGet('scope')));
if (! in_array($scope, ['active', 'favorite', 'all'], true)) {
$scope = 'active';
}
$table = $this->findExistingTable(['opd_med_master']);
if ($table === null) {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames($table);
$idField = in_array('id', $fields, true) ? 'id' : null;
$nameField = in_array('item_name', $fields, true) ? 'item_name' : null;
$formField = in_array('formulation', $fields, true) ? 'formulation' : null;
$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';
}
$builder = $this->db->table($table)->select($select);
if ($q !== '') {
$builder->groupStart()
->like($nameField, $q);
if ($formField !== null) {
$builder->orLike($formField, $q);
}
$builder->groupEnd();
}
$rows = $builder
->limit(500)
->get()
->getResultArray();
if (empty($rows)) {
return $this->response->setJSON(['rows' => []]);
}
$userId = (int) ($this->getCurrentUserId() ?? 0);
$favoriteMap = $userId > 0 ? $this->getUserFavoriteMedicineIds($userId) : [];
$usageMap = $userId > 0 ? $this->getUserMedicineUsageMap($userId) : [];
$qUpper = strtoupper($q);
foreach ($rows as &$row) {
$medId = (int) ($row['id'] ?? 0);
$name = trim((string) ($row['med_name'] ?? ''));
$nameUpper = strtoupper($name);
$isFavorite = isset($favoriteMap[$medId]);
$useCount = (int) ($usageMap[$medId]['use_count'] ?? 0);
$lastUsedAt = (string) ($usageMap[$medId]['last_used_at'] ?? '');
$score = 0;
if ($isFavorite) {
$score += 1000;
}
if ($useCount > 0) {
$score += min(300, $useCount * 10);
}
if ($lastUsedAt !== '') {
$daysAgo = (int) floor((time() - strtotime($lastUsedAt)) / 86400);
if ($daysAgo <= 7) {
$score += 120;
} elseif ($daysAgo <= 30) {
$score += 80;
} elseif ($daysAgo <= 90) {
$score += 40;
} elseif ($daysAgo <= 180) {
$score += 20;
}
}
if ($qUpper !== '') {
if (str_starts_with($nameUpper, $qUpper)) {
$score += 200;
} elseif (str_contains($nameUpper, $qUpper)) {
$score += 100;
}
}
$row['is_favorite'] = $isFavorite ? 1 : 0;
$row['use_count'] = $useCount;
$row['last_used_at'] = $lastUsedAt;
$row['_score'] = $score;
}
unset($row);
if ($scope === 'favorite') {
$rows = array_values(array_filter($rows, static fn(array $row): bool => (int) ($row['is_favorite'] ?? 0) === 1));
} elseif ($scope === 'active') {
$rows = array_values(array_filter($rows, static fn(array $row): bool => ((int) ($row['is_favorite'] ?? 0) === 1) || ((int) ($row['use_count'] ?? 0) > 0)));
}
usort($rows, static function (array $a, array $b): int {
$scoreA = (int) ($a['_score'] ?? 0);
$scoreB = (int) ($b['_score'] ?? 0);
if ($scoreA !== $scoreB) {
return $scoreB <=> $scoreA;
}
$nameA = strtoupper(trim((string) ($a['med_name'] ?? '')));
$nameB = strtoupper(trim((string) ($b['med_name'] ?? '')));
return $nameA <=> $nameB;
});
$rows = array_slice($rows, 0, 40);
foreach ($rows as &$row) {
unset($row['_score']);
}
unset($row);
return $this->response->setJSON(['rows' => $rows]);
}
public function medicine_substitutes()
{
$table = $this->findExistingTable(['opd_med_master']);
if ($table === null) {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames($table);
$idField = $this->resolveFirstField($fields, ['id']);
$nameField = $this->resolveFirstField($fields, ['item_name', 'med_name']);
$formField = $this->resolveFirstField($fields, ['formulation']);
$genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']);
$saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']);
$companyField = $this->resolveFirstField($fields, ['company_name', 'company']);
if ($idField === null || $nameField === null || ($genericField === null && $saltField === null)) {
return $this->response->setJSON(['rows' => []]);
}
$medId = (int) $this->request->getGet('med_id');
$medName = trim((string) $this->request->getGet('med_name'));
$current = [];
if ($medId > 0) {
$current = $this->db->table($table)->where($idField, $medId)->get(1)->getRowArray() ?? [];
}
if (empty($current) && $medName !== '') {
$current = $this->db->table($table)->where('UPPER(' . $nameField . ')', strtoupper($medName))->get(1)->getRowArray() ?? [];
if (! empty($current)) {
$medId = (int) ($current[$idField] ?? 0);
}
}
if (empty($current)) {
return $this->response->setJSON(['rows' => []]);
}
$generic = $genericField !== null ? trim((string) ($current[$genericField] ?? '')) : '';
$salt = $saltField !== null ? trim((string) ($current[$saltField] ?? '')) : '';
if ($generic === '' && $salt === '') {
return $this->response->setJSON(['rows' => []]);
}
$select = $idField . ' as id,' . $nameField . ' as med_name';
if ($formField !== null) {
$select .= ',' . $formField . ' as med_type';
}
if ($genericField !== null) {
$select .= ',' . $genericField . ' as genericname';
}
if ($saltField !== null) {
$select .= ',' . $saltField . ' as salt_name';
}
if ($companyField !== null) {
$select .= ',' . $companyField . ' as company_name';
}
$builder = $this->db->table($table)->select($select);
$builder->groupStart();
if ($genericField !== null && $generic !== '') {
$builder->where($genericField, $generic);
}
if ($saltField !== null && $salt !== '') {
if ($genericField !== null && $generic !== '') {
$builder->orWhere($saltField, $salt);
} else {
$builder->where($saltField, $salt);
}
}
$builder->groupEnd();
if ($medId > 0) {
$builder->where($idField . ' !=', $medId);
}
$rows = $builder->orderBy($nameField, 'ASC')->limit(25)->get()->getResultArray();
return $this->response->setJSON(['rows' => $rows]);
}
public function medicine_favorite_toggle()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$userId = (int) ($this->getCurrentUserId() ?? 0);
$medId = (int) $this->request->getPost('med_id');
if ($userId <= 0 || $medId <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid user or medicine']);
}
if (! $this->db->tableExists('opd_medicine_favorites')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine favorites table not found. Run migration first.']);
}
$exists = $this->db->table('opd_medicine_favorites')
->where('user_id', $userId)
->where('med_id', $medId)
->get(1)
->getRowArray();
if ($exists) {
$this->db->table('opd_medicine_favorites')
->where('user_id', $userId)
->where('med_id', $medId)
->delete();
return $this->response->setJSON([
'update' => 1,
'is_favorite' => 0,
'error_text' => 'Removed from favorites',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$this->db->table('opd_medicine_favorites')->insert([
'user_id' => $userId,
'med_id' => $medId,
'created_at' => date('Y-m-d H:i:s'),
]);
return $this->response->setJSON([
'update' => 1,
'is_favorite' => 1,
'error_text' => 'Added to favorites',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function medicine_list($sessionId)
{
$table = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']);
if ($table === null) {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames($table);
$builder = $this->db->table($table);
if (in_array('opd_pre_id', $fields, true)) {
if ((int) $sessionId <= 0) {
return $this->response->setJSON(['rows' => []]);
}
$builder->where('opd_pre_id', (int) $sessionId);
} else {
return $this->response->setJSON(['rows' => []]);
}
if (in_array('id', $fields, true)) {
$builder->orderBy('id', 'DESC');
}
$rows = $builder->get()->getResultArray();
$doseMap = $this->getDoseMasterLabelMap('opd_dose_shed', ['dose_shed_id', 'id'], ['dose_show_sign', 'dose_sign', 'dose_sign_desc', 'name']);
$whenMap = $this->getDoseMasterLabelMap('opd_dose_when', ['dose_when_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']);
$freqMap = $this->getDoseMasterLabelMap('opd_dose_frequency', ['dose_freq_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']);
$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']);
}
if ($medId <= 0) {
$medId = $this->resolveMedicineMasterIdByName($medName);
}
$fields = $this->db->getFieldNames($table);
$insert = [];
if (in_array('opd_pre_id', $fields, true)) {
$insert['opd_pre_id'] = $sessionId;
}
if (in_array('med_id', $fields, true) && $medId > 0) {
$insert['med_id'] = $medId;
}
if (in_array('med_name', $fields, true)) {
$insert['med_name'] = strtoupper($medName);
}
if (in_array('med_type', $fields, true)) {
$insert['med_type'] = $medType;
}
if (in_array('dosage', $fields, true)) {
$insert['dosage'] = trim((string) $this->request->getPost('dosage'));
}
if (in_array('dosage_when', $fields, true)) {
$insert['dosage_when'] = trim((string) $this->request->getPost('dosage_when'));
}
if (in_array('dosage_freq', $fields, true)) {
$insert['dosage_freq'] = trim((string) $this->request->getPost('dosage_freq'));
}
if (in_array('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'] ?? '',
]);
}
return $this->response->setJSON([
'update' => 1,
'opd_session_id' => $sessionId,
'error_text' => 'Medicine added',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
private function resolveMedicineMasterIdByName(string $medName): int
{
$medName = trim($medName);
if ($medName === '') {
return 0;
}
$table = $this->findExistingTable(['opd_med_master']);
if ($table === null) {
return 0;
}
$fields = $this->db->getFieldNames($table);
if (! in_array('id', $fields, true)) {
return 0;
}
$nameField = in_array('item_name', $fields, true) ? 'item_name' : (in_array('med_name', $fields, true) ? 'med_name' : null);
if ($nameField === null) {
return 0;
}
$row = $this->db->table($table)
->select('id')
->where('UPPER(' . $nameField . ')', strtoupper($medName))
->get(1)
->getRowArray();
return (int) ($row['id'] ?? 0);
}
private function trackMedicineUsage(int $medId): void
{
$userId = (int) ($this->getCurrentUserId() ?? 0);
if ($userId <= 0 || $medId <= 0 || ! $this->db->tableExists('opd_medicine_usage')) {
return;
}
$existing = $this->db->table('opd_medicine_usage')
->where('user_id', $userId)
->where('med_id', $medId)
->get(1)
->getRowArray();
if ($existing) {
$this->db->table('opd_medicine_usage')
->where('user_id', $userId)
->where('med_id', $medId)
->update([
'use_count' => (int) ($existing['use_count'] ?? 0) + 1,
'last_used_at' => date('Y-m-d H:i:s'),
]);
return;
}
$this->db->table('opd_medicine_usage')->insert([
'user_id' => $userId,
'med_id' => $medId,
'use_count' => 1,
'last_used_at' => date('Y-m-d H:i:s'),
]);
}
/**
* 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
{
if ($userId <= 0 || ! $this->db->tableExists('opd_medicine_favorites')) {
return [];
}
$rows = $this->db->table('opd_medicine_favorites')
->select('med_id')
->where('user_id', $userId)
->get()
->getResultArray();
$out = [];
foreach ($rows as $row) {
$medId = (int) ($row['med_id'] ?? 0);
if ($medId > 0) {
$out[$medId] = true;
}
}
return $out;
}
/**
* @return array<int, array{use_count:int,last_used_at:string}>
*/
private function getUserMedicineUsageMap(int $userId): array
{
if ($userId <= 0 || ! $this->db->tableExists('opd_medicine_usage')) {
return [];
}
$rows = $this->db->table('opd_medicine_usage')
->select('med_id,use_count,last_used_at')
->where('user_id', $userId)
->get()
->getResultArray();
$out = [];
foreach ($rows as $row) {
$medId = (int) ($row['med_id'] ?? 0);
if ($medId <= 0) {
continue;
}
$out[$medId] = [
'use_count' => (int) ($row['use_count'] ?? 0),
'last_used_at' => (string) ($row['last_used_at'] ?? ''),
];
}
return $out;
}
public function medicine_update($id)
{
if (!$this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$id = (int) $id;
if ($id <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid medicine id']);
}
$table = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']);
}
$fields = $this->db->getFieldNames($table);
if (!in_array('id', $fields, true)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine id field missing']);
}
$current = $this->db->table($table)->where('id', $id)->get(1)->getRowArray();
if (!$current) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine row not found']);
}
$update = [];
$map = [
'med_name' => strtoupper(trim((string) $this->request->getPost('med_name'))),
'med_type' => trim((string) $this->request->getPost('med_type')),
'dosage' => trim((string) $this->request->getPost('dosage')),
'dosage_when' => trim((string) $this->request->getPost('dosage_when')),
'dosage_freq' => trim((string) $this->request->getPost('dosage_freq')),
'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']);
}
$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) ($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'] ?? '',
]);
}
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Medicine updated',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
private function isDuplicateMedicineRow(string $table, array $fields, int $sessionId, array $row, int $excludeId = 0): bool
{
if ($sessionId <= 0 || ! in_array('opd_pre_id', $fields, true)) {
return false;
}
$builder = $this->db->table($table)->where('opd_pre_id', $sessionId);
if ($excludeId > 0 && in_array('id', $fields, true)) {
$builder->where('id !=', $excludeId);
}
foreach (['med_id', 'med_name', 'med_type', 'dosage', 'dosage_when', 'dosage_freq', 'no_of_days', 'qty', 'remark'] as $field) {
if (! in_array($field, $fields, true)) {
continue;
}
$value = isset($row[$field]) ? trim((string) $row[$field]) : '';
if ($value === '') {
$builder->groupStart()->where($field, '')->orWhere($field, null)->groupEnd();
} else {
$builder->where($field, $value);
}
}
return $builder->get(1)->getRowArray() !== null;
}
public function medicine_remove($id)
{
$table = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']);
}
$fields = $this->db->getFieldNames($table);
if (!in_array('id', $fields, true)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine id field missing']);
}
$row = $this->db->table($table)->where('id', (int) $id)->get(1)->getRowArray() ?? [];
$this->db->table($table)->where('id', (int) $id)->delete();
$this->auditClinicalUpdate('opd_prescription_medicine', 'removed', (int) $id, $row, null);
return $this->response->setJSON(['update' => 1, 'error_text' => 'Medicine removed']);
}
public function vital_data(int $opdSessionId)
{
if (! $this->db->tableExists('opd_prescription')) {
return $this->response->setBody('');
}
$row = $this->db->table('opd_prescription')->where('id', $opdSessionId)->get(1)->getRowArray();
if (empty($row)) {
return $this->response->setBody('');
}
$lines = [];
if (!empty($row['bp'])) {
$lines[] = '<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">No old prescriptions found.</p>');
}
$medicineTable = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']);
$html = '<div class="accordion" id="oldRxAccordion">';
foreach ($rows as $index => $row) {
$rxLines = [];
if ($medicineTable !== null && !empty($row['session_id'])) {
$medRows = $this->db->table($medicineTable)
->select('med_type,med_name,dosage,dosage_when,dosage_freq,no_of_days,qty,remark')
->where('opd_pre_id', (int) $row['session_id'])
->orderBy('id', 'ASC')
->get()
->getResultArray();
foreach ($medRows as $med) {
$rxLines[] = esc(trim((string) ($med['med_type'] ?? ''))) . ' ' . esc(trim((string) ($med['med_name'] ?? ''))) . ' | '
. esc(trim((string) ($med['dosage'] ?? ''))) . ' '
. esc(trim((string) ($med['dosage_when'] ?? ''))) . ' '
. esc(trim((string) ($med['dosage_freq'] ?? ''))) . ' '
. esc(trim((string) ($med['no_of_days'] ?? '')));
}
}
$dateLabel = !empty($row['apointment_date']) ? date('d-m-Y', strtotime((string) $row['apointment_date'])) : '';
$html .= '<div class="card mb-2">'
. '<div class="card-header"><strong>' . esc((string) ($row['opd_code'] ?? $row['opd_id'])) . '</strong> <small class="text-muted ms-2">' . esc($dateLabel) . '</small></div>'
. '<div class="card-body">'
. '<div><strong>Complaints:</strong> ' . esc((string) ($row['complaints'] ?? '')) . '</div>'
. '<div><strong>Diagnosis:</strong> ' . esc((string) ($row['diagnosis'] ?? '')) . '</div>'
. '<div><strong>Rx:</strong><br>' . (empty($rxLines) ? '<span class="text-muted">No medicine rows</span>' : implode('<br>', $rxLines)) . '</div>'
. '</div></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 = trim($remarks);
$clean = preg_replace('/\n?\s*Drug\s*Allergy\s*Status\s*:\s*.+$/im', '', $clean) ?? $clean;
$clean = preg_replace('/\n?\s*Drug\s*Allergy\s*Details\s*:\s*.+$/im', '', $clean) ?? $clean;
$clean = preg_replace('/\n?\s*ADR\s*History\s*:\s*.+$/im', '', $clean) ?? $clean;
$clean = preg_replace('/\n?\s*Current\s*Medications\s*:\s*.+$/im', '', $clean) ?? $clean;
$clean = trim($clean);
$lines = [];
if (trim((string) ($input['drug_allergy_status'] ?? '')) !== '') {
$lines[] = 'Drug Allergy Status: ' . trim((string) ($input['drug_allergy_status'] ?? ''));
}
if (trim((string) ($input['drug_allergy_details'] ?? '')) !== '') {
$lines[] = 'Drug Allergy Details: ' . trim((string) ($input['drug_allergy_details'] ?? ''));
}
if (trim((string) ($input['adr_history'] ?? '')) !== '') {
$lines[] = 'ADR History: ' . trim((string) ($input['adr_history'] ?? ''));
}
if (trim((string) ($input['current_medications'] ?? '')) !== '') {
$lines[] = 'Current Medications: ' . trim((string) ($input['current_medications'] ?? ''));
}
if (empty($lines)) {
return $clean;
}
if ($clean !== '') {
return $clean . "\n" . implode("\n", $lines);
}
return implode("\n", $lines);
}
private function ensureClinicalTemplateTable(): bool
{
if ($this->db->tableExists('opd_clinical_templates')) {
return true;
}
try {
$sql = "CREATE TABLE IF NOT EXISTS opd_clinical_templates (
id INT AUTO_INCREMENT PRIMARY KEY,
doc_id INT NOT NULL DEFAULT 0,
section_key VARCHAR(80) NOT NULL,
template_name VARCHAR(255) NOT NULL,
template_text TEXT NOT NULL,
is_active TINYINT NOT NULL DEFAULT 1,
created_at DATETIME NULL,
updated_at DATETIME NULL,
INDEX idx_doc_section (doc_id, section_key),
INDEX idx_section_active (section_key, is_active)
)";
$this->db->query($sql);
} catch (\Throwable $e) {
return false;
}
return $this->db->tableExists('opd_clinical_templates');
}
private function ensureClinicalTemplateUsageTable(): bool
{
if ($this->db->tableExists('opd_clinical_template_usage')) {
return true;
}
try {
$sql = "CREATE TABLE IF NOT EXISTS opd_clinical_template_usage (
id INT AUTO_INCREMENT PRIMARY KEY,
doc_id INT NOT NULL,
template_id INT NOT NULL,
section_key VARCHAR(80) NOT NULL,
use_count INT NOT NULL DEFAULT 0,
last_used_at DATETIME NULL,
created_at DATETIME NULL,
updated_at DATETIME NULL,
UNIQUE KEY uniq_doc_template_section (doc_id, template_id, section_key),
INDEX idx_template (template_id),
INDEX idx_section_use (section_key, use_count)
)";
$this->db->query($sql);
} catch (\Throwable $e) {
return false;
}
return $this->db->tableExists('opd_clinical_template_usage');
}
private function isTemplateSectionAllowed(string $section): bool
{
return in_array($section, ['finding_examinations', 'diagnosis', 'provisional_diagnosis', 'prescriber_remarks', 'advice'], true);
}
private function resolveSectionColumn(string $section): ?string
{
$map = [
'finding_examinations' => 'Finding_Examinations',
'diagnosis' => 'diagnosis',
'provisional_diagnosis' => 'Provisional_diagnosis',
'prescriber_remarks' => 'Prescriber_Remarks',
'advice' => 'advice',
];
return $map[$section] ?? null;
}
private function ensurePrescriptionSession(int $opdId, int $sessionId = 0): int
{
if (!$this->db->tableExists('opd_prescription')) {
return 0;
}
$fields = $this->db->getFieldNames('opd_prescription');
if ($sessionId > 0) {
$row = $this->db->table('opd_prescription')->where('id', $sessionId)->where('opd_id', $opdId)->get(1)->getRow();
if ($row) {
return $sessionId;
}
}
$last = $this->db->table('opd_prescription')->where('opd_id', $opdId)->orderBy('id', 'DESC')->get(1)->getRow();
if ($last && !empty($last->id)) {
return (int) $last->id;
}
$opdRow = $this->db->table('opd_master')->where('opd_id', $opdId)->get(1)->getRow();
if (!$opdRow) {
return 0;
}
$insert = [];
if (in_array('opd_id', $fields, true)) {
$insert['opd_id'] = $opdId;
}
if (in_array('p_id', $fields, true)) {
$insert['p_id'] = (int) ($opdRow->p_id ?? 0);
}
if (in_array('doc_id', $fields, true)) {
$insert['doc_id'] = (int) ($opdRow->doc_id ?? 0);
}
if (in_array('date_opd_visit', $fields, true)) {
$insert['date_opd_visit'] = date('Y-m-d');
}
if (in_array('visit_status', $fields, true)) {
$insert['visit_status'] = 0;
}
if (in_array('session_id', $fields, true)) {
$insert['session_id'] = 0;
}
if (in_array('queue_no', $fields, true)) {
$maxQueue = $this->db->table('opd_prescription')
->selectMax('queue_no', 'max_queue')
->where('date_opd_visit', date('Y-m-d'))
->where('doc_id', (int) ($opdRow->doc_id ?? 0))
->get()
->getRow();
$insert['queue_no'] = (int) ($maxQueue->max_queue ?? 0) + 1;
}
$this->db->table('opd_prescription')->insert($insert);
return (int) $this->db->insertID();
}
private function isValidAbhaAddress(string $value): bool
{
$validation = service('validation');
$validation->reset();
return $validation->setRules([
'abha_address' => 'valid_abha_address',
])->run([
'abha_address' => $value,
]);
}
private function resolvePatientAbhaField(): ?string
{
if (! $this->db->tableExists('patient_master')) {
return null;
}
$fields = $this->db->getFieldNames('patient_master');
foreach (['abha_address', 'abha', 'abha_id', 'abha_no'] as $field) {
if (in_array($field, $fields, true)) {
return $field;
}
}
return null;
}
/**
* @return array<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] ?? '')) : '';
$patient = [
'id' => (string) ($patientRow['id'] ?? ($opdRow['p_id'] ?? 0)),
'name' => (string) ($patientRow['p_fname'] ?? $opdRow['P_name'] ?? ''),
'gender' => $this->normalizeGender((string) ($patientRow['gender'] ?? '')),
'birthDate' => !empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '',
'abhaAddress' => $abhaAddress,
];
$encounter = [
'id' => (string) ($opdRow['opd_id'] ?? $opdId),
'status' => 'finished',
];
$medications = $this->getPrescriptionMedicines($sessionId);
$bundle = $this->fhirR4Builder->buildPrescriptionBundle($patient, $encounter, $medications);
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$user = auth()->user();
$generatedBy = (string) ($user->id ?? 'system');
$inserted = (bool) $this->db->table('opd_fhir_documents')->insert([
'opd_id' => $opdId,
'opd_session_id' => $sessionId,
'bundle_type' => 'MedicationRequestBundle',
'bundle_json' => $bundleJson,
'generated_by' => $generatedBy,
'generated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
]);
if (! $inserted) {
return false;
}
try {
$documentId = (int) $this->db->insertID();
$bridgeSync = new BridgeSyncService();
$bridgeSync->enqueue(
'opd.fhir.generated',
[
'opd_fhir_document_id' => $documentId,
'opd_id' => $opdId,
'opd_session_id' => $sessionId,
'bundle_type' => 'MedicationRequestBundle',
'generated_by' => $generatedBy,
'generated_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
'bundle_json' => $bundle,
],
'opd_fhir_document',
(string) $documentId
);
} catch (\Throwable $e) {
}
return true;
}
/**
* @return array<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();
$medications = [];
foreach ($rows as $row) {
$dosageParts = array_filter([
trim((string) ($row['dosage'] ?? '')),
trim((string) ($row['dosage_when'] ?? '')),
trim((string) ($row['dosage_freq'] ?? '')),
trim((string) ($row['no_of_days'] ?? '')) !== '' ? ('for ' . trim((string) $row['no_of_days']) . ' days') : '',
]);
$medications[] = [
'drug_name' => (string) ($row['med_name'] ?? ''),
'status' => 'active',
'dosage' => implode(' | ', $dosageParts),
];
}
return $medications;
}
private function normalizeGender(string $value): string
{
$value = trim($value);
if ($value === '1' || strtolower($value) === 'male') {
return 'male';
}
if ($value === '2' || strtolower($value) === 'female') {
return 'female';
}
return 'unknown';
}
/**
* @return string|int|null
*/
private function getCurrentUserId()
{
$user = auth()->user();
if ($user === null) {
return null;
}
return $user->id ?? null;
}
/**
* @return array<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.opd_id,p.complaints,p.diagnosis,p.bp,p.diastolic,p.pulse,p.temp,p.spo2,p.investigation,p.advice,o.apointment_date')
->join('opd_master o', 'o.opd_id=p.opd_id', 'left')
->where('p.p_id', $patientId)
->where('p.opd_id !=', $currentOpdId)
->orderBy('p.id', 'DESC')
->limit(5)
->get()
->getResultArray();
foreach ($rows as $row) {
$dateLabel = ! empty($row['apointment_date']) ? date('d-m-Y', strtotime((string) $row['apointment_date'])) : 'Previous OPD';
if (trim((string) ($row['diagnosis'] ?? '')) !== '') {
$alerts[] = [
'type' => 'Diagnosis History',
'message' => $dateLabel . ': ' . trim((string) $row['diagnosis']),
];
}
if (trim((string) ($row['complaints'] ?? '')) !== '') {
$alerts[] = [
'type' => 'Complaint History',
'message' => $dateLabel . ': ' . trim((string) $row['complaints']),
];
}
$vitalLine = [];
foreach ([
'BP' => trim((string) ($row['bp'] ?? '')),
'DIA' => trim((string) ($row['diastolic'] ?? '')),
'Pulse' => trim((string) ($row['pulse'] ?? '')),
'Temp' => trim((string) ($row['temp'] ?? '')),
'SpO2' => trim((string) ($row['spo2'] ?? '')),
] as $k => $v) {
if ($v !== '') {
$vitalLine[] = $k . ':' . $v;
}
}
if (! empty($vitalLine)) {
$alerts[] = [
'type' => 'Vitals Trend',
'message' => $dateLabel . ': ' . implode(' | ', $vitalLine),
];
}
}
if ($this->db->tableExists('patient_remark')) {
$remarks = $this->db->table('patient_remark')
->select('remark,insert_datetime')
->where('p_id', $patientId)
->orderBy('id', 'DESC')
->limit(3)
->get()
->getResultArray();
foreach ($remarks as $remarkRow) {
$msg = trim((string) ($remarkRow['remark'] ?? ''));
if ($msg === '') {
continue;
}
$time = ! empty($remarkRow['insert_datetime']) ? date('d-m-Y H:i', strtotime((string) $remarkRow['insert_datetime'])) : 'Recent';
$alerts[] = [
'type' => 'Patient Remark',
'message' => $time . ': ' . $msg,
];
}
}
return array_slice($alerts, 0, 8);
}
/**
* @return array<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)
{
$printConfig = $this->resolvePrescriptionLayoutByDoctorField($opdId, $printType);
$layoutMode = (string) ($printConfig['layout'] ?? 'content_only');
$templateKey = (string) ($printConfig['template'] ?? '');
$url = base_url('Opd/opd_lettre_pdf/' . (int) $opdId)
. '?session_id=' . (int) $opdSessionId
. '&layout=' . urlencode($layoutMode);
if ($templateKey !== '') {
$url .= '&template=' . urlencode($templateKey);
}
return redirect()->to($url);
}
/**
* @return array{layout:string,template:string}
*/
private function resolvePrescriptionLayoutByDoctorField(int $opdId, int $printType): array
{
$allowed = ['full', 'meta_content', 'content_only', 'header_meta', 'meta_only'];
$defaultByType = [
0 => 'content_only',
1 => 'meta_content',
2 => 'full',
];
$fallback = $defaultByType[$printType] ?? 'content_only';
$fieldByType = [
0 => 'rx_plain_paper',
1 => 'rx_blank_letter_head',
2 => 'rx_pre_print_letter_head_format',
];
$fieldName = $fieldByType[$printType] ?? '';
if ($fieldName === '') {
return ['layout' => $fallback, 'template' => ''];
}
$query = $this->db->query(
'SELECT d.' . $fieldName . ' AS layout_field
FROM opd_master o
LEFT JOIN doctor_master d ON d.id = o.doc_id
WHERE o.opd_id = ?
LIMIT 1',
[(int) $opdId]
);
$row = $query->getRowArray();
$raw = strtolower(trim((string) ($row['layout_field'] ?? '')));
if ($raw === '') {
return ['layout' => $fallback, 'template' => ''];
}
if (in_array($raw, $allowed, true)) {
return ['layout' => $raw, 'template' => ''];
}
if ($raw === '0') {
return ['layout' => 'content_only', 'template' => ''];
}
if ($raw === '1') {
return ['layout' => 'meta_content', 'template' => ''];
}
if ($raw === '2') {
return ['layout' => 'full', 'template' => ''];
}
$templateKey = preg_replace('/[^a-z0-9_\-]/', '', $raw) ?? '';
if ($templateKey !== '') {
return ['layout' => $fallback, 'template' => $templateKey];
}
return ['layout' => $fallback, 'template' => ''];
}
}