devsofttech_hms_ci4/app/Controllers/Opd_prescription.php

6274 lines
244 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 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),
'old_rx_fast' => $this->buildOldPrescriptionFastView((int) ($opdRow->p_id ?? 0), $opdId),
'current_opd_scans' => $this->getCurrentOpdScansForConsult($opdId),
]);
}
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->generateClinicalTextWithGemini($prompt);
if ($draft !== null && trim($draft) !== '') {
$provider = $this->lastAiProvider === 'azure' ? 'azure' : 'gemini';
$providerLabel = $provider === 'azure' ? 'Azure OpenAI' : 'Gemini';
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->generateClinicalTextWithGemini($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 === 'azure' ? 'azure' : 'gemini';
$message = 'Full clinical draft prepared using ' . ($aiMode === 'azure' ? 'Azure OpenAI' : 'Gemini') . '.';
}
return $this->response->setJSON([
'update' => 1,
'drafts' => $drafts,
'ai_mode' => $aiMode,
'error_text' => $message,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function local_clinical_assist()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$input = [
'complaints' => trim((string) $this->request->getPost('complaints')),
'finding_examinations' => trim((string) $this->request->getPost('finding_examinations')),
'diagnosis' => trim((string) $this->request->getPost('diagnosis')),
'investigation' => trim((string) $this->request->getPost('investigation')),
'advice' => trim((string) $this->request->getPost('advice')),
'bp' => trim((string) $this->request->getPost('bp')),
'diastolic' => trim((string) $this->request->getPost('diastolic')),
'pulse' => trim((string) $this->request->getPost('pulse')),
'temp' => trim((string) $this->request->getPost('temp')),
'spo2' => trim((string) $this->request->getPost('spo2')),
'rr_min' => trim((string) $this->request->getPost('rr_min')),
'pain_value' => trim((string) $this->request->getPost('pain_value')),
'pregnancy' => (int) $this->request->getPost('pregnancy'),
'lactation' => (int) $this->request->getPost('lactation'),
'dengue' => (int) $this->request->getPost('dengue'),
'corona_suspected' => (int) $this->request->getPost('corona_suspected'),
];
if ($input['complaints'] === '' && $input['finding_examinations'] === '') {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Enter complaints or findings first for clinical assist.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$assist = $this->buildLocalClinicalAssist($input);
return $this->response->setJSON([
'update' => 1,
'assist' => $assist,
'error_text' => 'Local clinical assist prepared. Final decision remains with doctor.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function section_template_save()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$section = trim((string) $this->request->getPost('section'));
$templateName = trim((string) $this->request->getPost('template_name'));
$templateText = trim((string) $this->request->getPost('template_text'));
$templateScope = strtolower(trim((string) $this->request->getPost('template_scope')));
$templateDocId = (int) $this->request->getPost('template_doc_id');
if ($templateName === '' || $templateText === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Template name and text are required']);
}
if (! $this->isTemplateSectionAllowed($section)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unsupported section']);
}
if (! $this->ensureClinicalTemplateTable()) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to access template storage']);
}
$docId = $this->getCurrentUserId();
if ($templateScope === 'master') {
$docId = 0;
} elseif ($templateDocId > 0) {
$docId = $templateDocId;
} elseif ($docId < 0) {
$docId = 0;
}
$table = $this->db->table('opd_clinical_templates');
$existing = $table
->where('doc_id', $docId)
->where('section_key', $section)
->where('template_name', $templateName)
->where('is_active', 1)
->get(1)
->getRowArray();
$now = date('Y-m-d H:i:s');
if (! empty($existing)) {
$table->where('id', (int) ($existing['id'] ?? 0))->update([
'template_text' => $templateText,
'updated_at' => $now,
]);
} else {
$table->insert([
'doc_id' => $docId,
'section_key' => $section,
'template_name' => $templateName,
'template_text' => $templateText,
'is_active' => 1,
'created_at' => $now,
'updated_at' => $now,
]);
}
return $this->response->setJSON([
'update' => 1,
'error_text' => ($docId === 0 ? 'Master template saved' : 'My template saved'),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function section_template_list()
{
$section = trim((string) $this->request->getGet('section'));
if (! $this->isTemplateSectionAllowed($section)) {
return $this->response->setJSON(['rows' => []]);
}
if (! $this->ensureClinicalTemplateTable()) {
return $this->response->setJSON(['rows' => []]);
}
$docId = $this->getCurrentUserId();
$rows = $this->db->table('opd_clinical_templates')
->select('id,template_name,template_text,doc_id,section_key')
->where('section_key', $section)
->where('is_active', 1)
->groupStart()
->where('doc_id', $docId)
->orWhere('doc_id', 0)
->groupEnd()
->orderBy('doc_id', 'DESC')
->orderBy('template_name', 'ASC')
->limit(100)
->get()
->getResultArray();
$usageByTemplateId = [];
if (! empty($rows) && $this->ensureClinicalTemplateUsageTable() && $docId !== null) {
$templateIds = array_map(static fn($row) => (int) ($row['id'] ?? 0), $rows);
$templateIds = array_values(array_filter($templateIds, static fn($id) => $id > 0));
if (! empty($templateIds)) {
$usageRows = $this->db->table('opd_clinical_template_usage')
->select('template_id,use_count,last_used_at')
->where('doc_id', (int) $docId)
->where('section_key', $section)
->whereIn('template_id', $templateIds)
->get()
->getResultArray();
foreach ($usageRows as $usageRow) {
$templateId = (int) ($usageRow['template_id'] ?? 0);
if ($templateId <= 0) {
continue;
}
$usageByTemplateId[$templateId] = [
'use_count' => (int) ($usageRow['use_count'] ?? 0),
'last_used_at' => (string) ($usageRow['last_used_at'] ?? ''),
];
}
}
}
foreach ($rows as &$row) {
$templateId = (int) ($row['id'] ?? 0);
$usage = $usageByTemplateId[$templateId] ?? ['use_count' => 0, 'last_used_at' => ''];
$row['use_count'] = (int) ($usage['use_count'] ?? 0);
$row['last_used_at'] = (string) ($usage['last_used_at'] ?? '');
}
unset($row);
usort($rows, static function (array $a, array $b): int {
$aUse = (int) ($a['use_count'] ?? 0);
$bUse = (int) ($b['use_count'] ?? 0);
if ($aUse !== $bUse) {
return $bUse <=> $aUse;
}
$aDoc = (int) ($a['doc_id'] ?? 0);
$bDoc = (int) ($b['doc_id'] ?? 0);
if ($aDoc !== $bDoc) {
return $bDoc <=> $aDoc;
}
$aLast = trim((string) ($a['last_used_at'] ?? ''));
$bLast = trim((string) ($b['last_used_at'] ?? ''));
if ($aLast !== $bLast) {
return strcmp($bLast, $aLast);
}
return strcasecmp((string) ($a['template_name'] ?? ''), (string) ($b['template_name'] ?? ''));
});
return $this->response->setJSON(['rows' => $rows]);
}
public function section_template_track_usage()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$section = trim((string) $this->request->getPost('section'));
$templateId = (int) $this->request->getPost('template_id');
if ($templateId <= 0 || ! $this->isTemplateSectionAllowed($section)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Invalid template usage payload',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
if (! $this->ensureClinicalTemplateTable() || ! $this->ensureClinicalTemplateUsageTable()) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Usage storage unavailable',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$docId = $this->getCurrentUserId();
if ($docId === null) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'User not found',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$templateRow = $this->db->table('opd_clinical_templates')
->select('id,section_key,is_active')
->where('id', $templateId)
->where('is_active', 1)
->get(1)
->getRowArray();
if (empty($templateRow)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Template not found',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
if (trim((string) ($templateRow['section_key'] ?? '')) !== $section) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Section mismatch for template',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$usageTable = $this->db->table('opd_clinical_template_usage');
$existing = $usageTable
->where('doc_id', (int) $docId)
->where('template_id', $templateId)
->where('section_key', $section)
->get(1)
->getRowArray();
$now = date('Y-m-d H:i:s');
if (! empty($existing)) {
$usageTable->where('id', (int) ($existing['id'] ?? 0))->update([
'use_count' => ((int) ($existing['use_count'] ?? 0)) + 1,
'last_used_at' => $now,
'updated_at' => $now,
]);
} else {
$usageTable->insert([
'doc_id' => (int) $docId,
'template_id' => $templateId,
'section_key' => $section,
'use_count' => 1,
'last_used_at' => $now,
'created_at' => $now,
'updated_at' => $now,
]);
}
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Template usage tracked',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function section_past_data()
{
$section = trim((string) $this->request->getGet('section'));
$opdId = (int) $this->request->getGet('opd_id');
if ($opdId <= 0 || ! $this->isTemplateSectionAllowed($section) || ! $this->db->tableExists('opd_master') || ! $this->db->tableExists('opd_prescription')) {
return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Past data not found']);
}
$field = $this->resolveSectionColumn($section);
if ($field === null) {
return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Unsupported section']);
}
$fields = $this->db->getFieldNames('opd_prescription');
if (! in_array($field, $fields, true)) {
return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Past data column missing']);
}
$opdRow = $this->db->table('opd_master')->select('p_id')->where('opd_id', $opdId)->get(1)->getRowArray();
$patientId = (int) ($opdRow['p_id'] ?? 0);
if ($patientId <= 0) {
return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'Patient not found']);
}
$row = $this->db->table('opd_prescription')
->select($field . ' as section_text,opd_id,id')
->where('p_id', $patientId)
->where('opd_id !=', $opdId)
->where($field . ' IS NOT NULL', null, false)
->where($field . ' !=', '')
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
if (empty($row)) {
return $this->response->setJSON(['update' => 0, 'past_text' => '', 'error_text' => 'No past data found']);
}
return $this->response->setJSON([
'update' => 1,
'past_text' => (string) ($row['section_text'] ?? ''),
'past_opd_id' => (int) ($row['opd_id'] ?? 0),
'error_text' => 'Past data loaded',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function template_workspace()
{
return view('billing/opd_template_workspace');
}
public function clinical_template_monitor()
{
if (! $this->ensureClinicalTemplateTable()) {
return $this->response->setJSON(['rows' => []]);
}
$section = trim((string) $this->request->getGet('section'));
$scope = strtolower(trim((string) $this->request->getGet('scope')));
$doctorId = (int) $this->request->getGet('doctor_id');
$docId = $this->getCurrentUserId();
$builder = $this->db->table('opd_clinical_templates')
->select('id,doc_id,section_key,template_name,template_text,is_active,created_at,updated_at')
->where('is_active', 1);
if ($this->isTemplateSectionAllowed($section)) {
$builder->where('section_key', $section);
}
if ($scope === 'master') {
$builder->where('doc_id', 0);
} elseif ($scope === 'doctor') {
if ($doctorId > 0) {
$builder->where('doc_id', $doctorId);
} else {
$builder->where('doc_id', $docId);
}
} else {
$builder->groupStart()
->where('doc_id', 0)
->orWhere('doc_id', $docId)
->groupEnd();
}
$rows = $builder
->orderBy('updated_at', 'DESC')
->orderBy('id', 'DESC')
->limit(300)
->get()
->getResultArray();
$doctorNameById = [];
if ($this->db->tableExists('doctor_master')) {
$doctorRows = $this->db->table('doctor_master')->select('id,p_fname')->get()->getResultArray();
foreach ($doctorRows as $doctorRow) {
$doctorNameById[(int) ($doctorRow['id'] ?? 0)] = (string) ($doctorRow['p_fname'] ?? '');
}
}
foreach ($rows as &$row) {
$row['scope_label'] = ((int) ($row['doc_id'] ?? -1) === 0) ? 'Master' : 'Doctor';
$row['doctor_name'] = ((int) ($row['doc_id'] ?? 0) > 0) ? ($doctorNameById[(int) ($row['doc_id'] ?? 0)] ?? '') : '';
}
unset($row);
return $this->response->setJSON(['rows' => $rows]);
}
public function clinical_template_update()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
if (! $this->ensureClinicalTemplateTable()) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Template storage unavailable']);
}
$id = (int) $this->request->getPost('id');
$section = trim((string) $this->request->getPost('section'));
$templateName = trim((string) $this->request->getPost('template_name'));
$templateText = trim((string) $this->request->getPost('template_text'));
$templateScope = strtolower(trim((string) $this->request->getPost('template_scope')));
$templateDocId = (int) $this->request->getPost('template_doc_id');
if ($id <= 0 || ! $this->isTemplateSectionAllowed($section) || $templateName === '' || $templateText === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Template id, section, name and text are required']);
}
$targetDocId = $templateScope === 'master' ? 0 : ($templateDocId > 0 ? $templateDocId : $this->getCurrentUserId());
if ($targetDocId < 0) {
$targetDocId = 0;
}
$current = $this->db->table('opd_clinical_templates')->where('id', $id)->where('is_active', 1)->get(1)->getRowArray();
if (empty($current)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Template not found']);
}
$currentDocId = (int) ($current['doc_id'] ?? -1);
$myDocId = $this->getCurrentUserId();
if ($currentDocId !== 0 && $currentDocId !== $myDocId) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'You can edit only your own or master templates']);
}
$existing = $this->db->table('opd_clinical_templates')
->where('id !=', $id)
->where('doc_id', $targetDocId)
->where('section_key', $section)
->where('template_name', $templateName)
->where('is_active', 1)
->get(1)
->getRowArray();
if (!empty($existing)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Another template with same name already exists in selected scope']);
}
$this->db->table('opd_clinical_templates')->where('id', $id)->update([
'doc_id' => $targetDocId,
'section_key' => $section,
'template_name' => $templateName,
'template_text' => $templateText,
'updated_at' => date('Y-m-d H:i:s'),
]);
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Template updated',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function clinical_template_doctors()
{
if (! $this->db->tableExists('doctor_master')) {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames('doctor_master');
$idField = $this->resolveFirstField($fields, ['id', 'doc_id']);
$nameField = $this->resolveFirstField($fields, ['p_fname', 'name', 'doc_name']);
if ($idField === null || $nameField === null) {
return $this->response->setJSON(['rows' => []]);
}
$builder = $this->db->table('doctor_master')
->select($idField . ' as id,' . $nameField . ' as name');
if (in_array('active', $fields, true)) {
$builder->where('active', 1);
}
$rows = $builder->orderBy($nameField, 'ASC')->get()->getResultArray();
return $this->response->setJSON(['rows' => $rows]);
}
public function clinical_template_usage_analytics()
{
if (! $this->ensureClinicalTemplateTable() || ! $this->ensureClinicalTemplateUsageTable()) {
return $this->response->setJSON([
'top_rows' => [],
'section_rows' => [],
'summary' => ['total_uses' => 0, 'used_templates' => 0],
]);
}
$section = trim((string) $this->request->getGet('section'));
$scope = strtolower(trim((string) $this->request->getGet('scope')));
$doctorId = (int) $this->request->getGet('doctor_id');
$currentDocId = (int) ($this->getCurrentUserId() ?? 0);
$targetDocId = $scope === 'doctor' && $doctorId > 0 ? $doctorId : $currentDocId;
$builder = $this->db->table('opd_clinical_template_usage u')
->select('u.template_id,u.section_key,u.use_count,u.last_used_at,t.template_name,t.doc_id')
->join('opd_clinical_templates t', 't.id = u.template_id', 'inner')
->where('u.doc_id', $targetDocId)
->where('t.is_active', 1);
if ($this->isTemplateSectionAllowed($section)) {
$builder->where('u.section_key', $section);
}
if ($scope === 'master') {
$builder->where('t.doc_id', 0);
} elseif ($scope === 'doctor') {
$builder->where('t.doc_id', $targetDocId);
} else {
$builder->groupStart()
->where('t.doc_id', 0)
->orWhere('t.doc_id', $targetDocId)
->groupEnd();
}
$usageRows = $builder->get()->getResultArray();
if (empty($usageRows)) {
return $this->response->setJSON([
'top_rows' => [],
'section_rows' => [],
'summary' => ['total_uses' => 0, 'used_templates' => 0],
]);
}
usort($usageRows, static function (array $a, array $b): int {
$aUse = (int) ($a['use_count'] ?? 0);
$bUse = (int) ($b['use_count'] ?? 0);
if ($aUse !== $bUse) {
return $bUse <=> $aUse;
}
return strcmp((string) ($b['last_used_at'] ?? ''), (string) ($a['last_used_at'] ?? ''));
});
$totalUses = 0;
$usedTemplates = [];
$sectionAgg = [];
foreach ($usageRows as $row) {
$use = (int) ($row['use_count'] ?? 0);
$sec = trim((string) ($row['section_key'] ?? ''));
$totalUses += $use;
$templateId = (int) ($row['template_id'] ?? 0);
if ($templateId > 0) {
$usedTemplates[$templateId] = true;
}
if ($sec === '') {
continue;
}
if (! isset($sectionAgg[$sec])) {
$sectionAgg[$sec] = ['section_key' => $sec, 'total_uses' => 0, 'template_count' => 0, 'template_ids' => []];
}
$sectionAgg[$sec]['total_uses'] += $use;
if ($templateId > 0 && ! isset($sectionAgg[$sec]['template_ids'][$templateId])) {
$sectionAgg[$sec]['template_ids'][$templateId] = true;
$sectionAgg[$sec]['template_count']++;
}
}
$sectionRows = array_values(array_map(static function (array $row): array {
unset($row['template_ids']);
return $row;
}, $sectionAgg));
usort($sectionRows, static function (array $a, array $b): int {
$aUse = (int) ($a['total_uses'] ?? 0);
$bUse = (int) ($b['total_uses'] ?? 0);
if ($aUse !== $bUse) {
return $bUse <=> $aUse;
}
return strcasecmp((string) ($a['section_key'] ?? ''), (string) ($b['section_key'] ?? ''));
});
return $this->response->setJSON([
'top_rows' => array_slice($usageRows, 0, 15),
'section_rows' => $sectionRows,
'summary' => [
'total_uses' => $totalUses,
'used_templates' => count($usedTemplates),
],
]);
}
public function rx_group_panel()
{
return view('billing/opd_rx_group_workspace', ['initial_rx_id' => 0]);
}
public function save_rx_group_edit(int $rxId = 0)
{
return view('billing/opd_rx_group_workspace', ['initial_rx_id' => max(0, $rxId)]);
}
public function rx_group_data()
{
if (! $this->db->tableExists('opd_prescription_template')) {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames('opd_prescription_template');
$idField = $this->resolveFirstField($fields, ['id']);
$nameField = $this->resolveFirstField($fields, ['rx_group_name']);
if ($idField === null || $nameField === null) {
return $this->response->setJSON(['rows' => []]);
}
$userId = max(0, $this->getCurrentUserId());
$hasDocId = in_array('doc_id', $fields, true);
$builder = $this->db->table('opd_prescription_template')
->select('MAX(' . $idField . ') as id,' . $nameField . ' as rx_group_name,COUNT(*) as row_count')
->where($nameField . ' <>', '');
if ($hasDocId) {
$builder->select('SUM(CASE WHEN doc_id = 0 THEN 1 ELSE 0 END) as global_count', false);
$builder->select('SUM(CASE WHEN doc_id = ' . (int) $userId . ' THEN 1 ELSE 0 END) as doctor_count', false);
}
if ($hasDocId) {
$builder->groupStart()
->where('doc_id', $userId)
->orWhere('doc_id', 0)
->groupEnd();
}
$rows = $builder
->groupBy($nameField)
->orderBy($nameField, 'ASC')
->limit(500)
->get()
->getResultArray();
foreach ($rows as &$row) {
if (! $hasDocId) {
$row['access_label'] = 'Global';
continue;
}
$globalCount = (int) ($row['global_count'] ?? 0);
$doctorCount = (int) ($row['doctor_count'] ?? 0);
if ($globalCount > 0 && $doctorCount > 0) {
$row['access_label'] = 'Global + Doctor';
} elseif ($globalCount > 0) {
$row['access_label'] = 'Global';
} elseif ($doctorCount > 0) {
$row['access_label'] = 'Doctor';
} else {
$row['access_label'] = 'Specific User';
}
}
unset($row);
return $this->response->setJSON(['rows' => $rows]);
}
public function rx_group_apply_to_session()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$opdId = (int) $this->request->getPost('opd_id');
$sessionId = (int) $this->request->getPost('opd_session_id');
$rxGroupId = (int) $this->request->getPost('rx_group_id');
if ($opdId <= 0 || $rxGroupId <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid OPD or Rx-Group']);
}
$templateTable = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']);
if ($templateTable === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx-Group medicine template not found']);
}
$sessionTable = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']);
if ($sessionTable === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']);
}
$templateFields = $this->db->getFieldNames($templateTable);
if (! in_array('rx_group_id', $templateFields, true)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid Rx-Group template structure']);
}
$templateRows = $this->db->table($templateTable)
->where('rx_group_id', $rxGroupId)
->orderBy('id', 'ASC')
->get()
->getResultArray();
if (empty($templateRows)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'No medicines found in selected Rx-Group']);
}
$sessionId = $this->ensurePrescriptionSession($opdId, $sessionId);
if ($sessionId <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create prescription session']);
}
$sessionFields = $this->db->getFieldNames($sessionTable);
$user = auth()->user();
$userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User';
$updateByText = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s');
$inserted = 0;
foreach ($templateRows as $row) {
$insert = [];
if (in_array('opd_pre_id', $sessionFields, true)) {
$insert['opd_pre_id'] = $sessionId;
}
if (in_array('opd_id', $sessionFields, true)) {
$insert['opd_id'] = $opdId;
}
foreach (['med_id', 'med_name', 'med_type', 'dosage', 'dosage_when', 'dosage_freq', 'dosage_where', 'no_of_days', 'qty', 'remark', 'genericname'] as $field) {
if (! in_array($field, $sessionFields, true) || ! array_key_exists($field, $row)) {
continue;
}
$value = $row[$field];
if (in_array($field, ['med_name', 'remark'], true)) {
$value = strtoupper((string) $value);
}
$insert[$field] = $value;
}
if (in_array('update_by', $sessionFields, true)) {
$insert['update_by'] = $updateByText;
}
if (! empty($insert)) {
$this->db->table($sessionTable)->insert($insert);
$inserted++;
$medId = (int) ($insert['med_id'] ?? 0);
if ($medId > 0) {
$this->trackMedicineUsage($medId);
}
}
}
return $this->response->setJSON([
'update' => 1,
'opd_session_id' => $sessionId,
'inserted_count' => $inserted,
'error_text' => $inserted > 0 ? ($inserted . ' medicine(s) added from Rx-Group.') : 'No medicine inserted from Rx-Group.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function rx_group_get(int $rxId)
{
if (! $this->db->tableExists('opd_prescription_template')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group table not found']);
}
$fields = $this->db->getFieldNames('opd_prescription_template');
$builder = $this->db->table('opd_prescription_template')->where('id', (int) $rxId);
if (in_array('doc_id', $fields, true)) {
$userId = max(0, $this->getCurrentUserId());
$builder->groupStart()
->where('doc_id', $userId)
->orWhere('doc_id', 0)
->groupEnd();
}
$row = $builder->get(1)->getRowArray() ?? [];
if (empty($row)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group not found']);
}
return $this->response->setJSON(['update' => 1, 'row' => $row]);
}
public function rx_group_save()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
if (! $this->db->tableExists('opd_prescription_template')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group table not found']);
}
$id = (int) $this->request->getPost('id');
$name = trim((string) $this->request->getPost('rx_group_name'));
if ($name === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx-Group Name is required']);
}
$fields = $this->db->getFieldNames('opd_prescription_template');
$data = [];
if (in_array('rx_group_name', $fields, true)) {
$data['rx_group_name'] = $name;
}
if (in_array('complaints', $fields, true)) {
$data['complaints'] = trim((string) $this->request->getPost('complaints'));
}
if (in_array('diagnosis', $fields, true)) {
$data['diagnosis'] = trim((string) $this->request->getPost('diagnosis'));
}
if (in_array('Finding_Examinations', $fields, true)) {
$data['Finding_Examinations'] = trim((string) $this->request->getPost('finding_examinations'));
}
if (in_array('investigation', $fields, true)) {
$data['investigation'] = trim((string) $this->request->getPost('investigation'));
}
$userId = max(0, $this->getCurrentUserId());
if (in_array('rx_group_name', $fields, true)) {
$dupBuilder = $this->db->table('opd_prescription_template')
->select('id')
->where('rx_group_name', $name);
if (in_array('doc_id', $fields, true)) {
$dupBuilder->groupStart()
->where('doc_id', $userId)
->orWhere('doc_id', 0)
->groupEnd();
}
if ($id > 0) {
$dupBuilder->where('id <>', $id);
}
$exists = $dupBuilder->orderBy('id', 'DESC')->get(1)->getRowArray();
if (! empty($exists)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Rx-Group name already exists. Please use Edit for existing group.',
'existing_id' => (int) ($exists['id'] ?? 0),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
}
if (in_array('doc_id', $fields, true) && $id <= 0) {
$data['doc_id'] = $userId;
}
$user = auth()->user();
if (in_array('update_by', $fields, true)) {
$userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User';
$data['update_by'] = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s');
}
if ($id > 0) {
$this->db->table('opd_prescription_template')->where('id', $id)->update($data);
$savedId = $id;
$message = 'Rx-Group updated';
} else {
$this->db->table('opd_prescription_template')->insert($data);
$savedId = (int) $this->db->insertID();
$message = 'Rx-Group saved';
}
return $this->response->setJSON([
'update' => 1,
'insertid' => $savedId,
'error_text' => $message,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function rx_group_medicine(int $rxId)
{
$rxGroupId = max(0, $rxId);
$rxGroupName = '';
if ($rxGroupId > 0 && $this->db->tableExists('opd_prescription_template')) {
$templateFields = $this->db->getFieldNames('opd_prescription_template');
$nameField = $this->resolveFirstField($templateFields, ['rx_group_name']);
if ($nameField !== null) {
$row = $this->db->table('opd_prescription_template')
->select($nameField . ' as rx_group_name')
->where('id', $rxGroupId)
->get(1)
->getRowArray();
$rxGroupName = trim((string) ($row['rx_group_name'] ?? ''));
}
}
return view('billing/rx_group_medicine_workspace', [
'rx_group_id' => $rxGroupId,
'rx_group_name' => $rxGroupName,
]);
}
public function rx_group_medicine_list(int $rxId)
{
$table = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']);
if ($table === null) {
return $this->response->setJSON(['rows' => [], 'rx_group_name' => '']);
}
$fields = $this->db->getFieldNames($table);
if (! in_array('rx_group_id', $fields, true)) {
return $this->response->setJSON(['rows' => [], 'rx_group_name' => '']);
}
$rxGroupName = '';
if ($this->db->tableExists('opd_prescription_template')) {
$templateFields = $this->db->getFieldNames('opd_prescription_template');
$nameField = $this->resolveFirstField($templateFields, ['rx_group_name']);
if ($nameField !== null) {
$groupRow = $this->db->table('opd_prescription_template')
->select($nameField . ' as rx_group_name')
->where('id', (int) $rxId)
->get(1)
->getRowArray();
$rxGroupName = trim((string) ($groupRow['rx_group_name'] ?? ''));
}
}
$builder = $this->db->table($table)->where('rx_group_id', (int) $rxId);
if (in_array('id', $fields, true)) {
$builder->orderBy('id', 'DESC');
}
$rows = $builder->get()->getResultArray();
if (empty($rows)) {
return $this->response->setJSON(['rows' => [], 'rx_group_name' => $rxGroupName]);
}
$medGenericMap = [];
$missingGenericMedIds = [];
foreach ($rows as $row) {
$existingGeneric = trim((string) ($row['genericname'] ?? ''));
$medId = (int) ($row['med_id'] ?? 0);
if ($existingGeneric === '' && $medId > 0) {
$missingGenericMedIds[$medId] = true;
}
}
if (! empty($missingGenericMedIds)) {
$masterTable = $this->findExistingTable(['opd_med_master']);
if ($masterTable !== null) {
$masterFields = $this->db->getFieldNames($masterTable);
$masterIdField = $this->resolveFirstField($masterFields, ['id']);
$masterGenericField = $this->resolveFirstField($masterFields, ['genericname', 'generic_name']);
$masterSaltField = $this->resolveFirstField($masterFields, ['salt_name', 'sal_name', 'salt', 'saltname']);
$labelField = $masterSaltField ?? $masterGenericField;
if ($masterIdField !== null && $labelField !== null) {
$medRows = $this->db->table($masterTable)
->select($masterIdField . ' as med_id,' . $labelField . ' as generic_fallback')
->whereIn($masterIdField, array_map('intval', array_keys($missingGenericMedIds)))
->get()
->getResultArray();
foreach ($medRows as $medRow) {
$id = (int) ($medRow['med_id'] ?? 0);
$label = trim((string) ($medRow['generic_fallback'] ?? ''));
if ($id > 0 && $label !== '') {
$medGenericMap[$id] = $label;
}
}
}
}
}
$doseMap = $this->getDoseMasterLabelMap('opd_dose_shed', ['dose_shed_id', 'id'], ['dose_show_sign', 'dose_sign', 'dose_sign_desc', 'name']);
$whenMap = $this->getDoseMasterLabelMap('opd_dose_when', ['dose_when_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']);
$freqMap = $this->getDoseMasterLabelMap('opd_dose_frequency', ['dose_freq_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']);
$whereMap = $this->getDoseMasterLabelMap('opd_dose_where', ['dose_where_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']);
foreach ($rows as &$row) {
if (trim((string) ($row['genericname'] ?? '')) === '') {
$inlineSalt = trim((string) ($row['salt_name'] ?? ''));
if ($inlineSalt !== '') {
$row['genericname'] = $inlineSalt;
}
$fallback = $medGenericMap[(int) ($row['med_id'] ?? 0)] ?? '';
if (($row['genericname'] ?? '') === '' && $fallback !== '') {
$row['genericname'] = $fallback;
}
}
$row['dosage_label'] = $this->resolveDoseMasterLabel($row['dosage'] ?? '', $doseMap);
$row['dosage_when_label'] = $this->resolveDoseMasterLabel($row['dosage_when'] ?? '', $whenMap);
$row['dosage_freq_label'] = $this->resolveDoseMasterLabel($row['dosage_freq'] ?? '', $freqMap);
$row['dosage_where_label'] = $this->resolveDoseMasterLabel($row['dosage_where'] ?? '', $whereMap);
}
unset($row);
return $this->response->setJSON(['rows' => $rows, 'rx_group_name' => $rxGroupName]);
}
public function rx_group_medicine_get(int $itemId)
{
$table = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx medicine table not found']);
}
$row = $this->db->table($table)->where('id', (int) $itemId)->get(1)->getRowArray() ?? [];
if (empty($row)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx medicine not found']);
}
return $this->response->setJSON(['update' => 1, 'row' => $row]);
}
public function rx_group_medicine_save(int $rxId)
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
if (! $this->db->tableExists('opd_prescription_template')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group table not found']);
}
$rxExists = $this->db->table('opd_prescription_template')->where('id', (int) $rxId)->get(1)->getRowArray();
if (empty($rxExists)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx group not found']);
}
$table = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx medicine table not found']);
}
$itemId = (int) $this->request->getPost('item_id');
$medId = (int) $this->request->getPost('med_id');
$medName = trim((string) $this->request->getPost('med_name'));
if ($medName === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']);
}
$genericName = trim((string) $this->request->getPost('genericname'));
$medType = trim((string) $this->request->getPost('med_type'));
$masterTable = $this->findExistingTable(['opd_med_master']);
if ($masterTable !== null) {
$masterFields = $this->db->getFieldNames($masterTable);
$masterNameField = $this->resolveFirstField($masterFields, ['item_name', 'med_name']);
$masterTypeField = $this->resolveFirstField($masterFields, ['formulation']);
$masterGenericField = $this->resolveFirstField($masterFields, ['genericname', 'generic_name']);
$masterSaltField = $this->resolveFirstField($masterFields, ['salt_name', 'sal_name', 'salt', 'saltname']);
if ($medId <= 0 && $masterNameField !== null) {
$existingMaster = $this->db->table($masterTable)->where($masterNameField, $medName)->get(1)->getRowArray();
if (! empty($existingMaster)) {
$medId = (int) ($existingMaster['id'] ?? 0);
}
}
if ($medId > 0) {
$updateMaster = [];
if ($masterTypeField !== null && $medType !== '') {
$updateMaster[$masterTypeField] = $medType;
}
if ($masterGenericField !== null && $genericName !== '') {
$updateMaster[$masterGenericField] = $genericName;
}
if ($masterSaltField !== null && $genericName !== '') {
$updateMaster[$masterSaltField] = $genericName;
}
if (! empty($updateMaster)) {
$this->db->table($masterTable)->where('id', $medId)->update($updateMaster);
}
} elseif ($masterNameField !== null) {
$insertMaster = [$masterNameField => $medName];
if ($masterTypeField !== null) {
$insertMaster[$masterTypeField] = $medType;
}
if ($masterGenericField !== null) {
$insertMaster[$masterGenericField] = $genericName;
}
if ($masterSaltField !== null) {
$insertMaster[$masterSaltField] = $genericName;
}
$this->db->table($masterTable)->insert($insertMaster);
$medId = (int) $this->db->insertID();
}
}
$fields = $this->db->getFieldNames($table);
$data = [];
if (in_array('rx_group_id', $fields, true)) {
$data['rx_group_id'] = (int) $rxId;
}
if (in_array('med_id', $fields, true)) {
$data['med_id'] = $medId;
}
if (in_array('med_name', $fields, true)) {
$data['med_name'] = strtoupper($medName);
}
if (in_array('med_type', $fields, true)) {
$data['med_type'] = $medType;
}
if (in_array('dosage', $fields, true)) {
$data['dosage'] = trim((string) $this->request->getPost('dosage'));
}
if (in_array('dosage_when', $fields, true)) {
$data['dosage_when'] = trim((string) $this->request->getPost('dosage_when'));
}
if (in_array('dosage_freq', $fields, true)) {
$data['dosage_freq'] = trim((string) $this->request->getPost('dosage_freq'));
}
if (in_array('dosage_where', $fields, true)) {
$data['dosage_where'] = trim((string) $this->request->getPost('dosage_where'));
}
if (in_array('no_of_days', $fields, true)) {
$data['no_of_days'] = trim((string) $this->request->getPost('no_of_days'));
}
if (in_array('qty', $fields, true)) {
$data['qty'] = trim((string) $this->request->getPost('qty'));
}
if (in_array('remark', $fields, true)) {
$data['remark'] = strtoupper(trim((string) $this->request->getPost('remark')));
}
if (in_array('genericname', $fields, true)) {
$data['genericname'] = $genericName;
}
$user = auth()->user();
if (in_array('update_by', $fields, true)) {
$userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User';
$data['update_by'] = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s');
}
if ($itemId > 0) {
$this->db->table($table)->where('id', $itemId)->update($data);
$savedId = $itemId;
$message = 'Rx group medicine updated';
} else {
$this->db->table($table)->insert($data);
$savedId = (int) $this->db->insertID();
$message = 'Rx group medicine added';
}
return $this->response->setJSON([
'update' => 1,
'insertid' => $savedId,
'error_text' => $message,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function rx_group_medicine_remove(int $itemId)
{
$table = $this->findExistingTable(['opd_prescrption_prescribed_template', 'opd_prescription_prescribed_template']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Rx medicine table not found']);
}
$this->db->table($table)->where('id', (int) $itemId)->delete();
return $this->response->setJSON(['update' => 1, 'error_text' => 'Rx group medicine removed']);
}
public function rx_group_medicine_suggest()
{
$q = trim((string) $this->request->getGet('q'));
$table = $this->findExistingTable(['opd_med_master']);
if ($table === null || $q === '') {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames($table);
$idField = $this->resolveFirstField($fields, ['id']);
$nameField = $this->resolveFirstField($fields, ['item_name', 'med_name']);
$typeField = $this->resolveFirstField($fields, ['formulation']);
$genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']);
$saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']);
$companyField = $this->resolveFirstField($fields, ['company_name', 'company']);
if ($idField === null || $nameField === null) {
return $this->response->setJSON(['rows' => []]);
}
$select = $idField . ' as med_id,' . $nameField . ' as med_name';
if ($typeField !== null) {
$select .= ',' . $typeField . ' as med_type';
}
if ($genericField !== null) {
$select .= ',' . $genericField . ' as genericname';
}
if ($saltField !== null) {
$select .= ',' . $saltField . ' as salt_name';
}
if ($companyField !== null) {
$select .= ',' . $companyField . ' as company_name';
}
$builder = $this->db->table($table)->select($select)->groupStart()->like($nameField, $q);
if ($genericField !== null) {
$builder->orLike($genericField, $q);
}
if ($saltField !== null) {
$builder->orLike($saltField, $q);
}
$rows = $builder->groupEnd()->orderBy($nameField, 'ASC')->limit(30)->get()->getResultArray();
return $this->response->setJSON(['rows' => $rows]);
}
public function rx_group_generic_suggest()
{
$q = trim((string) $this->request->getGet('q'));
$table = $this->findExistingTable(['opd_med_master']);
if ($table === null) {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames($table);
$genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']);
$saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']);
if ($genericField === null && $saltField === null) {
return $this->response->setJSON(['rows' => []]);
}
$values = [];
$builder = $this->db->table($table);
if ($genericField !== null) {
$gBuilder = clone $builder;
$gBuilder->select($genericField . ' as value')->where($genericField . ' !=', '');
if ($q !== '') {
$gBuilder->like($genericField, $q);
}
foreach ($gBuilder->orderBy($genericField, 'ASC')->limit(40)->get()->getResultArray() as $row) {
$value = trim((string) ($row['value'] ?? ''));
if ($value !== '') {
$values[$value] = ['value' => $value];
}
}
}
if ($saltField !== null) {
$sBuilder = clone $builder;
$sBuilder->select($saltField . ' as value')->where($saltField . ' !=', '');
if ($q !== '') {
$sBuilder->like($saltField, $q);
}
foreach ($sBuilder->orderBy($saltField, 'ASC')->limit(40)->get()->getResultArray() as $row) {
$value = trim((string) ($row['value'] ?? ''));
if ($value !== '') {
$values[$value] = ['value' => $value];
}
}
}
return $this->response->setJSON(['rows' => array_values($values)]);
}
public function rx_group_dose_masters()
{
return $this->response->setJSON([
'dose' => $this->getDoseMasterRows('opd_dose_shed', ['dose_shed_id', 'id'], ['dose_show_sign', 'dose_sign', 'dose_sign_desc', 'name']),
'when' => $this->getDoseMasterRows('opd_dose_when', ['dose_when_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']),
'freq' => $this->getDoseMasterRows('opd_dose_frequency', ['dose_freq_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']),
'where' => $this->getDoseMasterRows('opd_dose_where', ['dose_where_id', 'id'], ['dose_sign', 'dose_sign_desc', 'name']),
]);
}
public function opd_medicince()
{
return view('billing/opd_medicince_workspace', ['initial_med_id' => 0]);
}
public function opd_medicince_edit(int $medId)
{
return view('billing/opd_medicince_workspace', ['initial_med_id' => max(0, $medId)]);
}
public function opd_medicince_data()
{
$filter = trim((string) $this->request->getGet('filter'));
$scope = strtolower(trim((string) $this->request->getGet('scope')));
$showAll = (int) $this->request->getGet('show_all') === 1;
if (! in_array($scope, ['all', 'favorite', 'active'], true)) {
$scope = 'all';
}
return $this->response->setJSON([
'rows' => $this->getOpdMedicineRows($filter, $scope, $showAll),
'active_filter' => $filter,
'active_scope' => $scope,
'show_all' => $showAll ? 1 : 0,
]);
}
public function opd_medicince_export()
{
$filter = trim((string) $this->request->getGet('filter'));
$rows = $this->getOpdMedicineRows($filter);
$output = fopen('php://temp', 'r+');
if ($output === false) {
return $this->response->setStatusCode(500)->setBody('Unable to generate export file.');
}
fputcsv($output, ['ID', 'Medicine Name', 'Formulation', 'Generic Name', 'Salt Name', 'Company Name']);
foreach ($rows as $row) {
fputcsv($output, [
(int) ($row['id'] ?? 0),
(string) ($row['item_name'] ?? ''),
(string) ($row['formulation'] ?? ''),
(string) ($row['genericname'] ?? ''),
(string) ($row['salt_name'] ?? ''),
(string) ($row['company_name'] ?? ''),
]);
}
rewind($output);
$csv = stream_get_contents($output);
fclose($output);
if ($csv === false) {
return $this->response->setStatusCode(500)->setBody('Unable to generate export file.');
}
$suffix = $filter !== '' ? preg_replace('/[^a-z0-9_\-]/i', '_', strtolower($filter)) : 'all';
$filename = 'opd_medicine_' . $suffix . '_' . date('Ymd_His') . '.csv';
return $this->response
->setHeader('Content-Type', 'text/csv; charset=UTF-8')
->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
->setBody($csv);
}
private function getOpdMedicineRows(string $filter = '', string $scope = 'all', bool $showAll = false): array
{
$table = $this->findExistingTable(['opd_med_master']);
if ($table === null) {
return [];
}
$fields = $this->db->getFieldNames($table);
$idField = $this->resolveFirstField($fields, ['id']);
$nameField = $this->resolveFirstField($fields, ['item_name', 'med_name']);
$genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']);
$saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']);
$companyField = $this->resolveFirstField($fields, ['company_name', 'company']);
$restrictionField = $this->resolveFirstField($fields, ['dosage_restriction', 'dose_restriction', 'restriction_note', 'restriction']);
if ($idField === null || $nameField === null) {
return [];
}
$select = $idField . ' as id,' . $nameField . ' as item_name';
if (in_array('formulation', $fields, true)) {
$select .= ',formulation';
}
if ($genericField !== null) {
$select .= ',' . $genericField . ' as genericname';
}
if ($saltField !== null) {
$select .= ',' . $saltField . ' as salt_name';
}
if ($companyField !== null) {
$select .= ',' . $companyField . ' as company_name';
}
if ($restrictionField !== null) {
$select .= ',' . $restrictionField . ' as dosage_restriction';
}
$rows = $this->db->table($table)
->select($select)
->orderBy($nameField, 'ASC')
->limit(5000)
->get()
->getResultArray();
$unclearTokens = ['na', 'n/a', 'same', '-', '--', 'unknown', '?'];
$normalize = static function ($value): string {
$text = trim((string) $value);
if ($text === '') {
return '';
}
$text = preg_replace('/\s+/', ' ', $text) ?? $text;
return mb_strtolower($text);
};
if ($filter === 'generic_issue') {
$rows = array_values(array_filter($rows, static function (array $row) use ($normalize, $unclearTokens): bool {
$name = $normalize($row['item_name'] ?? '');
$generic = $normalize($row['genericname'] ?? '');
$salt = $normalize($row['salt_name'] ?? '');
$genericUnclear = ($generic === '' || in_array($generic, $unclearTokens, true) || ($name !== '' && $generic === $name));
$saltUnclear = ($salt === '' || in_array($salt, $unclearTokens, true) || ($name !== '' && $salt === $name));
return $genericUnclear && $saltUnclear;
}));
} elseif ($filter === 'generic_same_name') {
$rows = array_values(array_filter($rows, static function (array $row) use ($normalize): bool {
$name = $normalize($row['item_name'] ?? '');
$generic = $normalize($row['genericname'] ?? '');
$salt = $normalize($row['salt_name'] ?? '');
if ($name === '') {
return false;
}
return ($generic !== '' && $generic === $name) || ($salt !== '' && $salt === $name);
}));
} elseif ($filter === 'company_blank') {
$rows = array_values(array_filter($rows, static function (array $row) use ($normalize, $unclearTokens): bool {
$company = $normalize($row['company_name'] ?? '');
return $company === '' || in_array($company, $unclearTokens, true);
}));
}
foreach ($rows as &$row) {
$generic = trim((string) ($row['genericname'] ?? ''));
if ($generic === '') {
$salt = trim((string) ($row['salt_name'] ?? ''));
if ($salt !== '') {
$row['genericname'] = $salt;
}
}
}
unset($row);
$userId = (int) ($this->getCurrentUserId() ?? 0);
$favoriteMap = $userId > 0 ? $this->getUserFavoriteMedicineIds($userId) : [];
$usageMap = $userId > 0 ? $this->getUserMedicineUsageMap($userId) : [];
foreach ($rows as &$row) {
$medId = (int) ($row['id'] ?? 0);
$row['is_favorite'] = isset($favoriteMap[$medId]) ? 1 : 0;
$row['use_count'] = (int) ($usageMap[$medId]['use_count'] ?? 0);
$row['last_used_at'] = (string) ($usageMap[$medId]['last_used_at'] ?? '');
}
unset($row);
if (! $showAll) {
if ($scope === 'favorite') {
$rows = array_values(array_filter($rows, static fn(array $row): bool => (int) ($row['is_favorite'] ?? 0) === 1));
} elseif ($scope === 'active') {
$rows = array_values(array_filter($rows, static fn(array $row): bool => ((int) ($row['is_favorite'] ?? 0) === 1) || ((int) ($row['use_count'] ?? 0) > 0)));
}
}
return $rows;
}
public function opd_medicince_company_suggest()
{
if (! $this->db->tableExists('med_company')) {
return $this->response->setJSON(['rows' => []]);
}
$term = trim((string) $this->request->getGet('term'));
$fields = $this->db->getFieldNames('med_company');
$nameField = $this->resolveFirstField($fields, ['company_name', 'name', 'short_name']);
if ($nameField === null) {
return $this->response->setJSON(['rows' => []]);
}
$builder = $this->db->table('med_company')->select($nameField . ' as company_name')->orderBy($nameField, 'ASC');
if ($term !== '') {
$builder->like($nameField, $term);
}
$rows = $builder->limit(200)->get()->getResultArray();
$names = [];
foreach ($rows as $row) {
$name = trim((string) ($row['company_name'] ?? ''));
if ($name === '') {
continue;
}
$names[$name] = ['company_name' => $name];
}
return $this->response->setJSON(['rows' => array_values($names)]);
}
public function opd_medicince_get(int $medId)
{
$table = $this->findExistingTable(['opd_med_master']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']);
}
$row = $this->db->table($table)->where('id', (int) $medId)->get(1)->getRowArray() ?? [];
if (empty($row)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine not found']);
}
return $this->response->setJSON(['update' => 1, 'row' => $row]);
}
public function opd_medicince_save()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$table = $this->findExistingTable(['opd_med_master']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']);
}
$id = (int) $this->request->getPost('id');
$itemName = trim((string) $this->request->getPost('item_name'));
if ($itemName === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']);
}
$medicineModel = new OpdMedicineModel($this->db);
$duplicate = $medicineModel->findDuplicateByName($itemName, $id);
if (! empty($duplicate)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Duplicate medicine name found (ID: ' . (int) ($duplicate['id'] ?? 0) . '). Please use existing entry or merge duplicates.',
'duplicate_id' => (int) ($duplicate['id'] ?? 0),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$fields = $this->db->getFieldNames($table);
$genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']);
$saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']);
$companyField = $this->resolveFirstField($fields, ['company_name', 'company']);
$restrictionField = $this->resolveFirstField($fields, ['dosage_restriction', 'dose_restriction', 'restriction_note', 'restriction']);
$data = [];
if (in_array('item_name', $fields, true)) {
$data['item_name'] = $itemName;
}
if (in_array('formulation', $fields, true)) {
$data['formulation'] = trim((string) $this->request->getPost('formulation'));
}
if ($genericField !== null) {
$data[$genericField] = trim((string) $this->request->getPost('genericname'));
}
if ($saltField !== null) {
$data[$saltField] = trim((string) $this->request->getPost('salt_name'));
}
if ($companyField !== null) {
$data[$companyField] = trim((string) $this->request->getPost('company_name'));
}
if ($restrictionField !== null) {
$data[$restrictionField] = trim((string) $this->request->getPost('dosage_restriction'));
}
if ($id > 0) {
$this->db->table($table)->where('id', $id)->update($data);
$savedId = $id;
$message = 'Medicine updated';
} else {
$this->db->table($table)->insert($data);
$savedId = (int) $this->db->insertID();
$message = 'Medicine added';
}
return $this->response->setJSON([
'update' => 1,
'insertid' => $savedId,
'error_text' => $message,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function opd_medicince_ai_details()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$name = trim((string) $this->request->getPost('item_name'));
$formulation = trim((string) $this->request->getPost('formulation'));
if ($name === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']);
}
$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->generateClinicalTextWithGemini($prompt, 280);
if ($aiText === null || trim($aiText) === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'AI unavailable. Check AI settings/API key.']);
}
$parsed = $this->parseJsonFromAiResponse($aiText);
if ($parsed === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'AI response parse failed. Try again.']);
}
return $this->response->setJSON([
'update' => 1,
'provider' => $this->lastAiProvider,
'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(),
]);
}
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];
$prompt = "You are a clinical translation assistant for OPD patient advice in India.\n"
. "Translate the following English patient advice into {$targetLanguageName}.\n"
. "Keep meaning medically accurate, simple, and patient-friendly.\n"
. "Return ONLY valid JSON object: {\"translated_text\":\"...\"}.\n"
. "Do not include markdown, extra keys, or explanation.\n"
. "English Advice: {$sourceText}";
$this->lastAiProvider = 'none';
$aiText = $this->generateClinicalTextWithGemini($prompt, 220);
if ($aiText === null || trim($aiText) === '') {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'AI unavailable. Check AI settings/API key.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$parsed = $this->parseJsonFromAiResponse($aiText);
$translatedText = trim((string) ($parsed['translated_text'] ?? ''));
if ($translatedText === '') {
$translatedText = trim(strip_tags((string) $aiText));
$translatedText = preg_replace('/```(?:json)?|```/i', '', $translatedText) ?? $translatedText;
$translatedText = trim($translatedText);
}
if ($translatedText === '') {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Translation failed. Try again.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
return $this->response->setJSON([
'update' => 1,
'provider' => $this->lastAiProvider,
'target_lang' => $targetLang,
'target_language_name' => $targetLanguageName,
'translated_text' => $translatedText,
'error_text' => 'Advice translated',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function opd_advice_remove(int $adviceId)
{
if (! $this->db->tableExists('opd_advice')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice table not found']);
}
$fields = $this->db->getFieldNames('opd_advice');
$idField = $this->resolveFirstField($fields, ['id', 'Code']);
if ($idField === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice ID field not found']);
}
$this->db->table('opd_advice')->where($idField, (int) $adviceId)->delete();
return $this->response->setJSON(['update' => 1, 'error_text' => 'Advice removed']);
}
public function opd_medicince_duplicate_report()
{
$medicineModel = new OpdMedicineModel($this->db);
if (! $medicineModel->tableExists()) {
return $this->response->setJSON(['rows' => []]);
}
return $this->response->setJSON([
'rows' => $medicineModel->getDuplicateGroups(),
'update' => 1,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function opd_medicince_merge_duplicates()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$keepId = (int) $this->request->getPost('keep_id');
$mergeRaw = trim((string) $this->request->getPost('merge_ids'));
$mergeIds = [];
if ($mergeRaw !== '') {
foreach (explode(',', $mergeRaw) as $token) {
$value = (int) trim($token);
if ($value > 0) {
$mergeIds[] = $value;
}
}
}
$medicineModel = new OpdMedicineModel($this->db);
$merged = $medicineModel->mergeDuplicates($keepId, $mergeIds);
if (! ($merged['ok'] ?? false)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => (string) ($merged['error'] ?? 'Unable to merge duplicates'),
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Merged ' . (int) ($merged['merged_count'] ?? 0) . ' duplicate medicine(s).',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function opd_medicince_autofix_duplicates()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$medicineModel = new OpdMedicineModel($this->db);
if (! $medicineModel->tableExists()) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Medicine table not found',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$groups = $medicineModel->getDuplicateGroups();
if (empty($groups)) {
return $this->response->setJSON([
'update' => 1,
'error_text' => 'No duplicate medicine names found.',
'merged_groups' => 0,
'merged_rows' => 0,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$mergedGroups = 0;
$mergedRows = 0;
foreach ($groups as $group) {
$keepId = (int) ($group['keep_id'] ?? 0);
$mergeIds = is_array($group['merge_ids'] ?? null) ? $group['merge_ids'] : [];
if ($keepId <= 0 || empty($mergeIds)) {
continue;
}
$result = $medicineModel->mergeDuplicates($keepId, $mergeIds);
if (! ($result['ok'] ?? false)) {
return $this->response->setJSON([
'update' => 0,
'error_text' => (string) ($result['error'] ?? 'Auto-fix stopped due to merge failure'),
'merged_groups' => $mergedGroups,
'merged_rows' => $mergedRows,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$mergedGroups++;
$mergedRows += (int) ($result['merged_count'] ?? 0);
}
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Auto-fix completed. Merged ' . $mergedRows . ' duplicate medicine row(s) in ' . $mergedGroups . ' group(s).',
'merged_groups' => $mergedGroups,
'merged_rows' => $mergedRows,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function scan_text_extract()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$filePath = trim((string) $this->request->getPost('file_path'));
$scanType = trim((string) $this->request->getPost('scan_type'));
if ($filePath === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Scan file path is required']);
}
$localPath = $this->resolveLocalFilePathFromStoredPath($filePath);
if ($localPath === null || !is_file($localPath)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Scan file not found']);
}
$extracted = $this->extractClinicalTextFromScan($localPath, $scanType);
if ($extracted === null || trim($extracted) === '') {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Unable to extract report text from this scan currently.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
return $this->response->setJSON([
'update' => 1,
'extracted_text' => trim($extracted),
'error_text' => 'Scan text extracted',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function patient_scan_history(int $opdId)
{
if (! $this->canAccessPrescription()) {
return $this->response->setStatusCode(403)->setBody('<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';
$geminiDraft = $this->generateComplaintDraftWithGemini($finalComplaints, $currentText);
if ($geminiDraft !== null && trim($geminiDraft) !== '') {
$provider = $this->lastAiProvider === 'azure' ? 'azure' : 'gemini';
$providerLabel = $provider === 'azure' ? 'Azure OpenAI' : 'Gemini';
return $this->response->setJSON([
'update' => 1,
'draft_text' => trim($geminiDraft),
'error_text' => 'AI draft prepared using ' . $providerLabel,
'ai_mode' => $provider,
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$line = '';
if (! empty($finalComplaints)) {
$line = 'Chief complaints: ' . implode(', ', $finalComplaints) . '.';
}
$draft = trim($line . ' ' . $currentText);
if ($draft === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Please add complaint terms first.']);
}
return $this->response->setJSON([
'update' => 1,
'draft_text' => $draft,
'error_text' => 'Complaint draft prepared (template mode)',
'ai_mode' => 'template',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function advice_list($opdId, $sessionId = 0)
{
$table = $this->findExistingTable(['opd_prescription_advice']);
if ($table === null) {
return $this->response->setJSON(['rows' => []]);
}
$builder = $this->db->table($table);
$fields = $this->db->getFieldNames($table);
if (in_array('opd_pre_id', $fields, true)) {
if ((int) $sessionId <= 0) {
return $this->response->setJSON(['rows' => []]);
}
$builder->where('opd_pre_id', (int) $sessionId);
} elseif (in_array('opd_id', $fields, true)) {
$builder->where('opd_id', (int) $opdId);
} else {
return $this->response->setJSON(['rows' => []]);
}
if (in_array('id', $fields, true)) {
$builder->orderBy('id', 'DESC');
}
$rows = $builder->get()->getResultArray();
return $this->response->setJSON(['rows' => $rows]);
}
public function advice_add()
{
if (!$this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$opdId = (int) $this->request->getPost('opd_id');
$sessionId = (int) $this->request->getPost('opd_session_id');
$adviceText = trim((string) $this->request->getPost('advice_text'));
$adviceId = (int) $this->request->getPost('advice_id');
if ($opdId <= 0 || $adviceText === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice text is required']);
}
$table = $this->findExistingTable(['opd_prescription_advice']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice table not found']);
}
$sessionId = $this->ensurePrescriptionSession($opdId, $sessionId);
if ($sessionId <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create prescription session']);
}
$fields = $this->db->getFieldNames($table);
$insert = [];
if (in_array('opd_id', $fields, true)) {
$insert['opd_id'] = $opdId;
}
if (in_array('opd_pre_id', $fields, true)) {
$insert['opd_pre_id'] = $sessionId;
}
if (in_array('advice_id', $fields, true)) {
$insert['advice_id'] = $adviceId;
}
if (in_array('advice_txt', $fields, true)) {
$insert['advice_txt'] = $adviceText;
} elseif (in_array('advice', $fields, true)) {
$insert['advice'] = $adviceText;
}
$this->db->table($table)->insert($insert);
$insertId = (int) $this->db->insertID();
$this->auditClinicalUpdate('opd_prescription_advice', 'added', $insertId, null, $insert);
return $this->response->setJSON([
'update' => 1,
'opd_session_id' => $sessionId,
'error_text' => 'Advice added',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function advice_remove($id)
{
$table = $this->findExistingTable(['opd_prescription_advice']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice table not found']);
}
$fields = $this->db->getFieldNames($table);
if (!in_array('id', $fields, true)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Advice id field missing']);
}
$row = $this->db->table($table)->where('id', (int) $id)->get(1)->getRowArray() ?? [];
$this->db->table($table)->where('id', (int) $id)->delete();
$this->auditClinicalUpdate('opd_prescription_advice', 'removed', (int) $id, $row, null);
return $this->response->setJSON(['update' => 1, 'error_text' => 'Advice removed']);
}
public function investigation_search()
{
$q = trim((string) $this->request->getGet('q'));
if ($q === '' || !$this->db->tableExists('investigation')) {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames('investigation');
$codeField = in_array('Code', $fields, true) ? 'Code' : (in_array('code', $fields, true) ? 'code' : null);
$nameField = in_array('Name', $fields, true) ? 'Name' : (in_array('name', $fields, true) ? 'name' : null);
$shortField = in_array('short_name', $fields, true) ? 'short_name' : null;
if ($codeField === null || $nameField === null) {
return $this->response->setJSON(['rows' => []]);
}
$builder = $this->db->table('investigation')->select($codeField . ' as code,' . $nameField . ' as name');
$builder->groupStart()->like($nameField, $q);
if ($shortField !== null) {
$builder->orLike($shortField, $q);
}
$builder->groupEnd();
$builder->orderBy('LOWER(' . $this->db->protectIdentifiers($nameField) . ')', 'ASC', false);
$rows = $builder->limit(20)->get()->getResultArray();
return $this->response->setJSON(['rows' => $rows]);
}
public function investigation_shortcuts()
{
$profiles = [];
$shortTests = [];
if ($this->db->tableExists('investigation')) {
$invFields = $this->db->getFieldNames('investigation');
$codeField = $this->resolveFirstField($invFields, ['Code', 'code']);
$nameField = $this->resolveFirstField($invFields, ['Name', 'name']);
$shortField = $this->resolveFirstField($invFields, ['short_name', 'shortName', 'short']);
$sortField = $this->resolveFirstField($invFields, ['sort_id', 'sort_order', 'id']);
if ($codeField !== null && $nameField !== null && $shortField !== null) {
$builder = $this->db->table('investigation')
->select($codeField . ' as code,' . $nameField . ' as name,' . $shortField . ' as short_name')
->where($shortField . ' !=', '')
->where($shortField . ' is not null', null, false)
->limit(200);
if ($sortField !== null) {
$builder->orderBy($shortField, 'ASC')->orderBy($sortField, 'ASC');
} else {
$builder->orderBy($shortField, 'ASC')->orderBy($nameField, 'ASC');
}
$shortTests = $builder->get()->getResultArray();
}
}
if ($this->db->tableExists('invprofiles') && $this->db->tableExists('invtprofiles') && $this->db->tableExists('investigation')) {
$pFields = $this->db->getFieldNames('invprofiles');
$jFields = $this->db->getFieldNames('invtprofiles');
$iFields = $this->db->getFieldNames('investigation');
$pCode = $this->resolveFirstField($pFields, ['Code', 'code']);
$pName = $this->resolveFirstField($pFields, ['Name', 'name']);
$jProfileCode = $this->resolveFirstField($jFields, ['ProfileCode', 'profile_code']);
$jInvestCode = $this->resolveFirstField($jFields, ['InvestigationCode', 'investigation_code']);
$jPrintOrder = $this->resolveFirstField($jFields, ['printOrder', 'print_order', 'id']);
$iCode = $this->resolveFirstField($iFields, ['Code', 'code']);
$iName = $this->resolveFirstField($iFields, ['Name', 'name']);
if ($pCode !== null && $pName !== null && $jProfileCode !== null && $jInvestCode !== null && $iCode !== null && $iName !== null) {
$orderExpr = 'p.`' . $pName . '` ASC';
if ($jPrintOrder !== null) {
$orderExpr .= ', j.`' . $jPrintOrder . '` ASC';
}
$sql = 'SELECT '
. 'p.`' . $pCode . '` AS profile_code, '
. 'p.`' . $pName . '` AS profile_name, '
. 'i.`' . $iCode . '` AS investigation_code, '
. 'i.`' . $iName . '` AS investigation_name '
. 'FROM `invprofiles` p '
. 'JOIN `invtprofiles` j ON p.`' . $pCode . '` = j.`' . $jProfileCode . '` '
. 'JOIN `investigation` i ON i.`' . $iCode . '` = j.`' . $jInvestCode . '` '
. 'ORDER BY ' . $orderExpr;
$rows = $this->db->query($sql)->getResultArray();
$profileMap = [];
foreach ($rows as $row) {
$profileCode = trim((string) ($row['profile_code'] ?? ''));
$profileName = trim((string) ($row['profile_name'] ?? ''));
$testCode = trim((string) ($row['investigation_code'] ?? ''));
$testName = trim((string) ($row['investigation_name'] ?? ''));
if ($profileCode === '' || $profileName === '' || $testName === '') {
continue;
}
if (!isset($profileMap[$profileCode])) {
$profileMap[$profileCode] = [
'profile_code' => $profileCode,
'profile_name' => $profileName,
'tests' => [],
'_seen' => [],
];
}
$key = strtolower($testCode . '|' . $testName);
if (isset($profileMap[$profileCode]['_seen'][$key])) {
continue;
}
$profileMap[$profileCode]['_seen'][$key] = true;
$profileMap[$profileCode]['tests'][] = [
'code' => $testCode,
'name' => $testName,
];
}
foreach ($profileMap as $item) {
unset($item['_seen']);
$profiles[] = $item;
}
}
}
return $this->response->setJSON([
'update' => 1,
'profiles' => $profiles,
'short_tests' => $shortTests,
]);
}
public function investigation_list($opdId, $sessionId = 0)
{
$table = $this->findExistingTable(['opd_prescription_investigation']);
if ($table === null) {
return $this->response->setJSON(['rows' => []]);
}
$builder = $this->db->table($table);
$fields = $this->db->getFieldNames($table);
if (in_array('opd_pre_id', $fields, true)) {
if ((int) $sessionId <= 0) {
return $this->response->setJSON(['rows' => []]);
}
$builder->where('opd_pre_id', (int) $sessionId);
} elseif (in_array('opd_id', $fields, true)) {
$builder->where('opd_id', (int) $opdId);
} else {
return $this->response->setJSON(['rows' => []]);
}
if (in_array('investigation_name', $fields, true)) {
$builder->orderBy('investigation_name', 'ASC');
} elseif (in_array('id', $fields, true)) {
$builder->orderBy('id', 'DESC');
}
$rows = $builder->get()->getResultArray();
return $this->response->setJSON(['rows' => $rows]);
}
public function investigation_add()
{
if (!$this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$opdId = (int) $this->request->getPost('opd_id');
$sessionId = (int) $this->request->getPost('opd_session_id');
$code = trim((string) $this->request->getPost('investigation_code'));
$name = trim((string) $this->request->getPost('investigation_name'));
if ($opdId <= 0 || ($code === '' && $name === '')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation is required']);
}
$table = $this->findExistingTable(['opd_prescription_investigation']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation table not found']);
}
$sessionId = $this->ensurePrescriptionSession($opdId, $sessionId);
if ($sessionId <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create prescription session']);
}
$fields = $this->db->getFieldNames($table);
$insert = [];
if (in_array('opd_id', $fields, true)) {
$insert['opd_id'] = $opdId;
}
if (in_array('opd_pre_id', $fields, true)) {
$insert['opd_pre_id'] = $sessionId;
}
if (in_array('investigation_code', $fields, true)) {
$insert['investigation_code'] = $code;
}
if (in_array('investigation_name', $fields, true)) {
$insert['investigation_name'] = $name;
}
$this->db->table($table)->insert($insert);
$insertId = (int) $this->db->insertID();
$this->auditClinicalUpdate('opd_prescription_investigation', 'added', $insertId, null, $insert);
return $this->response->setJSON([
'update' => 1,
'opd_session_id' => $sessionId,
'error_text' => 'Investigation added',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function investigation_remove($id)
{
$table = $this->findExistingTable(['opd_prescription_investigation']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation table not found']);
}
$fields = $this->db->getFieldNames($table);
if (!in_array('id', $fields, true)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Investigation id field missing']);
}
$row = $this->db->table($table)->where('id', (int) $id)->get(1)->getRowArray() ?? [];
$this->db->table($table)->where('id', (int) $id)->delete();
$this->auditClinicalUpdate('opd_prescription_investigation', 'removed', (int) $id, $row, null);
return $this->response->setJSON(['update' => 1, 'error_text' => 'Investigation removed']);
}
public function medicine_search()
{
$q = trim((string) $this->request->getGet('q'));
$scope = strtolower(trim((string) $this->request->getGet('scope')));
if (! in_array($scope, ['active', 'favorite', 'all'], true)) {
$scope = 'active';
}
$table = $this->findExistingTable(['opd_med_master']);
if ($table === null) {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames($table);
$idField = in_array('id', $fields, true) ? 'id' : null;
$nameField = in_array('item_name', $fields, true) ? 'item_name' : null;
$formField = in_array('formulation', $fields, true) ? 'formulation' : null;
if ($idField === null || $nameField === null) {
return $this->response->setJSON(['rows' => []]);
}
$select = $idField . ' as id,' . $nameField . ' as med_name';
if ($formField !== null) {
$select .= ',' . $formField . ' as med_type';
}
$builder = $this->db->table($table)->select($select);
if ($q !== '') {
$builder->groupStart()
->like($nameField, $q);
if ($formField !== null) {
$builder->orLike($formField, $q);
}
$builder->groupEnd();
}
$rows = $builder
->limit(500)
->get()
->getResultArray();
if (empty($rows)) {
return $this->response->setJSON(['rows' => []]);
}
$userId = (int) ($this->getCurrentUserId() ?? 0);
$favoriteMap = $userId > 0 ? $this->getUserFavoriteMedicineIds($userId) : [];
$usageMap = $userId > 0 ? $this->getUserMedicineUsageMap($userId) : [];
$qUpper = strtoupper($q);
foreach ($rows as &$row) {
$medId = (int) ($row['id'] ?? 0);
$name = trim((string) ($row['med_name'] ?? ''));
$nameUpper = strtoupper($name);
$isFavorite = isset($favoriteMap[$medId]);
$useCount = (int) ($usageMap[$medId]['use_count'] ?? 0);
$lastUsedAt = (string) ($usageMap[$medId]['last_used_at'] ?? '');
$score = 0;
if ($isFavorite) {
$score += 1000;
}
if ($useCount > 0) {
$score += min(300, $useCount * 10);
}
if ($lastUsedAt !== '') {
$daysAgo = (int) floor((time() - strtotime($lastUsedAt)) / 86400);
if ($daysAgo <= 7) {
$score += 120;
} elseif ($daysAgo <= 30) {
$score += 80;
} elseif ($daysAgo <= 90) {
$score += 40;
} elseif ($daysAgo <= 180) {
$score += 20;
}
}
if ($qUpper !== '') {
if (str_starts_with($nameUpper, $qUpper)) {
$score += 200;
} elseif (str_contains($nameUpper, $qUpper)) {
$score += 100;
}
}
$row['is_favorite'] = $isFavorite ? 1 : 0;
$row['use_count'] = $useCount;
$row['last_used_at'] = $lastUsedAt;
$row['_score'] = $score;
}
unset($row);
if ($scope === 'favorite') {
$rows = array_values(array_filter($rows, static fn(array $row): bool => (int) ($row['is_favorite'] ?? 0) === 1));
} elseif ($scope === 'active') {
$rows = array_values(array_filter($rows, static fn(array $row): bool => ((int) ($row['is_favorite'] ?? 0) === 1) || ((int) ($row['use_count'] ?? 0) > 0)));
}
usort($rows, static function (array $a, array $b): int {
$scoreA = (int) ($a['_score'] ?? 0);
$scoreB = (int) ($b['_score'] ?? 0);
if ($scoreA !== $scoreB) {
return $scoreB <=> $scoreA;
}
$nameA = strtoupper(trim((string) ($a['med_name'] ?? '')));
$nameB = strtoupper(trim((string) ($b['med_name'] ?? '')));
return $nameA <=> $nameB;
});
$rows = array_slice($rows, 0, 40);
foreach ($rows as &$row) {
unset($row['_score']);
}
unset($row);
return $this->response->setJSON(['rows' => $rows]);
}
public function medicine_substitutes()
{
$table = $this->findExistingTable(['opd_med_master']);
if ($table === null) {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames($table);
$idField = $this->resolveFirstField($fields, ['id']);
$nameField = $this->resolveFirstField($fields, ['item_name', 'med_name']);
$formField = $this->resolveFirstField($fields, ['formulation']);
$genericField = $this->resolveFirstField($fields, ['genericname', 'generic_name']);
$saltField = $this->resolveFirstField($fields, ['salt_name', 'sal_name', 'salt', 'saltname']);
$companyField = $this->resolveFirstField($fields, ['company_name', 'company']);
if ($idField === null || $nameField === null || ($genericField === null && $saltField === null)) {
return $this->response->setJSON(['rows' => []]);
}
$medId = (int) $this->request->getGet('med_id');
$medName = trim((string) $this->request->getGet('med_name'));
$current = [];
if ($medId > 0) {
$current = $this->db->table($table)->where($idField, $medId)->get(1)->getRowArray() ?? [];
}
if (empty($current) && $medName !== '') {
$current = $this->db->table($table)->where('UPPER(' . $nameField . ')', strtoupper($medName))->get(1)->getRowArray() ?? [];
if (! empty($current)) {
$medId = (int) ($current[$idField] ?? 0);
}
}
if (empty($current)) {
return $this->response->setJSON(['rows' => []]);
}
$generic = $genericField !== null ? trim((string) ($current[$genericField] ?? '')) : '';
$salt = $saltField !== null ? trim((string) ($current[$saltField] ?? '')) : '';
if ($generic === '' && $salt === '') {
return $this->response->setJSON(['rows' => []]);
}
$select = $idField . ' as id,' . $nameField . ' as med_name';
if ($formField !== null) {
$select .= ',' . $formField . ' as med_type';
}
if ($genericField !== null) {
$select .= ',' . $genericField . ' as genericname';
}
if ($saltField !== null) {
$select .= ',' . $saltField . ' as salt_name';
}
if ($companyField !== null) {
$select .= ',' . $companyField . ' as company_name';
}
$builder = $this->db->table($table)->select($select);
$builder->groupStart();
if ($genericField !== null && $generic !== '') {
$builder->where($genericField, $generic);
}
if ($saltField !== null && $salt !== '') {
if ($genericField !== null && $generic !== '') {
$builder->orWhere($saltField, $salt);
} else {
$builder->where($saltField, $salt);
}
}
$builder->groupEnd();
if ($medId > 0) {
$builder->where($idField . ' !=', $medId);
}
$rows = $builder->orderBy($nameField, 'ASC')->limit(25)->get()->getResultArray();
return $this->response->setJSON(['rows' => $rows]);
}
public function medicine_favorite_toggle()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$userId = (int) ($this->getCurrentUserId() ?? 0);
$medId = (int) $this->request->getPost('med_id');
if ($userId <= 0 || $medId <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid user or medicine']);
}
if (! $this->db->tableExists('opd_medicine_favorites')) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine favorites table not found. Run migration first.']);
}
$exists = $this->db->table('opd_medicine_favorites')
->where('user_id', $userId)
->where('med_id', $medId)
->get(1)
->getRowArray();
if ($exists) {
$this->db->table('opd_medicine_favorites')
->where('user_id', $userId)
->where('med_id', $medId)
->delete();
return $this->response->setJSON([
'update' => 1,
'is_favorite' => 0,
'error_text' => 'Removed from favorites',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$this->db->table('opd_medicine_favorites')->insert([
'user_id' => $userId,
'med_id' => $medId,
'created_at' => date('Y-m-d H:i:s'),
]);
return $this->response->setJSON([
'update' => 1,
'is_favorite' => 1,
'error_text' => 'Added to favorites',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function medicine_list($sessionId)
{
$table = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']);
if ($table === null) {
return $this->response->setJSON(['rows' => []]);
}
$fields = $this->db->getFieldNames($table);
$builder = $this->db->table($table);
if (in_array('opd_pre_id', $fields, true)) {
if ((int) $sessionId <= 0) {
return $this->response->setJSON(['rows' => []]);
}
$builder->where('opd_pre_id', (int) $sessionId);
} else {
return $this->response->setJSON(['rows' => []]);
}
if (in_array('id', $fields, true)) {
$builder->orderBy('id', 'DESC');
}
$rows = $builder->get()->getResultArray();
return $this->response->setJSON(['rows' => $rows]);
}
public function medicine_add()
{
if (!$this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
}
$opdId = (int) $this->request->getPost('opd_id');
$sessionId = (int) $this->request->getPost('opd_session_id');
$medName = trim((string) $this->request->getPost('med_name'));
$medType = trim((string) $this->request->getPost('med_type'));
$medId = (int) $this->request->getPost('med_id');
if ($opdId <= 0 || $medName === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']);
}
$table = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']);
}
$sessionId = $this->ensurePrescriptionSession($opdId, $sessionId);
if ($sessionId <= 0) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create prescription session']);
}
if ($medId <= 0) {
$medId = $this->resolveMedicineMasterIdByName($medName);
}
$fields = $this->db->getFieldNames($table);
$insert = [];
if (in_array('opd_pre_id', $fields, true)) {
$insert['opd_pre_id'] = $sessionId;
}
if (in_array('med_id', $fields, true) && $medId > 0) {
$insert['med_id'] = $medId;
}
if (in_array('med_name', $fields, true)) {
$insert['med_name'] = strtoupper($medName);
}
if (in_array('med_type', $fields, true)) {
$insert['med_type'] = $medType;
}
if (in_array('dosage', $fields, true)) {
$insert['dosage'] = trim((string) $this->request->getPost('dosage'));
}
if (in_array('dosage_when', $fields, true)) {
$insert['dosage_when'] = trim((string) $this->request->getPost('dosage_when'));
}
if (in_array('dosage_freq', $fields, true)) {
$insert['dosage_freq'] = trim((string) $this->request->getPost('dosage_freq'));
}
if (in_array('no_of_days', $fields, true)) {
$insert['no_of_days'] = trim((string) $this->request->getPost('no_of_days'));
}
if (in_array('qty', $fields, true)) {
$insert['qty'] = trim((string) $this->request->getPost('qty'));
}
if (in_array('remark', $fields, true)) {
$insert['remark'] = strtoupper(trim((string) $this->request->getPost('remark')));
}
$user = auth()->user();
if (in_array('update_by', $fields, true)) {
$userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User';
$insert['update_by'] = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s');
}
$this->db->table($table)->insert($insert);
$insertId = (int) $this->db->insertID();
$this->auditClinicalUpdate('opd_prescription_medicine', 'added', $insertId, null, $insert);
if ($medId > 0) {
$this->trackMedicineUsage($medId);
}
return $this->response->setJSON([
'update' => 1,
'opd_session_id' => $sessionId,
'error_text' => 'Medicine added',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
private function resolveMedicineMasterIdByName(string $medName): int
{
$medName = trim($medName);
if ($medName === '') {
return 0;
}
$table = $this->findExistingTable(['opd_med_master']);
if ($table === null) {
return 0;
}
$fields = $this->db->getFieldNames($table);
if (! in_array('id', $fields, true)) {
return 0;
}
$nameField = in_array('item_name', $fields, true) ? 'item_name' : (in_array('med_name', $fields, true) ? 'med_name' : null);
if ($nameField === null) {
return 0;
}
$row = $this->db->table($table)
->select('id')
->where('UPPER(' . $nameField . ')', strtoupper($medName))
->get(1)
->getRowArray();
return (int) ($row['id'] ?? 0);
}
private function trackMedicineUsage(int $medId): void
{
$userId = (int) ($this->getCurrentUserId() ?? 0);
if ($userId <= 0 || $medId <= 0 || ! $this->db->tableExists('opd_medicine_usage')) {
return;
}
$existing = $this->db->table('opd_medicine_usage')
->where('user_id', $userId)
->where('med_id', $medId)
->get(1)
->getRowArray();
if ($existing) {
$this->db->table('opd_medicine_usage')
->where('user_id', $userId)
->where('med_id', $medId)
->update([
'use_count' => (int) ($existing['use_count'] ?? 0) + 1,
'last_used_at' => date('Y-m-d H:i:s'),
]);
return;
}
$this->db->table('opd_medicine_usage')->insert([
'user_id' => $userId,
'med_id' => $medId,
'use_count' => 1,
'last_used_at' => date('Y-m-d H:i:s'),
]);
}
/**
* @return array<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')),
'no_of_days' => trim((string) $this->request->getPost('no_of_days')),
'qty' => trim((string) $this->request->getPost('qty')),
'remark' => strtoupper(trim((string) $this->request->getPost('remark'))),
];
foreach ($map as $field => $value) {
if (in_array($field, $fields, true)) {
$update[$field] = $value;
}
}
if (empty($update)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'No editable fields found']);
}
if (isset($update['med_name']) && $update['med_name'] === '') {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine name is required']);
}
$user = auth()->user();
if (in_array('update_by', $fields, true)) {
$userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User';
$update['update_by'] = $userLabel . '[' . ($user->id ?? 0) . ']:' . date('d-m-Y H:i:s');
}
$this->db->table($table)->where('id', $id)->update($update);
$this->clinicalAuditTrail->logChangedFields('opd_prescription_medicine', $id, $current, array_replace($current, $update), $this->getCurrentUserId());
return $this->response->setJSON([
'update' => 1,
'error_text' => 'Medicine updated',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function medicine_remove($id)
{
$table = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']);
if ($table === null) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine table not found']);
}
$fields = $this->db->getFieldNames($table);
if (!in_array('id', $fields, true)) {
return $this->response->setJSON(['update' => 0, 'error_text' => 'Medicine id field missing']);
}
$row = $this->db->table($table)->where('id', (int) $id)->get(1)->getRowArray() ?? [];
$this->db->table($table)->where('id', (int) $id)->delete();
$this->auditClinicalUpdate('opd_prescription_medicine', 'removed', (int) $id, $row, null);
return $this->response->setJSON(['update' => 1, 'error_text' => 'Medicine removed']);
}
public function vital_data(int $opdSessionId)
{
if (! $this->db->tableExists('opd_prescription')) {
return $this->response->setBody('');
}
$row = $this->db->table('opd_prescription')->where('id', $opdSessionId)->get(1)->getRowArray();
if (empty($row)) {
return $this->response->setBody('');
}
$lines = [];
if (!empty($row['bp'])) {
$lines[] = '<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);
$labelField = $this->resolveFirstField($fields, $labelCandidates);
if ($idField === null || $labelField === null) {
return [];
}
return $this->db->table($table)
->select($idField . ' as id,' . $labelField . ' as label')
->where($labelField . ' !=', '')
->orderBy($labelField, 'ASC')
->get()
->getResultArray();
}
private function getDoseMasterLabelMap(string $table, array $idCandidates, array $labelCandidates): array
{
$rows = $this->getDoseMasterRows($table, $idCandidates, $labelCandidates);
if (empty($rows)) {
return [];
}
$map = [];
foreach ($rows as $row) {
$id = trim((string) ($row['id'] ?? ''));
$label = trim((string) ($row['label'] ?? ''));
if ($id === '' || $label === '') {
continue;
}
$map[$id] = $label;
}
return $map;
}
private function resolveDoseMasterLabel($value, array $map): string
{
$raw = trim((string) $value);
if ($raw === '') {
return '';
}
return $map[$raw] ?? $raw;
}
private function hydrateNabhPrescriptionFields(array $row, string $remarks): array
{
$pick = function (array $candidates) use ($row): string {
foreach ($candidates as $field) {
$value = trim((string) ($row[$field] ?? ''));
if ($value !== '') {
return $value;
}
}
return '';
};
$data = [
'drug_allergy_status' => $pick(['drug_allergy_status', 'allergy_status', 'drug_allergy']),
'drug_allergy_details' => $pick(['drug_allergy_details', 'allergy_details', 'drug_allergy_note', 'allergy_note']),
'adr_history' => $pick(['adr_history', 'adverse_drug_reaction', 'adr_details', 'adverse_reaction_history']),
'current_medications' => $pick(['current_medications', 'current_medication', 'current_medication_history', 'ongoing_medications']),
];
$parsed = $this->extractNabhFieldsFromRemarks($remarks);
foreach ($data as $key => $value) {
if ($value === '' && !empty($parsed[$key])) {
$data[$key] = (string) $parsed[$key];
}
}
return $data;
}
private function mapNabhFieldsToExistingColumns(array &$payload, array $fields, array $input): array
{
$mapping = [
'drug_allergy_status' => ['drug_allergy_status', 'allergy_status', 'drug_allergy'],
'drug_allergy_details' => ['drug_allergy_details', 'allergy_details', 'drug_allergy_note', 'allergy_note'],
'adr_history' => ['adr_history', 'adverse_drug_reaction', 'adr_details', 'adverse_reaction_history'],
'current_medications' => ['current_medications', 'current_medication', 'current_medication_history', 'ongoing_medications'],
];
$unmapped = [];
foreach ($mapping as $logical => $candidates) {
$value = trim((string) ($input[$logical] ?? ''));
$target = null;
foreach ($candidates as $candidate) {
if (in_array($candidate, $fields, true)) {
$target = $candidate;
break;
}
}
if ($target !== null) {
$payload[$target] = $value;
} else {
$unmapped[$logical] = $value;
}
}
return $unmapped;
}
private function extractNabhFieldsFromRemarks(string $remarks): array
{
$extract = static function (string $pattern, string $source): string {
if (preg_match($pattern, $source, $m) !== 1) {
return '';
}
return trim((string) ($m[1] ?? ''));
};
return [
'drug_allergy_status' => $extract('/^\s*Drug\s*Allergy\s*Status\s*:\s*(.+)$/im', $remarks),
'drug_allergy_details' => $extract('/^\s*Drug\s*Allergy\s*Details\s*:\s*(.+)$/im', $remarks),
'adr_history' => $extract('/^\s*ADR\s*History\s*:\s*(.+)$/im', $remarks),
'current_medications' => $extract('/^\s*Current\s*Medications\s*:\s*(.+)$/im', $remarks),
];
}
private function upsertNabhFieldsIntoRemarks(string $remarks, array $input): string
{
$clean = trim($remarks);
$clean = preg_replace('/\n?\s*Drug\s*Allergy\s*Status\s*:\s*.+$/im', '', $clean) ?? $clean;
$clean = preg_replace('/\n?\s*Drug\s*Allergy\s*Details\s*:\s*.+$/im', '', $clean) ?? $clean;
$clean = preg_replace('/\n?\s*ADR\s*History\s*:\s*.+$/im', '', $clean) ?? $clean;
$clean = preg_replace('/\n?\s*Current\s*Medications\s*:\s*.+$/im', '', $clean) ?? $clean;
$clean = trim($clean);
$lines = [];
if (trim((string) ($input['drug_allergy_status'] ?? '')) !== '') {
$lines[] = 'Drug Allergy Status: ' . trim((string) ($input['drug_allergy_status'] ?? ''));
}
if (trim((string) ($input['drug_allergy_details'] ?? '')) !== '') {
$lines[] = 'Drug Allergy Details: ' . trim((string) ($input['drug_allergy_details'] ?? ''));
}
if (trim((string) ($input['adr_history'] ?? '')) !== '') {
$lines[] = 'ADR History: ' . trim((string) ($input['adr_history'] ?? ''));
}
if (trim((string) ($input['current_medications'] ?? '')) !== '') {
$lines[] = 'Current Medications: ' . trim((string) ($input['current_medications'] ?? ''));
}
if (empty($lines)) {
return $clean;
}
if ($clean !== '') {
return $clean . "\n" . implode("\n", $lines);
}
return implode("\n", $lines);
}
private function ensureClinicalTemplateTable(): bool
{
if ($this->db->tableExists('opd_clinical_templates')) {
return true;
}
try {
$sql = "CREATE TABLE IF NOT EXISTS opd_clinical_templates (
id INT AUTO_INCREMENT PRIMARY KEY,
doc_id INT NOT NULL DEFAULT 0,
section_key VARCHAR(80) NOT NULL,
template_name VARCHAR(255) NOT NULL,
template_text TEXT NOT NULL,
is_active TINYINT NOT NULL DEFAULT 1,
created_at DATETIME NULL,
updated_at DATETIME NULL,
INDEX idx_doc_section (doc_id, section_key),
INDEX idx_section_active (section_key, is_active)
)";
$this->db->query($sql);
} catch (\Throwable $e) {
return false;
}
return $this->db->tableExists('opd_clinical_templates');
}
private function ensureClinicalTemplateUsageTable(): bool
{
if ($this->db->tableExists('opd_clinical_template_usage')) {
return true;
}
try {
$sql = "CREATE TABLE IF NOT EXISTS opd_clinical_template_usage (
id INT AUTO_INCREMENT PRIMARY KEY,
doc_id INT NOT NULL,
template_id INT NOT NULL,
section_key VARCHAR(80) NOT NULL,
use_count INT NOT NULL DEFAULT 0,
last_used_at DATETIME NULL,
created_at DATETIME NULL,
updated_at DATETIME NULL,
UNIQUE KEY uniq_doc_template_section (doc_id, template_id, section_key),
INDEX idx_template (template_id),
INDEX idx_section_use (section_key, use_count)
)";
$this->db->query($sql);
} catch (\Throwable $e) {
return false;
}
return $this->db->tableExists('opd_clinical_template_usage');
}
private function isTemplateSectionAllowed(string $section): bool
{
return in_array($section, ['finding_examinations', 'diagnosis', 'provisional_diagnosis', 'prescriber_remarks', 'advice'], true);
}
private function resolveSectionColumn(string $section): ?string
{
$map = [
'finding_examinations' => 'Finding_Examinations',
'diagnosis' => 'diagnosis',
'provisional_diagnosis' => 'Provisional_diagnosis',
'prescriber_remarks' => 'Prescriber_Remarks',
'advice' => 'advice',
];
return $map[$section] ?? null;
}
private function ensurePrescriptionSession(int $opdId, int $sessionId = 0): int
{
if (!$this->db->tableExists('opd_prescription')) {
return 0;
}
$fields = $this->db->getFieldNames('opd_prescription');
if ($sessionId > 0) {
$row = $this->db->table('opd_prescription')->where('id', $sessionId)->where('opd_id', $opdId)->get(1)->getRow();
if ($row) {
return $sessionId;
}
}
$last = $this->db->table('opd_prescription')->where('opd_id', $opdId)->orderBy('id', 'DESC')->get(1)->getRow();
if ($last && !empty($last->id)) {
return (int) $last->id;
}
$opdRow = $this->db->table('opd_master')->where('opd_id', $opdId)->get(1)->getRow();
if (!$opdRow) {
return 0;
}
$insert = [];
if (in_array('opd_id', $fields, true)) {
$insert['opd_id'] = $opdId;
}
if (in_array('p_id', $fields, true)) {
$insert['p_id'] = (int) ($opdRow->p_id ?? 0);
}
if (in_array('doc_id', $fields, true)) {
$insert['doc_id'] = (int) ($opdRow->doc_id ?? 0);
}
if (in_array('date_opd_visit', $fields, true)) {
$insert['date_opd_visit'] = date('Y-m-d');
}
if (in_array('visit_status', $fields, true)) {
$insert['visit_status'] = 0;
}
if (in_array('session_id', $fields, true)) {
$insert['session_id'] = 0;
}
if (in_array('queue_no', $fields, true)) {
$maxQueue = $this->db->table('opd_prescription')
->selectMax('queue_no', 'max_queue')
->where('date_opd_visit', date('Y-m-d'))
->where('doc_id', (int) ($opdRow->doc_id ?? 0))
->get()
->getRow();
$insert['queue_no'] = (int) ($maxQueue->max_queue ?? 0) + 1;
}
$this->db->table('opd_prescription')->insert($insert);
return (int) $this->db->insertID();
}
private function isValidAbhaAddress(string $value): bool
{
$validation = service('validation');
$validation->reset();
return $validation->setRules([
'abha_address' => 'valid_abha_address',
])->run([
'abha_address' => $value,
]);
}
private function resolvePatientAbhaField(): ?string
{
if (! $this->db->tableExists('patient_master')) {
return null;
}
$fields = $this->db->getFieldNames('patient_master');
foreach (['abha_address', 'abha', 'abha_id', 'abha_no'] as $field) {
if (in_array($field, $fields, true)) {
return $field;
}
}
return null;
}
/**
* @return array<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);
}
private function getGeminiApiKey(): string
{
foreach (['GEMINI_API_KEY', 'AI_GEMINI_API_KEY', 'APP_GEMINI_API_KEY', 'H_GEMINI_API_KEY'] as $constName) {
if (defined($constName)) {
$value = trim((string) constant($constName));
if ($value !== '') {
return $value;
}
}
}
if ($this->db->tableExists('hospital_setting')) {
$row = $this->db->table('hospital_setting')
->select('s_value')
->whereIn('s_name', ['GEMINI_API_KEY', 'AI_GEMINI_API_KEY', 'APP_GEMINI_API_KEY', 'H_GEMINI_API_KEY'])
->where('s_value !=', '')
->orderBy('s_name', 'ASC')
->get(1)
->getRowArray();
if (! empty($row['s_value'])) {
return trim((string) $row['s_value']);
}
}
return '';
}
/**
* @return array{endpoint:string,api_key:string,deployment:string,api_version:string}
*/
private function getAzureOpenAiConfig(): array
{
$endpoint = $this->readAiSettingValue(
['AZURE_OPENAI_ENDPOINT', 'AI_AZURE_OPENAI_ENDPOINT', 'APP_AZURE_OPENAI_ENDPOINT', 'H_AZURE_OPENAI_ENDPOINT'],
['AZURE_OPENAI_ENDPOINT']
);
$apiKey = $this->readAiSettingValue(
['AZURE_OPENAI_API_KEY', 'AI_AZURE_OPENAI_API_KEY', 'APP_AZURE_OPENAI_API_KEY', 'H_AZURE_OPENAI_API_KEY'],
['AZURE_OPENAI_API_KEY']
);
$deployment = $this->readAiSettingValue(
['AZURE_OPENAI_DEPLOYMENT', 'AI_AZURE_OPENAI_DEPLOYMENT', 'APP_AZURE_OPENAI_DEPLOYMENT', 'H_AZURE_OPENAI_DEPLOYMENT'],
['AZURE_OPENAI_DEPLOYMENT']
);
$apiVersion = $this->readAiSettingValue(
['AZURE_OPENAI_API_VERSION', 'AI_AZURE_OPENAI_API_VERSION', 'APP_AZURE_OPENAI_API_VERSION', 'H_AZURE_OPENAI_API_VERSION'],
['AZURE_OPENAI_API_VERSION']
);
if ($apiVersion === '') {
$apiVersion = '2024-10-21';
}
return [
'endpoint' => rtrim($endpoint, '/'),
'api_key' => $apiKey,
'deployment' => $deployment,
'api_version' => $apiVersion,
];
}
/**
* @param array<int, string> $constCandidates
* @param array<int, string> $settingCandidates
*/
private function readAiSettingValue(array $constCandidates, array $settingCandidates): string
{
foreach ($constCandidates as $constName) {
if (! defined($constName)) {
continue;
}
$value = trim((string) constant($constName));
if ($value !== '') {
return $value;
}
}
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 generateComplaintDraftWithGemini(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->generateClinicalTextWithGemini($prompt, 180);
}
private function generateClinicalTextWithAzureOpenAi(string $prompt, int $maxOutputTokens = 300): ?string
{
$cfg = $this->getAzureOpenAiConfig();
if ($cfg['endpoint'] === '' || $cfg['api_key'] === '' || $cfg['deployment'] === '' || trim($prompt) === '') {
return null;
}
try {
$httpOptions = [
'timeout' => 20,
'http_errors' => false,
];
if (defined('ENVIRONMENT') && in_array((string) ENVIRONMENT, ['development', 'testing'], true)) {
$httpOptions['verify'] = false;
}
$client = service('curlrequest', $httpOptions);
$url = $cfg['endpoint'] . '/openai/deployments/' . rawurlencode($cfg['deployment'])
. '/chat/completions?api-version=' . rawurlencode($cfg['api_version']);
$response = $client->post($url, [
'headers' => [
'api-key' => $cfg['api_key'],
'Content-Type' => 'application/json',
],
'json' => [
'messages' => [
['role' => 'system', 'content' => 'You are a clinical OPD assistant. Return plain text only, no markdown.'],
['role' => 'user', 'content' => $prompt],
],
'temperature' => 0.2,
'top_p' => 0.9,
'max_tokens' => max(64, $maxOutputTokens),
],
]);
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
return null;
}
$decoded = json_decode((string) $response->getBody(), true);
$text = trim((string) ($decoded['choices'][0]['message']['content'] ?? ''));
$text = trim($text);
if ($text === '') {
return null;
}
$this->lastAiProvider = 'azure';
return preg_replace('/\s+/', ' ', $text) ?: $text;
} catch (\Throwable $e) {
return null;
}
}
private function buildClinicalPrompt(string $mode, string $text): string
{
if ($mode === 'hinglish_to_english') {
return "You are a clinical OPD assistant. Convert Hinglish medical text to clear professional English.
Keep all clinical meaning unchanged. Do not add new diagnosis.
Return plain text only, no markdown.
Input: " . $text;
}
if ($mode === 'diagnosis') {
return "You are a clinical OPD assistant. Rewrite diagnosis text into concise professional medical English.
Preserve meaning, do not invent findings. Return plain text only.
Input: " . $text;
}
if ($mode === 'finding') {
return "You are a clinical OPD assistant. Rewrite findings/examination notes into concise clinical English.
Preserve original meaning and sequence. Return plain text only.
Input: " . $text;
}
return "You are a clinical OPD assistant. Rewrite this OPD note into concise, professional English suitable for prescription documentation.
Preserve meaning, do not add new facts. Return plain text only.
Input: " . $text;
}
private function buildClinicalAutotypeLocalFallback(string $mode, string $text): string
{
$source = trim((string) $text);
if ($source === '') {
return '';
}
if ($mode === 'hinglish_to_english') {
$dictionary = [
'bukhar' => 'fever',
'khansi' => 'cough',
'pet dard' => 'abdominal pain',
'sir dard' => 'headache',
'ulti' => 'vomiting',
'kamzori' => 'weakness',
'saans' => 'breath',
'saans phoolna' => 'breathlessness',
'jukaam' => 'cold',
'chakkar' => 'giddiness',
'dast' => 'loose motions',
'jalan' => 'burning sensation',
];
$converted = mb_strtolower($source);
foreach ($dictionary as $from => $to) {
$converted = preg_replace('/\b' . preg_quote($from, '/') . '\b/u', $to, $converted) ?? $converted;
}
return $this->normalizeClinicalSentenceFlow($converted);
}
return $this->normalizeClinicalSentenceFlow($source);
}
/**
* @return array<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 generateClinicalTextWithGemini(string $prompt, int $maxOutputTokens = 300): ?string
{
$azureText = $this->generateClinicalTextWithAzureOpenAi($prompt, $maxOutputTokens);
if ($azureText !== null && trim($azureText) !== '') {
return $azureText;
}
$apiKey = $this->getGeminiApiKey();
if ($apiKey === '' || trim($prompt) === '') {
return null;
}
try {
$httpOptions = [
'timeout' => 20,
'http_errors' => false,
];
if (defined('ENVIRONMENT') && in_array((string) ENVIRONMENT, ['development', 'testing'], true)) {
$httpOptions['verify'] = false;
}
$client = service('curlrequest', $httpOptions);
$payload = [
'contents' => [[
'parts' => [[
'text' => $prompt,
]],
]],
'generationConfig' => [
'temperature' => 0.2,
'topP' => 0.9,
'maxOutputTokens' => max(64, $maxOutputTokens),
],
];
foreach (['gemini-2.0-flash', 'gemini-1.5-flash', 'gemini-1.5-flash-latest'] as $modelName) {
$url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $modelName . ':generateContent?key=' . urlencode($apiKey);
$response = $client->post($url, [
'headers' => ['Content-Type' => 'application/json'],
'json' => $payload,
]);
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
continue;
}
$decoded = json_decode((string) $response->getBody(), true);
$parts = $decoded['candidates'][0]['content']['parts'] ?? [];
$text = '';
foreach ($parts as $part) {
$text .= (string) ($part['text'] ?? '');
}
$text = trim($text);
if ($text !== '') {
$this->lastAiProvider = 'gemini';
return preg_replace('/\s+/', ' ', $text) ?: $text;
}
}
} catch (\Throwable $e) {
return null;
}
return null;
}
/**
* @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
{
$apiKey = $this->getGeminiApiKey();
if ($apiKey === '') {
return null;
}
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
$mimeMap = [
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp',
'pdf' => 'application/pdf',
];
$mime = $mimeMap[$ext] ?? '';
if ($mime === '') {
return null;
}
$raw = @file_get_contents($filePath);
if ($raw === false || $raw === '') {
return null;
}
if (strlen($raw) > 8 * 1024 * 1024) {
return null;
}
$typeHint = $scanType !== '' ? strtoupper($scanType) : 'GENERAL';
$prompt = "You are a medical report transcription assistant.\n"
. "Extract readable text from this " . $typeHint . " scan.\n"
. "Return concise structured plain text with sections: Findings, Impression, Values(if present).\n"
. "Do not invent data. If unreadable, mention unreadable fields.";
try {
$client = service('curlrequest', [
'timeout' => 30,
'http_errors' => false,
]);
$payload = [
'contents' => [[
'parts' => [
['text' => $prompt],
[
'inlineData' => [
'mimeType' => $mime,
'data' => base64_encode($raw),
],
],
],
]],
'generationConfig' => [
'temperature' => 0.1,
'topP' => 0.8,
'maxOutputTokens' => 600,
],
];
foreach (['gemini-2.0-flash', 'gemini-1.5-flash'] as $modelName) {
$url = 'https://generativelanguage.googleapis.com/v1beta/models/' . $modelName . ':generateContent?key=' . urlencode($apiKey);
$response = $client->post($url, [
'headers' => ['Content-Type' => 'application/json'],
'json' => $payload,
]);
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
continue;
}
$decoded = json_decode((string) $response->getBody(), true);
$parts = $decoded['candidates'][0]['content']['parts'] ?? [];
$text = '';
foreach ($parts as $part) {
$text .= (string) ($part['text'] ?? '');
}
$text = trim($text);
if ($text !== '') {
return preg_replace('/\s+/', ' ', $text) ?: $text;
}
}
} catch (\Throwable $e) {
return null;
}
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' => ''];
}
}