3945 lines
157 KiB
PHP
3945 lines
157 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Controllers\BaseController;
|
|
use App\Models\OpdModel;
|
|
use App\Models\PaymentModel;
|
|
use CodeIgniter\I18n\Time;
|
|
use Mpdf\Mpdf;
|
|
|
|
class Opd extends BaseController
|
|
{
|
|
/**
|
|
* @var array<int, string>
|
|
*/
|
|
private array $scanAiDebug = [];
|
|
|
|
private function canAccessDoctorWorkPanel(): bool
|
|
{
|
|
$user = auth()->user();
|
|
if (! $user) {
|
|
return false;
|
|
}
|
|
|
|
if (method_exists($user, 'can') && $user->can('opd.doctor-panel.access')) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function canManageOpdPrintTemplates(): bool
|
|
{
|
|
$user = auth()->user();
|
|
if (! $user) {
|
|
return false;
|
|
}
|
|
|
|
if (method_exists($user, 'can') && $user->can('billing.opd.edit')) {
|
|
return true;
|
|
}
|
|
|
|
if (method_exists($user, 'inGroup') && $user->inGroup('OPDEdit')) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function print_template_builder()
|
|
{
|
|
if (! $this->canManageOpdPrintTemplates()) {
|
|
return $this->response->setStatusCode(403)->setBody('Access denied: OPD print template builder is restricted.');
|
|
}
|
|
|
|
$mode = strtolower(trim((string) $this->request->getGet('mode')));
|
|
if (!in_array($mode, ['list', 'edit'], true)) {
|
|
$mode = 'list';
|
|
}
|
|
|
|
$templateNames = $this->collectOpdTemplateNames();
|
|
$name = strtolower(trim((string) $this->request->getGet('name')));
|
|
$name = preg_replace('/[^a-z0-9_\-]/', '', $name) ?? '';
|
|
|
|
if ($mode === 'list') {
|
|
return view('billing/opd_template_builder_list', [
|
|
'template_names' => $templateNames,
|
|
]);
|
|
}
|
|
|
|
if ($name === '') {
|
|
$name = !empty($templateNames) ? (string) $templateNames[0] : 'default';
|
|
}
|
|
|
|
if (!in_array($name, $templateNames, true)) {
|
|
$templateNames[] = $name;
|
|
$templateNames = array_values(array_unique($templateNames));
|
|
sort($templateNames);
|
|
}
|
|
|
|
$content = $this->readOpdTemplateContentByName($name);
|
|
|
|
$placeholders = [
|
|
'hospital_name', 'hospital_address', 'hospital_phone', 'hospital_email', 'doctor_name', 'print_time',
|
|
'pName', 'pRelative', 'age_sex', 'phoneno', 'p_address', 'uhid_no', 'opd_sr_no', 'opd_no',
|
|
'opd_date', 'exp_date', 'SpecName', 'opd_fee_desc', 'total_no_visit', 'last_opdvisit_date', 'str_opd_book_date',
|
|
'Complaint', 'diagnosis', 'Provisional_diagnosis', 'Finding_Examinations', 'medical', 'investigation',
|
|
'Prescriber_Remarks', 'advice', 'next_visit', 'refer_to', 'vital_content', 'painscale_img',
|
|
'hospital_section', 'patient_section', 'content_section',
|
|
];
|
|
|
|
return view('billing/opd_template_builder', [
|
|
'selected_name' => $name,
|
|
'template_content' => $content,
|
|
'template_names' => $templateNames,
|
|
'placeholders' => $placeholders,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array<int,string>
|
|
*/
|
|
private function collectOpdTemplateNames(): array
|
|
{
|
|
$names = [];
|
|
|
|
$dir = APPPATH . 'Views/billing/opd_templates';
|
|
if (is_dir($dir)) {
|
|
$htmlFiles = glob($dir . '/*.html') ?: [];
|
|
foreach ($htmlFiles as $file) {
|
|
$key = basename((string) $file, '.html');
|
|
if ($key !== '') {
|
|
$names[] = strtolower($key);
|
|
}
|
|
}
|
|
|
|
$phpFiles = glob($dir . '/*.php') ?: [];
|
|
foreach ($phpFiles as $file) {
|
|
$key = basename((string) $file, '.php');
|
|
if ($key !== '') {
|
|
$names[] = strtolower($key);
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($this->db->tableExists('doctor_master')) {
|
|
$doctorFields = $this->db->getFieldNames('doctor_master');
|
|
$templateFields = array_values(array_intersect(
|
|
['opd_print_format', 'opd_blank_print', 'rx_pre_print_letter_head_format', 'rx_blank_letter_head', 'rx_plain_paper'],
|
|
$doctorFields
|
|
));
|
|
|
|
foreach ($templateFields as $field) {
|
|
$rows = $this->db->table('doctor_master')
|
|
->select($field . ' as template_name')
|
|
->where($field . ' IS NOT NULL', null, false)
|
|
->where($field . ' !=', '')
|
|
->distinct()
|
|
->get()
|
|
->getResultArray();
|
|
|
|
foreach ($rows as $row) {
|
|
$templateName = strtolower(trim((string) ($row['template_name'] ?? '')));
|
|
$templateName = preg_replace('/[^a-z0-9_\-]/', '', $templateName) ?? '';
|
|
if ($templateName !== '') {
|
|
$names[] = $templateName;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$names = array_values(array_unique($names));
|
|
sort($names);
|
|
|
|
return $names;
|
|
}
|
|
|
|
private function readOpdTemplateContentByName(string $name): string
|
|
{
|
|
$name = strtolower(trim($name));
|
|
$name = preg_replace('/[^a-z0-9_\-]/', '', $name) ?? '';
|
|
if ($name === '') {
|
|
return '';
|
|
}
|
|
|
|
$htmlFile = APPPATH . 'Views/billing/opd_templates/' . $name . '.html';
|
|
if (is_file($htmlFile)) {
|
|
return (string) file_get_contents($htmlFile);
|
|
}
|
|
|
|
$newPhpFile = APPPATH . 'Views/billing/opd_templates/' . $name . '.php';
|
|
if (is_file($newPhpFile)) {
|
|
return $this->convertLegacyTemplateSourceToBuilderHtml((string) file_get_contents($newPhpFile));
|
|
}
|
|
|
|
$legacyPhpFile = ROOTPATH . 'old_code_ci3/views/dashboard/' . $name . '.php';
|
|
if (is_file($legacyPhpFile)) {
|
|
return $this->convertLegacyTemplateSourceToBuilderHtml((string) file_get_contents($legacyPhpFile));
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
public function print_template_save()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON(['update' => 0, 'error_text' => 'Invalid request']);
|
|
}
|
|
|
|
if (! $this->canManageOpdPrintTemplates()) {
|
|
return $this->response->setStatusCode(403)->setJSON(['update' => 0, 'error_text' => 'Access denied']);
|
|
}
|
|
|
|
$allowedSections = ['full', 'layout', 'hospital', 'patient', 'content'];
|
|
$section = strtolower(trim((string) $this->request->getPost('section')));
|
|
if (!in_array($section, $allowedSections, true)) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Invalid section']);
|
|
}
|
|
|
|
$name = strtolower(trim((string) $this->request->getPost('name')));
|
|
$name = preg_replace('/[^a-z0-9_\-]/', '', $name) ?? '';
|
|
if ($name === '') {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Template name required']);
|
|
}
|
|
|
|
$content = (string) $this->request->getPost('content');
|
|
if (trim($content) === '') {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Template content required']);
|
|
}
|
|
|
|
$dir = $this->resolveTemplateSectionDirectory($section);
|
|
if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to create template directory']);
|
|
}
|
|
|
|
$file = $dir . '/' . $name . '.html';
|
|
$bytes = @file_put_contents($file, $content);
|
|
if ($bytes === false) {
|
|
return $this->response->setJSON(['update' => 0, 'error_text' => 'Unable to save template']);
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'error_text' => 'Template saved',
|
|
'section' => $section,
|
|
'name' => $name,
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
private function resolveTemplateSectionDirectory(string $section): string
|
|
{
|
|
$section = strtolower(trim($section));
|
|
if ($section === 'full') {
|
|
return APPPATH . 'Views/billing/opd_templates';
|
|
}
|
|
|
|
return APPPATH . 'Views/billing/opd_parts/' . $section;
|
|
}
|
|
|
|
private function resetScanAiDebug(): void
|
|
{
|
|
$this->scanAiDebug = [];
|
|
}
|
|
|
|
private function addScanAiDebug(string $message): void
|
|
{
|
|
$message = trim($message);
|
|
if ($message === '') {
|
|
return;
|
|
}
|
|
$this->scanAiDebug[] = $message;
|
|
}
|
|
|
|
private function formatScanAiDebug(): string
|
|
{
|
|
if (empty($this->scanAiDebug)) {
|
|
return '';
|
|
}
|
|
|
|
return implode(' | ', array_slice($this->scanAiDebug, 0, 6));
|
|
}
|
|
|
|
private function normalizeAzureEndpointBase(string $endpoint): string
|
|
{
|
|
$endpoint = trim($endpoint);
|
|
if ($endpoint === '') {
|
|
return '';
|
|
}
|
|
|
|
if (! str_starts_with(strtolower($endpoint), 'http://') && ! str_starts_with(strtolower($endpoint), 'https://')) {
|
|
$endpoint = 'https://' . ltrim($endpoint, '/');
|
|
}
|
|
|
|
$parts = parse_url($endpoint);
|
|
if (! is_array($parts) || empty($parts['scheme']) || empty($parts['host'])) {
|
|
return rtrim($endpoint, '/');
|
|
}
|
|
|
|
$base = strtolower((string) $parts['scheme']) . '://' . (string) $parts['host'];
|
|
if (! empty($parts['port'])) {
|
|
$base .= ':' . (int) $parts['port'];
|
|
}
|
|
|
|
return rtrim($base, '/');
|
|
}
|
|
|
|
private function shouldAllowInsecureSslForEndpoint(string $endpoint): bool
|
|
{
|
|
$toggle = strtolower($this->readScanAiSettingValue(
|
|
['AZURE_ALLOW_INSECURE_SSL', 'AI_AZURE_ALLOW_INSECURE_SSL', 'APP_AZURE_ALLOW_INSECURE_SSL', 'H_AZURE_ALLOW_INSECURE_SSL'],
|
|
['AZURE_ALLOW_INSECURE_SSL']
|
|
));
|
|
if (in_array($toggle, ['1', 'true', 'yes', 'on'], true)) {
|
|
return true;
|
|
}
|
|
|
|
$parts = parse_url($endpoint);
|
|
$host = strtolower((string) ($parts['host'] ?? ''));
|
|
if ($host === '') {
|
|
return false;
|
|
}
|
|
|
|
if (in_array($host, ['localhost', '127.0.0.1', '::1'], true)) {
|
|
return true;
|
|
}
|
|
|
|
if (filter_var($host, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
|
if (preg_match('/^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)/', $host) === 1) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (str_ends_with($host, '.local')) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private function isSslCertificateError(\Throwable $e): bool
|
|
{
|
|
$msg = strtolower($e->getMessage());
|
|
return str_contains($msg, 'ssl certificate problem')
|
|
|| str_contains($msg, 'unable to get local issuer certificate')
|
|
|| str_contains($msg, 'curl error 60')
|
|
|| str_contains($msg, 'error 60');
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function buildAzureCurlOptions(string $endpoint, int $timeout, bool $forceInsecure = false): array
|
|
{
|
|
$options = [
|
|
'timeout' => $timeout,
|
|
'http_errors' => false,
|
|
];
|
|
|
|
if ($forceInsecure || $this->shouldAllowInsecureSslForEndpoint($endpoint)) {
|
|
$options['verify'] = false;
|
|
$options['curl'] = [
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
CURLOPT_SSL_VERIFYHOST => 0,
|
|
];
|
|
$this->addScanAiDebug($forceInsecure ? 'ssl verify disabled after SSL error retry' : 'ssl verify disabled for intranet/local endpoint');
|
|
}
|
|
|
|
return $options;
|
|
}
|
|
|
|
public function appointment(string $opdDate = '')
|
|
{
|
|
return $this->get_appointment_data($opdDate);
|
|
}
|
|
|
|
/**
|
|
* Load OPD doctor dashboard for the selected date.
|
|
*/
|
|
public function get_appointment_data(string $opdDate = '')
|
|
{
|
|
if (! $this->canAccessDoctorWorkPanel()) {
|
|
return $this->response->setStatusCode(403)->setBody('Access denied: OPD doctor work panel is restricted.');
|
|
}
|
|
|
|
if ($opdDate === '') {
|
|
$opdDate = date('Y-m-d');
|
|
}
|
|
|
|
$opdDateSql = $this->db->escape($opdDate);
|
|
|
|
$sql = "select d.id as doc_id, d.p_fname,
|
|
group_concat(DISTINCT m.SpecName) as Spec,
|
|
count(DISTINCT o.opd_id) as No_opd,
|
|
count(DISTINCT case when o.opd_status=1 and p.id is null then o.opd_id end) as count_booking,
|
|
count(DISTINCT case when o.opd_status=1 and p.id is not null then o.opd_id end) as count_wait,
|
|
count(DISTINCT case when o.opd_status=2 then o.opd_id end) as count_visit,
|
|
count(DISTINCT case when o.opd_status=3 then o.opd_id end) as count_cancel,
|
|
MOD(d.id,5) as color_code
|
|
from (doctor_master d
|
|
join doc_spec s on d.id=s.doc_id
|
|
join med_spec m on m.id=s.med_spec_id)
|
|
left join opd_master o on d.id=o.doc_id and date(o.apointment_date)=" . $opdDateSql . "
|
|
left join opd_prescription p on o.opd_id=p.opd_id
|
|
where d.active=1
|
|
group by d.id
|
|
having count(DISTINCT o.opd_id)>0
|
|
order by d.p_fname";
|
|
|
|
$query = $this->db->query($sql);
|
|
$docMaster = $query->getResult();
|
|
|
|
$colorMap = [
|
|
'0' => 'bg-warning',
|
|
'1' => 'bg-danger',
|
|
'2' => 'bg-success',
|
|
'3' => 'bg-secondary',
|
|
'4' => 'bg-info',
|
|
'5' => 'bg-primary',
|
|
];
|
|
|
|
return view('billing/opd_appointment_dashboard', [
|
|
'opd_date' => $opdDate,
|
|
'doc_master' => $docMaster,
|
|
'color1' => $colorMap,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Load doctor-specific OPD queues split into booked, waiting, visited, and cancelled.
|
|
*/
|
|
public function get_appointment_list(int $docId, string $opdDate = '')
|
|
{
|
|
if (! $this->canAccessDoctorWorkPanel()) {
|
|
return $this->response->setStatusCode(403)->setBody('Access denied: OPD doctor work panel is restricted.');
|
|
}
|
|
|
|
if ($opdDate === '') {
|
|
$opdDate = date('Y-m-d');
|
|
}
|
|
|
|
$docId = (int) $docId;
|
|
$opdDateSql = $this->db->escape($opdDate);
|
|
|
|
$sql = "select d.id as doc_id, d.p_fname,
|
|
group_concat(DISTINCT m.SpecName) as Spec,
|
|
count(DISTINCT o.opd_id) as No_opd,
|
|
count(DISTINCT case when o.opd_status=1 and p.id is null then o.opd_id end) as count_booking,
|
|
count(DISTINCT case when o.opd_status=1 and p.id is not null then o.opd_id end) as count_wait,
|
|
count(DISTINCT case when o.opd_status=2 then o.opd_id end) as count_visit,
|
|
count(DISTINCT case when o.opd_status=3 then o.opd_id end) as count_cancel
|
|
from (doctor_master d
|
|
join doc_spec s on d.id=s.doc_id
|
|
join med_spec m on m.id=s.med_spec_id)
|
|
left join opd_master o on d.id=o.doc_id and date(o.apointment_date)=" . $opdDateSql . "
|
|
left join opd_prescription p on o.opd_id=p.opd_id
|
|
where d.id=" . $docId . "
|
|
group by d.id";
|
|
$query = $this->db->query($sql);
|
|
$docMaster = $query->getResult();
|
|
|
|
if (empty($docMaster)) {
|
|
return $this->response->setStatusCode(404)->setBody('Doctor not found for selected date.');
|
|
}
|
|
|
|
// Booked: OPD records without an initialized prescription row.
|
|
$sql = "select o.opd_id,o.opd_code,o.opd_no,p.id,p.p_code,p.p_fname as P_name,p.p_rname,
|
|
o.opd_fee_desc,o.opd_fee_amount,o.payment_status,
|
|
if(o.payment_mode=4,'Credit to ECHS',if(o.payment_mode=1,'Cash','Pending')) as opd_type,
|
|
if(exists(
|
|
select 1 from opd_prescription pv
|
|
where pv.opd_id=o.opd_id
|
|
and (
|
|
coalesce(trim(pv.pulse),'')<>''
|
|
or coalesce(trim(pv.spo2),'')<>''
|
|
or coalesce(trim(pv.bp),'')<>''
|
|
or coalesce(trim(pv.diastolic),'')<>''
|
|
or coalesce(trim(pv.temp),'')<>''
|
|
or coalesce(trim(pv.rr_min),'')<>''
|
|
or coalesce(trim(pv.height),'')<>''
|
|
or coalesce(trim(pv.weight),'')<>''
|
|
or coalesce(trim(pv.waist),'')<>''
|
|
)
|
|
),1,0) as has_vitals
|
|
from opd_master o
|
|
join patient_master p on o.p_id=p.id
|
|
left join opd_prescription pr on o.opd_id=pr.opd_id
|
|
where date(o.apointment_date)=" . $opdDateSql . " and o.doc_id=" . $docId . " and o.opd_status=1 and pr.id is null
|
|
order by o.opd_code desc";
|
|
$query = $this->db->query($sql);
|
|
$opdList0 = $query->getResult();
|
|
|
|
// Waiting: OPD records with prescription row and active (status=1).
|
|
$sql = "select o.opd_id,o.opd_code,o.opd_no,p.id,p.p_code,p.p_fname as P_name,p.p_rname,
|
|
o.opd_fee_desc,o.opd_fee_amount,o.payment_status,
|
|
if(o.payment_mode=4,'Credit to ECHS',if(o.payment_mode=1,'Cash','Pending')) as opd_type,
|
|
if(exists(
|
|
select 1 from opd_prescription pv
|
|
where pv.opd_id=o.opd_id
|
|
and (
|
|
coalesce(trim(pv.pulse),'')<>''
|
|
or coalesce(trim(pv.spo2),'')<>''
|
|
or coalesce(trim(pv.bp),'')<>''
|
|
or coalesce(trim(pv.diastolic),'')<>''
|
|
or coalesce(trim(pv.temp),'')<>''
|
|
or coalesce(trim(pv.rr_min),'')<>''
|
|
or coalesce(trim(pv.height),'')<>''
|
|
or coalesce(trim(pv.weight),'')<>''
|
|
or coalesce(trim(pv.waist),'')<>''
|
|
)
|
|
),1,0) as has_vitals,
|
|
coalesce(pr.queue_no,0) as queue_no
|
|
from opd_master o
|
|
join patient_master p on o.p_id=p.id
|
|
join opd_prescription pr on o.opd_id=pr.opd_id
|
|
where date(o.apointment_date)=" . $opdDateSql . " and o.doc_id=" . $docId . " and o.opd_status=1
|
|
order by has_vitals asc, coalesce(nullif(pr.queue_no,0),999999) asc, o.opd_id asc";
|
|
$query = $this->db->query($sql);
|
|
$opdList1 = $query->getResult();
|
|
|
|
// Visited: consultation completed (status=2).
|
|
$sql = "select o.opd_id,o.opd_code,o.opd_no,p.id,p.p_code,p.p_fname as P_name,p.p_rname,
|
|
o.opd_fee_desc,o.opd_fee_amount,o.payment_status,
|
|
if(o.payment_mode=4,'Credit to ECHS',if(o.payment_mode=1,'Cash','Pending')) as opd_type,
|
|
if(exists(
|
|
select 1 from opd_prescription pv
|
|
where pv.opd_id=o.opd_id
|
|
and (
|
|
coalesce(trim(pv.pulse),'')<>''
|
|
or coalesce(trim(pv.spo2),'')<>''
|
|
or coalesce(trim(pv.bp),'')<>''
|
|
or coalesce(trim(pv.diastolic),'')<>''
|
|
or coalesce(trim(pv.temp),'')<>''
|
|
or coalesce(trim(pv.rr_min),'')<>''
|
|
or coalesce(trim(pv.height),'')<>''
|
|
or coalesce(trim(pv.weight),'')<>''
|
|
or coalesce(trim(pv.waist),'')<>''
|
|
)
|
|
),1,0) as has_vitals,
|
|
coalesce(pr.queue_no,0) as queue_no
|
|
from opd_master o
|
|
join patient_master p on o.p_id=p.id
|
|
left join opd_prescription pr on o.opd_id=pr.opd_id
|
|
where date(o.apointment_date)=" . $opdDateSql . " and o.doc_id=" . $docId . " and o.opd_status=2
|
|
order by o.opd_id";
|
|
$query = $this->db->query($sql);
|
|
$opdList2 = $query->getResult();
|
|
|
|
// Cancelled: OPD records cancelled by staff/doctor (status=3).
|
|
$sql = "select o.opd_id,o.opd_code,o.opd_no,p.id,p.p_code,p.p_fname as P_name,p.p_rname,
|
|
o.opd_fee_desc,o.opd_fee_amount,o.payment_status,
|
|
if(o.payment_mode=4,'Credit to ECHS',if(o.payment_mode=1,'Cash','Pending')) as opd_type,
|
|
if(exists(
|
|
select 1 from opd_prescription pv
|
|
where pv.opd_id=o.opd_id
|
|
and (
|
|
coalesce(trim(pv.pulse),'')<>''
|
|
or coalesce(trim(pv.spo2),'')<>''
|
|
or coalesce(trim(pv.bp),'')<>''
|
|
or coalesce(trim(pv.diastolic),'')<>''
|
|
or coalesce(trim(pv.temp),'')<>''
|
|
or coalesce(trim(pv.rr_min),'')<>''
|
|
or coalesce(trim(pv.height),'')<>''
|
|
or coalesce(trim(pv.weight),'')<>''
|
|
or coalesce(trim(pv.waist),'')<>''
|
|
)
|
|
),1,0) as has_vitals,
|
|
coalesce(pr.queue_no,0) as queue_no
|
|
from opd_master o
|
|
join patient_master p on o.p_id=p.id
|
|
left join opd_prescription pr on o.opd_id=pr.opd_id
|
|
where date(o.apointment_date)=" . $opdDateSql . " and o.doc_id=" . $docId . " and o.opd_status=3
|
|
order by o.opd_id";
|
|
$query = $this->db->query($sql);
|
|
$opdList3 = $query->getResult();
|
|
|
|
return view('billing/opd_appointment_list', [
|
|
'opd_date' => $opdDate,
|
|
'doc_id' => $docId,
|
|
'doc_master' => $docMaster,
|
|
'opd_list_0' => $opdList0,
|
|
'opd_list_1' => $opdList1,
|
|
'opd_list_2' => $opdList2,
|
|
'opd_list_3' => $opdList3,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Update OPD status from queue actions.
|
|
*/
|
|
public function opd_status(int $opdId, int $opdStatus)
|
|
{
|
|
if (! $this->canAccessDoctorWorkPanel()) {
|
|
return $this->response->setStatusCode(403)->setBody('Access denied: OPD doctor work panel is restricted.');
|
|
}
|
|
|
|
$allowed = [1, 2, 3];
|
|
if (!in_array($opdStatus, $allowed, true)) {
|
|
return $this->response->setStatusCode(400)->setBody('Invalid OPD status');
|
|
}
|
|
|
|
$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');
|
|
}
|
|
|
|
$user = auth()->user();
|
|
$userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User';
|
|
$userId = $user->id ?? 0;
|
|
$statusRemark = 'Update By:' . $userLabel . '[' . $userId . '] T-' . date('d-m-Y H:i:s');
|
|
|
|
$this->db->table('opd_master')
|
|
->where('opd_id', (int) $opdId)
|
|
->update([
|
|
'opd_status' => (string) $opdStatus,
|
|
'opd_status_remark' => $statusRemark,
|
|
]);
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'opd_id' => (int) $opdId,
|
|
'opd_status' => (int) $opdStatus,
|
|
'message' => 'Status updated',
|
|
]);
|
|
}
|
|
|
|
public function addopd(int $pno, int $orgCaseId = 0)
|
|
{
|
|
$sql = "select *,if(gender=1,'Male','Female') as xgender
|
|
from patient_master where id='" . (int) $pno . "'";
|
|
$query = $this->db->query($sql);
|
|
$data['person_info'] = $query->getResult();
|
|
if (!empty($data['person_info'])) {
|
|
$row = $data['person_info'][0];
|
|
$row->age = get_age_1($row->dob ?? null, $row->age ?? '', $row->age_in_month ?? '', $row->estimate_dob ?? '');
|
|
}
|
|
|
|
$sql = "select d.id, d.p_fname, group_concat(m.SpecName) as SpecName
|
|
from doctor_master d
|
|
join doc_spec s join med_spec m on d.id = s.doc_id and s.med_spec_id = m.id
|
|
where d.active = 1
|
|
group by d.id";
|
|
$query = $this->db->query($sql);
|
|
$data['doc_spec_l'] = $query->getResult();
|
|
|
|
$data['org_case_id'] = $orgCaseId;
|
|
if ($orgCaseId > 0) {
|
|
$sql = "select * from organization_case_master where id=" . (int) $orgCaseId;
|
|
$query = $this->db->query($sql);
|
|
$data['org_case_master'] = $query->getResult();
|
|
}
|
|
|
|
return view('billing/opd_appointment_V', $data);
|
|
}
|
|
|
|
public function showfee()
|
|
{
|
|
if (!$this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'content' => '',
|
|
'error_text' => 'Invalid request',
|
|
]);
|
|
}
|
|
|
|
$docId = (int) $this->request->getPost('doc_id');
|
|
$pId = (int) $this->request->getPost('pid');
|
|
|
|
$sql = "select * from doctor_master where id=" . $docId;
|
|
$query = $this->db->query($sql);
|
|
$doctorMaster = $query->getResult();
|
|
|
|
$noOpdDays = 5 - 1;
|
|
if (count($doctorMaster) > 0 && !empty($doctorMaster[0]->opd_valid_no_days)) {
|
|
$noOpdDays = (int) $doctorMaster[0]->opd_valid_no_days - 1;
|
|
}
|
|
|
|
$sql = "select d.id, d.p_fname, group_concat(m.SpecName) as SpecName,
|
|
if(d.gender=1,'Male','Female') as xGender
|
|
from doctor_master d
|
|
join doc_spec s join med_spec m on d.id = s.doc_id and s.med_spec_id = m.id
|
|
where d.active = 1 and d.id=" . $docId . " group by d.id";
|
|
$query = $this->db->query($sql);
|
|
$docInfo = $query->getResult();
|
|
|
|
$sql = "select * from opd_master where p_id=" . $pId . " and doc_id=" . $docId .
|
|
" and apointment_date >= date_add(curdate(),interval -" . $noOpdDays . " day) and opd_fee_type<>3";
|
|
$query = $this->db->query($sql);
|
|
$opdRunning = $query->getResult();
|
|
|
|
$sql = "select * from opd_master where p_id=" . $pId;
|
|
$query = $this->db->query($sql);
|
|
$opdOld = $query->getResult();
|
|
|
|
$feeTypeList = '4,0';
|
|
$feeTypeSelect = 1;
|
|
|
|
if (count($opdOld) > 0) {
|
|
if (count($opdRunning) > 0) {
|
|
$feeTypeSelect = 3;
|
|
$feeTypeList .= ',3';
|
|
} else {
|
|
$feeTypeSelect = 2;
|
|
$feeTypeList .= ',2';
|
|
}
|
|
} else {
|
|
$feeTypeSelect = 1;
|
|
$feeTypeList .= ',1';
|
|
}
|
|
|
|
$content = '';
|
|
$content .= '<h5><strong>Doctor Name :</strong> ' . esc($docInfo[0]->p_fname ?? '') .
|
|
' <strong>/ Specialization:</strong> ' . esc($docInfo[0]->SpecName ?? '') .
|
|
' <strong>/ Gender :</strong> ' . esc($docInfo[0]->xGender ?? '') . ' </h5>';
|
|
$content .= '<input type="hidden" name="doc_id" id="doc_id" value="' . esc($docInfo[0]->id ?? 0) . '" />';
|
|
|
|
$sql = "select d.*, if(d.doc_fee_desc='', t.fee_type, d.doc_fee_desc) as fee_desc
|
|
from doc_opd_fee d join doc_fee_type t on d.doc_fee_type = t.id
|
|
where doc_id=" . $docId . " and t.id in (" . $feeTypeList . ")";
|
|
$query = $this->db->query($sql);
|
|
$docFeeA = $query->getResult();
|
|
|
|
$sql = "select d.*, if(d.doc_fee_desc='', t.fee_type, d.doc_fee_desc) as fee_desc
|
|
from doc_opd_fee d join doc_fee_type t on d.doc_fee_type = t.id
|
|
where doc_id=" . $docId . " and t.id not in (" . $feeTypeList . ")";
|
|
$query = $this->db->query($sql);
|
|
$docFeeB = $query->getResult();
|
|
|
|
foreach ($docFeeA as $row) {
|
|
$checked = ((int) $row->id === (int) $feeTypeSelect) ? 'checked' : '';
|
|
$content .= '<label class="d-block">';
|
|
$content .= '<input type="radio" name="fee_id" id="fee_id" class="form-check-input me-1" ' . $checked . ' value="' . (int) $row->id . '"> ';
|
|
$content .= 'Rs. ' . esc($row->amount) . ' [<i>' . esc($row->fee_desc) . '</i>]';
|
|
$content .= '</label>';
|
|
}
|
|
|
|
$content .= '<hr />';
|
|
|
|
$otherContent = '';
|
|
foreach ($docFeeB as $row) {
|
|
$checked = ((int) $row->id === (int) $feeTypeSelect) ? 'checked' : '';
|
|
$otherContent .= '<label class="d-block">';
|
|
$otherContent .= '<input type="radio" name="fee_id" id="fee_id" class="form-check-input me-1" ' . $checked . ' value="' . (int) $row->id . '"> ';
|
|
$otherContent .= 'Rs. ' . esc($row->amount) . ' [<i>' . esc($row->fee_desc) . '</i>]';
|
|
$otherContent .= '</label>';
|
|
}
|
|
|
|
$content .= '<div class="accordion" id="opdFeeAccordion">'
|
|
. '<div class="accordion-item">'
|
|
. '<h2 class="accordion-header" id="headingOne">'
|
|
. '<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="false" aria-controls="collapseOne">'
|
|
. 'Other Option'
|
|
. '</button>'
|
|
. '</h2>'
|
|
. '<div id="collapseOne" class="accordion-collapse collapse" aria-labelledby="headingOne" data-bs-parent="#opdFeeAccordion">'
|
|
. '<div class="accordion-body">'
|
|
. $otherContent
|
|
. '</div>'
|
|
. '</div>'
|
|
. '</div>'
|
|
. '</div>';
|
|
|
|
return $this->response->setJSON([
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
'content' => $content,
|
|
]);
|
|
}
|
|
|
|
public function confirm_opd()
|
|
{
|
|
if (!$this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'insertid' => 0,
|
|
'error_text' => 'Invalid request',
|
|
]);
|
|
}
|
|
|
|
$docId = (int) $this->request->getPost('doc_id');
|
|
$feeId = (int) $this->request->getPost('fee_id');
|
|
$pid = (int) $this->request->getPost('pid');
|
|
$appointmentDate = (string) $this->request->getPost('datepicker_appointment');
|
|
$abhaAddress = trim((string) $this->request->getPost('abha_address'));
|
|
|
|
if ($docId <= 0 || $feeId <= 0 || $pid <= 0 || $appointmentDate === '') {
|
|
return $this->response->setJSON([
|
|
'insertid' => 0,
|
|
'error_text' => 'Missing required fields.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
if ($abhaAddress !== '' && ! $this->isValidAbhaAddress($abhaAddress)) {
|
|
return $this->response->setJSON([
|
|
'insertid' => 0,
|
|
'error_text' => 'Invalid ABHA Address format.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$sql = "select * from patient_master where id=" . $pid;
|
|
$query = $this->db->query($sql);
|
|
$personInfo = $query->getResult();
|
|
|
|
$sql = "select d.id, d.p_fname, group_concat(m.SpecName) as SpecName,
|
|
if(d.gender=1,'Male','Female') as xGender
|
|
from doctor_master d
|
|
join doc_spec s join med_spec m on d.id = s.doc_id and s.med_spec_id = m.id
|
|
where d.active=1 and d.id=" . $docId . " group by d.id";
|
|
$query = $this->db->query($sql);
|
|
$docInfo = $query->getResult();
|
|
|
|
$sql = "select * from doc_opd_fee where id=" . $feeId;
|
|
$query = $this->db->query($sql);
|
|
$docFee = $query->getResult();
|
|
|
|
if (count($personInfo) === 0 || count($docInfo) === 0 || count($docFee) === 0) {
|
|
return $this->response->setJSON([
|
|
'insertid' => 0,
|
|
'error_text' => 'Invalid patient, doctor, or fee selection.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$sql = "select * from opd_master where p_id=" . $pid . " order by opd_id desc";
|
|
$query = $this->db->query($sql);
|
|
$opdMaster = $query->getResult();
|
|
|
|
$user = auth()->user();
|
|
$userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User';
|
|
$userId = $user->id ?? '';
|
|
$userNameInfo = $userLabel . ' [' . date('d-m-Y H:i:s') . '] ' . $userId;
|
|
|
|
$insert = [
|
|
'p_id' => $pid,
|
|
'P_name' => strtoupper((string) $personInfo[0]->p_fname),
|
|
'doc_id' => $docId,
|
|
'insurance_id' => '0',
|
|
'opd_fee_id' => $feeId,
|
|
'opd_fee_amount' => $docFee[0]->amount,
|
|
'opd_fee_gross_amount' => $docFee[0]->amount,
|
|
'opd_fee_desc' => $docFee[0]->doc_fee_desc,
|
|
'doc_name' => $docInfo[0]->p_fname,
|
|
'opd_fee_type' => $docFee[0]->doc_fee_type,
|
|
'apointment_date' => str_to_MysqlDate($appointmentDate),
|
|
'doc_spec' => $docInfo[0]->SpecName,
|
|
'prepared_by' => $userNameInfo,
|
|
];
|
|
|
|
if (count($opdMaster) > 0) {
|
|
$insert['last_opdvisit_date'] = $opdMaster[0]->apointment_date;
|
|
}
|
|
|
|
$opdModel = new OpdModel();
|
|
$insertId = $opdModel->insertOpd($insert);
|
|
|
|
if ($insertId > 0) {
|
|
$this->db->table('patient_master')
|
|
->where('id', $pid)
|
|
->update(['last_visit' => str_to_MysqlDate($appointmentDate)]);
|
|
|
|
$this->auditClinicalUpdate('opd_master', 'created', $insertId, null, $insert);
|
|
|
|
$abhaField = $this->resolvePatientAbhaField();
|
|
if ($abhaField !== null && $abhaAddress !== '') {
|
|
$oldAbha = (string) ($personInfo[0]->{$abhaField} ?? '');
|
|
if ($oldAbha !== $abhaAddress) {
|
|
$this->db->table('patient_master')
|
|
->where('id', $pid)
|
|
->update([$abhaField => $abhaAddress]);
|
|
|
|
$this->auditClinicalUpdate('patient_master', $abhaField, $pid, $oldAbha, $abhaAddress);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'insertid' => $insertId,
|
|
'error_text' => $insertId > 0 ? 'OPD Register' : 'OPD already exists for today.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
public function invoice(int $opdId)
|
|
{
|
|
$sql = "select *,date_format(apointment_date,'%d-%m-%Y') as str_apointment_date,
|
|
(case payment_status when 1 then 'Cash' when 2 then 'Bank Card' when 3 then 'Org. Credit' else 'Pending' end) as Payment_type_str
|
|
from opd_master where opd_id=" . (int) $opdId;
|
|
$query = $this->db->query($sql);
|
|
$data['opd_master'] = $query->getResult();
|
|
|
|
if (empty($data['opd_master'])) {
|
|
return $this->response->setStatusCode(404)->setBody('OPD not found');
|
|
}
|
|
|
|
$opdRow = $data['opd_master'][0];
|
|
|
|
$sql = "select *,if(gender=1,'Male','Female') as xgender
|
|
from patient_master where id='" . (int) $opdRow->p_id . "'";
|
|
$query = $this->db->query($sql);
|
|
$data['patient_master'] = $query->getResult();
|
|
if (!empty($data['patient_master'])) {
|
|
$pRow = $data['patient_master'][0];
|
|
$pRow->age = get_age_1($pRow->dob ?? null, $pRow->age ?? '', $pRow->age_in_month ?? '', $pRow->estimate_dob ?? '', $opdRow->apointment_date ?? null);
|
|
}
|
|
|
|
$sql = "select * from organization_case_master where status=0 and case_type=0 and p_id=" . (int) $opdRow->p_id;
|
|
$query = $this->db->query($sql);
|
|
$data['case_master'] = $query->getResult();
|
|
|
|
$sql = "select * from hc_insurance where id=" . (int) $opdRow->insurance_id;
|
|
$query = $this->db->query($sql);
|
|
$data['insurance'] = $query->getResult();
|
|
|
|
$sql = "select * from refund_order where refund_process=0 and refund_type=1 and refund_type_id=" . (int) $opdId;
|
|
$query = $this->db->query($sql);
|
|
$data['refund_order'] = $query->getResult();
|
|
$data['refund_status'] = count($data['refund_order']) > 0 ? 1 : 0;
|
|
|
|
$sql = "select sum(if(credit_debit=0,amount,amount*-1)) as paid_amount
|
|
from payment_history where payof_type=1 and payof_id=" . (int) $opdId;
|
|
$query = $this->db->query($sql);
|
|
$data['payment_history'] = $query->getResult();
|
|
|
|
$paidAmount = 0;
|
|
if (!empty($data['payment_history']) && $data['payment_history'][0]->paid_amount) {
|
|
$paidAmount = (float) $data['payment_history'][0]->paid_amount;
|
|
}
|
|
|
|
$data['paid_amount'] = $paidAmount;
|
|
$grossAmount = (float) $opdRow->opd_fee_amount;
|
|
$data['pending_amount'] = ((int) $opdRow->payment_mode === 4) ? 0 : ($grossAmount - $paidAmount);
|
|
|
|
$sql = "select d.id, d.p_fname, group_concat(m.SpecName) as SpecName
|
|
from doctor_master d
|
|
join doc_spec s join med_spec m on d.id = s.doc_id and s.med_spec_id = m.id
|
|
where d.active=1 group by d.id";
|
|
$query = $this->db->query($sql);
|
|
$data['doc_spec_l'] = $query->getResult();
|
|
|
|
$sql = "select s.id, s.pay_type, m.bank_name
|
|
from hospital_bank m join hospital_bank_payment_source s on m.id=s.bank_id";
|
|
$query = $this->db->query($sql);
|
|
$data['bank_data'] = $query->getResult();
|
|
|
|
return view('billing/opd_invoice_V', $data);
|
|
}
|
|
|
|
public function invoice_print(int $opdId)
|
|
{
|
|
$data = $this->buildOpdInvoicePrintData($opdId);
|
|
if ($data === null) {
|
|
return $this->response->setStatusCode(404)->setBody('OPD not found');
|
|
}
|
|
|
|
return view('billing/opd_invoice_print_v', $data);
|
|
}
|
|
|
|
public function invoice_print_pdf(int $opdId)
|
|
{
|
|
$data = $this->buildOpdInvoicePrintData($opdId);
|
|
if ($data === null) {
|
|
return $this->response->setStatusCode(404)->setBody('OPD not found');
|
|
}
|
|
|
|
$html = view('billing/opd_invoice_pdf_v', $data);
|
|
|
|
$mpdf = new Mpdf([
|
|
'mode' => 'utf-8',
|
|
'format' => 'A4',
|
|
'orientation' => 'P',
|
|
'margin_left' => 10,
|
|
'margin_right' => 10,
|
|
'margin_top' => 10,
|
|
'margin_bottom' => 10,
|
|
'default_font' => 'dejavusans',
|
|
'tempDir' => WRITEPATH . 'cache',
|
|
]);
|
|
|
|
$opdCode = (string) ($data['opd_master'][0]->opd_code ?? ('OPD-' . $opdId));
|
|
$mpdf->SetTitle('OPD Invoice ' . $opdCode);
|
|
$mpdf->WriteHTML($html);
|
|
|
|
$fileName = 'OPD_Invoice_' . str_replace('/', '-', $opdCode) . '.pdf';
|
|
|
|
return $this->response
|
|
->setHeader('Content-Type', 'application/pdf')
|
|
->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
|
|
->setBody($mpdf->Output($fileName, 'S'));
|
|
}
|
|
|
|
private function buildOpdInvoicePrintData(int $opdId): ?array
|
|
{
|
|
$data = [];
|
|
$sql = "select *,date_format(apointment_date,'%d-%m-%Y') as str_apointment_date,
|
|
(case payment_status when 1 then 'Cash' when 2 then 'Bank Card' when 3 then 'Org. Credit' else 'Pending' end) as Payment_type_str
|
|
from opd_master where opd_id=" . (int) $opdId;
|
|
$query = $this->db->query($sql);
|
|
$data['opd_master'] = $query->getResult();
|
|
|
|
if (empty($data['opd_master'])) {
|
|
return null;
|
|
}
|
|
|
|
$opdRow = $data['opd_master'][0];
|
|
|
|
$sql = "select *,if(gender=1,'Male','Female') as xgender
|
|
from patient_master where id='" . (int) $opdRow->p_id . "'";
|
|
$query = $this->db->query($sql);
|
|
$data['patient_master'] = $query->getResult();
|
|
|
|
if (!empty($data['patient_master'])) {
|
|
$pRow = $data['patient_master'][0];
|
|
$pRow->age = get_age_1($pRow->dob ?? null, $pRow->age ?? '', $pRow->age_in_month ?? '', $pRow->estimate_dob ?? '', $opdRow->apointment_date ?? null);
|
|
}
|
|
|
|
$sql = "select * from hc_insurance where id=" . (int) ($opdRow->insurance_id ?? 0);
|
|
$query = $this->db->query($sql);
|
|
$data['insurance'] = $query->getResult();
|
|
|
|
if ((int) ($opdRow->insurance_case_id ?? 0) > 0) {
|
|
$sql = "select * from organization_case_master where id=" . (int) $opdRow->insurance_case_id;
|
|
$query = $this->db->query($sql);
|
|
$data['case_master'] = $query->getResult();
|
|
} else {
|
|
$data['case_master'] = [];
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
public function opd_lettre_print(int $opdId)
|
|
{
|
|
$sessionId = (int) $this->request->getGet('session_id');
|
|
$layoutMode = strtolower(trim((string) $this->request->getGet('layout')));
|
|
if ($layoutMode === '') {
|
|
$layoutMode = 'full';
|
|
}
|
|
|
|
$includeContent = in_array($layoutMode, ['full', 'meta_content', 'content_only'], true);
|
|
|
|
$data = $this->buildOpdLetterPrintData($opdId, $sessionId, $includeContent);
|
|
if ($data === null) {
|
|
return $this->response->setStatusCode(404)->setBody('OPD not found');
|
|
}
|
|
|
|
$data['print_layout_mode'] = $layoutMode;
|
|
|
|
return view('billing/opd_letter_head', $data);
|
|
}
|
|
|
|
public function opd_lettre_pdf(int $opdId)
|
|
{
|
|
$sessionId = (int) $this->request->getGet('session_id');
|
|
$layoutMode = strtolower(trim((string) $this->request->getGet('layout')));
|
|
if ($layoutMode === '') {
|
|
$layoutMode = 'full';
|
|
}
|
|
|
|
$allowedModes = ['full', 'meta_content', 'content_only', 'header_meta', 'meta_only'];
|
|
if (! in_array($layoutMode, $allowedModes, true)) {
|
|
$layoutMode = 'full';
|
|
}
|
|
|
|
$includeContent = in_array($layoutMode, ['full', 'meta_content', 'content_only'], true);
|
|
|
|
$data = $this->buildOpdLetterPrintData($opdId, $sessionId, $includeContent);
|
|
if ($data === null) {
|
|
return $this->response->setStatusCode(404)->setBody('OPD not found');
|
|
}
|
|
|
|
$data['print_layout_mode'] = $layoutMode;
|
|
$templateKey = strtolower(trim((string) $this->request->getGet('template')));
|
|
$templateKey = preg_replace('/[^a-z0-9_\-]/', '', $templateKey) ?? '';
|
|
|
|
$isComposedTemplate = $templateKey !== '' && str_starts_with($templateKey, 'compose_');
|
|
if ($isComposedTemplate) {
|
|
$html = $this->renderComposedOpdPrint($data, $templateKey);
|
|
} else {
|
|
|
|
$viewPath = 'billing/opd_letter_head_pdf';
|
|
$customHtml = '';
|
|
if ($templateKey !== '') {
|
|
$customFile = APPPATH . 'Views/billing/opd_templates/' . $templateKey . '.php';
|
|
$customHtmlFile = APPPATH . 'Views/billing/opd_templates/' . $templateKey . '.html';
|
|
if (is_file($customFile)) {
|
|
$viewPath = 'billing/opd_templates/' . $templateKey;
|
|
} elseif (is_file($customHtmlFile)) {
|
|
$customHtml = (string) file_get_contents($customHtmlFile);
|
|
}
|
|
}
|
|
|
|
if ($viewPath !== 'billing/opd_letter_head_pdf') {
|
|
$html = view($viewPath, $data);
|
|
} elseif ($customHtml !== '') {
|
|
$legacyVars = $this->buildLegacyDashboardTemplateVars($data);
|
|
$html = $this->renderCurlyTemplate($customHtml, $legacyVars);
|
|
} elseif ($templateKey !== '') {
|
|
$legacyFile = ROOTPATH . 'old_code_ci3/views/dashboard/' . $templateKey . '.php';
|
|
if (is_file($legacyFile)) {
|
|
$legacyVars = $this->buildLegacyDashboardTemplateVars($data);
|
|
$html = $this->renderLegacyPhpTemplate($legacyFile, $legacyVars);
|
|
} else {
|
|
$html = view('billing/opd_letter_head_pdf', $data);
|
|
}
|
|
} else {
|
|
$html = view('billing/opd_letter_head_pdf', $data);
|
|
}
|
|
}
|
|
|
|
$paper = strtolower(trim((string) $this->request->getGet('paper')));
|
|
$format = 'A4';
|
|
if (in_array($paper, ['a5', 'a6', 'letter'], true)) {
|
|
$format = strtoupper($paper);
|
|
}
|
|
|
|
$marginLeft = 8;
|
|
$marginRight = 8;
|
|
$marginTop = 8;
|
|
$marginBottom = 8;
|
|
|
|
if ($format === 'A6') {
|
|
$marginLeft = 6;
|
|
$marginRight = 6;
|
|
$marginTop = 6;
|
|
$marginBottom = 6;
|
|
}
|
|
|
|
$mpdf = new Mpdf([
|
|
'mode' => 'utf-8',
|
|
'format' => $format,
|
|
'orientation' => 'P',
|
|
'margin_left' => $marginLeft,
|
|
'margin_right' => $marginRight,
|
|
'margin_top' => $marginTop,
|
|
'margin_bottom' => $marginBottom,
|
|
'default_font' => 'freeserif',
|
|
'autoScriptToLang' => true,
|
|
'autoLangToFont' => true,
|
|
'tempDir' => WRITEPATH . 'cache',
|
|
]);
|
|
|
|
$opdCode = (string) ($data['opd_master'][0]->opd_code ?? ('OPD-' . $opdId));
|
|
$mpdf->SetTitle('OPD Prescription ' . $opdCode);
|
|
$mpdf->WriteHTML($html);
|
|
|
|
$fileName = 'OPD_Prescription_' . str_replace('/', '-', $opdCode) . '.pdf';
|
|
|
|
return $this->response
|
|
->setHeader('Content-Type', 'application/pdf')
|
|
->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"')
|
|
->setBody($mpdf->Output($fileName, 'S'));
|
|
}
|
|
|
|
public function opd_pdf_print(int $opdId)
|
|
{
|
|
$printConfig = $this->resolveDoctorPrintConfigByField($opdId, 'opd_print_format', 'meta_only');
|
|
$url = base_url('Opd/opd_lettre_pdf/' . (int) $opdId . '?layout=' . urlencode($printConfig['layout']));
|
|
if ($printConfig['template'] !== '') {
|
|
$url .= '&template=' . urlencode($printConfig['template']);
|
|
}
|
|
return redirect()->to($url);
|
|
}
|
|
|
|
public function opd_blank_print(int $opdId)
|
|
{
|
|
$printConfig = $this->resolveDoctorPrintConfigByField($opdId, 'opd_blank_print', 'header_meta');
|
|
$url = base_url('Opd/opd_lettre_pdf/' . (int) $opdId . '?layout=' . urlencode($printConfig['layout']));
|
|
if ($printConfig['template'] !== '') {
|
|
$url .= '&template=' . urlencode($printConfig['template']);
|
|
}
|
|
return redirect()->to($url);
|
|
}
|
|
|
|
public function opd_cont_print(int $opdId)
|
|
{
|
|
$printConfig = $this->resolveDoctorPrintConfigByField($opdId, 'opd_print_format', 'content_only');
|
|
$url = base_url('Opd/opd_lettre_pdf/' . (int) $opdId . '?layout=' . urlencode($printConfig['layout']));
|
|
if ($printConfig['template'] !== '') {
|
|
$url .= '&template=' . urlencode($printConfig['template']);
|
|
}
|
|
return redirect()->to($url);
|
|
}
|
|
|
|
/**
|
|
* @return array{layout:string,template:string}
|
|
*/
|
|
private function resolveDoctorPrintConfigByField(int $opdId, string $fieldName, string $fallback): array
|
|
{
|
|
$allowed = ['full', 'meta_content', 'content_only', 'header_meta', 'meta_only'];
|
|
|
|
$fallbackLayout = strtolower(trim($fallback));
|
|
if (!in_array($fallbackLayout, $allowed, true)) {
|
|
$fallbackLayout = 'full';
|
|
}
|
|
|
|
$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' => $fallbackLayout, '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' => $fallbackLayout, 'template' => $templateKey];
|
|
}
|
|
|
|
return ['layout' => $fallbackLayout, 'template' => ''];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function buildLegacyDashboardTemplateVars(array $data): array
|
|
{
|
|
$opd = $data['opd_master'][0] ?? null;
|
|
$patient = $data['patient_master'][0] ?? null;
|
|
$doctor = $data['doctor_master'][0] ?? null;
|
|
$rx = is_array($data['opd_prescription'] ?? null) ? $data['opd_prescription'] : [];
|
|
|
|
$pName = strtoupper(trim((string) (($patient->title ?? '') . ' ' . ($patient->p_fname ?? ''))));
|
|
$pRelative = trim((string) (($patient->p_relative ?? '') . ' ' . ($patient->p_rname ?? '')));
|
|
$ageSex = trim((string) (($patient->xgender ?? '') . ' / ' . ($patient->age ?? '')));
|
|
$phone = (string) ($patient->mphone1 ?? '');
|
|
$pAddress = trim((string) (($patient->add1 ?? '') . ', ' . ($patient->add2 ?? '') . ', ' . ($patient->city ?? '')), ' ,');
|
|
|
|
$uhidNo = (string) ($patient->p_code ?? '');
|
|
$opdSrNo = (string) ($opd->opd_no ?? '');
|
|
$opdNo = (string) ($opd->opd_code ?? '');
|
|
$opdDate = (string) ($opd->str_apointment_date ?? '');
|
|
$expDate = trim((string) ($opd->opd_Exp_Date ?? ''));
|
|
$expDateText = $expDate !== '' ? ('<b>Valid Upto : </b>' . esc($expDate)) : '';
|
|
|
|
$specName = trim((string) ($doctor->SpecName ?? $opd->doc_spec ?? ''));
|
|
$opdFeeDesc = trim((string) (($opd->opd_fee_amount ?? '') . ' ' . ($opd->opd_fee_desc ?? '')));
|
|
$totalNoVisit = (string) ($opd->no_visit ?? '');
|
|
$lastVisitText = '';
|
|
if (!empty($data['old_opd'][0]->str_apointment_date ?? '')) {
|
|
$lastVisitText = 'Last Visit : ' . (string) $data['old_opd'][0]->str_apointment_date;
|
|
}
|
|
|
|
$bookTime = (string) ($opd->apointment_date ?? '');
|
|
|
|
$rxMeds = is_array($data['rx_medicines'] ?? null) ? $data['rx_medicines'] : [];
|
|
$medicalHtml = '';
|
|
if (!empty($rxMeds)) {
|
|
$medicalHtml .= '<table width="100%" style="border-collapse:collapse;font-size:12px;">';
|
|
$medicalHtml .= '<tr><th style="text-align:left;">#</th><th style="text-align:left;">Medicine</th><th style="text-align:left;">Dose/Freq</th><th style="text-align:left;">Day</th><th style="text-align:left;">Instruction</th></tr>';
|
|
$i = 0;
|
|
foreach ($rxMeds as $med) {
|
|
$i++;
|
|
$name = trim((string) ($med['drug_name'] ?? $med['medicine_name'] ?? $med['item_name'] ?? ''));
|
|
$dose = trim((string) ($med['drug_dose'] ?? $med['dose'] ?? ''));
|
|
$freq = trim((string) ($med['drug_freq'] ?? $med['frequency'] ?? ''));
|
|
$days = trim((string) ($med['drug_day'] ?? $med['days'] ?? ''));
|
|
$inst = trim((string) ($med['drug_instruction'] ?? $med['instruction'] ?? ''));
|
|
$medicalHtml .= '<tr>'
|
|
. '<td>' . $i . '</td>'
|
|
. '<td>' . esc($name) . '</td>'
|
|
. '<td>' . esc(trim($dose . ' ' . $freq)) . '</td>'
|
|
. '<td>' . esc($days) . '</td>'
|
|
. '<td>' . esc($inst) . '</td>'
|
|
. '</tr>';
|
|
}
|
|
$medicalHtml .= '</table>';
|
|
}
|
|
|
|
$rxInvestigation = trim((string) ($rx['investigation'] ?? ''));
|
|
if ($rxInvestigation === '' && !empty($data['rx_investigations']) && is_array($data['rx_investigations'])) {
|
|
$parts = [];
|
|
foreach ($data['rx_investigations'] as $inv) {
|
|
$txt = trim((string) ($inv['investigation_name'] ?? $inv['investigation'] ?? ''));
|
|
if ($txt !== '') {
|
|
$parts[] = $txt;
|
|
}
|
|
}
|
|
$rxInvestigation = implode(', ', $parts);
|
|
}
|
|
|
|
$rxAdvice = trim((string) ($rx['advice'] ?? ''));
|
|
if ($rxAdvice === '' && !empty($data['rx_advices']) && is_array($data['rx_advices'])) {
|
|
$parts = [];
|
|
foreach ($data['rx_advices'] as $adv) {
|
|
$txt = trim((string) ($adv['advice_txt'] ?? $adv['advice'] ?? ''));
|
|
if ($txt !== '') {
|
|
$parts[] = $txt;
|
|
}
|
|
}
|
|
$rxAdvice = implode(', ', $parts);
|
|
}
|
|
|
|
$bp = trim((string) ($rx['bp'] ?? ''));
|
|
$diastolic = trim((string) ($rx['diastolic'] ?? ''));
|
|
$pulse = trim((string) ($rx['pulse'] ?? ''));
|
|
$temp = trim((string) ($rx['temp'] ?? ''));
|
|
$spo2 = trim((string) ($rx['spo2'] ?? ''));
|
|
|
|
$vitals = [];
|
|
if ($bp !== '') {
|
|
$vitals[] = 'BP: ' . $bp . ($diastolic !== '' ? ('/' . $diastolic) : '');
|
|
}
|
|
if ($pulse !== '') {
|
|
$vitals[] = 'Pulse: ' . $pulse;
|
|
}
|
|
if ($temp !== '') {
|
|
$vitals[] = 'Temp: ' . $temp;
|
|
}
|
|
if ($spo2 !== '') {
|
|
$vitals[] = 'SpO2: ' . $spo2;
|
|
}
|
|
|
|
$printContent = '<table width="100%" border="0" style="font-size:10pt;">'
|
|
. '<tr>'
|
|
. '<td width="33.3%" valign="top">'
|
|
. 'Name : <strong>' . esc($pName) . '</strong><br>'
|
|
. esc($pRelative) . '<br>'
|
|
. 'Gender/Age : <b>' . esc($ageSex) . '</b><br>'
|
|
. 'Mob :' . esc($phone) . '<br>'
|
|
. 'Address :' . esc($pAddress)
|
|
. '</td>'
|
|
. '<td width="33.3%" valign="top">'
|
|
. 'UHID : ' . esc($uhidNo) . '<br>'
|
|
. 'Sr No.: <b>' . esc($opdSrNo) . '</b><br>'
|
|
. 'OPD No.: <b>' . esc($opdNo) . '</b><br>'
|
|
. '<br><b>Date: ' . esc($opdDate) . '</b><br>' . $expDateText
|
|
. '</td>'
|
|
. '<td width="33.3%" valign="top">'
|
|
. '<b>DEPARTMENT :</b><br>' . esc($specName) . '<br>'
|
|
. esc($opdFeeDesc) . '<br>'
|
|
. '<b>No. of Visit</b> : ' . esc($totalNoVisit) . '<br>'
|
|
. esc($lastVisitText) . '<br>'
|
|
. 'Book Time : ' . esc($bookTime)
|
|
. '</td>'
|
|
. '</tr>'
|
|
. '</table>';
|
|
|
|
return [
|
|
'opd_master' => $data['opd_master'] ?? [],
|
|
'patient_master' => $data['patient_master'] ?? [],
|
|
'doctor_master' => $data['doctor_master'] ?? [],
|
|
'old_opd' => $data['old_opd'] ?? [],
|
|
'insurance' => $data['insurance'] ?? [],
|
|
'case_master' => $data['case_master'] ?? [],
|
|
'content' => $printContent,
|
|
'content_2' => $printContent,
|
|
'content_3' => $printContent,
|
|
'content_4' => $printContent,
|
|
'pName' => $pName,
|
|
'pRelative' => $pRelative,
|
|
'age_sex' => $ageSex,
|
|
'phoneno' => $phone,
|
|
'p_address' => $pAddress,
|
|
'uhid_no' => $uhidNo,
|
|
'opd_sr_no' => $opdSrNo,
|
|
'opd_no' => $opdNo,
|
|
'opd_date' => $opdDate,
|
|
'exp_date' => $expDateText,
|
|
'SpecName' => $specName,
|
|
'opd_fee_desc' => $opdFeeDesc,
|
|
'total_no_visit' => $totalNoVisit,
|
|
'last_opdvisit_date' => $lastVisitText,
|
|
'str_opd_book_date' => $bookTime,
|
|
'Complaint' => (string) ($rx['complaints'] ?? ''),
|
|
'diagnosis' => (string) (($rx['Provisional_diagnosis'] ?? '') !== '' ? ($rx['Provisional_diagnosis'] ?? '') : ($rx['diagnosis'] ?? '')),
|
|
'Provisional_diagnosis' => (string) ($rx['Provisional_diagnosis'] ?? ''),
|
|
'investigation' => $rxInvestigation,
|
|
'medical' => $medicalHtml,
|
|
'doctor' => '<p style="text-align:right;">Dr. ' . esc((string) ($opd->doc_name ?? '')) . '</p>',
|
|
'top_content' => '',
|
|
'vital_content' => implode(' | ', $vitals),
|
|
'Finding_Examinations' => (string) ($rx['Finding_Examinations'] ?? ''),
|
|
'Prescriber_Remarks' => (string) ($rx['Prescriber_Remarks'] ?? ''),
|
|
'advice' => $rxAdvice,
|
|
'next_visit' => (string) ($rx['next_visit'] ?? ''),
|
|
'refer_to' => (string) ($rx['refer_to'] ?? ''),
|
|
'painscale' => '',
|
|
'painscale_img' => '',
|
|
'morbidities' => implode(', ', is_array($data['selected_morbidities'] ?? null) ? ($data['selected_morbidities'] ?? []) : []),
|
|
'Addiction' => '',
|
|
'Complication' => '',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $vars
|
|
*/
|
|
private function renderLegacyPhpTemplate(string $filePath, array $vars): string
|
|
{
|
|
ob_start();
|
|
extract($vars, EXTR_SKIP);
|
|
include $filePath;
|
|
return (string) ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $data
|
|
*/
|
|
private function renderComposedOpdPrint(array $data, string $templateKey): string
|
|
{
|
|
$vars = $this->buildLegacyDashboardTemplateVars($data);
|
|
|
|
$hospitalName = defined('H_Name') ? (string) constant('H_Name') : 'Hospital';
|
|
$hospitalAddress1 = defined('H_address_1') ? (string) constant('H_address_1') : '';
|
|
$hospitalAddress2 = defined('H_address_2') ? (string) constant('H_address_2') : '';
|
|
$hospitalPhone = defined('H_phone_No') ? (string) constant('H_phone_No') : '';
|
|
$hospitalEmail = defined('H_Email') ? (string) constant('H_Email') : '';
|
|
|
|
$vars['hospital_name'] = esc($hospitalName);
|
|
$vars['hospital_address'] = esc(trim($hospitalAddress1 . ', ' . $hospitalAddress2, ', '));
|
|
$vars['hospital_phone'] = esc($hospitalPhone);
|
|
$vars['hospital_email'] = esc($hospitalEmail);
|
|
$vars['print_time'] = esc(date('d-m-Y H:i:s'));
|
|
|
|
$doctorName = trim((string) ($vars['doctor_name'] ?? ''));
|
|
if ($doctorName === '') {
|
|
$opd = $data['opd_master'][0] ?? null;
|
|
$doctorName = (string) ($opd->doc_name ?? '');
|
|
}
|
|
$vars['doctor_name'] = esc($doctorName);
|
|
|
|
$themeKey = trim((string) substr($templateKey, strlen('compose_')));
|
|
if ($themeKey === '') {
|
|
$themeKey = 'default';
|
|
}
|
|
|
|
$hospitalPart = strtolower(trim((string) $this->request->getGet('hospital_part')));
|
|
if ($hospitalPart === '') {
|
|
$hospitalPart = $themeKey;
|
|
}
|
|
|
|
$contentPart = strtolower(trim((string) $this->request->getGet('content_part')));
|
|
if ($contentPart === '') {
|
|
$contentPart = $themeKey;
|
|
}
|
|
|
|
$patientView = strtolower(trim((string) $this->request->getGet('patient_view')));
|
|
if (!in_array($patientView, ['list', 'edit'], true)) {
|
|
$patientView = 'list';
|
|
}
|
|
|
|
$layoutHtml = $this->readComposedPartTemplate('layout', $themeKey);
|
|
$hospitalHtml = $this->readComposedPartTemplate('hospital', $hospitalPart);
|
|
$contentHtml = $this->readComposedPartTemplate('content', $contentPart);
|
|
$patientHtml = $this->readComposedPartTemplate('patient', $patientView);
|
|
|
|
$hospitalHtml = $this->renderCurlyTemplate($hospitalHtml, $vars);
|
|
$patientHtml = $this->renderCurlyTemplate($patientHtml, $vars);
|
|
$contentHtml = $this->renderCurlyTemplate($contentHtml, $vars);
|
|
|
|
$vars['hospital_section'] = $hospitalHtml;
|
|
$vars['patient_section'] = $patientHtml;
|
|
$vars['content_section'] = $contentHtml;
|
|
|
|
return $this->renderCurlyTemplate($layoutHtml, $vars);
|
|
}
|
|
|
|
private function readComposedPartTemplate(string $section, string $name): string
|
|
{
|
|
$safeSection = preg_replace('/[^a-z0-9_\-]/', '', strtolower(trim($section))) ?? '';
|
|
$safeName = preg_replace('/[^a-z0-9_\-]/', '', strtolower(trim($name))) ?? '';
|
|
if ($safeSection === '') {
|
|
$safeSection = 'layout';
|
|
}
|
|
if ($safeName === '') {
|
|
$safeName = 'default';
|
|
}
|
|
|
|
$baseDir = APPPATH . 'Views/billing/opd_parts/' . $safeSection . '/';
|
|
$target = $baseDir . $safeName . '.html';
|
|
$fallback = $baseDir . 'default.html';
|
|
|
|
if (is_file($target)) {
|
|
return (string) file_get_contents($target);
|
|
}
|
|
if (is_file($fallback)) {
|
|
return (string) file_get_contents($fallback);
|
|
}
|
|
|
|
return '{{content}}';
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $vars
|
|
*/
|
|
private function renderCurlyTemplate(string $html, array $vars): string
|
|
{
|
|
return (string) preg_replace_callback('/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/', static function (array $m) use ($vars) {
|
|
$key = (string) ($m[1] ?? '');
|
|
$value = $vars[$key] ?? '';
|
|
if (is_array($value) || is_object($value)) {
|
|
return '';
|
|
}
|
|
return (string) $value;
|
|
}, $html);
|
|
}
|
|
|
|
private function convertLegacyTemplateSourceToBuilderHtml(string $source): string
|
|
{
|
|
if (trim($source) === '') {
|
|
return '';
|
|
}
|
|
|
|
$html = str_replace(["\r\n", "\r"], "\n", $source);
|
|
|
|
$patterns = [
|
|
'/<\?php\s+echo\s+esc\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)[^\)]*\)\s*;?\s*\?>/i' => '{{$1}}',
|
|
'/<\?php\s+echo\s+\$([a-zA-Z_][a-zA-Z0-9_]*)\s*;?\s*\?>/i' => '{{$1}}',
|
|
'/<\?=\s*\$([a-zA-Z_][a-zA-Z0-9_]*)\s*;?\s*\?>/i' => '{{$1}}',
|
|
'/<\?php\s+echo\s*\(\s*\$([a-zA-Z_][a-zA-Z0-9_]*)\s*\)\s*;?\s*\?>/i' => '{{$1}}',
|
|
];
|
|
|
|
foreach ($patterns as $regex => $replacement) {
|
|
$html = (string) preg_replace($regex, $replacement, $html);
|
|
}
|
|
|
|
$html = (string) preg_replace('/<\?(?:php|=)[\s\S]*?\?>/i', '', $html);
|
|
$html = trim($html);
|
|
|
|
return $html;
|
|
}
|
|
|
|
private function buildOpdLetterPrintData(int $opdId, int $sessionId = 0, bool $includeContent = true): ?array
|
|
{
|
|
$sql = "Select * from opd_master where opd_id=" . (int) $opdId;
|
|
$query = $this->db->query($sql);
|
|
$opdMaster = $query->getResult();
|
|
|
|
if (empty($opdMaster)) {
|
|
return null;
|
|
}
|
|
|
|
$data = [];
|
|
$docId = (int) ($opdMaster[0]->doc_id ?? 0);
|
|
$noOpdDays = 5 - 1;
|
|
|
|
$data['doctor_master'] = [];
|
|
if ($docId > 0) {
|
|
$sql = "select d.*, group_concat(DISTINCT m.SpecName) as SpecName
|
|
from doctor_master d
|
|
left join doc_spec s on d.id=s.doc_id
|
|
left join med_spec m on m.id=s.med_spec_id
|
|
where d.id=" . $docId . "
|
|
group by d.id";
|
|
$query = $this->db->query($sql);
|
|
$data['doctor_master'] = $query->getResult();
|
|
if (!empty($data['doctor_master']) && !empty($data['doctor_master'][0]->opd_valid_no_days)) {
|
|
$noOpdDays = (int) $data['doctor_master'][0]->opd_valid_no_days - 1;
|
|
}
|
|
}
|
|
|
|
$sql = "select *,date_format(apointment_date,'%d-%m-%Y') as str_apointment_date,
|
|
date_format(date_add(apointment_date,interval " . $noOpdDays . " day),'%d-%m-%Y') as opd_Exp_Date,
|
|
(case payment_status when 1 then 'Cash' when 2 then 'Bank Card' when 3 then 'Org. Credit' else 'Pending' end) as Payment_type_str
|
|
from opd_master where opd_id=" . (int) $opdId;
|
|
$query = $this->db->query($sql);
|
|
$data['opd_master'] = $query->getResult();
|
|
|
|
$opdRow = $data['opd_master'][0];
|
|
|
|
if (!isset($opdRow->doc_sign) || trim((string) $opdRow->doc_sign) === '') {
|
|
$opdRow->doc_sign = (string) ($data['doctor_master'][0]->doc_sign ?? '');
|
|
}
|
|
|
|
$sql = "select *,if(gender=1,'M','F') as xgender
|
|
from patient_master where id='" . (int) $opdRow->p_id . "'";
|
|
$query = $this->db->query($sql);
|
|
$data['patient_master'] = $query->getResult();
|
|
|
|
if (!empty($data['patient_master'])) {
|
|
$pRow = $data['patient_master'][0];
|
|
$pRow->age = get_age_1($pRow->dob ?? null, $pRow->age ?? '', $pRow->age_in_month ?? '', $pRow->estimate_dob ?? '', $opdRow->apointment_date ?? null);
|
|
}
|
|
|
|
$sql = "select *,date_format(date_add(apointment_date,interval " . $noOpdDays . " day),'%d-%m-%Y') as opd_Exp_Date
|
|
from opd_master where p_id=" . (int) $opdRow->p_id . " and opd_id < " . (int) $opdId . " order by opd_id desc limit 1";
|
|
$query = $this->db->query($sql);
|
|
$data['old_opd'] = $query->getResult();
|
|
|
|
$sql = "select * from hc_insurance where id=" . (int) ($opdRow->insurance_id ?? 0);
|
|
$query = $this->db->query($sql);
|
|
$data['insurance'] = $query->getResult();
|
|
|
|
if ((int) ($opdRow->insurance_case_id ?? 0) > 0) {
|
|
$sql = "select * from organization_case_master where id=" . (int) $opdRow->insurance_case_id;
|
|
$query = $this->db->query($sql);
|
|
$data['case_master'] = $query->getResult();
|
|
} else {
|
|
$data['case_master'] = [];
|
|
}
|
|
|
|
$data['opd_prescription'] = null;
|
|
if ($this->db->tableExists('opd_prescription')) {
|
|
$rowBuilder = $this->db->table('opd_prescription')
|
|
->where('opd_id', (int) $opdId);
|
|
|
|
if ($sessionId > 0) {
|
|
$rowBuilder->where('id', $sessionId);
|
|
} else {
|
|
$rowBuilder->orderBy('id', 'DESC');
|
|
}
|
|
|
|
$row = $rowBuilder->get(1)->getRowArray();
|
|
|
|
if (!empty($row)) {
|
|
if (empty($row['women_related_problems'] ?? '')) {
|
|
$row['women_related_problems'] = $this->extractWomenRelatedProblemsFromRemarks((string) ($row['Prescriber_Remarks'] ?? ''));
|
|
}
|
|
|
|
$womenStructured = $this->parseWomenStructuredDetails((string) ($row['women_related_problems'] ?? ''));
|
|
$row['women_related_problems'] = (string) ($womenStructured['women_related_problems'] ?? '');
|
|
$row['women_lmp'] = trim((string) (
|
|
$row['women_lmp']
|
|
?? $row['lmp']
|
|
?? $row['lmp_date']
|
|
?? ($womenStructured['women_lmp'] ?? '')
|
|
));
|
|
$row['women_last_baby'] = trim((string) (
|
|
$row['women_last_baby']
|
|
?? $row['last_baby']
|
|
?? $row['last_baby_details']
|
|
?? ($womenStructured['women_last_baby'] ?? '')
|
|
));
|
|
$row['women_pregnancy_related'] = trim((string) (
|
|
$row['women_pregnancy_related']
|
|
?? $row['pregnancy_related']
|
|
?? $row['pregnancy_related_problem']
|
|
?? ($womenStructured['women_pregnancy_related'] ?? '')
|
|
));
|
|
|
|
$nabhInfo = $this->hydrateNabhPrintFields(
|
|
$row,
|
|
(string) ($row['Prescriber_Remarks'] ?? '')
|
|
);
|
|
$row['drug_allergy_status'] = (string) ($nabhInfo['drug_allergy_status'] ?? '');
|
|
$row['drug_allergy_details'] = (string) ($nabhInfo['drug_allergy_details'] ?? '');
|
|
$row['adr_history'] = (string) ($nabhInfo['adr_history'] ?? '');
|
|
$row['current_medications'] = (string) ($nabhInfo['current_medications'] ?? '');
|
|
$data['opd_prescription'] = $row;
|
|
}
|
|
}
|
|
|
|
if (! $includeContent) {
|
|
$data['selected_morbidities'] = [];
|
|
$data['rx_medicines'] = [];
|
|
$data['rx_investigations'] = [];
|
|
$data['rx_advices'] = [];
|
|
return $data;
|
|
}
|
|
|
|
$data['selected_morbidities'] = [];
|
|
if ($this->db->tableExists('patient_morbidities') && $this->db->tableExists('morbidities_master')) {
|
|
$pmFields = $this->db->getFieldNames('patient_morbidities');
|
|
$mmFields = $this->db->getFieldNames('morbidities_master');
|
|
|
|
$pmPatientField = $this->resolveFirstField($pmFields, ['p_id', 'patient_id']);
|
|
$pmMorField = $this->resolveFirstField($pmFields, ['morbidities', 'mor_id', 'morbidity_id']);
|
|
$mmMorField = $this->resolveFirstField($mmFields, ['mor_id', 'id']);
|
|
$mmNameField = $this->resolveFirstField($mmFields, ['morbidities', 'name', 'title']);
|
|
|
|
if ($pmPatientField !== null && $pmMorField !== null && $mmMorField !== null && $mmNameField !== null) {
|
|
$rows = $this->db->table('patient_morbidities pm')
|
|
->select('m.' . $mmNameField . ' as morbidities')
|
|
->join('morbidities_master m', 'm.' . $mmMorField . '=pm.' . $pmMorField, 'inner')
|
|
->where('pm.' . $pmPatientField, (int) ($opdRow->p_id ?? 0))
|
|
->orderBy('m.' . $mmNameField, 'ASC')
|
|
->get()
|
|
->getResultArray();
|
|
|
|
foreach ($rows as $morRow) {
|
|
$name = trim((string) ($morRow['morbidities'] ?? ''));
|
|
if ($name !== '') {
|
|
$data['selected_morbidities'][] = $name;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
$data['rx_medicines'] = [];
|
|
$data['rx_investigations'] = [];
|
|
$data['rx_advices'] = [];
|
|
|
|
$sessionId = (int) ($data['opd_prescription']['id'] ?? 0);
|
|
|
|
$medTable = $this->findExistingTable(['opd_prescrption_prescribed', 'opd_prescription_prescribed']);
|
|
if ($medTable !== null) {
|
|
$medFields = $this->db->getFieldNames($medTable);
|
|
$medBuilder = $this->db->table($medTable);
|
|
$hasFilter = false;
|
|
if (in_array('opd_pre_id', $medFields, true) && $sessionId > 0) {
|
|
$medBuilder->where('opd_pre_id', $sessionId);
|
|
$hasFilter = true;
|
|
} elseif (in_array('opd_id', $medFields, true)) {
|
|
$medBuilder->where('opd_id', (int) $opdId);
|
|
$hasFilter = true;
|
|
}
|
|
if ($hasFilter) {
|
|
if (in_array('id', $medFields, true)) {
|
|
$medBuilder->orderBy('id', 'ASC');
|
|
}
|
|
$data['rx_medicines'] = $medBuilder->get()->getResultArray();
|
|
} else {
|
|
$data['rx_medicines'] = [];
|
|
}
|
|
}
|
|
|
|
$invTable = $this->findExistingTable(['opd_prescription_investigation']);
|
|
if ($invTable !== null) {
|
|
$invFields = $this->db->getFieldNames($invTable);
|
|
$invBuilder = $this->db->table($invTable);
|
|
$hasFilter = false;
|
|
if (in_array('opd_pre_id', $invFields, true) && $sessionId > 0) {
|
|
$invBuilder->where('opd_pre_id', $sessionId);
|
|
$hasFilter = true;
|
|
} elseif (in_array('opd_id', $invFields, true)) {
|
|
$invBuilder->where('opd_id', (int) $opdId);
|
|
$hasFilter = true;
|
|
}
|
|
if ($hasFilter) {
|
|
if (in_array('investigation_name', $invFields, true)) {
|
|
$invBuilder->orderBy('investigation_name', 'ASC');
|
|
} elseif (in_array('id', $invFields, true)) {
|
|
$invBuilder->orderBy('id', 'ASC');
|
|
}
|
|
$data['rx_investigations'] = $invBuilder->get()->getResultArray();
|
|
} else {
|
|
$data['rx_investigations'] = [];
|
|
}
|
|
}
|
|
|
|
$adviceTable = $this->findExistingTable(['opd_prescription_advice']);
|
|
if ($adviceTable !== null) {
|
|
$advFields = $this->db->getFieldNames($adviceTable);
|
|
$advBuilder = $this->db->table($adviceTable);
|
|
$hasFilter = false;
|
|
if (in_array('opd_pre_id', $advFields, true) && $sessionId > 0) {
|
|
$advBuilder->where('opd_pre_id', $sessionId);
|
|
$hasFilter = true;
|
|
} elseif (in_array('opd_id', $advFields, true)) {
|
|
$advBuilder->where('opd_id', (int) $opdId);
|
|
$hasFilter = true;
|
|
}
|
|
if ($hasFilter) {
|
|
if (in_array('id', $advFields, true)) {
|
|
$advBuilder->orderBy('id', 'ASC');
|
|
}
|
|
$data['rx_advices'] = $advBuilder->get()->getResultArray();
|
|
} else {
|
|
$data['rx_advices'] = [];
|
|
}
|
|
}
|
|
|
|
if (!empty($data['rx_advices']) && $this->db->tableExists('opd_advice')) {
|
|
$masterFields = $this->db->getFieldNames('opd_advice');
|
|
$masterIdField = $this->resolveFirstField($masterFields, ['id', 'advice_id']);
|
|
$masterAdviceField = $this->resolveFirstField($masterFields, ['advice', 'advice_txt', 'advice_text']);
|
|
$masterHindiField = $this->resolveFirstField($masterFields, ['advice_hindi', 'advice_local', 'hindi_advice', 'advice_hin']);
|
|
|
|
if ($masterAdviceField !== null && $masterHindiField !== null) {
|
|
$rxAdviceIds = [];
|
|
$rxAdviceTexts = [];
|
|
foreach ($data['rx_advices'] as $advRow) {
|
|
$advIdVal = trim((string) ($advRow['advice_id'] ?? ''));
|
|
if ($advIdVal !== '') {
|
|
$rxAdviceIds[] = $advIdVal;
|
|
}
|
|
|
|
$advTextVal = trim((string) ($advRow['advice_txt'] ?? ($advRow['advice'] ?? '')));
|
|
if ($advTextVal !== '') {
|
|
$rxAdviceTexts[] = $advTextVal;
|
|
}
|
|
}
|
|
|
|
$rxAdviceIds = array_values(array_unique($rxAdviceIds));
|
|
$rxAdviceTexts = array_values(array_unique($rxAdviceTexts));
|
|
|
|
$selectParts = [
|
|
$masterAdviceField . ' as advice_text',
|
|
$masterHindiField . ' as advice_hindi',
|
|
];
|
|
if ($masterIdField !== null) {
|
|
$selectParts[] = $masterIdField . ' as advice_id';
|
|
}
|
|
|
|
$masterBuilder = $this->db->table('opd_advice')
|
|
->select(implode(',', $selectParts))
|
|
->where($masterAdviceField . ' !=', '');
|
|
|
|
$hasScopedFilter = false;
|
|
if ($masterIdField !== null && !empty($rxAdviceIds)) {
|
|
$masterBuilder->whereIn($masterIdField, $rxAdviceIds);
|
|
$hasScopedFilter = true;
|
|
}
|
|
|
|
if (!empty($rxAdviceTexts)) {
|
|
if ($hasScopedFilter) {
|
|
$masterBuilder->orWhereIn($masterAdviceField, $rxAdviceTexts);
|
|
} else {
|
|
$masterBuilder->whereIn($masterAdviceField, $rxAdviceTexts);
|
|
}
|
|
$hasScopedFilter = true;
|
|
}
|
|
|
|
if (!$hasScopedFilter) {
|
|
$masterRows = [];
|
|
} else {
|
|
$masterRows = $masterBuilder->limit(500)->get()->getResultArray();
|
|
}
|
|
|
|
$byId = [];
|
|
$byText = [];
|
|
foreach ($masterRows as $mRow) {
|
|
$rawHindi = (string) ($mRow['advice_hindi'] ?? '');
|
|
if ($rawHindi === '') {
|
|
continue;
|
|
}
|
|
|
|
$hindi = trim((string) preg_replace('/\r\n|\r|\n/', "\n", str_replace(['<br/>', '<br />', '<br>'], "\n", strip_tags($rawHindi))));
|
|
if ($hindi === '') {
|
|
continue;
|
|
}
|
|
|
|
$adviceText = trim((string) ($mRow['advice_text'] ?? ''));
|
|
$textKey = mb_strtolower(preg_replace('/\s+/', ' ', $adviceText) ?? $adviceText);
|
|
if ($textKey !== '') {
|
|
$byText[$textKey] = $hindi;
|
|
}
|
|
|
|
$idKey = trim((string) ($mRow['advice_id'] ?? ''));
|
|
if ($idKey !== '') {
|
|
$byId[$idKey] = $hindi;
|
|
}
|
|
}
|
|
|
|
foreach ($data['rx_advices'] as $idx => $advRow) {
|
|
$currentHindi = trim((string) ($advRow['advice_hindi'] ?? ''));
|
|
if ($currentHindi !== '') {
|
|
continue;
|
|
}
|
|
|
|
$resolved = '';
|
|
$advIdKey = trim((string) ($advRow['advice_id'] ?? ''));
|
|
if ($advIdKey !== '' && isset($byId[$advIdKey])) {
|
|
$resolved = (string) $byId[$advIdKey];
|
|
}
|
|
|
|
if ($resolved === '') {
|
|
$advText = trim((string) ($advRow['advice_txt'] ?? ($advRow['advice'] ?? '')));
|
|
$advTextKey = mb_strtolower(preg_replace('/\s+/', ' ', $advText) ?? $advText);
|
|
if ($advTextKey !== '' && isset($byText[$advTextKey])) {
|
|
$resolved = (string) $byText[$advTextKey];
|
|
}
|
|
}
|
|
|
|
if ($resolved !== '') {
|
|
$data['rx_advices'][$idx]['advice_hindi'] = $resolved;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
public function confirm_payment()
|
|
{
|
|
if (!$this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Invalid request',
|
|
]);
|
|
}
|
|
|
|
$mode = (int) $this->request->getPost('mode');
|
|
$oid = (int) $this->request->getPost('oid');
|
|
$spid = (string) $this->request->getPost('spid');
|
|
|
|
if ($oid <= 0) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Invalid OPD ID',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$sql = "select * from opd_master where opd_id='" . $oid . "'";
|
|
$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(),
|
|
]);
|
|
}
|
|
|
|
$sql = "select coalesce(sum(if(credit_debit=0,amount,amount*-1)),0) as opd_pay
|
|
from payment_history where payof_id='" . $oid . "' and payof_type=1";
|
|
$query = $this->db->query($sql);
|
|
$chkPayment = $query->getResult();
|
|
|
|
$paidAmount = (float) ($chkPayment[0]->opd_pay ?? 0);
|
|
$pendingAmt = (float) $opdMaster[0]->opd_fee_amount - $paidAmount;
|
|
|
|
$payRemark = '';
|
|
if (!empty($opdMaster[0]->opd_discount) && (float) $opdMaster[0]->opd_discount > 0) {
|
|
$payRemark = 'Dis.Amt.:' . $opdMaster[0]->opd_disc_remark . ' /Amount: ' . $opdMaster[0]->opd_discount .
|
|
'/Update:' . $opdMaster[0]->opd_disc_update_by;
|
|
}
|
|
|
|
$user = auth()->user();
|
|
$userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User';
|
|
$userId = $user->id ?? 0;
|
|
$userNameInfo = $userLabel . '[' . date('d-m-Y H:i:s') . ']';
|
|
|
|
$paymentModel = new PaymentModel();
|
|
$opdModel = new OpdModel();
|
|
|
|
if ($mode === 0) {
|
|
$paydata = [
|
|
'payment_mode' => '0',
|
|
'payof_type' => '1',
|
|
'payof_id' => $oid,
|
|
'payof_code' => $opdMaster[0]->opd_code,
|
|
'credit_debit' => '0',
|
|
'amount' => $opdMaster[0]->opd_fee_amount,
|
|
'payment_date' => date('Y-m-d H:i:s'),
|
|
'remark' => $payRemark,
|
|
'update_by' => $userNameInfo . ' [' . $userId . ']',
|
|
'update_by_id' => $userId,
|
|
'insert_code' => $spid,
|
|
];
|
|
|
|
$insertId = $paymentModel->insertPayment($paydata);
|
|
|
|
$data = [
|
|
'payment_mode' => '0',
|
|
'payment_status' => '1',
|
|
'payment_mode_desc' => 'No Cost',
|
|
'payment_id' => $insertId,
|
|
'confirm_pay_opd' => date('Y-m-d H:i:s'),
|
|
'prepared_by_id' => $userId,
|
|
];
|
|
|
|
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'showcontent' => 'Zero Cost',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
if ($mode === 1 && $pendingAmt > 0) {
|
|
$paydata = [
|
|
'payment_mode' => '1',
|
|
'payof_type' => '1',
|
|
'payof_id' => $oid,
|
|
'payof_code' => $opdMaster[0]->opd_code,
|
|
'credit_debit' => '0',
|
|
'amount' => $opdMaster[0]->opd_fee_amount,
|
|
'payment_date' => date('Y-m-d H:i:s'),
|
|
'remark' => $payRemark,
|
|
'update_by' => $userNameInfo . ' [' . $userId . ']',
|
|
'update_by_id' => $userId,
|
|
'insert_code' => $spid,
|
|
];
|
|
|
|
$insertId = $paymentModel->insertPayment($paydata);
|
|
|
|
$data = [
|
|
'payment_mode' => '1',
|
|
'payment_status' => '1',
|
|
'payment_mode_desc' => 'Cash',
|
|
'payment_id' => $insertId,
|
|
'confirm_pay_opd' => date('Y-m-d H:i:s'),
|
|
'prepared_by_id' => $userId,
|
|
];
|
|
|
|
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'showcontent' => 'CASH',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
if ($mode === 2 && $pendingAmt > 0) {
|
|
$cardTran = trim((string) $this->request->getPost('input_card_tran'));
|
|
if ($cardTran === '' || strlen($cardTran) < 3 || strlen($cardTran) > 15) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Card Transaction ID is required (3-15 chars).',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$paydata = [
|
|
'payment_mode' => '2',
|
|
'payof_type' => '1',
|
|
'payof_id' => $oid,
|
|
'payof_code' => $opdMaster[0]->opd_code,
|
|
'credit_debit' => '0',
|
|
'amount' => $opdMaster[0]->opd_fee_amount,
|
|
'payment_date' => date('Y-m-d H:i:s'),
|
|
'remark' => $payRemark,
|
|
'update_by' => $userNameInfo . ' [' . $userId . ']',
|
|
'pay_bank_id' => (int) $this->request->getPost('cbo_pay_type'),
|
|
'card_tran_id' => $cardTran,
|
|
'update_by_id' => $userId,
|
|
];
|
|
|
|
$insertId = $paymentModel->insertPayment($paydata);
|
|
|
|
$data = [
|
|
'payment_mode' => '2',
|
|
'payment_status' => '1',
|
|
'payment_mode_desc' => 'Bank/Online',
|
|
'confirm_pay_opd' => date('Y-m-d H:i:s'),
|
|
'payment_id' => $insertId,
|
|
'prepared_by_id' => $userId,
|
|
];
|
|
|
|
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'showcontent' => 'Bank Card',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
if ($mode === 4) {
|
|
$data = [
|
|
'payment_mode' => '4',
|
|
'payment_status' => '1',
|
|
'payment_mode_desc' => 'Org.Case Credit',
|
|
'confirm_pay_opd' => date('Y-m-d H:i:s'),
|
|
'insurance_case_id' => (int) $this->request->getPost('case_id'),
|
|
'prepared_by_id' => $userId,
|
|
];
|
|
|
|
$this->db->table('opd_master')->where('opd_id', $oid)->update($data);
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'showcontent' => 'Org. Case Credit',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Invalid payment request',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
public function opd_discount_update(int $opdId)
|
|
{
|
|
if (!$this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Invalid request',
|
|
]);
|
|
}
|
|
|
|
$opdId = (int) $this->request->getPost('oid');
|
|
if ($opdId <= 0) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Invalid OPD ID',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$discount = (float) $this->request->getPost('input_dis_amt');
|
|
$remark = (string) $this->request->getPost('input_dis_desc');
|
|
|
|
$user = auth()->user();
|
|
$userLabel = $user ? ($user->username ?? $user->email ?? 'User') : 'User';
|
|
$userId = $user->id ?? 0;
|
|
$userName = $userLabel . '[' . $userId . ']';
|
|
|
|
$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(),
|
|
]);
|
|
}
|
|
|
|
$grossAmount = (float) ($opdMaster[0]->opd_fee_gross_amount ?? 0);
|
|
$netAmount = $grossAmount - $discount;
|
|
if ($netAmount < 0) {
|
|
$netAmount = 0;
|
|
}
|
|
|
|
$dataUpdate = [
|
|
'opd_discount' => $discount,
|
|
'opd_disc_remark' => $remark,
|
|
'opd_fee_amount' => $netAmount,
|
|
'opd_disc_update_by' => $userName,
|
|
];
|
|
|
|
$this->db->table('opd_master')->where('opd_id', $opdId)->update($dataUpdate);
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'showcontent' => 'Discount updated',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Load scan/upload modal context for a specific OPD record.
|
|
*/
|
|
public function opd_load_doc(int $opdid)
|
|
{
|
|
$sql = "select *,date_format(apointment_date,'%d-%m-%Y') as str_apointment_date,
|
|
(case payment_status when 1 then 'Cash' when 2 then 'Bank Card' when 3 then 'Org. Credit' else 'Pending' end) as Payment_type_str
|
|
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];
|
|
$sql = "select *,if(gender=1,'Male','Female') as xgender from patient_master where id='" . (int) $opdRow->p_id . "'";
|
|
$query = $this->db->query($sql);
|
|
$personInfo = $query->getResult();
|
|
if (!empty($personInfo)) {
|
|
$personInfo[0]->age = get_age_1($personInfo[0]->dob ?? null, $personInfo[0]->age ?? '', $personInfo[0]->age_in_month ?? '', $personInfo[0]->estimate_dob ?? '', $opdRow->apointment_date ?? null);
|
|
}
|
|
|
|
return view('billing/opd_scan_modal_body', [
|
|
'opdid' => $opdid,
|
|
'opd_master' => $opdMaster,
|
|
'person_info' => $personInfo,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Save uploaded or captured scan file and queue it for asynchronous AI analysis.
|
|
*/
|
|
public function save_image(int $opdid)
|
|
{
|
|
$opdRow = $this->db->table('opd_master')->where('opd_id', $opdid)->get(1)->getRowArray();
|
|
if (empty($opdRow)) {
|
|
return $this->response->setStatusCode(404)->setJSON(['update' => 0, 'error_text' => 'OPD not found']);
|
|
}
|
|
|
|
$uploadsDir = rtrim(FCPATH, '/\\') . '/uploads/' . date('Ymd');
|
|
if (!is_dir($uploadsDir)) {
|
|
@mkdir($uploadsDir, 0777, true);
|
|
}
|
|
|
|
$baseFilename = 'OPD-' . $opdid . '-' . time();
|
|
$filename = $baseFilename . '.jpg';
|
|
$fullPath = $uploadsDir . '/' . $filename;
|
|
$detectedMimeType = 'image/jpeg';
|
|
$isImageFile = 1;
|
|
$detectedExt = '.jpg';
|
|
|
|
$saved = false;
|
|
|
|
// Source 1: direct webcam file upload.
|
|
$uploadedFile = $this->request->getFile('webcam');
|
|
if ($uploadedFile && $uploadedFile->isValid() && !$uploadedFile->hasMoved()) {
|
|
$ext = strtolower((string) ($uploadedFile->getClientExtension() ?: $uploadedFile->guessExtension() ?: 'jpg'));
|
|
if (!in_array($ext, ['jpg', 'jpeg', 'png', 'webp'], true)) {
|
|
$ext = 'jpg';
|
|
}
|
|
$filename = $baseFilename . '.' . $ext;
|
|
$fullPath = $uploadsDir . '/' . $filename;
|
|
$uploadedFile->move($uploadsDir, $filename);
|
|
$saved = is_file($fullPath);
|
|
$detectedMimeType = (string) ($uploadedFile->getClientMimeType() ?: 'image/jpeg');
|
|
$isImageFile = 1;
|
|
$detectedExt = '.' . $ext;
|
|
}
|
|
|
|
// Source 2: manual file upload (PDF/image).
|
|
if (!$saved) {
|
|
$uploadedFile = $this->request->getFile('userfile');
|
|
if ($uploadedFile && $uploadedFile->isValid() && !$uploadedFile->hasMoved()) {
|
|
$ext = strtolower((string) ($uploadedFile->getClientExtension() ?: $uploadedFile->guessExtension() ?: pathinfo((string) $uploadedFile->getName(), PATHINFO_EXTENSION)));
|
|
if (!in_array($ext, ['pdf', 'jpg', 'jpeg', 'png', 'webp'], true)) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Invalid file type. Upload PDF or image only.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
$filename = $baseFilename . '.' . $ext;
|
|
$fullPath = $uploadsDir . '/' . $filename;
|
|
$uploadedFile->move($uploadsDir, $filename);
|
|
$saved = is_file($fullPath);
|
|
$detectedMimeType = (string) ($uploadedFile->getClientMimeType() ?: ($ext === 'pdf' ? 'application/pdf' : 'image/jpeg'));
|
|
$isImageFile = $ext === 'pdf' ? 0 : 1;
|
|
$detectedExt = '.' . $ext;
|
|
}
|
|
}
|
|
|
|
// Source 3: base64 image data fallback from older capture flows.
|
|
if (!$saved) {
|
|
$dataUri = (string) ($this->request->getPost('webcam_data') ?? $this->request->getPost('image') ?? '');
|
|
if ($dataUri !== '' && preg_match('#^data:image/\w+;base64,#i', $dataUri) === 1) {
|
|
preg_match('#^data:image/(\w+);base64,#i', $dataUri, $match);
|
|
$imgExt = strtolower((string) ($match[1] ?? 'jpg'));
|
|
if ($imgExt === 'jpeg') {
|
|
$imgExt = 'jpg';
|
|
}
|
|
if (!in_array($imgExt, ['jpg', 'png', 'webp'], true)) {
|
|
$imgExt = 'jpg';
|
|
}
|
|
$filename = $baseFilename . '.' . $imgExt;
|
|
$fullPath = $uploadsDir . '/' . $filename;
|
|
$raw = base64_decode((string) preg_replace('#^data:image/\w+;base64,#i', '', $dataUri));
|
|
if ($raw !== false && $raw !== '') {
|
|
file_put_contents($fullPath, $raw);
|
|
$saved = is_file($fullPath);
|
|
$detectedMimeType = 'image/' . ($imgExt === 'jpg' ? 'jpeg' : $imgExt);
|
|
$isImageFile = 1;
|
|
$detectedExt = '.' . $imgExt;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$saved) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Unable to capture image. Please allow camera and retry.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$user = auth()->user();
|
|
$userId = (int) ($user->id ?? 0);
|
|
$userName = (string) ($user->username ?? $user->email ?? 'User');
|
|
|
|
$publicPath = '/uploads/' . date('Ymd') . '/' . $filename;
|
|
$insertId = $this->insertOpdFileUploadRecord(
|
|
$fullPath,
|
|
$publicPath,
|
|
(int) ($opdRow['p_id'] ?? 0),
|
|
$opdid,
|
|
(int) ($opdRow['insurance_case_id'] ?? 0),
|
|
$userName,
|
|
$userId,
|
|
$detectedMimeType,
|
|
$isImageFile,
|
|
$detectedExt
|
|
);
|
|
|
|
$scanType = $this->detectScanType((string) $filename);
|
|
|
|
if ($insertId > 0 && $this->db->tableExists('file_upload_data')) {
|
|
$fields = $this->db->getFieldNames('file_upload_data');
|
|
$update = [];
|
|
if (in_array('scan_type', $fields, true)) {
|
|
$update['scan_type'] = $scanType;
|
|
}
|
|
if (in_array('document_type', $fields, true)) {
|
|
$update['document_type'] = '';
|
|
}
|
|
if (in_array('content_description', $fields, true)) {
|
|
$update['content_description'] = 'Queued for AI analysis';
|
|
}
|
|
if (in_array('ai_status', $fields, true)) {
|
|
$update['ai_status'] = 'pending';
|
|
}
|
|
if (in_array('ai_alert_flag', $fields, true)) {
|
|
$update['ai_alert_flag'] = 0;
|
|
}
|
|
if (!empty($update)) {
|
|
$this->db->table('file_upload_data')->where('id', $insertId)->update($update);
|
|
}
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'filename' => $filename,
|
|
'file_path' => $publicPath,
|
|
'file_id' => $insertId,
|
|
'scan_type' => $scanType,
|
|
'extracted_text' => '',
|
|
'auto_saved_to_prescription' => 0,
|
|
'opd_session_id' => 0,
|
|
'target_field' => '',
|
|
'ai_status' => 'pending',
|
|
'error_text' => 'File uploaded successfully',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
public function opd_file_last_list(int $opdid = 0)
|
|
{
|
|
if (!$this->db->tableExists('file_upload_data')) {
|
|
return $this->response->setBody('<div class="text-muted">No scan table found.</div>');
|
|
}
|
|
|
|
$fields = $this->db->getFieldNames('file_upload_data');
|
|
$safeSelect = [
|
|
'id',
|
|
'full_path',
|
|
'file_ext',
|
|
'insert_date',
|
|
'pid',
|
|
'opd_id',
|
|
'show_type',
|
|
];
|
|
foreach (['scan_type', 'document_type', 'content_description', 'ai_status', 'ai_alert_flag', 'ai_alert_text'] as $optional) {
|
|
if (in_array($optional, $fields, true)) {
|
|
$safeSelect[] = $optional;
|
|
}
|
|
}
|
|
if (in_array('ai_provider', $fields, true)) {
|
|
$safeSelect[] = 'ai_provider';
|
|
}
|
|
|
|
$builder = $this->db->table('file_upload_data')
|
|
->select(implode(',', $safeSelect))
|
|
->where('show_type', 0)
|
|
->orderBy('id', 'DESC')
|
|
->limit(10);
|
|
|
|
if ($opdid > 0) {
|
|
$builder->where('opd_id', $opdid);
|
|
}
|
|
|
|
$rows = $builder->get()->getResultArray();
|
|
$list = [];
|
|
foreach ($rows as $row) {
|
|
$raw = (string) ($row['full_path'] ?? '');
|
|
$publicPath = $raw;
|
|
$pos = strpos(str_replace('\\', '/', $raw), '/uploads/');
|
|
if ($pos !== false) {
|
|
$publicPath = substr(str_replace('\\', '/', $raw), $pos);
|
|
}
|
|
|
|
$ext = strtolower((string) ($row['file_ext'] ?? pathinfo($publicPath, PATHINFO_EXTENSION)));
|
|
if ($ext !== '' && $ext[0] !== '.') {
|
|
$ext = '.' . $ext;
|
|
}
|
|
|
|
$list[] = [
|
|
'id' => (int) ($row['id'] ?? 0),
|
|
'path' => $publicPath,
|
|
'is_pdf' => $ext === '.pdf',
|
|
'insert_date' => !empty($row['insert_date']) ? date('d/m/Y-H:i', strtotime((string) $row['insert_date'])) : '',
|
|
'scan_type' => (string) ($row['scan_type'] ?? ''),
|
|
'document_type' => (string) ($row['document_type'] ?? ''),
|
|
'content_description' => (string) ($row['content_description'] ?? ''),
|
|
'ai_status' => (string) ($row['ai_status'] ?? ''),
|
|
'ai_alert_flag' => (int) ($row['ai_alert_flag'] ?? 0),
|
|
'ai_alert_text' => (string) ($row['ai_alert_text'] ?? ''),
|
|
'ai_provider' => (string) ($row['ai_provider'] ?? ''),
|
|
];
|
|
}
|
|
|
|
return view('billing/opd_scan_last_list', ['opd_file_list' => $list]);
|
|
}
|
|
|
|
public function scan_ai_assist()
|
|
{
|
|
if (! $this->request->isAJAX()) {
|
|
return $this->response->setStatusCode(400)->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Invalid request',
|
|
]);
|
|
}
|
|
|
|
$opdid = (int) $this->request->getPost('opd_id');
|
|
$scanType = trim((string) $this->request->getPost('scan_type'));
|
|
if ($scanType === '') {
|
|
$scanType = 'general';
|
|
}
|
|
|
|
$text = trim((string) $this->request->getPost('extracted_text'));
|
|
if ($text === '' || strlen($text) < 20) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Extracted report text is too short for AI diagnosis assist.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$analysis = $this->analyzeScanForDiagnosisSupport($text, $scanType);
|
|
if ($analysis === null) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'AI diagnosis assist unavailable right now.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$applyToOpd = (int) $this->request->getPost('apply_to_opd') === 1;
|
|
$applied = false;
|
|
$sessionId = 0;
|
|
$updatedFields = [];
|
|
|
|
if ($applyToOpd && $opdid > 0) {
|
|
$applyResult = $this->applyAiScanAssistToPrescription($opdid, $analysis);
|
|
$applied = (bool) ($applyResult['applied'] ?? false);
|
|
$sessionId = (int) ($applyResult['session_id'] ?? 0);
|
|
$updatedFields = is_array($applyResult['updated_fields'] ?? null) ? $applyResult['updated_fields'] : [];
|
|
}
|
|
|
|
return $this->response->setJSON([
|
|
'update' => 1,
|
|
'analysis' => $analysis,
|
|
'applied' => $applied ? 1 : 0,
|
|
'opd_session_id' => $sessionId,
|
|
'updated_fields' => $updatedFields,
|
|
'error_text' => $applied ? 'AI diagnosis assist prepared and added to consult fields.' : 'AI diagnosis assist prepared.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Process one uploaded scan file through AI and update per-file metadata.
|
|
*/
|
|
public function scan_ai_process_file()
|
|
{
|
|
try {
|
|
$fileId = (int) $this->request->getPost('file_id');
|
|
if ($fileId <= 0) {
|
|
return $this->response->setJSON([
|
|
'update' => 0,
|
|
'error_text' => 'Invalid file ID.',
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
|
|
$applyToOpd = (int) $this->request->getPost('apply_to_opd') === 1;
|
|
$result = $this->processAiForUploadedFile($fileId, $applyToOpd);
|
|
|
|
return $this->response->setJSON([
|
|
'update' => (int) ($result['update'] ?? 0),
|
|
'file_id' => $fileId,
|
|
'ai_status' => (string) ($result['ai_status'] ?? 'failed'),
|
|
'document_type' => (string) ($result['document_type'] ?? ''),
|
|
'content_description' => (string) ($result['content_description'] ?? ''),
|
|
'ai_alert_flag' => (int) ($result['ai_alert_flag'] ?? 0),
|
|
'ai_alert_text' => (string) ($result['ai_alert_text'] ?? ''),
|
|
'error_text' => (string) ($result['error_text'] ?? ''),
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'scan_ai_process_file failed: {message}', ['message' => $e->getMessage()]);
|
|
|
|
return $this->response->setStatusCode(200)->setJSON([
|
|
'update' => 0,
|
|
'ai_status' => 'failed',
|
|
'error_text' => 'Background AI processing failed.',
|
|
'csrfName' => csrf_token(),
|
|
'csrfHash' => csrf_hash(),
|
|
]);
|
|
}
|
|
}
|
|
|
|
public function opd_file_hide(int $fileId)
|
|
{
|
|
if (!$this->db->tableExists('file_upload_data')) {
|
|
return $this->response->setStatusCode(404)->setBody('File table not found');
|
|
}
|
|
|
|
$row = $this->db->table('file_upload_data')->select('id,opd_id')->where('id', $fileId)->get(1)->getRowArray();
|
|
if (empty($row)) {
|
|
return $this->response->setStatusCode(404)->setBody('File not found');
|
|
}
|
|
|
|
$user = auth()->user();
|
|
$userId = (int) ($user->id ?? 0);
|
|
|
|
$fields = $this->db->getFieldNames('file_upload_data');
|
|
$update = ['show_type' => 1];
|
|
if (in_array('upload_by_id', $fields, true)) {
|
|
$update['upload_by_id'] = $userId;
|
|
}
|
|
$this->db->table('file_upload_data')->where('id', $fileId)->update($update);
|
|
|
|
return $this->opd_file_last_list((int) ($row['opd_id'] ?? 0));
|
|
}
|
|
|
|
private function insertOpdFileUploadRecord(string $fullPath, string $publicPath, int $pid, int $opdid, int $caseId, string $uploadBy, int $uploadById, string $mimeType = 'image/jpeg', int $isImageFile = 1, string $fileExt = '.jpg'): int
|
|
{
|
|
if (!$this->db->tableExists('file_upload_data')) {
|
|
return 0;
|
|
}
|
|
|
|
$fields = $this->db->getFieldNames('file_upload_data');
|
|
$ext = $fileExt !== '' ? $fileExt : ('.' . strtolower((string) pathinfo($fullPath, PATHINFO_EXTENSION)));
|
|
$mimeType = trim($mimeType) !== '' ? $mimeType : 'application/octet-stream';
|
|
$isImageFile = $isImageFile > 0 ? 1 : 0;
|
|
$insert = [];
|
|
|
|
$fieldMap = [
|
|
'name' => basename($fullPath),
|
|
'file_name' => basename($fullPath),
|
|
'orig_name' => basename($fullPath),
|
|
'client_name' => basename($fullPath),
|
|
'file_ext' => $ext,
|
|
'file_type' => $mimeType,
|
|
'full_path' => str_replace('\\', '/', $fullPath),
|
|
'file_path' => str_replace('\\', '/', dirname($fullPath)) . '/',
|
|
'raw_name' => pathinfo($fullPath, PATHINFO_FILENAME),
|
|
'file_size' => @filesize($fullPath) ? round((float) filesize($fullPath) / 1024, 2) : 0,
|
|
'is_image' => $isImageFile,
|
|
'image_width' => 0,
|
|
'image_height' => 0,
|
|
'image_type' => ltrim($ext, '.'),
|
|
'image_size_str' => '',
|
|
'insert_date' => Time::now('Asia/Kolkata')->toDateTimeString(),
|
|
'upload_by' => $uploadBy,
|
|
'upload_by_id' => $uploadById,
|
|
'pid' => $pid,
|
|
'opd_id' => $opdid,
|
|
'ipd_id' => 0,
|
|
'case_id' => $caseId,
|
|
'show_type' => 0,
|
|
'public_path' => $publicPath,
|
|
];
|
|
|
|
foreach ($fieldMap as $key => $value) {
|
|
if (in_array($key, $fields, true)) {
|
|
$insert[$key] = $value;
|
|
}
|
|
}
|
|
|
|
$this->db->table('file_upload_data')->insert($insert);
|
|
return (int) $this->db->insertID();
|
|
}
|
|
|
|
private function detectScanType(string $filename): string
|
|
{
|
|
$low = strtolower($filename);
|
|
if (str_contains($low, 'ecg')) {
|
|
return 'ecg';
|
|
}
|
|
if (str_contains($low, 'lab') || str_contains($low, 'report')) {
|
|
return 'lab';
|
|
}
|
|
|
|
return 'general';
|
|
}
|
|
|
|
/**
|
|
* Run the full AI pipeline for one uploaded scan file and persist per-file AI metadata.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function processAiForUploadedFile(int $fileId, bool $applyToOpd = false): array
|
|
{
|
|
$this->resetScanAiDebug();
|
|
|
|
if (! $this->db->tableExists('file_upload_data')) {
|
|
return ['update' => 0, 'ai_status' => 'failed', 'error_text' => 'File table not found'];
|
|
}
|
|
|
|
$fields = $this->db->getFieldNames('file_upload_data');
|
|
$select = ['id'];
|
|
foreach (['opd_id', 'full_path', 'file_ext', 'file_type', 'name', 'file_name', 'scan_type'] as $col) {
|
|
if (in_array($col, $fields, true)) {
|
|
$select[] = $col;
|
|
}
|
|
}
|
|
|
|
$row = $this->db->table('file_upload_data')
|
|
->select(implode(',', $select))
|
|
->where('id', $fileId)
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
if (empty($row)) {
|
|
return ['update' => 0, 'ai_status' => 'failed', 'error_text' => 'Scan file not found'];
|
|
}
|
|
|
|
// Mark row as processing so UI can reflect asynchronous progress.
|
|
if (in_array('ai_status', $fields, true)) {
|
|
$this->db->table('file_upload_data')->where('id', $fileId)->update(['ai_status' => 'processing']);
|
|
}
|
|
|
|
$fullPath = $this->resolveScanFilePath($row);
|
|
if ($fullPath === '' || !is_file($fullPath)) {
|
|
$this->updateScanAiColumns($fileId, [
|
|
'ai_status' => 'failed',
|
|
'content_description' => 'Scan file not accessible for AI analysis',
|
|
], $fields);
|
|
|
|
return ['update' => 0, 'ai_status' => 'failed', 'error_text' => 'Scan file not accessible'];
|
|
}
|
|
|
|
$filename = (string) ($row['name'] ?? $row['file_name'] ?? basename($fullPath));
|
|
$scanType = trim((string) ($row['scan_type'] ?? ''));
|
|
if ($scanType === '') {
|
|
$scanType = $this->detectScanType($filename);
|
|
}
|
|
|
|
$mimeType = $this->guessMimeTypeFromPath($fullPath, (string) ($row['file_type'] ?? ''));
|
|
$readiness = $this->getScanAiReadiness();
|
|
$this->addScanAiDebug('readiness: ' . $readiness['summary']);
|
|
|
|
// Fail early when neither Azure OpenAI nor Document Intelligence is configured.
|
|
if (! $readiness['azure_openai'] && ! $readiness['docintel']) {
|
|
$msg = 'AI not configured. Set Azure OpenAI deployment (vision-capable). Optional DocIntel improves OCR.';
|
|
$this->updateScanAiColumns($fileId, [
|
|
'scan_type' => $scanType,
|
|
'document_type' => $this->classifyDocumentType('', $scanType, $filename),
|
|
'content_description' => $msg,
|
|
'ai_status' => 'failed',
|
|
'ai_alert_flag' => 0,
|
|
'ai_alert_text' => '',
|
|
], $fields);
|
|
|
|
return [
|
|
'update' => 0,
|
|
'ai_status' => 'failed',
|
|
'document_type' => $this->classifyDocumentType('', $scanType, $filename),
|
|
'content_description' => $msg,
|
|
'ai_alert_flag' => 0,
|
|
'ai_alert_text' => '',
|
|
'error_text' => $msg,
|
|
];
|
|
}
|
|
|
|
// First try visual interpretation and OCR extraction in parallel-friendly sequence.
|
|
$visual = $this->analyzeScanVisualSupport($fullPath, $scanType, $mimeType);
|
|
$text = $this->extractTextFromScanFile($fullPath, $scanType, $mimeType);
|
|
if ($text === null || trim($text) === '') {
|
|
// If OCR text is weak, keep visual result as a safe fallback summary.
|
|
if (is_array($visual) && !empty($visual)) {
|
|
$documentType = trim((string) ($visual['document_type'] ?? ''));
|
|
if ($documentType === '') {
|
|
$documentType = $this->classifyDocumentType('', $scanType, $filename);
|
|
}
|
|
|
|
$contentDescription = trim((string) ($visual['content_description'] ?? ''));
|
|
$risk = strtolower(trim((string) ($visual['risk_level'] ?? '')));
|
|
$aiAlertText = trim((string) ($visual['alert_text'] ?? ''));
|
|
$detailLines = [];
|
|
if ($contentDescription !== '') {
|
|
$detailLines[] = 'Summary: ' . $contentDescription;
|
|
}
|
|
if ($risk !== '') {
|
|
$detailLines[] = 'Risk level: ' . ucfirst($risk);
|
|
}
|
|
if ($aiAlertText !== '') {
|
|
$detailLines[] = 'Alert: ' . $aiAlertText;
|
|
}
|
|
if (empty($detailLines)) {
|
|
$detailLines[] = 'AI visual review completed.';
|
|
}
|
|
$contentDescription = mb_substr(implode("\n", $detailLines), 0, 2000);
|
|
|
|
$aiAlertFlag = in_array($risk, ['high', 'critical'], true) ? 1 : 0;
|
|
if ($aiAlertFlag === 1 && $aiAlertText === '') {
|
|
$aiAlertText = 'AI flagged possible abnormal finding';
|
|
}
|
|
|
|
$this->updateScanAiColumns($fileId, [
|
|
'scan_type' => $scanType,
|
|
'document_type' => $documentType,
|
|
'content_description' => $contentDescription,
|
|
'ai_status' => 'completed',
|
|
'ai_alert_flag' => $aiAlertFlag,
|
|
'ai_alert_text' => $aiAlertText,
|
|
], $fields);
|
|
|
|
return [
|
|
'update' => 1,
|
|
'ai_status' => 'completed',
|
|
'document_type' => $documentType,
|
|
'content_description' => $contentDescription,
|
|
'ai_alert_flag' => $aiAlertFlag,
|
|
'ai_alert_text' => $aiAlertText,
|
|
'error_text' => 'AI visual analysis completed',
|
|
];
|
|
}
|
|
|
|
$documentType = $this->classifyDocumentType('', $scanType, $filename);
|
|
$debugInfo = $this->formatScanAiDebug();
|
|
$failMsg = 'Azure providers could not read usable text from this file. ' . $readiness['summary'] . '. Ensure Azure deployment supports image input and DocIntel endpoint/API is valid.';
|
|
if ($debugInfo !== '') {
|
|
$failMsg .= ' Debug: ' . $debugInfo;
|
|
}
|
|
log_message('warning', 'scan_ai_process_file extraction failed fileId={id}; {summary}', [
|
|
'id' => $fileId,
|
|
'summary' => $readiness['summary'],
|
|
]);
|
|
$this->updateScanAiColumns($fileId, [
|
|
'scan_type' => $scanType,
|
|
'document_type' => $documentType,
|
|
'content_description' => $failMsg,
|
|
'ai_status' => 'failed',
|
|
'ai_alert_flag' => 0,
|
|
'ai_alert_text' => '',
|
|
], $fields);
|
|
|
|
return [
|
|
'update' => 0,
|
|
'ai_status' => 'failed',
|
|
'document_type' => $documentType,
|
|
'content_description' => $failMsg,
|
|
'ai_alert_flag' => 0,
|
|
'ai_alert_text' => '',
|
|
'error_text' => $failMsg,
|
|
];
|
|
}
|
|
|
|
// Build structured diagnosis-support output from extracted text.
|
|
$analysis = $this->analyzeScanForDiagnosisSupport($text, $scanType);
|
|
$documentType = trim((string) ($visual['document_type'] ?? ''));
|
|
if ($documentType === '') {
|
|
$documentType = $this->classifyDocumentType($text, $scanType, $filename);
|
|
}
|
|
|
|
$visualSummary = trim((string) ($visual['content_description'] ?? ''));
|
|
$contentDescription = $this->buildScanContentDescription($analysis, $text, $documentType);
|
|
if ($visualSummary !== '') {
|
|
$contentDescription = $contentDescription !== ''
|
|
? mb_substr($visualSummary . "\n" . $contentDescription, 0, 2000)
|
|
: mb_substr($visualSummary, 0, 2000);
|
|
}
|
|
|
|
if (strtoupper($documentType) === 'ECG') {
|
|
$contentDescription = $this->buildEcgDetailedDescription($text, $analysis, $contentDescription);
|
|
}
|
|
|
|
[$aiAlertFlag, $aiAlertText] = $this->buildScanAlert($analysis, $text);
|
|
|
|
$visualRisk = strtolower(trim((string) ($visual['risk_level'] ?? '')));
|
|
if (in_array($visualRisk, ['high', 'critical'], true)) {
|
|
$aiAlertFlag = 1;
|
|
$visualAlertText = trim((string) ($visual['alert_text'] ?? ''));
|
|
if ($visualAlertText !== '') {
|
|
$aiAlertText = $visualAlertText;
|
|
}
|
|
}
|
|
|
|
// Optionally append AI support to OPD prescription notes.
|
|
if ($applyToOpd && !empty($analysis) && ((string) ($analysis['diagnosis_suitable'] ?? 'no')) === 'yes') {
|
|
$this->applyAiScanAssistToPrescription((int) ($row['opd_id'] ?? 0), $analysis);
|
|
}
|
|
|
|
$this->updateScanAiColumns($fileId, [
|
|
'scan_type' => $scanType,
|
|
'document_type' => $documentType,
|
|
'content_description' => $contentDescription,
|
|
'extract_text' => $text,
|
|
'extracted_text' => $text,
|
|
'ai_status' => 'completed',
|
|
'ai_alert_flag' => $aiAlertFlag,
|
|
'ai_alert_text' => $aiAlertText,
|
|
], $fields);
|
|
|
|
return [
|
|
'update' => 1,
|
|
'ai_status' => 'completed',
|
|
'document_type' => $documentType,
|
|
'content_description' => $contentDescription,
|
|
'ai_alert_flag' => $aiAlertFlag,
|
|
'ai_alert_text' => $aiAlertText,
|
|
'error_text' => 'AI analysis completed',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $row
|
|
*/
|
|
private function resolveScanFilePath(array $row): string
|
|
{
|
|
// Accept both stored absolute path and recovered path under /public/uploads.
|
|
$fullPath = str_replace('\\', '/', (string) ($row['full_path'] ?? ''));
|
|
if ($fullPath !== '' && is_file($fullPath)) {
|
|
return $fullPath;
|
|
}
|
|
|
|
if ($fullPath !== '') {
|
|
$uploadsPos = strpos($fullPath, '/uploads/');
|
|
if ($uploadsPos !== false) {
|
|
$relative = substr($fullPath, $uploadsPos + 1);
|
|
$candidate = rtrim(FCPATH, '/\\') . '/' . str_replace('\\', '/', (string) $relative);
|
|
if (is_file($candidate)) {
|
|
return $candidate;
|
|
}
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Infer MIME type from saved extension when upload metadata is missing.
|
|
*/
|
|
private function guessMimeTypeFromPath(string $path, string $existingMime = ''): string
|
|
{
|
|
$existingMime = trim($existingMime);
|
|
if ($existingMime !== '') {
|
|
return $existingMime;
|
|
}
|
|
|
|
$ext = strtolower((string) pathinfo($path, PATHINFO_EXTENSION));
|
|
if ($ext === 'pdf') {
|
|
return 'application/pdf';
|
|
}
|
|
if ($ext === 'png') {
|
|
return 'image/png';
|
|
}
|
|
if ($ext === 'webp') {
|
|
return 'image/webp';
|
|
}
|
|
|
|
return 'image/jpeg';
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed>|null $analysis
|
|
*/
|
|
private function buildScanContentDescription(?array $analysis, string $text, string $documentType): string
|
|
{
|
|
if (!empty($analysis)) {
|
|
$summary = trim((string) ($analysis['clinical_summary'] ?? ''));
|
|
$dx = trim((string) ($analysis['probable_diagnosis'] ?? ''));
|
|
$reason = trim((string) ($analysis['suitability_reason'] ?? ''));
|
|
$keyFindings = $this->sanitizeAiStringList($analysis['key_findings'] ?? []);
|
|
$suggestedTests = $this->sanitizeAiStringList($analysis['suggested_tests'] ?? []);
|
|
$redFlags = $this->sanitizeAiStringList($analysis['red_flags'] ?? []);
|
|
$confidence = trim((string) ($analysis['confidence_note'] ?? ''));
|
|
$parts = [];
|
|
if ($summary !== '') {
|
|
$parts[] = 'Summary: ' . $summary;
|
|
}
|
|
if ($dx !== '') {
|
|
$parts[] = 'Impression: ' . $dx;
|
|
}
|
|
if (!empty($keyFindings)) {
|
|
$parts[] = 'Findings: ' . implode('; ', array_slice($keyFindings, 0, 6));
|
|
}
|
|
if (!empty($redFlags)) {
|
|
$parts[] = 'Red flags: ' . implode('; ', array_slice($redFlags, 0, 4));
|
|
}
|
|
if (!empty($suggestedTests)) {
|
|
$parts[] = 'Suggested tests: ' . implode('; ', array_slice($suggestedTests, 0, 5));
|
|
}
|
|
if ($reason !== '') {
|
|
$parts[] = 'Suitability: ' . $reason;
|
|
}
|
|
if ($confidence !== '') {
|
|
$parts[] = 'Confidence: ' . $confidence;
|
|
}
|
|
|
|
if (!empty($parts)) {
|
|
return mb_substr(implode("\n", $parts), 0, 2000);
|
|
}
|
|
}
|
|
|
|
$plain = trim((string) preg_replace('/\s+/', ' ', $text));
|
|
if ($plain === '') {
|
|
return $documentType . ' report uploaded.';
|
|
}
|
|
|
|
return mb_substr($plain, 0, 500);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed>|null $analysis
|
|
*/
|
|
private function buildEcgDetailedDescription(string $text, ?array $analysis, string $baseSummary): string
|
|
{
|
|
$flat = trim((string) preg_replace('/\s+/', ' ', $text));
|
|
$metrics = [];
|
|
|
|
$patterns = [
|
|
'Ventricular rate' => '/(?:vent\.?\s*rate|heart\s*rate|hr)\s*[:=]?\s*(\d{2,3})\s*(?:bpm)?/i',
|
|
'PR interval' => '/pr\s*(?:int|interval)?\s*[:=]?\s*(\d{2,4})\s*(?:ms)?/i',
|
|
'QRS duration' => '/qrs\s*(?:int|duration)?\s*[:=]?\s*(\d{2,4})\s*(?:ms)?/i',
|
|
'QT/QTc' => '/qt\/?qtc\s*(?:int)?\s*[:=]?\s*([\d\.\-\s\/]{3,20})/i',
|
|
'Axis' => '/axis\s*(?:deg)?\s*[:=]?\s*([\d\-\+\s\/]{1,20})/i',
|
|
];
|
|
|
|
foreach ($patterns as $label => $regex) {
|
|
if (preg_match($regex, $flat, $match) === 1) {
|
|
$value = trim((string) ($match[1] ?? ''));
|
|
if ($value !== '') {
|
|
$metrics[] = $label . ': ' . $value;
|
|
}
|
|
}
|
|
}
|
|
|
|
$findings = [];
|
|
if (is_array($analysis)) {
|
|
$summary = trim((string) ($analysis['clinical_summary'] ?? ''));
|
|
$dx = trim((string) ($analysis['probable_diagnosis'] ?? ''));
|
|
$keyFindings = $this->sanitizeAiStringList($analysis['key_findings'] ?? []);
|
|
if ($summary !== '') {
|
|
$findings[] = $summary;
|
|
}
|
|
if ($dx !== '') {
|
|
$findings[] = 'Impression: ' . $dx;
|
|
}
|
|
if (!empty($keyFindings)) {
|
|
$findings[] = 'Findings: ' . implode('; ', array_slice($keyFindings, 0, 4));
|
|
}
|
|
}
|
|
|
|
if (empty($findings) && trim($baseSummary) !== '') {
|
|
$findings[] = trim($baseSummary);
|
|
}
|
|
|
|
$chunks = [];
|
|
if (!empty($metrics)) {
|
|
$chunks[] = implode(' | ', array_slice($metrics, 0, 5));
|
|
}
|
|
if (!empty($findings)) {
|
|
$chunks[] = implode(' | ', array_slice($findings, 0, 3));
|
|
}
|
|
|
|
if (empty($chunks)) {
|
|
return 'ECG uploaded. AI review completed.';
|
|
}
|
|
|
|
return mb_substr(implode("\n", $chunks), 0, 1000);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed>|null $analysis
|
|
* @return array{0:int,1:string}
|
|
*/
|
|
private function buildScanAlert(?array $analysis, string $text): array
|
|
{
|
|
if (!empty($analysis)) {
|
|
$redFlags = $this->sanitizeAiStringList($analysis['red_flags'] ?? []);
|
|
if (!empty($redFlags)) {
|
|
return [1, mb_substr(implode('; ', $redFlags), 0, 250)];
|
|
}
|
|
|
|
$probableDx = strtolower(trim((string) ($analysis['probable_diagnosis'] ?? '')));
|
|
foreach (['abnormal', 'acute', 'critical', 'infarct', 'ischemia', 'stroke', 'sepsis', 'hemorrhage', 'emergency'] as $keyword) {
|
|
if ($probableDx !== '' && str_contains($probableDx, $keyword)) {
|
|
return [1, 'AI flagged possible ' . $keyword . ' risk'];
|
|
}
|
|
}
|
|
}
|
|
|
|
$txt = strtolower($text);
|
|
foreach (['critical', 'urgent', 'abnormal', 'positive for', 'high risk'] as $keyword) {
|
|
if (str_contains($txt, $keyword)) {
|
|
return [1, 'Report text suggests ' . $keyword];
|
|
}
|
|
}
|
|
|
|
return [0, ''];
|
|
}
|
|
|
|
/**
|
|
* Classify broad document type using scan hints, filename and text keywords.
|
|
*/
|
|
private function classifyDocumentType(string $text, string $scanType, string $filename): string
|
|
{
|
|
$pool = strtolower($scanType . ' ' . $filename . ' ' . $text);
|
|
$map = [
|
|
'ECG' => ['ecg', 'electrocardio', 'st segment', 'qrs'],
|
|
'Pathology' => ['pathology', 'haemoglobin', 'hemoglobin', 'cbc', 'blood', 'urine', 'biochemistry', 'platelet'],
|
|
'XRay' => ['xray', 'x-ray', 'radiograph', 'chest pa'],
|
|
'CT' => ['ct scan', 'computed tomography'],
|
|
'MRI' => ['mri', 'magnetic resonance'],
|
|
'Ultrasound' => ['usg', 'ultrasound', 'sonography'],
|
|
];
|
|
|
|
foreach ($map as $type => $keywords) {
|
|
foreach ($keywords as $keyword) {
|
|
if (str_contains($pool, $keyword)) {
|
|
return $type;
|
|
}
|
|
}
|
|
}
|
|
|
|
return strtoupper($scanType) !== 'GENERAL' ? strtoupper($scanType) : 'General';
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>|null
|
|
*/
|
|
private function analyzeScanVisualSupport(string $filePath, string $scanType, string $mimeType): ?array
|
|
{
|
|
if (!is_file($filePath)) {
|
|
return null;
|
|
}
|
|
|
|
$raw = @file_get_contents($filePath);
|
|
if ($raw === false || $raw === '') {
|
|
return null;
|
|
}
|
|
|
|
$maxBytes = $mimeType === 'application/pdf' ? (12 * 1024 * 1024) : (8 * 1024 * 1024);
|
|
if (strlen($raw) > $maxBytes) {
|
|
return null;
|
|
}
|
|
|
|
$prompt = "You are a clinical assistant. Analyze this medical report image/document visually (even if OCR text is unclear).\n"
|
|
. "Return ONLY valid JSON object with keys: document_type, content_description, risk_level, alert_text.\n"
|
|
. "Rules:\n"
|
|
. "- document_type must be one of: ECG, Pathology, XRay, CT, MRI, Ultrasound, General.\n"
|
|
. "- risk_level must be one of: low, moderate, high, critical.\n"
|
|
. "- content_description should be 1 short cautious summary line.\n"
|
|
. "- alert_text should be short and only when risk is high/critical; else empty string.\n"
|
|
. "- If uncertain, set document_type based on visible format pattern and keep conservative language.\n"
|
|
. "Scan type hint: " . strtoupper($scanType);
|
|
|
|
// Ask vision-capable model for a conservative JSON summary.
|
|
$azureRaw = $this->callAzureOpenAiVision($filePath, $mimeType, $prompt, 500, true);
|
|
$azureParsed = $this->parseJsonFromAiRaw($azureRaw);
|
|
if (is_array($azureParsed)) {
|
|
$documentType = trim((string) ($azureParsed['document_type'] ?? 'General'));
|
|
if (!in_array($documentType, ['ECG', 'Pathology', 'XRay', 'CT', 'MRI', 'Ultrasound', 'General'], true)) {
|
|
$documentType = 'General';
|
|
}
|
|
|
|
$riskLevel = strtolower(trim((string) ($azureParsed['risk_level'] ?? 'low')));
|
|
if (!in_array($riskLevel, ['low', 'moderate', 'high', 'critical'], true)) {
|
|
$riskLevel = 'low';
|
|
}
|
|
|
|
return [
|
|
'document_type' => $documentType,
|
|
'content_description' => trim((string) ($azureParsed['content_description'] ?? '')),
|
|
'risk_level' => $riskLevel,
|
|
'alert_text' => trim((string) ($azureParsed['alert_text'] ?? '')),
|
|
];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @param array<int, string> $fields
|
|
*/
|
|
private function updateScanAiColumns(int $fileId, array $payload, array $fields): void
|
|
{
|
|
$update = [];
|
|
foreach ($payload as $key => $value) {
|
|
if (in_array($key, $fields, true)) {
|
|
$update[$key] = $value;
|
|
}
|
|
}
|
|
|
|
if (!empty($update)) {
|
|
$this->db->table('file_upload_data')->where('id', $fileId)->update($update);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private function analyzeScanForDiagnosisSupport(string $extractedText, string $scanType): ?array
|
|
{
|
|
// No text means there is nothing reliable to assess.
|
|
if (trim($extractedText) === '') {
|
|
return null;
|
|
}
|
|
|
|
$prompt = "You are a clinical decision-support assistant for OPD doctors.\n"
|
|
. "Input is OCR text from scanned/uploaded patient reports (lab/xray/ecg/general).\n"
|
|
. "Determine if this report text is suitable for diagnosis support.\n"
|
|
. "Return ONLY valid JSON object with keys:\n"
|
|
. "diagnosis_suitable (yes/no), suitability_reason, probable_diagnosis, clinical_summary, key_findings, suggested_tests, red_flags, confidence_note.\n"
|
|
. "Rules:\n"
|
|
. "- Keep content concise, medically cautious, non-final.\n"
|
|
. "- key_findings/suggested_tests/red_flags must be JSON arrays of short strings.\n"
|
|
. "- Do not invent values not present in text.\n"
|
|
. "Scan Type: " . strtoupper($scanType) . "\n"
|
|
. "Report Text:\n" . $extractedText;
|
|
|
|
$azureRaw = $this->callAzureOpenAiText($prompt, 900, true);
|
|
$azureParsed = $this->parseJsonFromAiRaw($azureRaw);
|
|
if (is_array($azureParsed)) {
|
|
return [
|
|
'diagnosis_suitable' => strtolower(trim((string) ($azureParsed['diagnosis_suitable'] ?? 'no'))) === 'yes' ? 'yes' : 'no',
|
|
'suitability_reason' => trim((string) ($azureParsed['suitability_reason'] ?? '')),
|
|
'probable_diagnosis' => trim((string) ($azureParsed['probable_diagnosis'] ?? '')),
|
|
'clinical_summary' => trim((string) ($azureParsed['clinical_summary'] ?? '')),
|
|
'key_findings' => $this->sanitizeAiStringList($azureParsed['key_findings'] ?? []),
|
|
'suggested_tests' => $this->sanitizeAiStringList($azureParsed['suggested_tests'] ?? []),
|
|
'red_flags' => $this->sanitizeAiStringList($azureParsed['red_flags'] ?? []),
|
|
'confidence_note' => trim((string) ($azureParsed['confidence_note'] ?? '')),
|
|
];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return array{applied:bool,session_id:int,updated_fields:array<int,string>}
|
|
*/
|
|
private function applyAiScanAssistToPrescription(int $opdid, array $analysis): array
|
|
{
|
|
if (! $this->db->tableExists('opd_prescription')) {
|
|
return ['applied' => false, 'session_id' => 0, 'updated_fields' => []];
|
|
}
|
|
|
|
$fields = $this->db->getFieldNames('opd_prescription');
|
|
$diagnosisField = $this->resolveFirstField($fields, ['diagnosis', 'provisional_diagnosis']);
|
|
$findingField = $this->resolveFirstField($fields, ['Finding_Examinations', 'finding_examinations']);
|
|
$investigationField = $this->resolveFirstField($fields, ['investigation']);
|
|
|
|
$row = $this->db->table('opd_prescription')
|
|
->where('opd_id', $opdid)
|
|
->orderBy('id', 'DESC')
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
if (empty($row)) {
|
|
$opdMaster = $this->db->table('opd_master')->where('opd_id', $opdid)->get(1)->getRowArray();
|
|
if (empty($opdMaster)) {
|
|
return ['applied' => false, 'session_id' => 0, 'updated_fields' => []];
|
|
}
|
|
|
|
$insert = [];
|
|
foreach ([
|
|
'opd_id' => $opdid,
|
|
'p_id' => (int) ($opdMaster['p_id'] ?? 0),
|
|
'doc_id' => (int) ($opdMaster['doc_id'] ?? 0),
|
|
'date_opd_visit' => date('Y-m-d'),
|
|
'visit_status' => 0,
|
|
'session_id' => 0,
|
|
] as $k => $v) {
|
|
if (in_array($k, $fields, true)) {
|
|
$insert[$k] = $v;
|
|
}
|
|
}
|
|
|
|
$this->db->table('opd_prescription')->insert($insert);
|
|
$newId = (int) $this->db->insertID();
|
|
$row = $this->db->table('opd_prescription')->where('id', $newId)->get(1)->getRowArray();
|
|
}
|
|
|
|
if (empty($row)) {
|
|
return ['applied' => false, 'session_id' => 0, 'updated_fields' => []];
|
|
}
|
|
|
|
$sessionId = (int) ($row['id'] ?? 0);
|
|
$prefix = '[AI-SCAN ' . date('d-m-Y H:i') . ']';
|
|
$summary = trim((string) ($analysis['clinical_summary'] ?? ''));
|
|
$probableDx = trim((string) ($analysis['probable_diagnosis'] ?? ''));
|
|
$confidence = trim((string) ($analysis['confidence_note'] ?? ''));
|
|
$findings = $this->sanitizeAiStringList($analysis['key_findings'] ?? []);
|
|
$tests = $this->sanitizeAiStringList($analysis['suggested_tests'] ?? []);
|
|
$redFlags = $this->sanitizeAiStringList($analysis['red_flags'] ?? []);
|
|
|
|
$updateData = [];
|
|
$updatedFields = [];
|
|
|
|
if ($diagnosisField !== null && $probableDx !== '') {
|
|
$chunk = $prefix . ' Probable diagnosis support: ' . $probableDx;
|
|
if ($confidence !== '') {
|
|
$chunk .= ' (' . $confidence . ')';
|
|
}
|
|
$old = trim((string) ($row[$diagnosisField] ?? ''));
|
|
if (! str_contains($old, $chunk)) {
|
|
$updateData[$diagnosisField] = $old === '' ? $chunk : ($old . "\n" . $chunk);
|
|
$updatedFields[] = $diagnosisField;
|
|
}
|
|
}
|
|
|
|
if ($findingField !== null) {
|
|
$parts = [];
|
|
if ($summary !== '') {
|
|
$parts[] = 'Summary: ' . $summary;
|
|
}
|
|
if (! empty($findings)) {
|
|
$parts[] = 'Findings: ' . implode('; ', $findings);
|
|
}
|
|
if (! empty($redFlags)) {
|
|
$parts[] = 'Red flags: ' . implode('; ', $redFlags);
|
|
}
|
|
|
|
if (! empty($parts)) {
|
|
$chunk = $prefix . ' ' . implode(' | ', $parts);
|
|
$old = trim((string) ($row[$findingField] ?? ''));
|
|
if (! str_contains($old, $chunk)) {
|
|
$updateData[$findingField] = $old === '' ? $chunk : ($old . "\n" . $chunk);
|
|
$updatedFields[] = $findingField;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($investigationField !== null && ! empty($tests)) {
|
|
$chunk = $prefix . ' Suggested tests: ' . implode('; ', $tests);
|
|
$old = trim((string) ($row[$investigationField] ?? ''));
|
|
if (! str_contains($old, $chunk)) {
|
|
$updateData[$investigationField] = $old === '' ? $chunk : ($old . "\n" . $chunk);
|
|
$updatedFields[] = $investigationField;
|
|
}
|
|
}
|
|
|
|
if (! empty($updateData)) {
|
|
$this->db->table('opd_prescription')->where('id', $sessionId)->update($updateData);
|
|
}
|
|
|
|
return [
|
|
'applied' => ! empty($updateData),
|
|
'session_id' => $sessionId,
|
|
'updated_fields' => $updatedFields,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param mixed $raw
|
|
* @return array<int, string>
|
|
*/
|
|
private function sanitizeAiStringList($raw): array
|
|
{
|
|
// Normalize AI arrays into unique, non-empty strings.
|
|
if (! is_array($raw)) {
|
|
return [];
|
|
}
|
|
|
|
$out = [];
|
|
foreach ($raw as $item) {
|
|
$txt = trim((string) $item);
|
|
if ($txt !== '') {
|
|
$out[] = $txt;
|
|
}
|
|
}
|
|
|
|
return array_values(array_unique($out));
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
private function parseJsonFromAiRaw(string $raw): ?array
|
|
{
|
|
$text = trim($raw);
|
|
if ($text === '') {
|
|
return null;
|
|
}
|
|
|
|
// First attempt: raw response is already valid JSON.
|
|
$decoded = json_decode($text, true);
|
|
if (is_array($decoded)) {
|
|
return $decoded;
|
|
}
|
|
|
|
// Second attempt: extract JSON object embedded in model prose.
|
|
if (preg_match('/\{(?:[^{}]|(?R))*\}/s', $text, $match) === 1) {
|
|
$decoded = json_decode((string) ($match[0] ?? ''), true);
|
|
if (is_array($decoded)) {
|
|
return $decoded;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, string> $constCandidates
|
|
* @param array<int, string> $settingCandidates
|
|
*/
|
|
private function readScanAiSettingValue(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 '';
|
|
}
|
|
|
|
/**
|
|
* @return array{endpoint:string,api_key:string,deployment:string,api_version:string}
|
|
*/
|
|
private function getAzureOpenAiConfigForScan(): array
|
|
{
|
|
$endpoint = $this->readScanAiSettingValue(
|
|
['AZURE_OPENAI_ENDPOINT', 'AI_AZURE_OPENAI_ENDPOINT', 'APP_AZURE_OPENAI_ENDPOINT', 'H_AZURE_OPENAI_ENDPOINT'],
|
|
['AZURE_OPENAI_ENDPOINT']
|
|
);
|
|
|
|
$apiKey = $this->readScanAiSettingValue(
|
|
['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->readScanAiSettingValue(
|
|
['AZURE_OPENAI_DEPLOYMENT', 'AI_AZURE_OPENAI_DEPLOYMENT', 'APP_AZURE_OPENAI_DEPLOYMENT', 'H_AZURE_OPENAI_DEPLOYMENT'],
|
|
['AZURE_OPENAI_DEPLOYMENT']
|
|
);
|
|
|
|
$apiVersion = $this->readScanAiSettingValue(
|
|
['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';
|
|
}
|
|
|
|
$endpoint = $this->normalizeAzureEndpointBase($endpoint);
|
|
|
|
return [
|
|
'endpoint' => $endpoint,
|
|
'api_key' => $apiKey,
|
|
'deployment' => $deployment,
|
|
'api_version' => $apiVersion,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{azure_openai:bool,docintel:bool,summary:string}
|
|
*/
|
|
private function getScanAiReadiness(): array
|
|
{
|
|
$azure = $this->getAzureOpenAiConfigForScan();
|
|
$azureOpenAi = ($azure['endpoint'] !== '' && $azure['api_key'] !== '' && $azure['deployment'] !== '');
|
|
|
|
$docIntelEndpoint = rtrim($this->readScanAiSettingValue(
|
|
['AZURE_DOCINTEL_ENDPOINT', 'AI_AZURE_DOCINTEL_ENDPOINT', 'APP_AZURE_DOCINTEL_ENDPOINT', 'H_AZURE_DOCINTEL_ENDPOINT'],
|
|
['AZURE_DOCINTEL_ENDPOINT']
|
|
), '/');
|
|
$docIntelKey = $this->readScanAiSettingValue(
|
|
['AZURE_DOCINTEL_KEY', 'AI_AZURE_DOCINTEL_KEY', 'APP_AZURE_DOCINTEL_KEY', 'H_AZURE_DOCINTEL_KEY'],
|
|
['AZURE_DOCINTEL_KEY']
|
|
);
|
|
$docIntel = ($docIntelEndpoint !== '' && $docIntelKey !== '');
|
|
|
|
$parts = [];
|
|
$parts[] = 'AzureOpenAI=' . ($azureOpenAi ? 'set' : 'missing endpoint/key/deployment');
|
|
$parts[] = 'DocIntel=' . ($docIntel ? 'set' : 'missing endpoint/key');
|
|
|
|
return [
|
|
'azure_openai' => $azureOpenAi,
|
|
'docintel' => $docIntel,
|
|
'summary' => implode(', ', $parts),
|
|
];
|
|
}
|
|
|
|
private function callAzureOpenAiVision(string $filePath, string $mimeType, string $prompt, int $maxTokens = 1200, bool $jsonObject = false): string
|
|
{
|
|
$cfg = $this->getAzureOpenAiConfigForScan();
|
|
if ($cfg['endpoint'] === '' || $cfg['api_key'] === '' || $cfg['deployment'] === '') {
|
|
$this->addScanAiDebug('azure vision skipped: endpoint/key/deployment missing');
|
|
return '';
|
|
}
|
|
|
|
if (! str_starts_with($mimeType, 'image/')) {
|
|
$this->addScanAiDebug('azure vision skipped: non-image mime ' . $mimeType);
|
|
return '';
|
|
}
|
|
|
|
$bytes = @file_get_contents($filePath);
|
|
if ($bytes === false || $bytes === '') {
|
|
$this->addScanAiDebug('azure vision skipped: file read failed');
|
|
return '';
|
|
}
|
|
|
|
$dataUrl = 'data:' . $mimeType . ';base64,' . base64_encode($bytes);
|
|
$url = $cfg['endpoint'] . '/openai/deployments/' . rawurlencode($cfg['deployment']) . '/chat/completions?api-version=' . rawurlencode($cfg['api_version']);
|
|
|
|
for ($attempt = 0; $attempt < 2; $attempt++) {
|
|
$forceInsecure = $attempt === 1;
|
|
try {
|
|
$client = service('curlrequest', $this->buildAzureCurlOptions($cfg['endpoint'], 30, $forceInsecure));
|
|
|
|
$payload = [
|
|
'messages' => [[
|
|
'role' => 'user',
|
|
'content' => [
|
|
['type' => 'text', 'text' => $prompt],
|
|
['type' => 'image_url', 'image_url' => ['url' => $dataUrl]],
|
|
],
|
|
]],
|
|
'temperature' => 0.1,
|
|
'max_tokens' => $maxTokens,
|
|
];
|
|
|
|
if ($jsonObject) {
|
|
$payload['response_format'] = ['type' => 'json_object'];
|
|
}
|
|
|
|
$response = $client->post($url, [
|
|
'headers' => [
|
|
'api-key' => $cfg['api_key'],
|
|
'Content-Type' => 'application/json',
|
|
],
|
|
'json' => $payload,
|
|
]);
|
|
|
|
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
|
|
$this->addScanAiDebug('azure vision HTTP ' . $response->getStatusCode());
|
|
return '';
|
|
}
|
|
|
|
$decoded = json_decode((string) $response->getBody(), true);
|
|
$content = trim((string) ($decoded['choices'][0]['message']['content'] ?? ''));
|
|
if ($content === '') {
|
|
$this->addScanAiDebug('azure vision returned empty content');
|
|
}
|
|
return $content;
|
|
} catch (\Throwable $e) {
|
|
if (! $forceInsecure && $this->isSslCertificateError($e)) {
|
|
$this->addScanAiDebug('azure vision SSL error; retry insecure');
|
|
continue;
|
|
}
|
|
$this->addScanAiDebug('azure vision exception: ' . $e->getMessage());
|
|
return '';
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function callAzureOpenAiText(string $prompt, int $maxTokens = 1200, bool $jsonObject = false): string
|
|
{
|
|
$cfg = $this->getAzureOpenAiConfigForScan();
|
|
if ($cfg['endpoint'] === '' || $cfg['api_key'] === '' || $cfg['deployment'] === '') {
|
|
$this->addScanAiDebug('azure text skipped: endpoint/key/deployment missing');
|
|
return '';
|
|
}
|
|
|
|
$url = $cfg['endpoint'] . '/openai/deployments/' . rawurlencode($cfg['deployment']) . '/chat/completions?api-version=' . rawurlencode($cfg['api_version']);
|
|
|
|
for ($attempt = 0; $attempt < 2; $attempt++) {
|
|
$forceInsecure = $attempt === 1;
|
|
try {
|
|
$client = service('curlrequest', $this->buildAzureCurlOptions($cfg['endpoint'], 30, $forceInsecure));
|
|
|
|
$payload = [
|
|
'messages' => [[
|
|
'role' => 'user',
|
|
'content' => $prompt,
|
|
]],
|
|
'temperature' => 0.1,
|
|
'max_tokens' => $maxTokens,
|
|
];
|
|
if ($jsonObject) {
|
|
$payload['response_format'] = ['type' => 'json_object'];
|
|
}
|
|
|
|
$response = $client->post($url, [
|
|
'headers' => [
|
|
'api-key' => $cfg['api_key'],
|
|
'Content-Type' => 'application/json',
|
|
],
|
|
'json' => $payload,
|
|
]);
|
|
|
|
if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 300) {
|
|
$this->addScanAiDebug('azure text HTTP ' . $response->getStatusCode());
|
|
return '';
|
|
}
|
|
|
|
$decoded = json_decode((string) $response->getBody(), true);
|
|
$content = trim((string) ($decoded['choices'][0]['message']['content'] ?? ''));
|
|
if ($content === '') {
|
|
$this->addScanAiDebug('azure text returned empty content');
|
|
}
|
|
return $content;
|
|
} catch (\Throwable $e) {
|
|
if (! $forceInsecure && $this->isSslCertificateError($e)) {
|
|
$this->addScanAiDebug('azure text SSL error; retry insecure');
|
|
continue;
|
|
}
|
|
$this->addScanAiDebug('azure text exception: ' . $e->getMessage());
|
|
return '';
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function extractTextWithAzureDocumentIntelligence(string $filePath): string
|
|
{
|
|
$endpoint = rtrim($this->readScanAiSettingValue(
|
|
['AZURE_DOCINTEL_ENDPOINT', 'AI_AZURE_DOCINTEL_ENDPOINT', 'APP_AZURE_DOCINTEL_ENDPOINT', 'H_AZURE_DOCINTEL_ENDPOINT'],
|
|
['AZURE_DOCINTEL_ENDPOINT']
|
|
), '/');
|
|
$endpoint = $this->normalizeAzureEndpointBase($endpoint);
|
|
$key = $this->readScanAiSettingValue(
|
|
['AZURE_DOCINTEL_KEY', 'AI_AZURE_DOCINTEL_KEY', 'APP_AZURE_DOCINTEL_KEY', 'H_AZURE_DOCINTEL_KEY'],
|
|
['AZURE_DOCINTEL_KEY']
|
|
);
|
|
|
|
if ($endpoint === '' || $key === '' || !is_file($filePath)) {
|
|
$this->addScanAiDebug('docintel skipped: endpoint/key/file missing');
|
|
return '';
|
|
}
|
|
|
|
for ($attempt = 0; $attempt < 2; $attempt++) {
|
|
$forceInsecure = $attempt === 1;
|
|
try {
|
|
$client = service('curlrequest', $this->buildAzureCurlOptions($endpoint, 40, $forceInsecure));
|
|
|
|
$bytes = @file_get_contents($filePath);
|
|
if ($bytes === false || $bytes === '') {
|
|
return '';
|
|
}
|
|
|
|
$mimeType = $this->guessMimeTypeFromPath($filePath);
|
|
$analyzeUrls = [
|
|
$endpoint . '/documentintelligence/documentModels/prebuilt-read:analyze?api-version=2024-02-29-preview',
|
|
$endpoint . '/documentintelligence/documentModels/prebuilt-read:analyze?api-version=2023-07-31',
|
|
$endpoint . '/formrecognizer/documentModels/prebuilt-read:analyze?api-version=2023-07-31',
|
|
];
|
|
|
|
// Try multiple API routes for compatibility with different Azure environments.
|
|
foreach ($analyzeUrls as $analyzeUrl) {
|
|
$submit = $client->post($analyzeUrl, [
|
|
'headers' => [
|
|
'Content-Type' => $mimeType !== '' ? $mimeType : 'application/octet-stream',
|
|
'Ocp-Apim-Subscription-Key' => $key,
|
|
],
|
|
'body' => $bytes,
|
|
]);
|
|
|
|
if ($submit->getStatusCode() < 200 || $submit->getStatusCode() >= 300) {
|
|
$this->addScanAiDebug('docintel submit HTTP ' . $submit->getStatusCode() . ' @ ' . basename($analyzeUrl));
|
|
continue;
|
|
}
|
|
|
|
$submitBody = json_decode((string) $submit->getBody(), true);
|
|
if (is_array($submitBody)) {
|
|
$inlineText = $this->extractTextFromDocIntelPayload($submitBody);
|
|
if ($inlineText !== '') {
|
|
return $inlineText;
|
|
}
|
|
}
|
|
|
|
$operationLocation = trim((string) $submit->getHeaderLine('Operation-Location'));
|
|
if ($operationLocation === '') {
|
|
$this->addScanAiDebug('docintel missing operation-location @ ' . basename($analyzeUrl));
|
|
continue;
|
|
}
|
|
|
|
for ($i = 0; $i < 14; $i++) {
|
|
usleep(900000);
|
|
$poll = $client->get($operationLocation, [
|
|
'headers' => [
|
|
'Ocp-Apim-Subscription-Key' => $key,
|
|
],
|
|
]);
|
|
|
|
if ($poll->getStatusCode() < 200 || $poll->getStatusCode() >= 300) {
|
|
$this->addScanAiDebug('docintel poll HTTP ' . $poll->getStatusCode());
|
|
continue;
|
|
}
|
|
|
|
$decoded = json_decode((string) $poll->getBody(), true);
|
|
if (!is_array($decoded)) {
|
|
continue;
|
|
}
|
|
|
|
$status = strtolower((string) ($decoded['status'] ?? ''));
|
|
if ($status === 'succeeded') {
|
|
$text = $this->extractTextFromDocIntelPayload($decoded);
|
|
if ($text !== '') {
|
|
return $text;
|
|
}
|
|
}
|
|
|
|
if ($status === 'failed') {
|
|
$this->addScanAiDebug('docintel status failed');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return '';
|
|
} catch (\Throwable $e) {
|
|
if (! $forceInsecure && $this->isSslCertificateError($e)) {
|
|
$this->addScanAiDebug('docintel SSL error; retry insecure');
|
|
continue;
|
|
}
|
|
$this->addScanAiDebug('docintel exception: ' . $e->getMessage());
|
|
return '';
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function extractTextFromDocIntelPayload(array $payload): string
|
|
{
|
|
// Prefer native concatenated content when present.
|
|
$content = trim((string) ($payload['analyzeResult']['content'] ?? ''));
|
|
if ($content !== '') {
|
|
return $content;
|
|
}
|
|
|
|
// Fallback: rebuild text from line entries.
|
|
$lines = [];
|
|
$pages = $payload['analyzeResult']['pages'] ?? [];
|
|
foreach ($pages as $page) {
|
|
$pageLines = $page['lines'] ?? [];
|
|
foreach ($pageLines as $line) {
|
|
$txt = trim((string) ($line['content'] ?? ''));
|
|
if ($txt !== '') {
|
|
$lines[] = $txt;
|
|
}
|
|
}
|
|
}
|
|
|
|
return trim(implode("\n", $lines));
|
|
}
|
|
|
|
private function extractTextFromScanFile(string $filePath, string $scanType, ?string $mimeType = null): ?string
|
|
{
|
|
if (!is_file($filePath)) {
|
|
return null;
|
|
}
|
|
|
|
$raw = @file_get_contents($filePath);
|
|
if ($raw === false || $raw === '') {
|
|
return null;
|
|
}
|
|
|
|
$mimeType = trim((string) ($mimeType ?? ''));
|
|
if ($mimeType === '') {
|
|
$mimeType = $this->guessMimeTypeFromPath($filePath);
|
|
}
|
|
|
|
$maxBytes = $mimeType === 'application/pdf' ? (12 * 1024 * 1024) : (8 * 1024 * 1024);
|
|
if (strlen($raw) > $maxBytes) {
|
|
return null;
|
|
}
|
|
|
|
// OCR priority: Document Intelligence first, then vision model fallback.
|
|
$prompt = 'Extract readable clinical text from this ' . strtoupper($scanType) . ' medical document. Return plain text with sections: Findings, Impression, Values. Do not invent data.';
|
|
|
|
$docIntelText = $this->extractTextWithAzureDocumentIntelligence($filePath);
|
|
if ($docIntelText !== '') {
|
|
return preg_replace('/\s+/', ' ', trim($docIntelText)) ?: trim($docIntelText);
|
|
}
|
|
|
|
$azurePrompt = 'Read and extract all visible clinical text from this ' . strtoupper($scanType) . ' medical image. Return plain text only. If unclear, return best readable lines.';
|
|
$azureText = $this->callAzureOpenAiVision($filePath, $mimeType, $azurePrompt, 1800, false);
|
|
if ($azureText !== '') {
|
|
return preg_replace('/\s+/', ' ', trim($azureText)) ?: trim($azureText);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @return array{updated:bool,session_id:int,target_field:string}
|
|
*/
|
|
private function appendExtractedTextToOpdPrescription(int $opdid, string $scanType, string $text): array
|
|
{
|
|
if (!$this->db->tableExists('opd_prescription')) {
|
|
return ['updated' => false, 'session_id' => 0, 'target_field' => ''];
|
|
}
|
|
|
|
$fields = $this->db->getFieldNames('opd_prescription');
|
|
$targetField = in_array('Finding_Examinations', $fields, true) ? 'Finding_Examinations' : '';
|
|
|
|
if (in_array($scanType, ['ecg', 'lab'], true) && in_array('investigation', $fields, true)) {
|
|
$targetField = 'investigation';
|
|
} elseif ($targetField === '' && in_array('diagnosis', $fields, true)) {
|
|
$targetField = 'diagnosis';
|
|
}
|
|
|
|
if ($targetField === '') {
|
|
return ['updated' => false, 'session_id' => 0, 'target_field' => ''];
|
|
}
|
|
|
|
$row = $this->db->table('opd_prescription')
|
|
->where('opd_id', $opdid)
|
|
->orderBy('id', 'DESC')
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
if (empty($row)) {
|
|
$opdMaster = $this->db->table('opd_master')->where('opd_id', $opdid)->get(1)->getRowArray();
|
|
if (empty($opdMaster)) {
|
|
return ['updated' => false, 'session_id' => 0, 'target_field' => ''];
|
|
}
|
|
|
|
$insert = [];
|
|
foreach ([
|
|
'opd_id' => $opdid,
|
|
'p_id' => (int) ($opdMaster['p_id'] ?? 0),
|
|
'doc_id' => (int) ($opdMaster['doc_id'] ?? 0),
|
|
'date_opd_visit' => date('Y-m-d'),
|
|
'visit_status' => 0,
|
|
'session_id' => 0,
|
|
] as $k => $v) {
|
|
if (in_array($k, $fields, true)) {
|
|
$insert[$k] = $v;
|
|
}
|
|
}
|
|
|
|
$this->db->table('opd_prescription')->insert($insert);
|
|
$newId = (int) $this->db->insertID();
|
|
$row = $this->db->table('opd_prescription')->where('id', $newId)->get(1)->getRowArray();
|
|
}
|
|
|
|
if (empty($row)) {
|
|
return ['updated' => false, 'session_id' => 0, 'target_field' => ''];
|
|
}
|
|
|
|
$sessionId = (int) ($row['id'] ?? 0);
|
|
$oldValue = trim((string) ($row[$targetField] ?? ''));
|
|
$prefix = '[SCAN-' . strtoupper($scanType) . ' ' . date('d-m-Y H:i') . ']';
|
|
$newChunk = $prefix . ' ' . trim($text);
|
|
|
|
if ($oldValue !== '' && str_contains($oldValue, $newChunk)) {
|
|
return ['updated' => true, 'session_id' => $sessionId, 'target_field' => $targetField];
|
|
}
|
|
|
|
$newValue = $oldValue === '' ? $newChunk : ($oldValue . "\n" . $newChunk);
|
|
$this->db->table('opd_prescription')->where('id', $sessionId)->update([$targetField => $newValue]);
|
|
|
|
return ['updated' => true, 'session_id' => $sessionId, 'target_field' => $targetField];
|
|
}
|
|
|
|
private function resolveFirstField(array $fields, array $candidates): ?string
|
|
{
|
|
foreach ($candidates as $candidate) {
|
|
if (in_array($candidate, $fields, true)) {
|
|
return $candidate;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function findExistingTable(array $candidates): ?string
|
|
{
|
|
foreach ($candidates as $table) {
|
|
if ($this->db->tableExists($table)) {
|
|
return $table;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function hydrateNabhPrintFields(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 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 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 '';
|
|
}
|
|
|
|
/**
|
|
* @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,
|
|
];
|
|
}
|
|
}
|