6274 lines
244 KiB
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' => ''];
|
|
}
|
|
}
|